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

79 lines
2.7 KiB
Go
Raw 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 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 (MonFri) 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,
}
}