feat: Phase 5 API 개발 완료 (사용자 초대, 알림설정, 계정관리, 거래명세서)

5.1 사용자 초대 기능:
- UserInvitation 마이그레이션, 모델, 서비스, 컨트롤러, Swagger
- 초대 발송/수락/취소/재발송 API

5.2 알림설정 확장:
- NotificationSetting 마이그레이션, 모델, 서비스, 컨트롤러, Swagger
- 채널별/유형별 알림 설정 관리

5.3 계정정보 수정 API:
- 회원탈퇴, 사용중지, 약관동의 관리
- AccountService, AccountController, Swagger

5.4 매출 거래명세서 API:
- 거래명세서 조회/발행/이메일발송
- SaleService 확장, Swagger 문서화
This commit is contained in:
2025-12-19 14:52:53 +09:00
parent c7b25710a0
commit 3020026abf
31 changed files with 2735 additions and 8 deletions

View File

@@ -0,0 +1,226 @@
<?php
namespace App\Models;
use App\Traits\BelongsToTenant;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class NotificationSetting extends Model
{
use BelongsToTenant;
protected $fillable = [
'tenant_id',
'user_id',
'notification_type',
'push_enabled',
'email_enabled',
'sms_enabled',
'in_app_enabled',
'kakao_enabled',
'settings',
];
protected $casts = [
'push_enabled' => 'boolean',
'email_enabled' => 'boolean',
'sms_enabled' => 'boolean',
'in_app_enabled' => 'boolean',
'kakao_enabled' => 'boolean',
'settings' => 'array',
];
/**
* 알림 유형 상수
*/
public const TYPE_APPROVAL = 'approval'; // 결재
public const TYPE_ORDER = 'order'; // 수주
public const TYPE_DEPOSIT = 'deposit'; // 입금
public const TYPE_WITHDRAWAL = 'withdrawal'; // 출금
public const TYPE_NOTICE = 'notice'; // 공지사항
public const TYPE_SYSTEM = 'system'; // 시스템
public const TYPE_MARKETING = 'marketing'; // 마케팅
public const TYPE_SECURITY = 'security'; // 보안 (로그인, 비밀번호 변경 등)
/**
* 알림 채널 상수
*/
public const CHANNEL_PUSH = 'push';
public const CHANNEL_EMAIL = 'email';
public const CHANNEL_SMS = 'sms';
public const CHANNEL_IN_APP = 'in_app';
public const CHANNEL_KAKAO = 'kakao';
/**
* 모든 알림 유형 반환
*/
public static function getAllTypes(): array
{
return [
self::TYPE_APPROVAL,
self::TYPE_ORDER,
self::TYPE_DEPOSIT,
self::TYPE_WITHDRAWAL,
self::TYPE_NOTICE,
self::TYPE_SYSTEM,
self::TYPE_MARKETING,
self::TYPE_SECURITY,
];
}
/**
* 모든 채널 반환
*/
public static function getAllChannels(): array
{
return [
self::CHANNEL_PUSH,
self::CHANNEL_EMAIL,
self::CHANNEL_SMS,
self::CHANNEL_IN_APP,
self::CHANNEL_KAKAO,
];
}
/**
* 알림 유형별 기본 설정 반환
*/
public static function getDefaultSettings(string $type): array
{
// 보안 관련 알림은 이메일 기본 활성화
if ($type === self::TYPE_SECURITY) {
return [
'push_enabled' => true,
'email_enabled' => true,
'sms_enabled' => false,
'in_app_enabled' => true,
'kakao_enabled' => false,
];
}
// 마케팅은 기본 비활성화
if ($type === self::TYPE_MARKETING) {
return [
'push_enabled' => false,
'email_enabled' => false,
'sms_enabled' => false,
'in_app_enabled' => false,
'kakao_enabled' => false,
];
}
// 기타 알림은 푸시, 인앱만 기본 활성화
return [
'push_enabled' => true,
'email_enabled' => false,
'sms_enabled' => false,
'in_app_enabled' => true,
'kakao_enabled' => false,
];
}
/**
* 알림 유형 레이블
*/
public static function getTypeLabels(): array
{
return [
self::TYPE_APPROVAL => '전자결재',
self::TYPE_ORDER => '수주',
self::TYPE_DEPOSIT => '입금',
self::TYPE_WITHDRAWAL => '출금',
self::TYPE_NOTICE => '공지사항',
self::TYPE_SYSTEM => '시스템',
self::TYPE_MARKETING => '마케팅',
self::TYPE_SECURITY => '보안',
];
}
/**
* 채널 레이블
*/
public static function getChannelLabels(): array
{
return [
self::CHANNEL_PUSH => '푸시 알림',
self::CHANNEL_EMAIL => '이메일',
self::CHANNEL_SMS => 'SMS',
self::CHANNEL_IN_APP => '인앱 알림',
self::CHANNEL_KAKAO => '카카오 알림톡',
];
}
/**
* 사용자 관계
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
/**
* 테넌트 관계
*/
public function tenant(): BelongsTo
{
return $this->belongsTo(Tenant::class);
}
/**
* Scope: 특정 사용자의 설정
*/
public function scopeForUser($query, int $userId)
{
return $query->where('user_id', $userId);
}
/**
* Scope: 특정 알림 유형
*/
public function scopeOfType($query, string $type)
{
return $query->where('notification_type', $type);
}
/**
* 특정 채널이 활성화되어 있는지 확인
*/
public function isChannelEnabled(string $channel): bool
{
return match ($channel) {
self::CHANNEL_PUSH => $this->push_enabled,
self::CHANNEL_EMAIL => $this->email_enabled,
self::CHANNEL_SMS => $this->sms_enabled,
self::CHANNEL_IN_APP => $this->in_app_enabled,
self::CHANNEL_KAKAO => $this->kakao_enabled,
default => false,
};
}
/**
* 채널 설정 업데이트
*/
public function setChannelEnabled(string $channel, bool $enabled): void
{
match ($channel) {
self::CHANNEL_PUSH => $this->push_enabled = $enabled,
self::CHANNEL_EMAIL => $this->email_enabled = $enabled,
self::CHANNEL_SMS => $this->sms_enabled = $enabled,
self::CHANNEL_IN_APP => $this->in_app_enabled = $enabled,
self::CHANNEL_KAKAO => $this->kakao_enabled = $enabled,
default => null,
};
}
}

