// 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)", ScopeText: "기술문서(STED)·라벨링·사용적합성 보고서 작성 및 검토", ScopeGraphic: "기기 도면·UI 목업·포장 그래픽 제작", 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]) } }