theorose49 df09d23662
All checks were successful
build-and-push / build (push) Successful in 35s
feat: 인턴 직급 + 초과근무 관리자 집계화 + SSO 로그아웃 URL + 디바이스/FCM
- 직급에 인턴 추가(기본 할당량 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>
2026-06-28 10:55:53 +09:00

116 lines
4.4 KiB
Go

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 (
RankIntern = "인턴"
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
AvatarKey string `json:"avatarKey"` // S3 key of profile photo (empty = none)
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
// HasAvatar reports whether a profile photo is set.
func (m *Member) HasAvatar() bool { return m.AvatarKey != "" }
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 }
// Notification is a per-member inbox event (프로젝트 추가·휴가 승인·인센티브 반영 등).
type Notification struct {
Base
Recipient string `gorm:"index" json:"recipient"` // member email
Type string `json:"type"` // project | leave | overtime | incentive | settlement
Title string `json:"title"`
Body string `json:"body"`
Link string `json:"link"` // in-app path, e.g. /projects/{id}
Read bool `gorm:"index" json:"read"`
CreatedAt time.Time `gorm:"index" json:"createdAt"`
}
func (m *Notification) BeforeCreate(*gorm.DB) error { m.ensureID(); return nil }
// WorkStatusEvent logs a member's presence change (출근/퇴근/휴식/미팅/이동) for the
// admin work-management timeline. 출근/퇴근 additionally update the Attendance row.
type WorkStatusEvent struct {
Base
MemberEmail string `gorm:"index" json:"memberEmail"`
Date string `gorm:"index" json:"date"` // YYYY-MM-DD (KST)
Status string `json:"status"` // in|out|break|meeting|move
At time.Time `json:"at"`
Note string `json:"note"`
}
func (m *WorkStatusEvent) BeforeCreate(*gorm.DB) error { m.ensureID(); return nil }
// Device is a registered push target (spin-mobile Flutter 앱 FCM 토큰). 한 유저가
// 여러 기기를 가질 수 있고, 토큰은 고유.
type Device struct {
Base
MemberEmail string `gorm:"index" json:"memberEmail"`
Token string `gorm:"uniqueIndex" json:"token"`
Platform string `json:"platform"` // android | ios
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
func (m *Device) BeforeCreate(*gorm.DB) error { m.ensureID(); return nil }