'boolean', 'is_read' => 'boolean', 'read_at' => 'datetime', 'expires_at' => 'datetime', ]; // 소스 타입 상수 public const SOURCE_ORDER = 'order'; public const SOURCE_BAD_DEBT = 'bad_debt'; public const SOURCE_STOCK = 'stock'; public const SOURCE_EXPENSE = 'expense'; public const SOURCE_TAX = 'tax'; public const SOURCE_APPROVAL = 'approval'; public const SOURCE_CLIENT = 'client'; public const SOURCE_DEPOSIT = 'deposit'; public const SOURCE_WITHDRAWAL = 'withdrawal'; // 뱃지 타입 상수 public const BADGE_ORDER_REGISTER = '수주등록'; public const BADGE_COLLECTION_ISSUE = '추심이슈'; public const BADGE_SAFETY_STOCK = '안전재고'; public const BADGE_EXPENSE_PENDING = '지출 승인대기'; public const BADGE_TAX_REPORT = '세금 신고'; public const BADGE_APPROVAL_REQUEST = '결재 요청'; public const BADGE_NEW_CLIENT = '신규거래처'; public const BADGE_DEPOSIT = '입금'; public const BADGE_WITHDRAWAL = '출금'; // 뱃지 → notification_type 매핑 public const BADGE_TO_NOTIFICATION_TYPE = [ self::BADGE_ORDER_REGISTER => 'sales_order', self::BADGE_NEW_CLIENT => 'new_vendor', self::BADGE_APPROVAL_REQUEST => 'approval_request', self::BADGE_COLLECTION_ISSUE => 'bad_debt', self::BADGE_SAFETY_STOCK => 'safety_stock', self::BADGE_EXPENSE_PENDING => 'expected_expense', self::BADGE_TAX_REPORT => 'vat_report', self::BADGE_DEPOSIT => 'deposit', self::BADGE_WITHDRAWAL => 'withdrawal', ]; // 중요 알림 (긴급 푸시) - 수주등록, 신규거래처, 결재요청 // 입금, 출금, 카드 등은 일반 푸시(push_default)로 발송 public const IMPORTANT_NOTIFICATIONS = [ 'sales_order', 'new_vendor', 'approval_request', ]; /** * 확인한 사용자 */ public function reader(): BelongsTo { return $this->belongsTo(User::class, 'read_by'); } /** * 읽지 않은 이슈 스코프 */ public function scopeUnread($query) { return $query->where('is_read', false); } /** * 만료되지 않은 이슈 스코프 */ public function scopeActive($query) { return $query->where(function ($q) { $q->whereNull('expires_at') ->orWhere('expires_at', '>', now()); }); } /** * 오늘 날짜 이슈 스코프 */ public function scopeToday($query) { return $query->whereDate('created_at', today()); } /** * 뱃지별 필터 스코프 */ public function scopeByBadge($query, string $badge) { return $query->where('badge', $badge); } /** * 소스별 필터 스코프 */ public function scopeBySource($query, string $sourceType, ?int $sourceId = null) { $query->where('source_type', $sourceType); if ($sourceId !== null) { $query->where('source_id', $sourceId); } return $query; } /** * 이슈 확인 처리 */ public function markAsRead(int $userId): bool { return $this->update([ 'is_read' => true, 'read_by' => $userId, 'read_at' => now(), ]); } /** * 이슈 생성 헬퍼 (정적 메서드) */ public static function createIssue( int $tenantId, string $sourceType, ?int $sourceId, string $badge, string $content, ?string $path = null, bool $needsApproval = false, ?\DateTime $expiresAt = null ): self { // badge에서 notification_type 자동 매핑 $notificationType = self::BADGE_TO_NOTIFICATION_TYPE[$badge] ?? null; return self::updateOrCreate( [ 'tenant_id' => $tenantId, 'source_type' => $sourceType, 'source_id' => $sourceId, ], [ 'badge' => $badge, 'notification_type' => $notificationType, 'content' => $content, 'path' => $path, 'needs_approval' => $needsApproval, 'expires_at' => $expiresAt, 'is_read' => false, 'read_by' => null, 'read_at' => null, ] ); } /** * 중요 알림 여부 확인 */ public function isImportantNotification(): bool { return in_array($this->notification_type, self::IMPORTANT_NOTIFICATIONS, true); } /** * 소스 기준 이슈 삭제 헬퍼 (정적 메서드) */ public static function removeBySource(int $tenantId, string $sourceType, int $sourceId): bool { return self::where('tenant_id', $tenantId) ->where('source_type', $sourceType) ->where('source_id', $sourceId) ->delete() > 0; } }