- 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:
co-authored by
Claude Opus 4.8
parent
ce4023ca59
commit
f83724b995
@@ -0,0 +1,63 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Transaction kinds for the ledger. 인센티브 지급 links the accounting side to the
|
||||
// incentive engine so the "가상 포인트 vs 실제 현금" gap can be reconciled.
|
||||
const (
|
||||
TxnIncome = "income" // 수입 (계약 입금 등)
|
||||
TxnExpense = "expense" // 비용
|
||||
TxnTax = "tax" // 세금
|
||||
TxnPayroll = "payroll" // 급여
|
||||
TxnIncentive = "incentive" // 인센티브 지급
|
||||
)
|
||||
|
||||
// Account is a chart-of-accounts entry (계정과목).
|
||||
type Account struct {
|
||||
Base
|
||||
Code string `json:"code"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"` // income | expense | tax | asset | liability
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
}
|
||||
|
||||
func (m *Account) BeforeCreate(*gorm.DB) error { m.ensureID(); return nil }
|
||||
|
||||
// Transaction is a single ledger line. Optional ProjectID / MemberEmail tie it to
|
||||
// project profitability and per-member incentive payouts.
|
||||
type Transaction struct {
|
||||
Base
|
||||
Date string `gorm:"index" json:"date"` // YYYY-MM-DD
|
||||
Kind string `gorm:"index" json:"kind"`
|
||||
AccountID *string `json:"accountId"`
|
||||
Amount float64 `json:"amount"` // signed (income +, expense/tax/payroll −)
|
||||
ProjectID *string `gorm:"index" json:"projectId"`
|
||||
MemberEmail *string `json:"memberEmail"`
|
||||
Counterparty string `json:"counterparty"`
|
||||
Memo string `json:"memo"`
|
||||
CreatedBy string `json:"createdBy"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
func (m *Transaction) BeforeCreate(*gorm.DB) error { m.ensureID(); return nil }
|
||||
|
||||
// TaxRecord captures periodic tax obligations (부가세/원천세 등) for the dashboard.
|
||||
type TaxRecord struct {
|
||||
Base
|
||||
Period string `json:"period"` // YYYY-MM or YYYY-Q
|
||||
Type string `json:"type"`
|
||||
Base_ float64 `gorm:"column:base" json:"base"`
|
||||
Amount float64 `json:"amount"`
|
||||
DueDate string `json:"dueDate"`
|
||||
Paid bool `json:"paid"`
|
||||
Memo string `json:"memo"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
func (m *TaxRecord) BeforeCreate(*gorm.DB) error { m.ensureID(); return nil }
|
||||
@@ -0,0 +1,109 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Request status shared by leave/overtime approval workflows.
|
||||
const (
|
||||
StatusPending = "pending"
|
||||
StatusApproved = "approved"
|
||||
StatusRejected = "rejected"
|
||||
StatusCanceled = "canceled"
|
||||
)
|
||||
|
||||
// Leave types (한국 근로기준법 기준). 연차 draws down the annual balance; the rest
|
||||
// are 인정/무급 categories that still count toward worked time where applicable.
|
||||
const (
|
||||
LeaveAnnual = "annual" // 연차
|
||||
LeaveHalfAM = "half_am" // 오전 반차
|
||||
LeaveHalfPM = "half_pm" // 오후 반차
|
||||
LeavePublic = "public" // 공가
|
||||
LeaveSick = "sick" // 병가
|
||||
LeaveFamily = "family" // 경조사
|
||||
LeaveUnpaid = "unpaid" // 무급
|
||||
)
|
||||
|
||||
// Attendance is one day's clock record for a member (date is YYYY-MM-DD in KST).
|
||||
type Attendance struct {
|
||||
Base
|
||||
MemberEmail string `gorm:"index" json:"memberEmail"`
|
||||
Date string `gorm:"index" json:"date"` // YYYY-MM-DD
|
||||
ClockIn *time.Time `json:"clockIn"`
|
||||
ClockOut *time.Time `json:"clockOut"`
|
||||
WorkMinutes int `json:"workMinutes"` // computed net worked minutes
|
||||
Source string `json:"source"` // web | admin | auto
|
||||
Note string `json:"note"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
func (m *Attendance) BeforeCreate(*gorm.DB) error { m.ensureID(); return nil }
|
||||
|
||||
// LeaveRequest is a member-submitted leave application. Members may only create;
|
||||
// admins approve/reject. Days is fractional to support 반차 (0.5).
|
||||
type LeaveRequest struct {
|
||||
Base
|
||||
MemberEmail string `gorm:"index" json:"memberEmail"`
|
||||
Type string `json:"type"`
|
||||
StartDate string `json:"startDate"` // YYYY-MM-DD
|
||||
EndDate string `json:"endDate"`
|
||||
Days float64 `json:"days"`
|
||||
Reason string `json:"reason"`
|
||||
Status string `gorm:"index" json:"status"`
|
||||
Approver string `json:"approver"`
|
||||
DecidedAt *time.Time `json:"decidedAt"`
|
||||
DecisionMemo string `json:"decisionMemo"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
func (m *LeaveRequest) BeforeCreate(*gorm.DB) error { m.ensureID(); return nil }
|
||||
|
||||
// OvertimeRequest is a member-submitted overtime application (관리자만 확인/승인).
|
||||
type OvertimeRequest struct {
|
||||
Base
|
||||
MemberEmail string `gorm:"index" json:"memberEmail"`
|
||||
Date string `json:"date"`
|
||||
Minutes int `json:"minutes"`
|
||||
Reason string `json:"reason"`
|
||||
Status string `gorm:"index" json:"status"`
|
||||
Approver string `json:"approver"`
|
||||
DecidedAt *time.Time `json:"decidedAt"`
|
||||
DecisionMemo string `json:"decisionMemo"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
func (m *OvertimeRequest) BeforeCreate(*gorm.DB) error { m.ensureID(); return nil }
|
||||
|
||||
// WorkPolicy is the company-wide work-hours policy used by the monthly timesheet
|
||||
// roll-up. Singleton-ish (one active row); admin editable.
|
||||
type WorkPolicy struct {
|
||||
Base
|
||||
Name string `json:"name"`
|
||||
WeeklyHours float64 `json:"weeklyHours"` // 주 소정근로시간 (기본 40)
|
||||
DailyStandardMin int `json:"dailyStandardMin"` // 일 소정근로분 (기본 480)
|
||||
CoreStart string `json:"coreStart"` // "09:00"
|
||||
CoreEnd string `json:"coreEnd"` // "18:00"
|
||||
LunchMinutes int `json:"lunchMinutes"` // 휴게(기본 60)
|
||||
AnnualLeaveBase float64 `json:"annualLeaveBase"` // 1년 미만/이상 기준 부여일
|
||||
Active bool `json:"active"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
func (m *WorkPolicy) BeforeCreate(*gorm.DB) error { m.ensureID(); return nil }
|
||||
|
||||
// LeaveBalance tracks per-member annual leave usage for a given year.
|
||||
type LeaveBalance struct {
|
||||
Base
|
||||
MemberEmail string `gorm:"index" json:"memberEmail"`
|
||||
Year int `gorm:"index" json:"year"`
|
||||
Granted float64 `json:"granted"`
|
||||
Used float64 `json:"used"`
|
||||
}
|
||||
|
||||
func (m *LeaveBalance) BeforeCreate(*gorm.DB) error { m.ensureID(); return nil }
|
||||
@@ -0,0 +1,112 @@
|
||||
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 }
|
||||
@@ -0,0 +1,70 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Rank is the career grade that drives incentive point quotas.
|
||||
// 주임(junior) · 선임(senior) · 책임(lead) · 파트너(partner)
|
||||
// Stored as the Korean label so it round-trips to the UI directly.
|
||||
const (
|
||||
RankJunior = "주임"
|
||||
RankSenior = "선임"
|
||||
RankLead = "책임"
|
||||
RankPartner = "파트너"
|
||||
)
|
||||
|
||||
// Member roles within spin. Account lifecycle (create/disable) is Keycloak's
|
||||
// job; spin only distinguishes admin from regular member for authorization.
|
||||
const (
|
||||
RoleAdmin = "admin"
|
||||
RoleMember = "user"
|
||||
)
|
||||
|
||||
// Member is a company employee who uses spin, matched to the logged-in Keycloak
|
||||
// identity by email (case-insensitive). It carries the org/HR profile spin needs
|
||||
// (rank, department, partner flag) that Keycloak does not hold.
|
||||
type Member struct {
|
||||
Base
|
||||
Email string `gorm:"index" json:"email"`
|
||||
DisplayName string `json:"displayName"`
|
||||
Rank string `json:"rank"` // 주임/선임/책임/파트너
|
||||
DepartmentID *string `json:"departmentId"`
|
||||
Role string `json:"role"` // admin | user
|
||||
IsPartner bool `json:"isPartner"` // shares non-BE profit pool
|
||||
Phone string `json:"phone"`
|
||||
Position string `json:"position"` // free-text job title
|
||||
Status string `json:"status"` // active | inactive
|
||||
JoinDate *time.Time `json:"joinDate"`
|
||||
AnnualLeave float64 `json:"annualLeave"` // granted 연차 days for the year
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
func (m *Member) BeforeCreate(*gorm.DB) error { m.ensureID(); return nil }
|
||||
|
||||
// Department is an org unit. Lightweight; the lead is a Member email.
|
||||
type Department struct {
|
||||
Base
|
||||
Name string `json:"name"`
|
||||
LeadEmail string `json:"leadEmail"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
}
|
||||
|
||||
func (m *Department) BeforeCreate(*gorm.DB) error { m.ensureID(); return nil }
|
||||
|
||||
// AuditLog records sensitive actions (approvals, incentive fixes, contract edits)
|
||||
// for the admin trail. Entity/EntityID point at the affected row.
|
||||
type AuditLog struct {
|
||||
Base
|
||||
Actor string `gorm:"index" json:"actor"`
|
||||
Action string `json:"action"`
|
||||
Entity string `gorm:"index" json:"entity"`
|
||||
EntityID string `gorm:"index" json:"entityId"`
|
||||
Detail string `json:"detail"`
|
||||
CreatedAt time.Time `gorm:"index" json:"createdAt"`
|
||||
}
|
||||
|
||||
func (m *AuditLog) BeforeCreate(*gorm.DB) error { m.ensureID(); return nil }
|
||||
@@ -0,0 +1,39 @@
|
||||
// Package models defines the GORM models for spin. Each domain slice adds its
|
||||
// own file (member.go, attendance.go, project.go, incentive.go, accounting.go);
|
||||
// this file holds the shared Base type and the AutoMigrate registry.
|
||||
package models
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Base provides a string UUID primary key populated in a BeforeCreate hook.
|
||||
type Base struct {
|
||||
ID string `gorm:"primaryKey" json:"id"`
|
||||
}
|
||||
|
||||
func (b *Base) ensureID() {
|
||||
if b.ID == "" {
|
||||
b.ID = uuid.NewString()
|
||||
}
|
||||
}
|
||||
|
||||
// All returns every model for AutoMigrate. Each slice appends its models here.
|
||||
func All() []interface{} {
|
||||
return []interface{}{
|
||||
// slice 1 — members / org
|
||||
&Member{}, &Department{}, &AuditLog{},
|
||||
// slice 2 — attendance / leave
|
||||
&Attendance{}, &LeaveRequest{}, &OvertimeRequest{}, &WorkPolicy{}, &LeaveBalance{},
|
||||
// slice 3 — projects
|
||||
&Company{}, &Product{}, &Version{}, &Project{}, &ProjectMember{},
|
||||
&ClientContact{}, &ProjectTask{}, &Contract{}, &ContractFile{}, &PaymentSplit{},
|
||||
// slice 4 — incentive
|
||||
&IncentiveConfig{}, &PaymentStage{}, &UserIncentive{}, &QuarterlySettlement{},
|
||||
// slice 5 — accounting
|
||||
&Account{}, &Transaction{}, &TaxRecord{},
|
||||
}
|
||||
}
|
||||
|
||||
var _ = gorm.ErrRecordNotFound // keep gorm imported for slice files
|
||||
@@ -0,0 +1,164 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/datatypes"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Company → Product → Version is the consulting hierarchy. A Project is created
|
||||
// per (company, product, version) engagement and is otherwise independent.
|
||||
type Company struct {
|
||||
Base
|
||||
Name string `json:"name"`
|
||||
Code string `json:"code"`
|
||||
Note string `json:"note"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
}
|
||||
|
||||
func (m *Company) BeforeCreate(*gorm.DB) error { m.ensureID(); return nil }
|
||||
|
||||
type Product struct {
|
||||
Base
|
||||
CompanyID string `gorm:"index" json:"companyId"`
|
||||
Name string `json:"name"`
|
||||
Code string `json:"code"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
}
|
||||
|
||||
func (m *Product) BeforeCreate(*gorm.DB) error { m.ensureID(); return nil }
|
||||
|
||||
type Version struct {
|
||||
Base
|
||||
ProductID string `gorm:"index" json:"productId"`
|
||||
Label string `json:"label"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
}
|
||||
|
||||
func (m *Version) BeforeCreate(*gorm.DB) error { m.ensureID(); return nil }
|
||||
|
||||
// Consulting metadata enums (stored as free strings for flexibility).
|
||||
const (
|
||||
ScopeText = "text" // 글
|
||||
ScopeGraphic = "graphic" // 그림
|
||||
ScopeBoth = "both" // 글+그림
|
||||
)
|
||||
|
||||
// Project is the core engagement record. The bracketed [admin-only] fields in the
|
||||
// spec live on Contract / PaymentSplit, not here, so Project is safe to expose to
|
||||
// any member who belongs to it.
|
||||
type Project struct {
|
||||
Base
|
||||
Name string `json:"name"`
|
||||
CompanyID string `gorm:"index" json:"companyId"`
|
||||
ProductID string `json:"productId"`
|
||||
VersionID string `json:"versionId"`
|
||||
CompanyName string `json:"companyName"` // denormalized 업체명 snapshot
|
||||
ProductName string `json:"productName"`
|
||||
VersionName string `json:"versionName"`
|
||||
ConsultingType string `json:"consultingType"` // 컨설팅 종류
|
||||
Country string `json:"country"` // 제출 국가
|
||||
Scope string `json:"scope"` // text | graphic | both
|
||||
PMEmail string `json:"pmEmail"` // 프로젝트 PM
|
||||
Cautions string `json:"cautions"` // 주의사항 (구성원 공개)
|
||||
Status string `json:"status"` // planned | active | hold | done | dropped
|
||||
StartDate string `json:"startDate"`
|
||||
DueDate string `json:"dueDate"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
func (m *Project) BeforeCreate(*gorm.DB) error { m.ensureID(); return nil }
|
||||
|
||||
// ProjectMember links a Member to a Project with a portion (기여도, 0–100) that
|
||||
// drives incentive distribution. Role is a free label (작업자/리뷰어/...).
|
||||
type ProjectMember struct {
|
||||
Base
|
||||
ProjectID string `gorm:"index" json:"projectId"`
|
||||
MemberEmail string `gorm:"index" json:"memberEmail"`
|
||||
Portion float64 `json:"portion"` // 기여도 percent
|
||||
Role string `json:"role"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
}
|
||||
|
||||
func (m *ProjectMember) BeforeCreate(*gorm.DB) error { m.ensureID(); return nil }
|
||||
|
||||
// ClientContact is a 업체 담당자 (직무/이름/연락처).
|
||||
type ClientContact struct {
|
||||
Base
|
||||
ProjectID string `gorm:"index" json:"projectId"`
|
||||
Name string `json:"name"`
|
||||
Title string `json:"title"` // 직무
|
||||
Phone string `json:"phone"`
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
func (m *ClientContact) BeforeCreate(*gorm.DB) error { m.ensureID(); return nil }
|
||||
|
||||
// ProjectTask feeds both the Gantt and Kanban views and matches the real
|
||||
// calendar via Start/End (YYYY-MM-DD). Lane is the Kanban column.
|
||||
type ProjectTask struct {
|
||||
Base
|
||||
ProjectID string `gorm:"index" json:"projectId"`
|
||||
Title string `json:"title"`
|
||||
Lane string `json:"lane"` // todo | doing | review | done
|
||||
Start string `json:"start"` // YYYY-MM-DD
|
||||
End string `json:"end"`
|
||||
Assignee string `json:"assignee"`
|
||||
OrderIdx int `json:"orderIdx"`
|
||||
Progress int `json:"progress"` // 0–100
|
||||
DependsOn datatypes.JSONSlice[string] `json:"dependsOn"`
|
||||
Color string `json:"color"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
func (m *ProjectTask) BeforeCreate(*gorm.DB) error { m.ensureID(); return nil }
|
||||
|
||||
// Contract holds the [admin-only] commercial terms of a project. BE is the
|
||||
// break-even floor (손해가 안 나는 최소 금액). Exposed ONLY to admins.
|
||||
type Contract struct {
|
||||
Base
|
||||
ProjectID string `gorm:"uniqueIndex" json:"projectId"`
|
||||
TotalAmount float64 `json:"totalAmount"` // 계약 금액
|
||||
BEAmount float64 `json:"beAmount"` // BE (break-even 최소 금액)
|
||||
AdminCaution string `json:"adminCaution"` // 관리자 주의사항
|
||||
Memo string `json:"memo"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
func (m *Contract) BeforeCreate(*gorm.DB) error { m.ensureID(); return nil }
|
||||
|
||||
// ContractFile is an [admin-only] attachment (계약서/기타 자료, 여러개 가능) in S3.
|
||||
type ContractFile struct {
|
||||
Base
|
||||
ProjectID string `gorm:"index" json:"projectId"`
|
||||
Kind string `json:"kind"` // contract | other
|
||||
Filename string `json:"filename"`
|
||||
S3Key string `json:"s3Key"`
|
||||
Size int64 `json:"size"`
|
||||
UploadedBy string `json:"uploadedBy"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
}
|
||||
|
||||
func (m *ContractFile) BeforeCreate(*gorm.DB) error { m.ensureID(); return nil }
|
||||
|
||||
// PaymentSplit is one [admin-only] custom installment of the contract amount.
|
||||
// Amounts arrive in arbitrary splits across dates, so every field is editable.
|
||||
type PaymentSplit struct {
|
||||
Base
|
||||
ProjectID string `gorm:"index" json:"projectId"`
|
||||
Label string `json:"label"`
|
||||
Amount float64 `json:"amount"`
|
||||
ExpectedDate string `json:"expectedDate"` // 예상 일정
|
||||
PaidDate string `json:"paidDate"` // 실제 입금일 (빈값=미입금)
|
||||
Paid bool `json:"paid"`
|
||||
Memo string `json:"memo"`
|
||||
OrderIdx int `json:"orderIdx"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
func (m *PaymentSplit) BeforeCreate(*gorm.DB) error { m.ensureID(); return nil }
|
||||
Reference in New Issue
Block a user