All checks were successful
build-and-push / build (push) Successful in 39s
- config/db/storage/auth/router/perms: eQMS 규약 미러링, 권한 2-tier (관리자 전체 / 구성원 본인·신청만), oauth2-proxy 헤더 인증 + DEV_AUTH mock - 모델: 구성원/부서, 근무(출퇴근·휴가·공가·초과), 프로젝트(회사/제품/버전· 작업자portion·담당자·태스크·계약·첨부·분할입금), 인센티브(설정·단계· 유저배분·분기정산), 회계(거래·세금) - internal/worktime: 근로기준법 월 집계 엔진 - internal/incentive: BE/non-BE × 계약금/중도금/잔금 3단계 계산 + 시뮬레이션 - 시드 데이터, Go 멀티스테이지 Dockerfile - ADMIN_GROUPS 기본값 'admin' (전 내부 앱 공통 그룹) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
166 lines
6.3 KiB
Go
166 lines
6.3 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
|
|
"spin/internal/config"
|
|
"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
|
|
}
|
|
|
|
// 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) http.Handler {
|
|
s := &Server{db: db, store: store, cfg: cfg}
|
|
|
|
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))
|
|
|
|
// identity / navigation
|
|
r.Get("/me", s.handleMe)
|
|
r.Get("/me/nav", s.handleNav)
|
|
|
|
// ---- slice 1: members / org ----
|
|
r.Get("/members", s.handleListMembers)
|
|
r.Post("/members", s.handleCreateMember)
|
|
r.Get("/members/{id}", s.handleGetMember)
|
|
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.Get("/attendance/timesheet", s.handleTimesheet) // monthly roll-up
|
|
r.Get("/leave", s.handleListLeave)
|
|
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.Get("/products", s.handleListProducts)
|
|
r.Post("/products", s.handleCreateProduct)
|
|
r.Get("/versions", s.handleListVersions)
|
|
r.Post("/versions", s.handleCreateVersion)
|
|
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.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)
|
|
}
|