theorose49 f83724b995
All checks were successful
build-and-push / build (push) Successful in 39s
feat: spin 백엔드 전체 구현 (근무·프로젝트·인센티브·회계)
- 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>
2026-06-28 08:57:35 +09:00

113 lines
5.2 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package models
import (
"time"
"gorm.io/datatypes"
"gorm.io/gorm"
)
// Payment stage kinds (계약금/중도금/잔금) and scopes (BE / non-BE).
const (
StageDeposit = "deposit" // 계약금
StageMiddle = "middle" // 중도금
StageFinal = "final" // 잔금
ScopeBE = "be" // 손익분기 금액분 → 작업자 인센티브 포인트 풀
ScopeNonBE = "non_be" // BE 초과분 → 회사:파트너 분배
)
// Fix lifecycle for a user's incentive on a project stage:
// 예정 → 반영중(회사 입금) → 반영완료(포인트 반영) → 지급완료(급여 지급)
const (
FixPlanned = "planned" // 예정
FixApplying = "applying" // 반영중
FixApplied = "applied" // 반영완료
FixPaid = "paid" // 지급완료
)
// IncentiveConfig is the per-year rule set. Admin tunes it; once happy it gets
// frozen for the year. RankQuota maps rank label → annual point quota.
type IncentiveConfig struct {
Base
Year int `gorm:"uniqueIndex" json:"year"`
PointRate float64 `json:"pointRate"` // KRW per 1 incentive point (환율)
DepositPct float64 `json:"depositPct"` // 계약금 비율 (%)
MiddlePct float64 `json:"middlePct"` // 중도금 비율 (%)
FinalPct float64 `json:"finalPct"` // 잔금 비율 (%)
NonBECompanyPct float64 `json:"nonBeCompanyPct"` // non-BE 회사 몫 (%)
NonBEPartnerPct float64 `json:"nonBePartnerPct"` // non-BE 파트너 몫 (%)
RankQuota datatypes.JSONMap `json:"rankQuota"` // {"주임":n,...} annual point quota
Frozen bool `json:"frozen"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
func (m *IncentiveConfig) BeforeCreate(*gorm.DB) error { m.ensureID(); return nil }
// PaymentStage is a project-level stage bucket (one per kind×scope). The admin
// toggles Status as money arrives ("계약금 들어옴", "중도금까지 들어옴").
type PaymentStage struct {
Base
ProjectID string `gorm:"index" json:"projectId"`
Kind string `json:"kind"` // deposit | middle | final
Scope string `json:"scope"` // be | non_be
Amount float64 `json:"amount"` // KRW allocated to this bucket
Pct float64 `json:"pct"` // % of scope total
ExpectedDate string `json:"expectedDate"`
FixedDate string `json:"fixedDate"`
Status string `json:"status"` // planned | applying | applied | paid
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
func (m *PaymentStage) BeforeCreate(*gorm.DB) error { m.ensureID(); return nil }
// UserIncentive is the per (project, member, stage, scope) incentive record. It
// is derived from the member's portion but fully overridable per the spec
// ("특정 유저만 픽스하지 않는 상황" etc). Points = Amount × portion ÷ pointRate.
type UserIncentive struct {
Base
ProjectID string `gorm:"index" json:"projectId"`
MemberEmail string `gorm:"index" json:"memberEmail"`
StageID string `gorm:"index" json:"stageId"`
Kind string `json:"kind"`
Scope string `json:"scope"`
Year int `gorm:"index" json:"year"`
Quarter int `json:"quarter"` // 1..4 settlement bucket
Portion float64 `json:"portion"`
Amount float64 `json:"amount"` // KRW share
Points float64 `json:"points"`
FixStatus string `json:"fixStatus"` // planned | applying | applied | paid
Override bool `json:"override"` // manually adjusted (engine won't recompute)
Memo string `json:"memo"`
AppliedAt *time.Time `json:"appliedAt"`
PaidAt *time.Time `json:"paidAt"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
func (m *UserIncentive) BeforeCreate(*gorm.DB) error { m.ensureID(); return nil }
// QuarterlySettlement is the 3/6/9/12월 calculation snapshot per member: how much
// of their accumulated points exceed the rank quota, and the incremental payout.
type QuarterlySettlement struct {
Base
MemberEmail string `gorm:"index" json:"memberEmail"`
Year int `gorm:"index" json:"year"`
Quarter int `gorm:"index" json:"quarter"`
Rank string `json:"rank"`
Quota float64 `json:"quota"`
PointsCumul float64 `json:"pointsCumul"` // cumulative applied points to date
ExcessPoints float64 `json:"excessPoints"` // cumul quota (floored at 0)
PaidPointsYTD float64 `json:"paidPointsYtd"` // already paid in prior quarters
PayoutPoints float64 `json:"payoutPoints"` // excess paidYTD (this quarter's delta)
PayoutAmount float64 `json:"payoutAmount"` // KRW = payoutPoints × pointRate
Fixed bool `json:"fixed"`
FixedAt *time.Time `json:"fixedAt"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
func (m *QuarterlySettlement) BeforeCreate(*gorm.DB) error { m.ensureID(); return nil }