Compare commits

..

4 Commits

5 changed files with 112 additions and 20 deletions
+16 -16
View File
@@ -425,9 +425,9 @@ Todo:
- 텔레그램 전송 실패 시 로그만 남기고 앱 정상 운영 유지 - 텔레그램 전송 실패 시 로그만 남기고 앱 정상 운영 유지
Todo: Todo:
- [ ] BackgroundService 또는 Hangfire 기반 스케줄러 추가 - [x] BackgroundService 또는 Hangfire 기반 스케줄러 추가
- [ ] 일간/주간 리포트 메시지 템플릿 - [x] 일간/주간 리포트 메시지 템플릿
- [ ] TelegramNotificationService에 리포트 메서드 추가 - [x] TelegramNotificationService에 리포트 메서드 추가
## WBS-CRM-07 고객 포털 (읽기 전용) — Phase 3 ## WBS-CRM-07 고객 포털 (읽기 전용) — Phase 3
@@ -439,9 +439,9 @@ Todo:
- 개인정보 열람 범위는 세무사가 허용한 항목만 - 개인정보 열람 범위는 세무사가 허용한 항목만
Todo: Todo:
- [ ] 고객 포털 설계 (인증 방식 결정 — WBS-CRM-08 선행) - [x] 고객 포털 설계 (인증 방식 결정 — WBS-CRM-08 선행)
- [ ] 고객 전용 Razor Pages 추가 - [x] 고객 전용 Razor Pages 추가
- [ ] 세무사 허용 권한 설정 UI - [x] 세무사 허용 권한 설정 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:
- [ ] WBS-CRM-07 고객 포털 기본 구조 완성 (선행) - [x] WBS-CRM-07 고객 포털 기본 구조 완성 (선행)
- [ ] OAuth 앱 등록 (네이버·카카오·구글 개발자 콘솔) - [x] OAuth 앱 등록 (네이버·카카오·구글 개발자 콘솔)
- [ ] V011__CreatePortalUsers.sql 마이그레이션 - [x] V011__CreatePortalUsers.sql 마이그레이션 (실제 V016__CreatePortalUsers.sql로 대체됨)
- [ ] PortalUser 엔티티 / IPortalUserRepository / PortalUserRepository - [x] PortalUser 엔티티 / IPortalUserRepository / PortalUserRepository
- [ ] 네이버 OAuth Handler 구현 - [x] 네이버 OAuth Handler 구현
- [ ] 카카오·구글 패키지 추가 및 설정 - [x] 카카오·구글 패키지 추가 및 설정
- [ ] 기본 계정 회원가입 폼 (`/taxbaik/portal/register`) - [x] 기본 계정 회원가입 폼 (`/taxbaik/portal/register`)
- [ ] 소셜 로그인 콜백 처리 → portal_users 자동 생성 - [x] 소셜 로그인 콜백 처리 → portal_users 자동 생성
- [ ] 신규 가입 시 clients 테이블 연결 또는 신규 생성 - [x] 신규 가입 시 clients 테이블 연결 또는 신규 생성
- [ ] 포털 로그인 페이지 (`/taxbaik/portal/login`) — 소셜 버튼 + 이메일 폼 - [x] 포털 로그인 페이지 (`/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 @bind-Value="model.CategoryId" Label="카테고리" <MudSelect T="int?" @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 Value="@category.Id">@category.Name</MudSelectItem> <MudSelectItem T="int?" Value="@((int?)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 @bind-Value="model.CategoryId" Label="카테고리" <MudSelect T="int?" @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 Value="@category.Id">@category.Name</MudSelectItem> <MudSelectItem T="int?" Value="@((int?)category.Id)">@category.Name</MudSelectItem>
} }
</MudSelect> </MudSelect>
+85
View File
@@ -0,0 +1,85 @@
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("&", "&amp;")
.Replace("<", "&lt;")
.Replace(">", "&gt;");
}
}
+7
View File
@@ -38,6 +38,13 @@ 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)