feat: spin 백엔드 전체 구현 (근무·프로젝트·인센티브·회계)
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>
This commit is contained in:
theorose49
2026-06-28 08:57:35 +09:00
co-authored by Claude Opus 4.8
parent ce4023ca59
commit f83724b995
28 changed files with 3960 additions and 5 deletions
+78
View File
@@ -0,0 +1,78 @@
// Package worktime computes monthly working-time roll-ups under the Korean Labor
// Standards Act model: a 주 소정근로시간 (default 40h) baseline, business-day
// expansion for the month, plus recognized leave/overtime.
package worktime
import "time"
// MonthBusinessDays returns the count of weekdays (Mon–Fri) in the given month.
// Public holidays are out of scope here; admins can adjust via leave/공가.
func MonthBusinessDays(year int, month time.Month) int {
n := 0
d := time.Date(year, month, 1, 0, 0, 0, 0, time.UTC)
for d.Month() == month {
if wd := d.Weekday(); wd != time.Saturday && wd != time.Sunday {
n++
}
d = d.AddDate(0, 0, 1)
}
return n
}
// StandardMinutes is the expected worked minutes for the month: business days ×
// the policy's daily standard minutes.
func StandardMinutes(year int, month time.Month, dailyStandardMin int) int {
if dailyStandardMin <= 0 {
dailyStandardMin = 480
}
return MonthBusinessDays(year, month) * dailyStandardMin
}
// NetWorkMinutes computes worked minutes between clock-in and clock-out minus an
// unpaid break. Returns 0 if either bound is missing or the span is negative.
func NetWorkMinutes(in, out *time.Time, lunchMinutes int) int {
if in == nil || out == nil {
return 0
}
mins := int(out.Sub(*in).Minutes()) - lunchMinutes
if mins < 0 {
return 0
}
return mins
}
// Summary is the monthly timesheet roll-up returned to the UI.
type Summary struct {
Year int `json:"year"`
Month int `json:"month"`
BusinessDays int `json:"businessDays"`
StandardMinutes int `json:"standardMinutes"`
WorkedMinutes int `json:"workedMinutes"`
LeaveMinutes int `json:"leaveMinutes"` // recognized (연차/공가 등) as worked-equivalent
OvertimeMinutes int `json:"overtimeMinutes"` // approved overtime
RecognizedTotal int `json:"recognizedTotal"` // worked + leave
FulfillmentPct float64 `json:"fulfillmentPct"` // recognizedTotal / standard
DaysPresent int `json:"daysPresent"`
}
// Compute assembles the summary from raw minute inputs.
func Compute(year, month, dailyStandardMin, worked, leave, overtime, daysPresent int) Summary {
std := StandardMinutes(year, time.Month(month), dailyStandardMin)
recognized := worked + leave
pct := 0.0
if std > 0 {
pct = float64(recognized) / float64(std) * 100
}
return Summary{
Year: year,
Month: month,
BusinessDays: MonthBusinessDays(year, time.Month(month)),
StandardMinutes: std,
WorkedMinutes: worked,
LeaveMinutes: leave,
OvertimeMinutes: overtime,
RecognizedTotal: recognized,
FulfillmentPct: pct,
DaysPresent: daysPresent,
}
}