theorose49 751aa8ed97
All checks were successful
build-and-push / build (push) Successful in 32s
feat(mail): 프로젝트 고객사 도메인 메일 연동(Google Workspace 도메인위임) + 공동 메모
- Project.ClientDomain 필드, MailNote(프로젝트 구성원 공동 메모) 모델
- internal/mailsync: 서비스계정+도메인위임으로 팀 메일함을 도메인 검색·집계(stdlib만, push 패턴 재사용)
  · GOOGLE_SA_CREDENTIALS_FILE 미설정 시 비활성(graceful)
- GET /projects/{id}/mails (3분 캐시), GET/PUT /projects/{id}/mail-notes
- fix: handlePatchProject map-key Updates가 camelCase 멀티워드 필드(consultingType·
  scopeText·pmEmail·clientDomain·날짜)를 컬럼에 못 맞춰 저장 실패하던 버그 → snakeKeys 변환

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 10:41:26 +09:00

189 lines
7.6 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 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 }
// 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"` // 제출 국가
// 계약 범위: 글/그림 각각 자유 입력(무엇을 포함하는지 텍스트로 기술).
ScopeText string `json:"scopeText"` // 글 계약 범위
ScopeGraphic string `json:"scopeGraphic"` // 그림 계약 범위
PMEmail string `json:"pmEmail"` // 프로젝트 PM
ClientDomain string `json:"clientDomain"` // 고객사 메일 도메인(postfix) — 관련 메일 집계용
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 (기여도, 0100) 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"`
Description string `json:"description"` // 상세 설명 (JIRA형 카드 본문)
Lane string `json:"lane"` // todo | doing | review | done
Priority string `json:"priority"` // low | medium | high | urgent
Labels datatypes.JSONSlice[string] `json:"labels"` // 라벨/태그
Start string `json:"start"` // YYYY-MM-DD
End string `json:"end"`
Assignee string `json:"assignee"`
OrderIdx int `json:"orderIdx"`
Progress int `json:"progress"` // 0100
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 }
// TaskComment is a JIRA-style comment thread on a task (작성자·본문·시각).
type TaskComment struct {
Base
TaskID string `gorm:"index" json:"taskId"`
AuthorEmail string `json:"authorEmail"`
Body string `json:"body"`
CreatedAt time.Time `json:"createdAt"`
}
func (m *TaskComment) BeforeCreate(*gorm.DB) error { m.ensureID(); return nil }
// MailNote is a project's SHARED memo on one client-domain email — any project
// member can edit it (협업 메모). Keyed by (project, RFC822 Message-ID).
type MailNote struct {
Base
ProjectID string `gorm:"index:idx_mailnote_pm,unique" json:"projectId"`
MessageID string `gorm:"index:idx_mailnote_pm,unique" json:"messageId"`
Body string `json:"body"`
LastEditedBy string `json:"lastEditedBy"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
func (m *MailNote) 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 }