Files
sam-api/app/Models/Tenants/TodayIssue.php
권혁성 d75f6f5bd1 feat(API): 입금/출금 알림 Observer 추가 및 LoanController 수정
- DepositIssueObserver, WithdrawalIssueObserver 신규 추가
- TodayIssueObserverService에 입금/출금 핸들러 및 디버그 로그 추가
- TodayIssue 모델에 입금/출금 상수 추가
- AppServiceProvider에 Observer 등록
- ApprovalService에 기존 결재선 사용 시 수동 알림 트리거 추가
- LoanController ApiResponse::handle() → ApiResponse::success() 수정

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-23 10:05:50 +09:00

221 lines
5.6 KiB
PHP

<?php
namespace App\Models\Tenants;
use App\Models\Users\User;
use App\Traits\BelongsToTenant;
use App\Traits\ModelTrait;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
* 오늘의 이슈 모델
*
* CEO 대시보드용 실시간 이슈 저장
*/
class TodayIssue extends Model
{
use BelongsToTenant, HasFactory, ModelTrait;
protected $table = 'today_issues';
protected $fillable = [
'tenant_id',
'source_type',
'source_id',
'badge',
'notification_type',
'content',
'path',
'needs_approval',
'is_read',
'read_by',
'read_at',
'expires_at',
];
protected $casts = [
'needs_approval' => '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',
];
// 중요 알림 (푸시 알림음) - 수주등록, 신규거래처, 결재요청, 입금, 출금
public const IMPORTANT_NOTIFICATIONS = [
'sales_order',
'new_vendor',
'approval_request',
'deposit',
'withdrawal',
];
/**
* 확인한 사용자
*/
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 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;
}
}