Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c0dc05c58c |
+16
-16
@@ -425,9 +425,9 @@ Todo:
|
|||||||
- 텔레그램 전송 실패 시 로그만 남기고 앱 정상 운영 유지
|
- 텔레그램 전송 실패 시 로그만 남기고 앱 정상 운영 유지
|
||||||
|
|
||||||
Todo:
|
Todo:
|
||||||
- [x] BackgroundService 또는 Hangfire 기반 스케줄러 추가
|
- [ ] BackgroundService 또는 Hangfire 기반 스케줄러 추가
|
||||||
- [x] 일간/주간 리포트 메시지 템플릿
|
- [ ] 일간/주간 리포트 메시지 템플릿
|
||||||
- [x] TelegramNotificationService에 리포트 메서드 추가
|
- [ ] TelegramNotificationService에 리포트 메서드 추가
|
||||||
|
|
||||||
## WBS-CRM-07 고객 포털 (읽기 전용) — Phase 3
|
## WBS-CRM-07 고객 포털 (읽기 전용) — Phase 3
|
||||||
|
|
||||||
@@ -439,9 +439,9 @@ Todo:
|
|||||||
- 개인정보 열람 범위는 세무사가 허용한 항목만
|
- 개인정보 열람 범위는 세무사가 허용한 항목만
|
||||||
|
|
||||||
Todo:
|
Todo:
|
||||||
- [x] 고객 포털 설계 (인증 방식 결정 — WBS-CRM-08 선행)
|
- [ ] 고객 포털 설계 (인증 방식 결정 — WBS-CRM-08 선행)
|
||||||
- [x] 고객 전용 Razor Pages 추가
|
- [ ] 고객 전용 Razor Pages 추가
|
||||||
- [x] 세무사 허용 권한 설정 UI
|
- [ ] 세무사 허용 권한 설정 UI
|
||||||
|
|
||||||
## WBS-CRM-08 고객 회원가입 · 소셜 로그인 — Phase 3
|
## WBS-CRM-08 고객 회원가입 · 소셜 로그인 — Phase 3
|
||||||
|
|
||||||
@@ -485,16 +485,16 @@ DB 스키마:
|
|||||||
- `GOOGLE_CLIENT_ID` / `GOOGLE_CLIENT_SECRET`
|
- `GOOGLE_CLIENT_ID` / `GOOGLE_CLIENT_SECRET`
|
||||||
|
|
||||||
Todo:
|
Todo:
|
||||||
- [x] WBS-CRM-07 고객 포털 기본 구조 완성 (선행)
|
- [ ] WBS-CRM-07 고객 포털 기본 구조 완성 (선행)
|
||||||
- [x] OAuth 앱 등록 (네이버·카카오·구글 개발자 콘솔)
|
- [ ] OAuth 앱 등록 (네이버·카카오·구글 개발자 콘솔)
|
||||||
- [x] V011__CreatePortalUsers.sql 마이그레이션 (실제 V016__CreatePortalUsers.sql로 대체됨)
|
- [ ] V011__CreatePortalUsers.sql 마이그레이션
|
||||||
- [x] PortalUser 엔티티 / IPortalUserRepository / PortalUserRepository
|
- [ ] PortalUser 엔티티 / IPortalUserRepository / PortalUserRepository
|
||||||
- [x] 네이버 OAuth Handler 구현
|
- [ ] 네이버 OAuth Handler 구현
|
||||||
- [x] 카카오·구글 패키지 추가 및 설정
|
- [ ] 카카오·구글 패키지 추가 및 설정
|
||||||
- [x] 기본 계정 회원가입 폼 (`/taxbaik/portal/register`)
|
- [ ] 기본 계정 회원가입 폼 (`/taxbaik/portal/register`)
|
||||||
- [x] 소셜 로그인 콜백 처리 → portal_users 자동 생성
|
- [ ] 소셜 로그인 콜백 처리 → portal_users 자동 생성
|
||||||
- [x] 신규 가입 시 clients 테이블 연결 또는 신규 생성
|
- [ ] 신규 가입 시 clients 테이블 연결 또는 신규 생성
|
||||||
- [x] 포털 로그인 페이지 (`/taxbaik/portal/login`) — 소셜 버튼 + 이메일 폼
|
- [ ] 포털 로그인 페이지 (`/taxbaik/portal/login`) — 소셜 버튼 + 이메일 폼
|
||||||
- [ ] Gitea Secrets에 OAuth 키 추가
|
- [ ] Gitea Secrets에 OAuth 키 추가
|
||||||
- [ ] 배포 후 소셜 로그인 3종 E2E 테스트
|
- [ ] 배포 후 소셜 로그인 3종 E2E 테스트
|
||||||
|
|
||||||
|
|||||||
@@ -24,11 +24,11 @@
|
|||||||
<MudTextField @bind-Value="model.Title" Label="제목"
|
<MudTextField @bind-Value="model.Title" Label="제목"
|
||||||
Variant="Variant.Outlined" Class="mb-4" Required="true" />
|
Variant="Variant.Outlined" Class="mb-4" Required="true" />
|
||||||
|
|
||||||
<MudSelect T="int?" @bind-Value="model.CategoryId" Label="카테고리"
|
<MudSelect @bind-Value="model.CategoryId" Label="카테고리"
|
||||||
Variant="Variant.Outlined" Class="mb-4">
|
Variant="Variant.Outlined" Class="mb-4">
|
||||||
@foreach (var category in categories)
|
@foreach (var category in categories)
|
||||||
{
|
{
|
||||||
<MudSelectItem T="int?" Value="@((int?)category.Id)">@category.Name</MudSelectItem>
|
<MudSelectItem Value="@category.Id">@category.Name</MudSelectItem>
|
||||||
}
|
}
|
||||||
</MudSelect>
|
</MudSelect>
|
||||||
|
|
||||||
|
|||||||
@@ -35,11 +35,11 @@ else
|
|||||||
<MudTextField @bind-Value="model.Title" Label="제목"
|
<MudTextField @bind-Value="model.Title" Label="제목"
|
||||||
Variant="Variant.Outlined" Class="mb-4" Required="true" />
|
Variant="Variant.Outlined" Class="mb-4" Required="true" />
|
||||||
|
|
||||||
<MudSelect T="int?" @bind-Value="model.CategoryId" Label="카테고리"
|
<MudSelect @bind-Value="model.CategoryId" Label="카테고리"
|
||||||
Variant="Variant.Outlined" Class="mb-4">
|
Variant="Variant.Outlined" Class="mb-4">
|
||||||
@foreach (var category in categories)
|
@foreach (var category in categories)
|
||||||
{
|
{
|
||||||
<MudSelectItem T="int?" Value="@((int?)category.Id)">@category.Name</MudSelectItem>
|
<MudSelectItem Value="@category.Id">@category.Name</MudSelectItem>
|
||||||
}
|
}
|
||||||
</MudSelect>
|
</MudSelect>
|
||||||
|
|
||||||
|
|||||||
@@ -1,85 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Net.Http;
|
|
||||||
using System.Net.Http.Json;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Serilog.Core;
|
|
||||||
using Serilog.Events;
|
|
||||||
|
|
||||||
namespace TaxBaik.Web.Logging;
|
|
||||||
|
|
||||||
public class TelegramSink : ILogEventSink
|
|
||||||
{
|
|
||||||
private readonly string _botToken;
|
|
||||||
private readonly string _chatId;
|
|
||||||
private readonly HttpClient _httpClient;
|
|
||||||
|
|
||||||
public TelegramSink(string botToken, string chatId)
|
|
||||||
{
|
|
||||||
_botToken = botToken;
|
|
||||||
_chatId = chatId;
|
|
||||||
_httpClient = new HttpClient();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Emit(LogEvent logEvent)
|
|
||||||
{
|
|
||||||
if (logEvent.Level < LogEventLevel.Error)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Emit is a synchronous method, so we dispatch the network call asynchronously
|
|
||||||
Task.Run(async () =>
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var timestamp = logEvent.Timestamp.ToString("yyyy-MM-dd HH:mm:ss.fff zzz");
|
|
||||||
var level = logEvent.Level.ToString().ToUpper();
|
|
||||||
var message = logEvent.RenderMessage();
|
|
||||||
var exceptionDetails = logEvent.Exception?.ToString();
|
|
||||||
|
|
||||||
var sb = new StringBuilder();
|
|
||||||
sb.AppendLine($"<b>🚨 [{level}] 에러 발생</b>");
|
|
||||||
sb.AppendLine($"<b>시간:</b> {timestamp}");
|
|
||||||
sb.AppendLine($"<b>메시지:</b> {EscapeHtml(message)}");
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(exceptionDetails))
|
|
||||||
{
|
|
||||||
var escapedException = EscapeHtml(exceptionDetails);
|
|
||||||
if (escapedException.Length > 3000)
|
|
||||||
{
|
|
||||||
escapedException = escapedException.Substring(0, 3000) + "\n[이하 생략]";
|
|
||||||
}
|
|
||||||
sb.AppendLine($"<b>Exception 상세:</b>\n<pre>{escapedException}</pre>");
|
|
||||||
}
|
|
||||||
|
|
||||||
var url = $"https://api.telegram.org/bot{_botToken}/sendMessage";
|
|
||||||
var payload = new
|
|
||||||
{
|
|
||||||
chat_id = _chatId,
|
|
||||||
text = sb.ToString(),
|
|
||||||
parse_mode = "HTML"
|
|
||||||
};
|
|
||||||
|
|
||||||
var response = await _httpClient.PostAsJsonAsync(url, payload);
|
|
||||||
if (!response.IsSuccessStatusCode)
|
|
||||||
{
|
|
||||||
var errorResponse = await response.Content.ReadAsStringAsync();
|
|
||||||
Console.WriteLine($"[TelegramSink] Failed to send log to Telegram: {response.StatusCode} - {errorResponse}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"[TelegramSink] Error in TelegramSink: {ex.Message}");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string EscapeHtml(string text)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(text)) return text;
|
|
||||||
return text.Replace("&", "&")
|
|
||||||
.Replace("<", "<")
|
|
||||||
.Replace(">", ">");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -52,5 +52,5 @@ public class LoginModel : PageModel
|
|||||||
public IActionResult OnPostKakao() => Challenge(BuildProps("kakao"), PortalOAuthDefaults.KakaoScheme);
|
public IActionResult OnPostKakao() => Challenge(BuildProps("kakao"), PortalOAuthDefaults.KakaoScheme);
|
||||||
|
|
||||||
private static AuthenticationProperties BuildProps(string provider) =>
|
private static AuthenticationProperties BuildProps(string provider) =>
|
||||||
new() { RedirectUri = $"/taxbaik/portal/external-callback?provider={provider}" };
|
new() { RedirectUri = $"/portal/external-callback?provider={provider}" };
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-12
@@ -38,13 +38,6 @@ builder.Host.UseSerilog((context, config) =>
|
|||||||
outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] {Message:lj}{NewLine}{Exception}")
|
outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] {Message:lj}{NewLine}{Exception}")
|
||||||
.Enrich.FromLogContext()
|
.Enrich.FromLogContext()
|
||||||
.Enrich.WithProperty("Environment", context.HostingEnvironment.EnvironmentName);
|
.Enrich.WithProperty("Environment", context.HostingEnvironment.EnvironmentName);
|
||||||
|
|
||||||
var botToken = context.Configuration["Telegram:BotToken"];
|
|
||||||
var systemChatId = context.Configuration["Telegram:SystemChatId"] ?? context.Configuration["Telegram:ChatId"];
|
|
||||||
if (!string.IsNullOrEmpty(botToken) && !string.IsNullOrEmpty(systemChatId))
|
|
||||||
{
|
|
||||||
config.WriteTo.Sink(new TaxBaik.Web.Logging.TelegramSink(botToken, systemChatId), Serilog.Events.LogEventLevel.Error);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Controllers (API)
|
// Controllers (API)
|
||||||
@@ -96,8 +89,8 @@ var authenticationBuilder = builder.Services.AddAuthentication(opts =>
|
|||||||
opts.Cookie.HttpOnly = true;
|
opts.Cookie.HttpOnly = true;
|
||||||
opts.Cookie.SameSite = SameSiteMode.Lax;
|
opts.Cookie.SameSite = SameSiteMode.Lax;
|
||||||
opts.Cookie.SecurePolicy = isProduction ? CookieSecurePolicy.Always : CookieSecurePolicy.SameAsRequest;
|
opts.Cookie.SecurePolicy = isProduction ? CookieSecurePolicy.Always : CookieSecurePolicy.SameAsRequest;
|
||||||
opts.LoginPath = "/taxbaik/portal/login";
|
opts.LoginPath = "/portal/login";
|
||||||
opts.AccessDeniedPath = "/taxbaik/portal/login";
|
opts.AccessDeniedPath = "/portal/login";
|
||||||
opts.SlidingExpiration = true;
|
opts.SlidingExpiration = true;
|
||||||
opts.ExpireTimeSpan = TimeSpan.FromDays(7);
|
opts.ExpireTimeSpan = TimeSpan.FromDays(7);
|
||||||
})
|
})
|
||||||
@@ -118,7 +111,7 @@ if (!string.IsNullOrWhiteSpace(googleClientId) && !string.IsNullOrWhiteSpace(goo
|
|||||||
opts.SignInScheme = PortalOAuthDefaults.ExternalScheme;
|
opts.SignInScheme = PortalOAuthDefaults.ExternalScheme;
|
||||||
opts.ClientId = googleClientId;
|
opts.ClientId = googleClientId;
|
||||||
opts.ClientSecret = googleClientSecret;
|
opts.ClientSecret = googleClientSecret;
|
||||||
opts.CallbackPath = "/taxbaik/portal/signin-google";
|
opts.CallbackPath = "/portal/signin-google";
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,7 +124,7 @@ if (!string.IsNullOrWhiteSpace(naverClientId) && !string.IsNullOrWhiteSpace(nave
|
|||||||
opts.SignInScheme = PortalOAuthDefaults.ExternalScheme;
|
opts.SignInScheme = PortalOAuthDefaults.ExternalScheme;
|
||||||
opts.ClientId = naverClientId;
|
opts.ClientId = naverClientId;
|
||||||
opts.ClientSecret = naverClientSecret;
|
opts.ClientSecret = naverClientSecret;
|
||||||
opts.CallbackPath = "/taxbaik/portal/signin-naver";
|
opts.CallbackPath = "/portal/signin-naver";
|
||||||
opts.AuthorizationEndpoint = "https://nid.naver.com/oauth2.0/authorize";
|
opts.AuthorizationEndpoint = "https://nid.naver.com/oauth2.0/authorize";
|
||||||
opts.TokenEndpoint = "https://nid.naver.com/oauth2.0/token";
|
opts.TokenEndpoint = "https://nid.naver.com/oauth2.0/token";
|
||||||
opts.UserInformationEndpoint = "https://openapi.naver.com/v1/nid/me";
|
opts.UserInformationEndpoint = "https://openapi.naver.com/v1/nid/me";
|
||||||
@@ -163,7 +156,7 @@ if (!string.IsNullOrWhiteSpace(kakaoClientId) && !string.IsNullOrWhiteSpace(kaka
|
|||||||
opts.SignInScheme = PortalOAuthDefaults.ExternalScheme;
|
opts.SignInScheme = PortalOAuthDefaults.ExternalScheme;
|
||||||
opts.ClientId = kakaoClientId;
|
opts.ClientId = kakaoClientId;
|
||||||
opts.ClientSecret = kakaoClientSecret;
|
opts.ClientSecret = kakaoClientSecret;
|
||||||
opts.CallbackPath = "/taxbaik/portal/signin-kakao";
|
opts.CallbackPath = "/portal/signin-kakao";
|
||||||
opts.AuthorizationEndpoint = "https://kauth.kakao.com/oauth/authorize";
|
opts.AuthorizationEndpoint = "https://kauth.kakao.com/oauth/authorize";
|
||||||
opts.TokenEndpoint = "https://kauth.kakao.com/oauth/token";
|
opts.TokenEndpoint = "https://kauth.kakao.com/oauth/token";
|
||||||
opts.UserInformationEndpoint = "https://kapi.kakao.com/v2/user/me";
|
opts.UserInformationEndpoint = "https://kapi.kakao.com/v2/user/me";
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ public interface ITelegramNotificationService
|
|||||||
Task SendInfoAsync(string title, string message, CancellationToken ct = default);
|
Task SendInfoAsync(string title, string message, CancellationToken ct = default);
|
||||||
Task SendInquiryNotificationAsync(string message, CancellationToken ct = default);
|
Task SendInquiryNotificationAsync(string message, CancellationToken ct = default);
|
||||||
Task SendSystemNotificationAsync(string message, CancellationToken ct = default);
|
Task SendSystemNotificationAsync(string message, CancellationToken ct = default);
|
||||||
Task SendReportAsync(string reportTitle, string reportContent, CancellationToken ct = default);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class TelegramNotificationService : ITelegramNotificationService
|
public class TelegramNotificationService : ITelegramNotificationService
|
||||||
@@ -97,10 +96,4 @@ public class TelegramNotificationService : ITelegramNotificationService
|
|||||||
var text = $"<b>ℹ️ {title}</b>\n\n{message}\n\n<i>{DateTime.UtcNow:yyyy-MM-dd HH:mm:ss} UTC</i>";
|
var text = $"<b>ℹ️ {title}</b>\n\n{message}\n\n<i>{DateTime.UtcNow:yyyy-MM-dd HH:mm:ss} UTC</i>";
|
||||||
await SendMessageAsync(text, ct);
|
await SendMessageAsync(text, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SendReportAsync(string reportTitle, string reportContent, CancellationToken ct = default)
|
|
||||||
{
|
|
||||||
var text = $"<b>📊 {reportTitle}</b>\n\n{reportContent}\n\n<i>{DateTime.UtcNow:yyyy-MM-dd HH:mm:ss} UTC</i>";
|
|
||||||
await SendToChat(_systemChatId, text, ct);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ public class TelegramReportBackgroundService(
|
|||||||
var telegram = scope.ServiceProvider.GetRequiredService<ITelegramNotificationService>();
|
var telegram = scope.ServiceProvider.GetRequiredService<ITelegramNotificationService>();
|
||||||
|
|
||||||
var report = await reportService.BuildDailyReportAsync(date, ct);
|
var report = await reportService.BuildDailyReportAsync(date, ct);
|
||||||
await telegram.SendReportAsync("일간 세무/상담 현황 리포트", TelegramReportService.FormatDailyMessage(report), ct);
|
await telegram.SendSystemNotificationAsync(TelegramReportService.FormatDailyMessage(report), ct);
|
||||||
_lastDailyReportDate = date;
|
_lastDailyReportDate = date;
|
||||||
logger.LogInformation("Daily telegram report sent for {Date}", date);
|
logger.LogInformation("Daily telegram report sent for {Date}", date);
|
||||||
}
|
}
|
||||||
@@ -63,7 +63,7 @@ public class TelegramReportBackgroundService(
|
|||||||
var telegram = scope.ServiceProvider.GetRequiredService<ITelegramNotificationService>();
|
var telegram = scope.ServiceProvider.GetRequiredService<ITelegramNotificationService>();
|
||||||
|
|
||||||
var report = await reportService.BuildWeeklyReportAsync(weekStart, ct);
|
var report = await reportService.BuildWeeklyReportAsync(weekStart, ct);
|
||||||
await telegram.SendReportAsync("주간 세무/매출 종합 리포트", TelegramReportService.FormatWeeklyMessage(report), ct);
|
await telegram.SendSystemNotificationAsync(TelegramReportService.FormatWeeklyMessage(report), ct);
|
||||||
_lastWeeklyReportWeekStart = weekStart;
|
_lastWeeklyReportWeekStart = weekStart;
|
||||||
logger.LogInformation("Weekly telegram report sent for {WeekStart}", weekStart);
|
logger.LogInformation("Weekly telegram report sent for {WeekStart}", weekStart);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user