View File

@@ -0,0 +1,176 @@
<?php
namespace App\Models;
use App\Models\Members\User;
use App\Models\Permissions\Role;
use App\Models\Tenants\Tenant;
use App\Traits\BelongsToTenant;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Str;
class UserInvitation extends Model
{
use BelongsToTenant;
// 상태 상수
public const STATUS_PENDING = 'pending';
public const STATUS_ACCEPTED = 'accepted';
public const STATUS_EXPIRED = 'expired';
public const STATUS_CANCELLED = 'cancelled';
// 기본 만료 기간 (일)
public const DEFAULT_EXPIRES_DAYS = 7;
protected $fillable = [
'tenant_id',
'email',
'role_id',
'message',
'token',
'status',
'invited_by',
'expires_at',
'accepted_at',
];
protected $casts = [
'expires_at' => 'datetime',
'accepted_at' => 'datetime',
];
/**
* 테넌트 관계
*/
public function tenant(): BelongsTo
{
return $this->belongsTo(Tenant::class);
}
/**
* 역할 관계
*/
public function role(): BelongsTo
{
return $this->belongsTo(Role::class);
}
/**
* 초대한 사용자 관계
*/
public function inviter(): BelongsTo
{
return $this->belongsTo(User::class, 'invited_by');
}
/**
* 토큰 생성
*/
public static function generateToken(): string
{
return Str::random(64);
}
/**
* 만료 일시 계산
*/
public static function calculateExpiresAt(int $days = self::DEFAULT_EXPIRES_DAYS): \DateTime
{
return now()->addDays($days);
}
/**
* 만료 여부 확인
*/
public function isExpired(): bool
{
return $this->expires_at->isPast();
}
/**
* 수락 가능 여부 확인
*/
public function canAccept(): bool
{
return $this->status === self::STATUS_PENDING && ! $this->isExpired();
}
/**
* 취소 가능 여부 확인
*/
public function canCancel(): bool
{
return $this->status === self::STATUS_PENDING;
}
/**
* 초대 수락 처리
*/
public function markAsAccepted(): void
{
$this->status = self::STATUS_ACCEPTED;
$this->accepted_at = now();
$this->save();
}
/**
* 초대 만료 처리
*/
public function markAsExpired(): void
{
$this->status = self::STATUS_EXPIRED;
$this->save();
}
/**
* 초대 취소 처리
*/
public function markAsCancelled(): void
{
$this->status = self::STATUS_CANCELLED;
$this->save();
}
/**
* Scope: 대기 중인 초대만
*/
public function scopePending($query)
{
return $query->where('status', self::STATUS_PENDING);
}
/**
* Scope: 만료된 초대 (상태 업데이트 대상)
*/
public function scopeExpiredPending($query)
{
return $query->where('status', self::STATUS_PENDING)
->where('expires_at', '<', now());
}
/**
* Scope: 특정 이메일 초대
*/
public function scopeForEmail($query, string $email)
{
return $query->where('email', $email);
}
/**
* 상태 라벨 반환
*/
public function getStatusLabelAttribute(): string
{
return match ($this->status) {
self::STATUS_PENDING => '대기중',
self::STATUS_ACCEPTED => '수락됨',
self::STATUS_EXPIRED => '만료됨',
self::STATUS_CANCELLED => '취소됨',
default => $this->status,
};
}
}