diff --git a/TaxBaik.Web/Program.cs b/TaxBaik.Web/Program.cs index ca54b5b..4239eb5 100644 --- a/TaxBaik.Web/Program.cs +++ b/TaxBaik.Web/Program.cs @@ -8,6 +8,7 @@ using Microsoft.AspNetCore.HttpOverrides; using Microsoft.AspNetCore.ResponseCompression; using Microsoft.IdentityModel.Tokens; using MudBlazor.Services; +using Serilog; using TaxBaik.Application; using TaxBaik.Application.Services; using TaxBaik.Infrastructure; @@ -16,6 +17,20 @@ using TaxBaik.Web.Services; var builder = WebApplication.CreateBuilder(args); var isProduction = builder.Environment.IsProduction(); +// Serilog 설정 +builder.Host.UseSerilog((context, config) => +{ + config + .MinimumLevel.Information() + .WriteTo.Console() + .WriteTo.File( + path: "logs/taxbaik-.log", + rollingInterval: RollingInterval.Day, + outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] {Message:lj}{NewLine}{Exception}") + .Enrich.FromLogContext() + .Enrich.WithProperty("Environment", context.HostingEnvironment.EnvironmentName); +}); + // Controllers (API) builder.Services.AddControllers(); builder.Services.AddProblemDetails(); @@ -72,6 +87,9 @@ builder.Services.AddAuthorizationCore(); // Notifications (SignalR) builder.Services.AddScoped(); +// Telegram Notification +builder.Services.AddHttpClient(); + // HTTP Client for API (with automatic token refresh) builder.Services.AddScoped(); builder.Services.AddScoped(); @@ -202,4 +220,39 @@ app.MapRazorComponents() .AddInteractiveServerRenderMode() .AllowAnonymous(); -app.Run(); +// 애플리케이션 시작/종료 로깅 +try +{ + Log.Information("애플리케이션 시작: {Environment}", app.Environment.EnvironmentName); + if (!app.Environment.IsDevelopment()) + { + using (var scope = app.Services.CreateScope()) + { + var telegramService = scope.ServiceProvider.GetRequiredService(); + await telegramService.SendInfoAsync( + "서버 시작", + $"환경: {app.Environment.EnvironmentName}"); + } + } + app.Run(); +} +catch (Exception ex) +{ + Log.Fatal(ex, "애플리케이션 강종"); + if (!app.Environment.IsDevelopment()) + { + using (var scope = app.Services.CreateScope()) + { + var telegramService = scope.ServiceProvider.GetRequiredService(); + await telegramService.SendErrorAsync( + "서버 오류", + $"환경: {app.Environment.EnvironmentName}\n오류: {ex.Message}"); + } + } + throw; +} +finally +{ + Log.Information("애플리케이션 종료"); + Log.CloseAndFlush(); +} diff --git a/TaxBaik.Web/Services/AnnouncementBrowserClient.cs b/TaxBaik.Web/Services/AnnouncementBrowserClient.cs index 0b46980..7ab0b00 100644 --- a/TaxBaik.Web/Services/AnnouncementBrowserClient.cs +++ b/TaxBaik.Web/Services/AnnouncementBrowserClient.cs @@ -1,5 +1,6 @@ namespace TaxBaik.Web.Services; +using System.Net.Http; using System.Net.Http.Json; using TaxBaik.Application.DTOs; using TaxBaik.Domain.Entities; @@ -17,17 +18,28 @@ public class AnnouncementBrowserClient : IAnnouncementBrowserClient { private readonly HttpClient _http; private readonly ILogger _logger; + private readonly ITokenStore _tokenStore; - public AnnouncementBrowserClient(HttpClient http, ILogger logger) + public AnnouncementBrowserClient(HttpClient http, ILogger logger, ITokenStore tokenStore) { _http = http; _logger = logger; + _tokenStore = tokenStore; + } + + private void EnsureAuthHeader() + { + if (!string.IsNullOrEmpty(_tokenStore.AccessToken) && !_http.DefaultRequestHeaders.Contains("Authorization")) + { + _http.DefaultRequestHeaders.Authorization = new("Bearer", _tokenStore.AccessToken); + } } public async Task> GetAllAsync(CancellationToken ct = default) { try { + EnsureAuthHeader(); var result = await _http.GetFromJsonAsync("announcement", cancellationToken: ct); return result?.Data ?? []; } @@ -42,6 +54,7 @@ public class AnnouncementBrowserClient : IAnnouncementBrowserClient { try { + EnsureAuthHeader(); return await _http.GetFromJsonAsync($"announcement/{id}", cancellationToken: ct); } catch (HttpRequestException ex) @@ -55,6 +68,7 @@ public class AnnouncementBrowserClient : IAnnouncementBrowserClient { try { + EnsureAuthHeader(); var response = await _http.PostAsJsonAsync("announcement", dto, cancellationToken: ct); if (!response.IsSuccessStatusCode) return null; @@ -74,6 +88,7 @@ public class AnnouncementBrowserClient : IAnnouncementBrowserClient { try { + EnsureAuthHeader(); var response = await _http.PutAsJsonAsync($"announcement/{id}", dto, cancellationToken: ct); if (!response.IsSuccessStatusCode) return null; @@ -93,6 +108,7 @@ public class AnnouncementBrowserClient : IAnnouncementBrowserClient { try { + EnsureAuthHeader(); var response = await _http.DeleteAsync($"announcement/{id}", cancellationToken: ct); return response.IsSuccessStatusCode; } diff --git a/TaxBaik.Web/Services/AuthService.cs b/TaxBaik.Web/Services/AuthService.cs index 8b420f1..90423bf 100644 --- a/TaxBaik.Web/Services/AuthService.cs +++ b/TaxBaik.Web/Services/AuthService.cs @@ -12,15 +12,21 @@ public class AuthService { private readonly IAdminUserRepository _adminUserRepository; private readonly ILogger _logger; + private readonly ITelegramNotificationService _telegramService; private readonly string _jwtSecretKey; private readonly string? _passwordResetToken; private readonly int _accessTokenExpirationMinutes = 60; // Access Token: 1시간 (사용성 향상) private readonly int _refreshTokenExpirationMinutes = 10080; // Refresh Token: 7일 - public AuthService(IAdminUserRepository adminUserRepository, ILogger logger, IConfiguration configuration) + public AuthService( + IAdminUserRepository adminUserRepository, + ILogger logger, + IConfiguration configuration, + ITelegramNotificationService telegramService) { _adminUserRepository = adminUserRepository; _logger = logger; + _telegramService = telegramService; _jwtSecretKey = configuration["Jwt:SecretKey"] ?? throw new InvalidOperationException("Missing 'Jwt:SecretKey' configuration."); _passwordResetToken = configuration["Admin:PasswordResetToken"]; } @@ -46,10 +52,16 @@ public class AuthService if (!BCrypt.Net.BCrypt.Verify(password, user.PasswordHash)) { _logger.LogWarning("로그인 시도: 잘못된 비밀번호 '{Username}'", username); + await _telegramService.SendErrorAsync( + "로그인 실패", + $"사용자: {username}\n실패 사유: 잘못된 비밀번호"); return null; } _logger.LogInformation("로그인 성공: {Username}", username); + await _telegramService.SendInfoAsync( + "관리자 로그인", + $"사용자: {username}"); await _adminUserRepository.UpdateLastLoginAtAsync(user.Id); return GenerateTokenPair(user); } diff --git a/TaxBaik.Web/Services/ClientBrowserClient.cs b/TaxBaik.Web/Services/ClientBrowserClient.cs index c2cc048..a47c484 100644 --- a/TaxBaik.Web/Services/ClientBrowserClient.cs +++ b/TaxBaik.Web/Services/ClientBrowserClient.cs @@ -1,5 +1,6 @@ namespace TaxBaik.Web.Services; +using System.Net.Http; using System.Net.Http.Json; using TaxBaik.Application.DTOs; using TaxBaik.Domain.Entities; @@ -22,11 +23,21 @@ public class ClientBrowserClient : IClientBrowserClient { private readonly HttpClient _http; private readonly ILogger _logger; + private readonly ITokenStore _tokenStore; - public ClientBrowserClient(HttpClient http, ILogger logger) + public ClientBrowserClient(HttpClient http, ILogger logger, ITokenStore tokenStore) { _http = http; _logger = logger; + _tokenStore = tokenStore; + } + + private void EnsureAuthHeader() + { + if (!string.IsNullOrEmpty(_tokenStore.AccessToken) && !_http.DefaultRequestHeaders.Contains("Authorization")) + { + _http.DefaultRequestHeaders.Authorization = new("Bearer", _tokenStore.AccessToken); + } } public async Task<(IEnumerable Items, int Total)> GetPagedAsync( @@ -34,6 +45,7 @@ public class ClientBrowserClient : IClientBrowserClient { try { + EnsureAuthHeader(); var query = $"client?page={page}&pageSize={pageSize}"; if (!string.IsNullOrEmpty(status)) query += $"&status={status}"; @@ -54,6 +66,7 @@ public class ClientBrowserClient : IClientBrowserClient { try { + EnsureAuthHeader(); return await _http.GetFromJsonAsync($"client/{id}", cancellationToken: ct); } catch (HttpRequestException ex) @@ -67,6 +80,7 @@ public class ClientBrowserClient : IClientBrowserClient { try { + EnsureAuthHeader(); var response = await _http.PostAsJsonAsync("client", dto, cancellationToken: ct); if (!response.IsSuccessStatusCode) return null; @@ -87,6 +101,7 @@ public class ClientBrowserClient : IClientBrowserClient { try { + EnsureAuthHeader(); var response = await _http.PutAsJsonAsync($"client/{id}", dto, cancellationToken: ct); if (!response.IsSuccessStatusCode) return null; @@ -107,6 +122,7 @@ public class ClientBrowserClient : IClientBrowserClient { try { + EnsureAuthHeader(); var response = await _http.DeleteAsync($"client/{id}", cancellationToken: ct); return response.IsSuccessStatusCode; } diff --git a/TaxBaik.Web/Services/FaqBrowserClient.cs b/TaxBaik.Web/Services/FaqBrowserClient.cs index 6c31d99..736b949 100644 --- a/TaxBaik.Web/Services/FaqBrowserClient.cs +++ b/TaxBaik.Web/Services/FaqBrowserClient.cs @@ -1,5 +1,6 @@ namespace TaxBaik.Web.Services; +using System.Net.Http; using System.Net.Http.Json; using TaxBaik.Domain.Entities; @@ -16,17 +17,28 @@ public class FaqBrowserClient : IFaqBrowserClient { private readonly HttpClient _http; private readonly ILogger _logger; + private readonly ITokenStore _tokenStore; - public FaqBrowserClient(HttpClient http, ILogger logger) + public FaqBrowserClient(HttpClient http, ILogger logger, ITokenStore tokenStore) { _http = http; _logger = logger; + _tokenStore = tokenStore; + } + + private void EnsureAuthHeader() + { + if (!string.IsNullOrEmpty(_tokenStore.AccessToken) && !_http.DefaultRequestHeaders.Contains("Authorization")) + { + _http.DefaultRequestHeaders.Authorization = new("Bearer", _tokenStore.AccessToken); + } } public async Task> GetAllAsync(CancellationToken ct = default) { try { + EnsureAuthHeader(); var result = await _http.GetFromJsonAsync("faq", cancellationToken: ct); return result?.Data ?? []; } @@ -41,6 +53,7 @@ public class FaqBrowserClient : IFaqBrowserClient { try { + EnsureAuthHeader(); return await _http.GetFromJsonAsync($"faq/{id}", cancellationToken: ct); } catch (HttpRequestException ex) @@ -54,6 +67,7 @@ public class FaqBrowserClient : IFaqBrowserClient { try { + EnsureAuthHeader(); var response = await _http.PostAsJsonAsync("faq", faq, cancellationToken: ct); if (!response.IsSuccessStatusCode) return null; @@ -73,6 +87,7 @@ public class FaqBrowserClient : IFaqBrowserClient { try { + EnsureAuthHeader(); var response = await _http.PutAsJsonAsync($"faq/{id}", faq, cancellationToken: ct); if (!response.IsSuccessStatusCode) return null; @@ -92,6 +107,7 @@ public class FaqBrowserClient : IFaqBrowserClient { try { + EnsureAuthHeader(); var response = await _http.DeleteAsync($"faq/{id}", cancellationToken: ct); return response.IsSuccessStatusCode; } diff --git a/TaxBaik.Web/Services/TaxFilingBrowserClient.cs b/TaxBaik.Web/Services/TaxFilingBrowserClient.cs index 91c0aac..f0fd6c2 100644 --- a/TaxBaik.Web/Services/TaxFilingBrowserClient.cs +++ b/TaxBaik.Web/Services/TaxFilingBrowserClient.cs @@ -1,5 +1,6 @@ namespace TaxBaik.Web.Services; +using System.Net.Http; using System.Net.Http.Json; using TaxBaik.Domain.Entities; @@ -20,17 +21,28 @@ public class TaxFilingBrowserClient : ITaxFilingBrowserClient { private readonly HttpClient _http; private readonly ILogger _logger; + private readonly ITokenStore _tokenStore; - public TaxFilingBrowserClient(HttpClient http, ILogger logger) + public TaxFilingBrowserClient(HttpClient http, ILogger logger, ITokenStore tokenStore) { _http = http; _logger = logger; + _tokenStore = tokenStore; + } + + private void EnsureAuthHeader() + { + if (!string.IsNullOrEmpty(_tokenStore.AccessToken) && !_http.DefaultRequestHeaders.Contains("Authorization")) + { + _http.DefaultRequestHeaders.Authorization = new("Bearer", _tokenStore.AccessToken); + } } public async Task> GetUpcomingAsync(int daysAhead = 30, CancellationToken ct = default) { try { + EnsureAuthHeader(); var result = await _http.GetFromJsonAsync( $"tax-filing/upcoming?daysAhead={daysAhead}", cancellationToken: ct); return result?.Data ?? []; @@ -46,6 +58,7 @@ public class TaxFilingBrowserClient : ITaxFilingBrowserClient { try { + EnsureAuthHeader(); var result = await _http.GetFromJsonAsync( $"tax-filing/client/{clientId}", cancellationToken: ct); return result?.Data ?? []; @@ -61,6 +74,7 @@ public class TaxFilingBrowserClient : ITaxFilingBrowserClient { try { + EnsureAuthHeader(); return await _http.GetFromJsonAsync( $"tax-filing/{id}", cancellationToken: ct); } @@ -75,6 +89,7 @@ public class TaxFilingBrowserClient : ITaxFilingBrowserClient { try { + EnsureAuthHeader(); var response = await _http.PostAsJsonAsync("tax-filing", filing, cancellationToken: ct); if (!response.IsSuccessStatusCode) return null; @@ -95,6 +110,7 @@ public class TaxFilingBrowserClient : ITaxFilingBrowserClient { try { + EnsureAuthHeader(); var response = await _http.PutAsJsonAsync($"tax-filing/{id}", filing, cancellationToken: ct); if (!response.IsSuccessStatusCode) return null; @@ -115,6 +131,7 @@ public class TaxFilingBrowserClient : ITaxFilingBrowserClient { try { + EnsureAuthHeader(); var response = await _http.DeleteAsync($"tax-filing/{id}", cancellationToken: ct); return response.IsSuccessStatusCode; } diff --git a/TaxBaik.Web/Services/TelegramNotificationService.cs b/TaxBaik.Web/Services/TelegramNotificationService.cs new file mode 100644 index 0000000..762c367 --- /dev/null +++ b/TaxBaik.Web/Services/TelegramNotificationService.cs @@ -0,0 +1,76 @@ +namespace TaxBaik.Web.Services; + +using System.Net.Http.Json; + +/// +/// Telegram Bot 알림 서비스 +/// 중요 로깅 및 오류를 Telegram으로 전송 +/// +public interface ITelegramNotificationService +{ + Task SendMessageAsync(string message, CancellationToken ct = default); + Task SendErrorAsync(string title, string details, CancellationToken ct = default); + Task SendInfoAsync(string title, string message, CancellationToken ct = default); +} + +public class TelegramNotificationService : ITelegramNotificationService +{ + private readonly HttpClient _httpClient; + private readonly ILogger _logger; + private readonly string _botToken; + private readonly string _chatId; + private const string TelegramApiUrl = "https://api.telegram.org"; + + public TelegramNotificationService( + HttpClient httpClient, + ILogger logger, + IConfiguration config) + { + _httpClient = httpClient; + _logger = logger; + _botToken = config["Telegram:BotToken"] ?? ""; + _chatId = config["Telegram:ChatId"] ?? ""; + } + + public async Task SendMessageAsync(string message, CancellationToken ct = default) + { + if (string.IsNullOrEmpty(_botToken) || string.IsNullOrEmpty(_chatId)) + { + _logger.LogWarning("Telegram credentials not configured"); + return; + } + + try + { + var url = $"{TelegramApiUrl}/bot{_botToken}/sendMessage"; + var payload = new + { + chat_id = _chatId, + text = message, + parse_mode = "HTML" + }; + + var response = await _httpClient.PostAsJsonAsync(url, payload, cancellationToken: ct); + if (!response.IsSuccessStatusCode) + { + _logger.LogError("Failed to send Telegram message: {StatusCode}", response.StatusCode); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error sending Telegram message"); + } + } + + public async Task SendErrorAsync(string title, string details, CancellationToken ct = default) + { + var message = $"❌ {title}\n\n{details}\n\n{DateTime.UtcNow:yyyy-MM-dd HH:mm:ss} UTC"; + await SendMessageAsync(message, ct); + } + + public async Task SendInfoAsync(string title, string message, CancellationToken ct = default) + { + var text = $"ℹ️ {title}\n\n{message}\n\n{DateTime.UtcNow:yyyy-MM-dd HH:mm:ss} UTC"; + await SendMessageAsync(text, ct); + } +} diff --git a/TaxBaik.Web/TaxBaik.Web.csproj b/TaxBaik.Web/TaxBaik.Web.csproj index b17e71b..6c05a86 100644 --- a/TaxBaik.Web/TaxBaik.Web.csproj +++ b/TaxBaik.Web/TaxBaik.Web.csproj @@ -17,6 +17,9 @@ + + + diff --git a/TaxBaik.Web/appsettings.json b/TaxBaik.Web/appsettings.json index 909ee40..df573f3 100644 --- a/TaxBaik.Web/appsettings.json +++ b/TaxBaik.Web/appsettings.json @@ -18,8 +18,8 @@ "BaseUrl": "http://localhost:5001/taxbaik/api/" }, "Telegram": { - "BotToken": "", - "ChatId": "" + "BotToken": "8679990909:AAGLLRUIAuEbYAZVGOYDu-UuTu4ihroEiX0", + "ChatId": "-5585148480" }, "Admin": { "PasswordResetToken": "dev-reset-token-12345" diff --git a/debug-settings.js b/debug-settings.js new file mode 100644 index 0000000..8b21a9a --- /dev/null +++ b/debug-settings.js @@ -0,0 +1,57 @@ +import { chromium } from '@playwright/test'; + +const browser = await chromium.launch(); +const page = await browser.newPage(); + +try { + // 1. 로그인 + console.log('🔓 로그인 중...'); + await page.goto('http://178.104.200.7/taxbaik/admin/login', { waitUntil: 'networkidle' }); + await page.fill('input[placeholder="사용자명"]', 'test_admin'); + await page.fill('input[placeholder="비밀번호"]', 'TestAdmin@123456'); + await page.click('button:has-text("로그인")'); + await page.waitForURL(/\/taxbaik\/admin\/dashboard$/, { timeout: 10000 }); + console.log('✅ 로그인 성공'); + + // 2. Settings 페이지로 이동 + console.log('\n📍 Settings 페이지로 이동...'); + await page.goto('http://178.104.200.7/taxbaik/admin/settings', { waitUntil: 'domcontentloaded' }); + + // 3. 다양한 대기 전략 시도 + console.log('⏳ 페이지 로드 대기 중...'); + + for (let i = 1; i <= 5; i++) { + await page.waitForTimeout(1000); + const title = await page.locator('h4:has-text("설정")').count(); + const body = await page.locator('body').evaluate(el => el.innerHTML.length); + const mudComponents = await page.locator('[class*="mud-"]').count(); + + console.log(`시도 ${i}: body=${body}bytes, mud=${mudComponents}, title=${title}`); + + if (mudComponents > 10 && body > 5000) { + console.log('✅ 페이지 렌더링 감지됨!'); + break; + } + } + + // 4. 최종 상태 확인 + console.log('\n📊 최종 상태:'); + const hasContent = await page.locator('body').evaluate(el => el.innerText.length > 100); + const hasComponents = await page.locator('[class*="mud-"]').count(); + + console.log(`- 텍스트 콘텐츠: ${hasContent ? '있음' : '없음'}`); + console.log(`- MudBlazor 컴포넌트: ${hasComponents}개`); + + if (!hasContent) { + console.log('\n❌ Settings 페이지 렌더링 실패'); + console.log('HTML 스니펫:'); + const html = await page.content(); + const bodyMatch = html.match(/]*>([\s\S]{0,500})/); + if (bodyMatch) console.log(bodyMatch[1]); + } + +} catch (error) { + console.error('❌ 에러:', error.message); +} + +await browser.close(); diff --git a/final-test.js b/final-test.js new file mode 100644 index 0000000..3ab985d --- /dev/null +++ b/final-test.js @@ -0,0 +1,52 @@ +import { chromium } from '@playwright/test'; + +const browser = await chromium.launch(); +const page = await browser.newPage(); + +try { + console.log('🧪 최종 테스트: Settings 페이지 로딩 인디케이터'); + console.log(''); + + // 로그인 + await page.goto('http://178.104.200.7/taxbaik/admin/login', { waitUntil: 'networkidle' }); + await page.fill('input[placeholder="사용자명"]', 'test_admin'); + await page.fill('input[placeholder="비밀번호"]', 'TestAdmin@123456'); + await page.click('button:has-text("로그인")'); + await page.waitForURL(/\/taxbaik\/admin\/dashboard$/); + console.log('✅ 로그인 성공'); + + // Settings 페이지로 이동 + console.log('📍 Settings 페이지로 이동...'); + await page.goto('http://178.104.200.7/taxbaik/admin/settings', { waitUntil: 'domcontentloaded' }); + + // 로딩 인디케이터 상태 확인 + console.log(''); + console.log('⏱️ 로딩 상태 모니터링:'); + + for (let i = 1; i <= 5; i++) { + await page.waitForTimeout(500); + + const loadingVisible = await page.locator('#blazor-loading.show').isVisible().catch(() => false); + const mudCount = await page.locator('[class*="mud-"]').count(); + const formElements = await page.locator('input, .admin-section-header').count(); + + console.log(` ${i}초: Loading=${loadingVisible ? '보임' : '안보임'}, Mud=${mudCount}, Form=${formElements}`); + + if (!loadingVisible && mudCount > 20) { + console.log(''); + console.log('✅ 로딩 인디케이터 정상 작동!'); + console.log(' → 페이지 로드 중: 스피너 표시'); + console.log(' → 페이지 완료: 스피너 숨김'); + break; + } + } + + // 스크린샷 + await page.screenshot({ path: 'settings-final.png' }); + console.log('✅ 스크린샷 저장: settings-final.png'); + +} catch (error) { + console.error('❌ 오류:', error.message); +} + +await browser.close(); diff --git a/settings-page.png b/settings-page.png new file mode 100644 index 0000000..f3f54a6 Binary files /dev/null and b/settings-page.png differ diff --git a/test-settings.js b/test-settings.js new file mode 100644 index 0000000..98bc960 --- /dev/null +++ b/test-settings.js @@ -0,0 +1,45 @@ +import { chromium } from '@playwright/test'; + +const browser = await chromium.launch(); +const page = await browser.newPage(); + +try { + console.log('📍 1. Login page 접속...'); + await page.goto('http://178.104.200.7/taxbaik/admin/login', { waitUntil: 'networkidle' }); + + console.log('📍 2. 로그인 입력...'); + await page.fill('input[placeholder="사용자명"]', 'test_admin'); + await page.fill('input[placeholder="비밀번호"]', 'TestAdmin@123456'); + await page.click('button:has-text("로그인")'); + + console.log('📍 3. Dashboard 로드 대기...'); + await page.waitForURL(/\/taxbaik\/admin\/dashboard$/, { timeout: 10000 }); + console.log('✅ Dashboard로 이동 성공'); + + console.log('📍 4. Settings page 접속...'); + await page.goto('http://178.104.200.7/taxbaik/admin/settings', { waitUntil: 'domcontentloaded' }); + + console.log('📍 5. Settings 페이지 렌더링 대기...'); + await page.waitForTimeout(1500); + + console.log('📍 6. 페이지 콘텐츠 확인...'); + const formElements = await page.locator('input, button, .admin-section-header').count(); + + console.log(`✅ 렌더링된 폼 요소: ${formElements}개`); + + if (formElements > 5) { + console.log('✅ Settings 페이지 완전 렌더링됨 (흰 화면 없음)'); + } else { + console.log('⚠️ Settings 페이지 부분 렌더링됨'); + } + + console.log('📍 7. 스크린샷 저장...'); + await page.screenshot({ path: 'settings-page.png' }); + console.log('✅ settings-page.png 저장됨'); + +} catch (error) { + console.error('❌ 테스트 실패:', error.message); + process.exit(1); +} + +await browser.close(); diff --git a/tests/e2e/dashboard-check.spec.ts b/tests/e2e/dashboard-check.spec.ts new file mode 100644 index 0000000..b8767e4 --- /dev/null +++ b/tests/e2e/dashboard-check.spec.ts @@ -0,0 +1,50 @@ +import { test } from '@playwright/test'; + +test('check dashboard metrics', async ({ page }) => { + const baseUrl = 'http://178.104.200.7/taxbaik'; + + // Login + await page.goto(`${baseUrl}/admin/login`); + await page.fill('input[placeholder="사용자명"]', 'test_admin'); + await page.fill('input[placeholder="비밀번호"]', 'TestAdmin@123456'); + await page.click('button[type="submit"]'); + await page.waitForURL(/admin\/dashboard/); + + // Wait for page load + await page.waitForSelector('.admin-page-hero', { timeout: 5000 }); + await page.waitForTimeout(2000); + + // Check for metric cards + const metrics = page.locator('.admin-metric-card'); + const metricCount = await metrics.count(); + console.log(`\nFound ${metricCount} metric cards`); + + // Log each metric value + for (let i = 0; i < Math.min(metricCount, 4); i++) { + const metric = metrics.nth(i); + const text = await metric.textContent(); + console.log(` Metric ${i + 1}: ${text?.trim().slice(0, 100)}`); + } + + // Check for data in main content area + const contentText = await page.locator('body').textContent(); + const has총문의 = contentText?.includes('총 문의') || false; + const has신규문의 = contentText?.includes('신규 문의') || false; + const has활성공지 = contentText?.includes('활성 공지') || false; + const has예정세무신고 = contentText?.includes('예정 세무신고') || false; + + console.log(`\nDashboard content check:`); + console.log(` - 총 문의: ${has총문의}`); + console.log(` - 신규 문의: ${has신규문의}`); + console.log(` - 활성 공지: ${has활성공지}`); + console.log(` - 예정 세무신고: ${has예정세무신고}`); + + // Try to get text content from specific areas + const dashContent = await page.locator('.admin-content').textContent(); + if (dashContent) { + console.log(`\nDashboard content length: ${dashContent.length}`); + // Check if there are numbers + const numbers = dashContent.match(/\d+/g) || []; + console.log(`Numbers found: ${numbers.slice(0, 10).join(', ')}`); + } +}); diff --git a/tests/e2e/full-admin-pages.spec.ts b/tests/e2e/full-admin-pages.spec.ts new file mode 100644 index 0000000..8c3bcb7 --- /dev/null +++ b/tests/e2e/full-admin-pages.spec.ts @@ -0,0 +1,96 @@ +import { test, expect } from '@playwright/test'; + +test('production: verify all admin pages load correctly', async ({ page }) => { + const baseUrl = 'http://178.104.200.7/taxbaik'; + + // Login + console.log('🔐 Logging in...'); + await page.goto(`${baseUrl}/admin/login`); + await page.fill('input[placeholder="사용자명"]', 'test_admin'); + await page.fill('input[placeholder="비밀번호"]', 'TestAdmin@123456'); + await page.click('button[type="submit"]'); + await page.waitForURL(/admin\/dashboard/); + console.log('✓ Login successful\n'); + + const pageHero = page.locator('.admin-page-hero').first(); + const loadingOverlay = page.locator('#blazor-loading'); + + // List of all admin pages to test (using direct URLs) + const pages = [ + { name: '📊 Dashboard', url: `${baseUrl}/admin/dashboard`, hasData: false }, + { name: '👥 Clients', url: `${baseUrl}/admin/clients`, hasData: true }, + { name: '📅 Tax Filings', url: `${baseUrl}/admin/tax-filings`, hasData: true }, + { name: '📢 Announcements', url: `${baseUrl}/admin/announcements`, hasData: false }, + { name: '❓ FAQs', url: `${baseUrl}/admin/faqs`, hasData: true }, + { name: '📝 Blog', url: `${baseUrl}/admin/blog`, hasData: true }, + { name: '🎭 Season Simulator', url: `${baseUrl}/admin/season-simulator`, hasData: false }, + { name: '❔ Inquiries', url: `${baseUrl}/admin/inquiries`, hasData: true }, + { name: '⚙️ Settings', url: `${baseUrl}/admin/settings`, hasData: false }, + ]; + + for (const pageInfo of pages) { + console.log(`${'─'.repeat(60)}`); + console.log(`Testing: ${pageInfo.name}`); + console.log(`URL: ${pageInfo.url}`); + console.log(`${'─'.repeat(60)}`); + + const startTime = Date.now(); + + try { + // Navigate to page + await page.goto(pageInfo.url); + + // Wait for page hero or basic element + try { + await pageHero.waitFor({ state: 'visible', timeout: 3000 }); + console.log(` ✓ Page hero visible`); + } catch { + // Some pages might not have page hero, that's OK + } + + // Check if page loaded successfully by looking for content + const pageContent = page.locator('body').first(); + await pageContent.waitFor({ state: 'visible', timeout: 5000 }); + + // Wait for data if expected + if (pageInfo.hasData) { + try { + // Try to find table rows + await page.waitForSelector('tbody tr', { timeout: 8000 }); + const rowCount = await page.locator('tbody tr').count(); + if (rowCount > 0) { + console.log(` ✓ Data loaded: ${rowCount} rows`); + } else { + console.log(` ⚠️ Table found but no rows`); + } + } catch { + console.log(` ℹ️ No table data (may not have table)`); + } + } + + // Verify overlay is hidden + const overlayShown = await loadingOverlay.evaluate((el: HTMLElement) => + el.classList.contains('show') + ).catch(() => false); + + if (!overlayShown) { + console.log(` ✓ Loading overlay hidden`); + } else { + console.log(` ⚠️ Loading overlay still visible`); + } + + const totalTime = Date.now() - startTime; + console.log(` ⏱️ Load time: ${totalTime}ms`); + console.log(` ✅ PAGE LOADED SUCCESSFULLY\n`); + } catch (error) { + const totalTime = Date.now() - startTime; + console.log(` ❌ FAILED: ${error}`); + console.log(` ⏱️ Time: ${totalTime}ms\n`); + throw error; + } + } + + console.log(`${'═'.repeat(60)}`); + console.log('✅ ALL PAGES VERIFIED SUCCESSFULLY'); + console.log(`${'═'.repeat(60)}`); +});