refactor: move buildable .NET source into src/, update CI/doc paths
TaxBaik CI/CD / build-and-deploy (push) Successful in 2m7s
TaxBaik CI/CD / build-and-deploy (push) Successful in 2m7s
Groups the repo root into src (buildable source), docs (already existed), and everything else (db/, scripts/, tests/, deploy/ - deployment/ops/test assets that aren't compiled, already organized as their own folders). CI now only needs src/ to build: dotnet restore/build/test/publish all point at src/TaxBaik.sln, src/TaxBaik.Web/, src/TaxBaik.Proxy/. - git mv every project (Domain, Infrastructure, Application, Application.Tests, Web, Web.Client, Proxy) and TaxBaik.sln into src/ as a unit, so relative ProjectReference/.sln paths stay valid unchanged. - .gitea/workflows/deploy.yml: 6 dotnet restore/clean/build/test/publish invocations now point at src/. db/migrations and scripts/ stay at root (deploy_gb.sh and browser-e2e.yml only touch published output and the deployed URL, not source paths - verified, no changes needed there). - scripts/validate_admin_render.sh: admin render-mode file paths now src/TaxBaik.Web.Client/... - scripts/validate_kst_timestamps.sh: dropped deploy.sh from its target list - that script was removed in the prior cleanup commit (dead, no CI workflow referenced it) but this validator still expected it to exist. - CLAUDE.md, docs/ENGINEERING_HARNESS.md, docs/ADMIN_PATTERN_CRITIQUE_WBS.md: updated project-structure diagram, dotnet run/build commands, and grep targets to the new src/ paths (also fixed a pre-existing stale path in ADMIN_PATTERN_CRITIQUE_WBS.md that still said TaxBaik.Web/Components/Admin from before that ever moved to TaxBaik.Web.Client). - Added a Repo Root harness rule + Architecture Guardrail entries: new files belong under src/docs/tests/scripts/db/deploy, not loose at root; temp work stays outside the repo (or under a gitignored .scratch/) and is never committed. Verified locally: dotnet build/test src/TaxBaik.sln (26/26 tests), and all three scripts/validate_*.sh pass against the new layout. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
using System.Net.Http.Json;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using TaxBaik.Application.Services;
|
||||
|
||||
namespace TaxBaik.Web.Services;
|
||||
|
||||
public class TelegramInquiryNotificationService : IInquiryNotificationService
|
||||
{
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly ILogger<TelegramInquiryNotificationService> _logger;
|
||||
private readonly string _baseUrl;
|
||||
|
||||
public TelegramInquiryNotificationService(IHttpClientFactory httpClientFactory, IConfiguration configuration, ILogger<TelegramInquiryNotificationService> logger)
|
||||
{
|
||||
_httpClientFactory = httpClientFactory;
|
||||
_configuration = configuration;
|
||||
_logger = logger;
|
||||
_baseUrl = (_configuration["App:PublicBaseUrl"] ?? "http://178.104.200.7/taxbaik").TrimEnd('/');
|
||||
}
|
||||
|
||||
public async Task NotifyCreatedAsync(int inquiryId, string name, string phone, string serviceType, string message, string? ipAddress, DateTime createdAtUtc, CancellationToken ct = default)
|
||||
{
|
||||
var botToken = _configuration["Telegram:BotToken"];
|
||||
var chatId = _configuration["Telegram:InquiryChatId"];
|
||||
if (string.IsNullOrWhiteSpace(chatId))
|
||||
chatId = _configuration["Telegram:ChatId"];
|
||||
if (string.IsNullOrWhiteSpace(botToken) || string.IsNullOrWhiteSpace(chatId))
|
||||
{
|
||||
_logger.LogWarning("텔레그램 새 문의 알림 설정이 누락되었습니다. Telegram:BotToken 또는 Telegram:InquiryChatId/ChatId를 확인하세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
var adminLink = $"{_baseUrl}/admin/inquiries";
|
||||
var summary = message.Length > 180 ? message[..180] + "..." : message;
|
||||
var createdAtKst = createdAtUtc.AddHours(9);
|
||||
var text = new StringBuilder()
|
||||
.AppendLine("🆕 새 문의가 접수되었습니다.")
|
||||
.AppendLine()
|
||||
.AppendLine($"문의 번호: #{inquiryId}")
|
||||
.AppendLine($"제목: {serviceType}")
|
||||
.AppendLine($"이름: {name}")
|
||||
.AppendLine($"연락처: {phone}")
|
||||
.AppendLine($"접수 시각: {createdAtKst:yyyy-MM-dd HH:mm:ss} KST")
|
||||
.AppendLine($"IP: {(string.IsNullOrWhiteSpace(ipAddress) ? "-" : ipAddress)}")
|
||||
.AppendLine()
|
||||
.AppendLine("내용 요약:")
|
||||
.AppendLine(summary)
|
||||
.AppendLine()
|
||||
.AppendLine($"답변 목록 링크: {adminLink}")
|
||||
.ToString();
|
||||
|
||||
var client = _httpClientFactory.CreateClient();
|
||||
var url = $"https://api.telegram.org/bot{botToken}/sendMessage";
|
||||
var payload = new
|
||||
{
|
||||
chat_id = chatId,
|
||||
text,
|
||||
disable_web_page_preview = false
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
var response = await client.PostAsJsonAsync(url, payload, ct);
|
||||
var responseBody = await response.Content.ReadAsStringAsync(ct);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
_logger.LogWarning("텔레그램 알림 전송 실패: {StatusCode} {ResponseBody}", response.StatusCode, Truncate(responseBody));
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation("텔레그램 새 문의 알림 전송 성공: #{InquiryId}, message_id={MessageId}", inquiryId, TryGetMessageId(responseBody));
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "텔레그램 알림 전송 중 오류 발생");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task NotifyStatusChangedAsync(int inquiryId, string name, string phone, string serviceType, string previousStatus, string newStatus, string? changedBy = null, CancellationToken ct = default)
|
||||
{
|
||||
var botToken = _configuration["Telegram:BotToken"];
|
||||
var chatId = _configuration["Telegram:InquiryChatId"];
|
||||
if (string.IsNullOrWhiteSpace(chatId))
|
||||
chatId = _configuration["Telegram:ChatId"];
|
||||
if (string.IsNullOrWhiteSpace(botToken) || string.IsNullOrWhiteSpace(chatId))
|
||||
{
|
||||
_logger.LogWarning("텔레그램 상태 변경 알림 설정이 누락되었습니다. Telegram:BotToken 또는 Telegram:InquiryChatId/ChatId를 확인하세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
var adminLink = $"{_baseUrl}/admin/inquiries";
|
||||
var text = new StringBuilder()
|
||||
.AppendLine("✏️ 문의 상태가 변경되었습니다.")
|
||||
.AppendLine()
|
||||
.AppendLine($"문의 번호: #{inquiryId}")
|
||||
.AppendLine($"제목: {serviceType}")
|
||||
.AppendLine($"이름: {name}")
|
||||
.AppendLine($"연락처: {phone}")
|
||||
.AppendLine($"상태: {FormatStatus(previousStatus)} -> {FormatStatus(newStatus)}")
|
||||
.AppendLine($"변경자: {(string.IsNullOrWhiteSpace(changedBy) ? "-" : changedBy)}")
|
||||
.AppendLine()
|
||||
.AppendLine($"답변 목록 링크: {adminLink}")
|
||||
.ToString();
|
||||
|
||||
var client = _httpClientFactory.CreateClient();
|
||||
var url = $"https://api.telegram.org/bot{botToken}/sendMessage";
|
||||
var payload = new
|
||||
{
|
||||
chat_id = chatId,
|
||||
text,
|
||||
disable_web_page_preview = false
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
var response = await client.PostAsJsonAsync(url, payload, ct);
|
||||
var responseBody = await response.Content.ReadAsStringAsync(ct);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
_logger.LogWarning("텔레그램 상태 변경 알림 실패: {StatusCode} {ResponseBody}", response.StatusCode, Truncate(responseBody));
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation("텔레그램 상태 변경 알림 전송 성공: #{InquiryId}, message_id={MessageId}", inquiryId, TryGetMessageId(responseBody));
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "텔레그램 상태 변경 알림 중 오류 발생");
|
||||
}
|
||||
}
|
||||
|
||||
private static string FormatStatus(string status) => status switch
|
||||
{
|
||||
"new" => "신규",
|
||||
"contacted" => "연락함",
|
||||
"completed" => "완료",
|
||||
_ => status
|
||||
};
|
||||
|
||||
private static string TryGetMessageId(string responseBody)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var document = JsonDocument.Parse(responseBody);
|
||||
if (document.RootElement.TryGetProperty("result", out var result)
|
||||
&& result.TryGetProperty("message_id", out var messageId))
|
||||
{
|
||||
return messageId.ToString();
|
||||
}
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
private static string Truncate(string value)
|
||||
=> value.Length <= 500 ? value : value[..500] + "...";
|
||||
}
|
||||
Reference in New Issue
Block a user