'integer', 'task_id' => 'integer', 'start_date' => 'date', 'due_date' => 'date', 'estimated_hours' => 'integer', 'is_urgent' => 'boolean', 'department_id' => 'integer', 'assignee_id' => 'integer', 'created_by' => 'integer', 'updated_by' => 'integer', 'deleted_by' => 'integer', ]; /** * 타입 상수 */ public const TYPE_BUG = 'bug'; public const TYPE_FEATURE = 'feature'; public const TYPE_IMPROVEMENT = 'improvement'; /** * 상태 상수 */ public const STATUS_OPEN = 'open'; public const STATUS_IN_PROGRESS = 'in_progress'; public const STATUS_RESOLVED = 'resolved'; public const STATUS_CLOSED = 'closed'; /** * 타입 목록 */ public static function getTypes(): array { return [ self::TYPE_BUG => '버그', self::TYPE_FEATURE => '기능', self::TYPE_IMPROVEMENT => '개선', ]; } /** * 상태 목록 */ public static function getStatuses(): array { return [ self::STATUS_OPEN => '대기중', self::STATUS_IN_PROGRESS => '처리중', self::STATUS_RESOLVED => '해결됨', self::STATUS_CLOSED => '종료', ]; } /** * 타입별 필터 */ public function scopeType($query, string $type) { return $query->where('type', $type); } /** * 상태별 필터 */ public function scopeStatus($query, string $status) { return $query->where('status', $status); } /** * 열린 이슈 (open, in_progress) */ public function scopeOpen($query) { return $query->whereIn('status', [self::STATUS_OPEN, self::STATUS_IN_PROGRESS]); } /** * 마감일 지난 이슈 (미해결) */ public function scopeOverdue($query) { return $query->whereNotNull('due_date') ->where('due_date', '<', now()->startOfDay()) ->whereIn('status', [self::STATUS_OPEN, self::STATUS_IN_PROGRESS]); } /** * 마감일 임박 (N일 이내, 미해결) */ public function scopeDueSoon($query, int $days = 3) { return $query->whereNotNull('due_date') ->whereBetween('due_date', [now()->startOfDay(), now()->addDays($days)->endOfDay()]) ->whereIn('status', [self::STATUS_OPEN, self::STATUS_IN_PROGRESS]); } /** * 주의 필요 이슈 (마감 초과 + 마감 임박 + 긴급) */ public function scopeNeedsAttention($query) { return $query->whereIn('status', [self::STATUS_OPEN, self::STATUS_IN_PROGRESS]) ->where(function ($q) { $q->where('is_urgent', true) ->orWhere(function ($q2) { $q2->whereNotNull('due_date') ->where('due_date', '<=', now()->addDays(3)->endOfDay()); }); }); } /** * D-day 계산 */ public function getDdayAttribute(): ?int { if (! $this->due_date) { return null; } return now()->startOfDay()->diffInDays($this->due_date, false); } /** * 마감 상태 (overdue, due_soon, normal, null) */ public function getDueStatusAttribute(): ?string { if (! $this->due_date || in_array($this->status, [self::STATUS_RESOLVED, self::STATUS_CLOSED])) { return null; } $dday = $this->dday; if ($dday < 0) { return 'overdue'; } if ($dday <= 3) { return 'due_soon'; } return 'normal'; } /** * 관계: 프로젝트 */ public function project(): BelongsTo { return $this->belongsTo(AdminPmProject::class, 'project_id'); } /** * 관계: 연결된 작업 */ public function task(): BelongsTo { return $this->belongsTo(AdminPmTask::class, 'task_id'); } /** * 관계: 생성자 */ public function creator(): BelongsTo { return $this->belongsTo(User::class, 'created_by'); } /** * 관계: 수정자 */ public function updater(): BelongsTo { return $this->belongsTo(User::class, 'updated_by'); } /** * 관계: 부서 (연동 시) */ public function department(): BelongsTo { return $this->belongsTo(Department::class, 'department_id'); } /** * 관계: 담당자 (연동 시) */ public function assignee(): BelongsTo { return $this->belongsTo(User::class, 'assignee_id'); } /** * 팀/부서 표시명 (FK 우선, 없으면 문자열) */ public function getTeamDisplayAttribute(): ?string { if ($this->department_id && $this->department) { return $this->department->name; } return $this->team; } /** * 담당자 표시명 (FK 우선, 없으면 문자열) */ public function getAssigneeDisplayAttribute(): ?string { if ($this->assignee_id && $this->assignee) { return $this->assignee->name; } return $this->assignee_name; } /** * 타입 아이콘 */ public function getTypeIconAttribute(): string { return match ($this->type) { self::TYPE_BUG => 'bug', self::TYPE_FEATURE => 'sparkles', self::TYPE_IMPROVEMENT => 'arrow-trending-up', default => 'question-mark-circle', }; } /** * 상태 색상 */ public function getStatusColorAttribute(): string { return match ($this->status) { self::STATUS_OPEN => 'red', self::STATUS_IN_PROGRESS => 'yellow', self::STATUS_RESOLVED => 'green', self::STATUS_CLOSED => 'gray', default => 'gray', }; } }