All checks were successful
build-and-push / build (push) Successful in 35s
- 직급에 인턴 추가(기본 할당량 15), 직책(position)은 UI에서 제거(컬럼은 유지) - 초과근무: 유저 신청 제거 → 관리자 근무관리에서 실제 출퇴근 기록 기반 자동 집계 - 로그아웃: infra 공통 LOGOUT_URL(/me로 전달) 사용 → oauth2-proxy 종료 + Keycloak end-session - (이전 커밋 포함) Device 등록 + FCM HTTP v1 sender + notify 연동 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
191 lines
9.7 KiB
Go
191 lines
9.7 KiB
Go
// Package seed loads sample data for local development (SEED=true). Production
|
|
// runs with SEED=false so the cluster DB stays empty until real data arrives.
|
|
package seed
|
|
|
|
import (
|
|
"log"
|
|
"time"
|
|
|
|
"spin/internal/incentive"
|
|
"spin/internal/models"
|
|
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// Run is idempotent-ish: it no-ops if members already exist.
|
|
func Run(db *gorm.DB) error {
|
|
var n int64
|
|
db.Model(&models.Member{}).Count(&n)
|
|
if n > 0 {
|
|
log.Printf("seed: data present, skipping")
|
|
return nil
|
|
}
|
|
log.Printf("seed: loading sample data")
|
|
|
|
year := time.Now().Year()
|
|
jan := time.Date(year, 1, 2, 0, 0, 0, 0, time.UTC)
|
|
|
|
// departments
|
|
consulting := models.Department{Name: "컨설팅본부", LeadEmail: "admin@special-partners.com"}
|
|
mgmt := models.Department{Name: "경영지원", LeadEmail: "admin@special-partners.com"}
|
|
db.Create(&consulting)
|
|
db.Create(&mgmt)
|
|
|
|
// members (admin = theorose49 to match dev-auth mock)
|
|
members := []models.Member{
|
|
{Email: "theorose49@gmail.com", DisplayName: "관리자", Rank: models.RankPartner, Role: models.RoleAdmin, IsPartner: true, Position: "대표 컨설턴트", DepartmentID: &mgmt.ID, Status: "active", JoinDate: &jan, AnnualLeave: 15, Phone: "010-0000-0001"},
|
|
{Email: "member@special-partners.com", DisplayName: "김주임", Rank: models.RankJunior, Role: models.RoleMember, Position: "컨설턴트", DepartmentID: &consulting.ID, Status: "active", JoinDate: &jan, AnnualLeave: 15, Phone: "010-1111-2222"},
|
|
{Email: "lee@special-partners.com", DisplayName: "이선임", Rank: models.RankSenior, Role: models.RoleMember, Position: "선임 컨설턴트", DepartmentID: &consulting.ID, Status: "active", JoinDate: &jan, AnnualLeave: 15, Phone: "010-3333-4444"},
|
|
{Email: "park@special-partners.com", DisplayName: "박책임", Rank: models.RankLead, Role: models.RoleMember, Position: "책임 컨설턴트", DepartmentID: &consulting.ID, Status: "active", JoinDate: &jan, AnnualLeave: 15, Phone: "010-5555-6666"},
|
|
{Email: "choi@special-partners.com", DisplayName: "최파트너", Rank: models.RankPartner, Role: models.RoleMember, IsPartner: true, Position: "파트너", DepartmentID: &consulting.ID, Status: "active", JoinDate: &jan, AnnualLeave: 15, Phone: "010-7777-8888"},
|
|
}
|
|
for i := range members {
|
|
db.Create(&members[i])
|
|
}
|
|
|
|
// work policy
|
|
db.Create(&models.WorkPolicy{Name: "기본 근무제", WeeklyHours: 40, DailyStandardMin: 480,
|
|
CoreStart: "09:00", CoreEnd: "18:00", LunchMinutes: 60, AnnualLeaveBase: 15, Active: true})
|
|
|
|
// leave balances
|
|
for _, m := range members {
|
|
db.Create(&models.LeaveBalance{MemberEmail: m.Email, Year: year, Granted: 15, Used: 0})
|
|
}
|
|
|
|
// some attendance for the current month (member + lee)
|
|
seedAttendance(db, "member@special-partners.com")
|
|
seedAttendance(db, "lee@special-partners.com")
|
|
|
|
// a couple of pending requests
|
|
today := time.Now().Format("2006-01-02")
|
|
db.Create(&models.LeaveRequest{MemberEmail: "member@special-partners.com", Type: models.LeaveAnnual,
|
|
StartDate: today, EndDate: today, Days: 1, Reason: "개인 사유", Status: models.StatusPending})
|
|
db.Create(&models.OvertimeRequest{MemberEmail: "lee@special-partners.com", Date: today,
|
|
Minutes: 120, Reason: "마감 대응", Status: models.StatusPending})
|
|
|
|
// incentive config
|
|
cfg := models.IncentiveConfig{Year: year, PointRate: 1_000_000, DepositPct: 30, MiddlePct: 40,
|
|
FinalPct: 30, NonBECompanyPct: 60, NonBEPartnerPct: 40,
|
|
RankQuota: map[string]interface{}{
|
|
models.RankIntern: 15.0, models.RankJunior: 30.0, models.RankSenior: 50.0, models.RankLead: 80.0, models.RankPartner: 120.0,
|
|
}}
|
|
db.Create(&cfg)
|
|
|
|
// companies / products / versions / projects
|
|
comp := models.Company{Name: "메디테크㈜", Code: "MTC", Note: "의료기기 제조사"}
|
|
db.Create(&comp)
|
|
prod := models.Product{CompanyID: comp.ID, Name: "CardioScan", Code: "CS"}
|
|
db.Create(&prod)
|
|
ver := models.Version{ProductID: prod.ID, Label: "v1.0"}
|
|
db.Create(&ver)
|
|
|
|
proj := models.Project{Name: "CardioScan FDA 510(k)", CompanyID: comp.ID, ProductID: prod.ID, VersionID: ver.ID,
|
|
CompanyName: comp.Name, ProductName: prod.Name, VersionName: ver.Label,
|
|
ConsultingType: "510(k) 인허가", Country: "미국(FDA)", Scope: models.ScopeBoth,
|
|
PMEmail: "park@special-partners.com", Cautions: "임상 데이터 보완 필요", Status: "active",
|
|
StartDate: time.Now().Format("2006-01-02"), DueDate: time.Now().AddDate(0, 4, 0).Format("2006-01-02")}
|
|
db.Create(&proj)
|
|
|
|
pms := []models.ProjectMember{
|
|
{ProjectID: proj.ID, MemberEmail: "member@special-partners.com", Portion: 30, Role: "작업자"},
|
|
{ProjectID: proj.ID, MemberEmail: "lee@special-partners.com", Portion: 30, Role: "작업자"},
|
|
{ProjectID: proj.ID, MemberEmail: "park@special-partners.com", Portion: 25, Role: "PM"},
|
|
{ProjectID: proj.ID, MemberEmail: "choi@special-partners.com", Portion: 15, Role: "파트너 검수"},
|
|
}
|
|
for i := range pms {
|
|
db.Create(&pms[i])
|
|
}
|
|
|
|
db.Create(&models.ClientContact{ProjectID: proj.ID, Name: "John Kim", Title: "RA Manager",
|
|
Phone: "+1-202-555-0100", Email: "john.kim@meditech.com"})
|
|
|
|
seedTasks(db, proj.ID)
|
|
|
|
// contract (admin-only)
|
|
contract := models.Contract{ProjectID: proj.ID, TotalAmount: 100_000_000, BEAmount: 60_000_000,
|
|
AdminCaution: "BE 이하로 협상 금지", Memo: "선금 30% 수령 완료"}
|
|
db.Create(&contract)
|
|
|
|
// payment splits
|
|
db.Create(&models.PaymentSplit{ProjectID: proj.ID, Label: "계약금", Amount: 30_000_000,
|
|
ExpectedDate: time.Now().Format("2006-01-02"), PaidDate: time.Now().Format("2006-01-02"), Paid: true, OrderIdx: 0})
|
|
db.Create(&models.PaymentSplit{ProjectID: proj.ID, Label: "중도금", Amount: 40_000_000,
|
|
ExpectedDate: time.Now().AddDate(0, 2, 0).Format("2006-01-02"), OrderIdx: 1})
|
|
db.Create(&models.PaymentSplit{ProjectID: proj.ID, Label: "잔금", Amount: 30_000_000,
|
|
ExpectedDate: time.Now().AddDate(0, 4, 0).Format("2006-01-02"), OrderIdx: 2})
|
|
|
|
// incentive stages + user allocations via the engine
|
|
eng := incentive.Config{PointRate: cfg.PointRate, DepositPct: cfg.DepositPct, MiddlePct: cfg.MiddlePct,
|
|
FinalPct: cfg.FinalPct, NonBECompanyPct: cfg.NonBECompanyPct, NonBEPartnerPct: cfg.NonBEPartnerPct,
|
|
RankQuota: map[string]float64{models.RankJunior: 30, models.RankSenior: 50, models.RankLead: 80, models.RankPartner: 120}}
|
|
stages := incentive.ComputeStages(contract.TotalAmount, contract.BEAmount, eng)
|
|
stageID := map[string]string{}
|
|
for _, st := range stages {
|
|
row := models.PaymentStage{ProjectID: proj.ID, Kind: st.Kind, Scope: st.Scope, Amount: st.Amount, Pct: st.Pct, Status: models.FixPlanned}
|
|
// mark the deposit stages as applied so dashboards show points
|
|
if st.Kind == incentive.KindDeposit {
|
|
row.Status = models.FixApplied
|
|
row.FixedDate = time.Now().Format("2006-01-02")
|
|
}
|
|
db.Create(&row)
|
|
stageID[st.Kind+"|"+st.Scope] = row.ID
|
|
}
|
|
mps := make([]incentive.MemberPortion, 0, len(pms))
|
|
for _, pm := range pms {
|
|
isP := pm.MemberEmail == "choi@special-partners.com"
|
|
mps = append(mps, incentive.MemberPortion{Email: pm.MemberEmail, Portion: pm.Portion, IsPartner: isP})
|
|
}
|
|
for _, a := range incentive.ComputeAllocs(stages, mps, eng) {
|
|
status := models.FixPlanned
|
|
if a.Kind == incentive.KindDeposit {
|
|
status = models.FixApplied
|
|
}
|
|
db.Create(&models.UserIncentive{ProjectID: proj.ID, MemberEmail: a.Email, StageID: stageID[a.Kind+"|"+a.Scope],
|
|
Kind: a.Kind, Scope: a.Scope, Year: year, Portion: a.Portion, Amount: a.Amount, Points: a.Points, FixStatus: status})
|
|
}
|
|
|
|
// accounting: a few transactions
|
|
pidPtr := proj.ID
|
|
db.Create(&models.Account{Code: "4000", Name: "컨설팅 매출", Type: "income"})
|
|
db.Create(&models.Account{Code: "5100", Name: "인건비", Type: "expense"})
|
|
db.Create(&models.Account{Code: "5200", Name: "운영비", Type: "expense"})
|
|
db.Create(&models.Transaction{Date: time.Now().Format("2006-01-02"), Kind: models.TxnIncome, Amount: 30_000_000, ProjectID: &pidPtr, Counterparty: "메디테크㈜", Memo: "계약금 입금", CreatedBy: "theorose49@gmail.com"})
|
|
db.Create(&models.Transaction{Date: time.Now().Format("2006-01-02"), Kind: models.TxnExpense, Amount: -8_000_000, Counterparty: "사무실", Memo: "임대료", CreatedBy: "theorose49@gmail.com"})
|
|
db.Create(&models.Transaction{Date: time.Now().Format("2006-01-02"), Kind: models.TxnTax, Amount: -3_000_000, Memo: "부가세 예정", CreatedBy: "theorose49@gmail.com"})
|
|
|
|
log.Printf("seed: done")
|
|
return nil
|
|
}
|
|
|
|
func seedAttendance(db *gorm.DB, email string) {
|
|
now := time.Now()
|
|
for d := 1; d <= now.Day() && d <= 20; d++ {
|
|
date := time.Date(now.Year(), now.Month(), d, 0, 0, 0, 0, time.UTC)
|
|
if wd := date.Weekday(); wd == time.Saturday || wd == time.Sunday {
|
|
continue
|
|
}
|
|
in := time.Date(now.Year(), now.Month(), d, 9, 0, 0, 0, time.UTC)
|
|
out := time.Date(now.Year(), now.Month(), d, 18, 30, 0, 0, time.UTC)
|
|
db.Create(&models.Attendance{MemberEmail: email, Date: date.Format("2006-01-02"),
|
|
ClockIn: &in, ClockOut: &out, WorkMinutes: 510, Source: "web"})
|
|
}
|
|
}
|
|
|
|
func seedTasks(db *gorm.DB, projectID string) {
|
|
base := time.Now()
|
|
tasks := []models.ProjectTask{
|
|
{Title: "사전 미팅 & 범위 확정", Lane: "done", Progress: 100, OrderIdx: 0,
|
|
Start: base.Format("2006-01-02"), End: base.AddDate(0, 0, 7).Format("2006-01-02")},
|
|
{Title: "기술문서(STED) 작성", Lane: "doing", Progress: 60, OrderIdx: 1,
|
|
Start: base.AddDate(0, 0, 7).Format("2006-01-02"), End: base.AddDate(0, 1, 0).Format("2006-01-02"), Assignee: "member@special-partners.com"},
|
|
{Title: "임상 데이터 정리", Lane: "todo", Progress: 0, OrderIdx: 2,
|
|
Start: base.AddDate(0, 1, 0).Format("2006-01-02"), End: base.AddDate(0, 2, 0).Format("2006-01-02"), Assignee: "lee@special-partners.com"},
|
|
{Title: "FDA 제출 & 대응", Lane: "todo", Progress: 0, OrderIdx: 3,
|
|
Start: base.AddDate(0, 2, 0).Format("2006-01-02"), End: base.AddDate(0, 4, 0).Format("2006-01-02"), Assignee: "park@special-partners.com"},
|
|
}
|
|
for i := range tasks {
|
|
tasks[i].ProjectID = projectID
|
|
db.Create(&tasks[i])
|
|
}
|
|
}
|