feat: [email] 테넌트 이메일 설정 관리 기능 추가
- TenantMailConfigController: 목록, 편집, 저장, SMTP 테스트 API - TenantMailConfig, MailLog 모델 추가 - SmtpConnectionTester: SMTP 연결 테스트 서비스 (에러 코드, 트러블슈팅) - TenantMailService: 테넌트 설정 기반 메일 발송 (쿼터, Fallback) - config/mail-presets.php: Gmail/Naver/MS365 등 8개 SMTP 프리셋 - Blade 뷰: 테넌트 목록 현황 + 설정 폼 (프리셋 자동 채움, 연결 테스트) - 라우트 추가: /system/tenant-mail/*
This commit is contained in:
250
app/Http/Controllers/System/TenantMailConfigController.php
Normal file
250
app/Http/Controllers/System/TenantMailConfigController.php
Normal file
@@ -0,0 +1,250 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\System;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Tenants\MailLog;
|
||||
use App\Models\Tenants\Tenant;
|
||||
use App\Models\Tenants\TenantMailConfig;
|
||||
use App\Services\Mail\SmtpConnectionTester;
|
||||
use App\Services\Mail\TenantMailService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class TenantMailConfigController extends Controller
|
||||
{
|
||||
/**
|
||||
* 전체 테넌트 메일 설정 현황
|
||||
*/
|
||||
public function index(Request $request): View|Response
|
||||
{
|
||||
if ($request->header('HX-Request')) {
|
||||
return response('', 200)->header('HX-Redirect', route('system.tenant-mail.index'));
|
||||
}
|
||||
|
||||
$tenants = Tenant::orderBy('company_name')
|
||||
->get()
|
||||
->map(function ($tenant) {
|
||||
$config = TenantMailConfig::withoutGlobalScopes()
|
||||
->where('tenant_id', $tenant->id)
|
||||
->first();
|
||||
|
||||
$todayCount = MailLog::withoutGlobalScopes()
|
||||
->where('tenant_id', $tenant->id)
|
||||
->whereDate('created_at', today())
|
||||
->whereIn('status', ['queued', 'sent'])
|
||||
->count();
|
||||
|
||||
return (object) [
|
||||
'tenant' => $tenant,
|
||||
'config' => $config,
|
||||
'today_count' => $todayCount,
|
||||
];
|
||||
});
|
||||
|
||||
return view('system.tenant-mail.index', compact('tenants'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 테넌트 메일 설정 편집 폼
|
||||
*/
|
||||
public function edit(Request $request, int $tenantId): View|Response
|
||||
{
|
||||
if ($request->header('HX-Request')) {
|
||||
return response('', 200)->header('HX-Redirect', route('system.tenant-mail.edit', $tenantId));
|
||||
}
|
||||
|
||||
$tenant = Tenant::findOrFail($tenantId);
|
||||
|
||||
$config = TenantMailConfig::withoutGlobalScopes()
|
||||
->where('tenant_id', $tenantId)
|
||||
->first();
|
||||
|
||||
$presets = config('mail-presets', []);
|
||||
|
||||
$mailService = app(TenantMailService::class);
|
||||
$todayCount = $mailService->getTodayCount($tenantId);
|
||||
$monthCount = $mailService->getMonthCount($tenantId);
|
||||
|
||||
return view('system.tenant-mail.edit', compact(
|
||||
'tenant',
|
||||
'config',
|
||||
'presets',
|
||||
'todayCount',
|
||||
'monthCount'
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* 메일 설정 저장
|
||||
*/
|
||||
public function update(Request $request, int $tenantId): JsonResponse
|
||||
{
|
||||
$tenant = Tenant::findOrFail($tenantId);
|
||||
|
||||
$validated = $request->validate([
|
||||
'provider' => 'required|string|in:platform,smtp',
|
||||
'from_name' => 'required|string|max:255',
|
||||
'from_address' => 'required|email|max:255',
|
||||
'reply_to' => 'nullable|email|max:255',
|
||||
'daily_limit' => 'required|integer|min:1|max:99999',
|
||||
'is_active' => 'boolean',
|
||||
// SMTP 설정
|
||||
'preset' => 'nullable|string',
|
||||
'smtp_host' => 'required_if:provider,smtp|nullable|string|max:255',
|
||||
'smtp_port' => 'required_if:provider,smtp|nullable|integer|min:1|max:65535',
|
||||
'smtp_encryption' => 'required_if:provider,smtp|nullable|string|in:tls,ssl',
|
||||
'smtp_username' => 'required_if:provider,smtp|nullable|string|max:255',
|
||||
'smtp_password' => 'nullable|string|max:255',
|
||||
// 브랜딩
|
||||
'branding_company_name' => 'nullable|string|max:255',
|
||||
'branding_primary_color' => 'nullable|string|max:7',
|
||||
'branding_company_address' => 'nullable|string|max:500',
|
||||
'branding_company_phone' => 'nullable|string|max:50',
|
||||
'branding_footer_text' => 'nullable|string|max:500',
|
||||
]);
|
||||
|
||||
$config = TenantMailConfig::withoutGlobalScopes()
|
||||
->where('tenant_id', $tenantId)
|
||||
->first();
|
||||
|
||||
$data = [
|
||||
'tenant_id' => $tenantId,
|
||||
'provider' => $validated['provider'],
|
||||
'from_name' => $validated['from_name'],
|
||||
'from_address' => $validated['from_address'],
|
||||
'reply_to' => $validated['reply_to'] ?? null,
|
||||
'daily_limit' => $validated['daily_limit'],
|
||||
'is_active' => $validated['is_active'] ?? true,
|
||||
];
|
||||
|
||||
// Options 구성
|
||||
$options = $config?->options ?? [];
|
||||
|
||||
// SMTP 설정
|
||||
if ($validated['provider'] === 'smtp') {
|
||||
$options['preset'] = $validated['preset'] ?? 'custom';
|
||||
$options['smtp'] = [
|
||||
'host' => $validated['smtp_host'],
|
||||
'port' => $validated['smtp_port'],
|
||||
'encryption' => $validated['smtp_encryption'],
|
||||
'username' => $validated['smtp_username'],
|
||||
];
|
||||
|
||||
// 비밀번호: 새로 입력된 경우에만 업데이트
|
||||
if (! empty($validated['smtp_password'])) {
|
||||
$options['smtp']['password'] = encrypt($validated['smtp_password']);
|
||||
} elseif (isset($config?->options['smtp']['password'])) {
|
||||
$options['smtp']['password'] = $config->options['smtp']['password'];
|
||||
}
|
||||
} else {
|
||||
// platform 모드에서는 SMTP 설정 제거
|
||||
unset($options['smtp'], $options['preset']);
|
||||
}
|
||||
|
||||
// 브랜딩
|
||||
$options['branding'] = array_filter([
|
||||
'company_name' => $validated['branding_company_name'] ?? null,
|
||||
'primary_color' => $validated['branding_primary_color'] ?? '#1a56db',
|
||||
'company_address' => $validated['branding_company_address'] ?? null,
|
||||
'company_phone' => $validated['branding_company_phone'] ?? null,
|
||||
'footer_text' => $validated['branding_footer_text'] ?? 'SAM 시스템에서 발송된 메일입니다.',
|
||||
]);
|
||||
|
||||
$data['options'] = $options;
|
||||
$data['updated_by'] = auth()->id();
|
||||
|
||||
if ($config) {
|
||||
$config->update($data);
|
||||
} else {
|
||||
$data['created_by'] = auth()->id();
|
||||
$config = TenantMailConfig::create($data);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'message' => '메일 설정이 저장되었습니다.',
|
||||
'data' => $config->fresh(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* SMTP 연결 테스트
|
||||
*/
|
||||
public function test(Request $request, int $tenantId): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'host' => 'required|string',
|
||||
'port' => 'required|integer',
|
||||
'encryption' => 'required|string|in:tls,ssl',
|
||||
'username' => 'required|string',
|
||||
'password' => 'required|string',
|
||||
'preset' => 'nullable|string',
|
||||
'send_test_mail' => 'boolean',
|
||||
]);
|
||||
|
||||
$tester = new SmtpConnectionTester;
|
||||
$result = $tester->testViaLaravel(
|
||||
host: $validated['host'],
|
||||
port: $validated['port'],
|
||||
encryption: $validated['encryption'],
|
||||
username: $validated['username'],
|
||||
password: $validated['password'],
|
||||
testRecipient: ($validated['send_test_mail'] ?? false) ? $validated['username'] : null
|
||||
);
|
||||
|
||||
// 테스트 결과를 config에 기록
|
||||
$config = TenantMailConfig::withoutGlobalScopes()
|
||||
->where('tenant_id', $tenantId)
|
||||
->first();
|
||||
|
||||
if ($config) {
|
||||
$config->setOption('connection_test', [
|
||||
'last_tested_at' => now()->toIso8601String(),
|
||||
'last_result' => $result['success'] ? 'success' : 'failed',
|
||||
'response_time_ms' => $result['response_time_ms'],
|
||||
'server_banner' => $result['server_banner'] ?? null,
|
||||
'tested_by' => auth()->user()?->email,
|
||||
]);
|
||||
$config->save();
|
||||
}
|
||||
|
||||
if ($result['success']) {
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'message' => $result['message'],
|
||||
'data' => [
|
||||
'response_time_ms' => $result['response_time_ms'],
|
||||
'server_banner' => $result['server_banner'],
|
||||
'test_mail_sent' => $result['test_mail_sent'] ?? false,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'ok' => false,
|
||||
'message' => $result['message'],
|
||||
'data' => [
|
||||
'error_code' => $result['error_code'],
|
||||
'troubleshoot' => SmtpConnectionTester::getTroubleshoot(
|
||||
$result['error_code'] ?? 'UNKNOWN',
|
||||
$validated['preset'] ?? null
|
||||
),
|
||||
'response_time_ms' => $result['response_time_ms'],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* SMTP 프리셋 목록
|
||||
*/
|
||||
public function presets(): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'data' => config('mail-presets', []),
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user