Codemaru 1.0.0.0
QR 코드, 모바일 랜딩 페이지, vCard 연락처 저장과 명함 디자인을 한 화면에서 제공하는 디지털 명함 서비스입니다.
로딩중...
검색중...
일치하는것 없음
Program.cs
이 파일의 문서화 페이지로 가기
1using System.IO;
2using System.Text.Encodings.Web;
3using Codemaru.Blazor;
4using Codemaru.Blazor.Pages;
10using Codemaru.Views;
11using Dreamine.Hybrid.Wpf.DependencyInjection;
12using Dreamine.Hybrid.Wpf.Hosting;
13using Dreamine.Identity;
14using Dreamine.Identity.Options;
15using Microsoft.AspNetCore.Http;
16using Microsoft.Extensions.Configuration;
17using Microsoft.Extensions.DependencyInjection;
18using Microsoft.Extensions.Hosting;
19
20namespace Codemaru;
21
30public static class Program
31{
40 [STAThread]
41 public static void Main()
42 {
43 HostApplicationBuilder builder = Host.CreateApplicationBuilder();
44
45 // Razor/WPF SDK 조합에서는 [assembly: UserSecretsId] 가 자동으로 심어지지 않는
46 // 경우가 있어 assembly 기반 오버로드가 조용히 실패한다. csproj의 UserSecretsId
47 // 값과 동일한 문자열을 명시적으로 지정해 반드시 로드되도록 한다.
48 builder.Configuration.AddUserSecrets("codemaru-oauth-2ba4e1b2");
49
50 int serverPort = GetInt(builder.Configuration, "CodemaruServer:Port", 5040);
51 bool listenAnyIp = GetBool(builder.Configuration, "CodemaruServer:ListenAnyIp", true);
52
53 AuthOptions authOptions = builder.Configuration
54 .GetSection(AuthOptions.SectionName)
55 .Get<AuthOptions>() ?? new AuthOptions();
56
57 string usersDbPath = Path.Combine(AppContext.BaseDirectory, "App_Data", "codemaru.db");
58
59 builder.Services.AddDreamineHybridWpf();
60 builder.Services.AddDreamineIdentityWpfHost();
61
62 builder.Services.AddSingleton<IQrSvgGenerator, FreeQrSvgGenerator>();
63 builder.Services.AddSingleton<ImageBackgroundRemover>();
64 builder.Services.AddSingleton<LandingPageExporter>();
65 builder.Services.AddSingleton<LandingPagePublisher>();
66 builder.Services.AddSingleton<CardProfileImporter>();
67 builder.Services.AddSingleton<ICardProfileStore, JsonCardProfileStore>();
68 builder.Services.AddSingleton<CardHybridSession>();
69 builder.Services.AddScoped<CardHybridCircuitSession>();
70 builder.Services.Configure<CertificateMonitorOptions>(
71 builder.Configuration.GetSection("CertificateMonitor"));
72 builder.Services.AddSingleton<IProcessRunner, ProcessRunner>();
73 builder.Services.AddSingleton<ICertificateMonitorService, X509CertificateMonitorService>();
74 builder.Services.AddSingleton<IWinAcmeService, WinAcmeService>();
75 builder.Services.AddSingleton<INginxReloadService, NginxReloadService>();
76 builder.Services.AddSingleton<ICertificateSettingsWriter, CertificateSettingsWriter>();
77 builder.Services.AddSingleton<CertificateMonitorViewModel>();
78
79 builder.Services.AddSingleton<MainWindow>();
80 builder.Services.AddSingleton<MainWindowViewModel>();
81 builder.Services.Configure<Contact.MailSettings>(
82 builder.Configuration.GetSection("MailSettings"));
83 builder.Services.Configure<FfmpegOptions>(
84 builder.Configuration.GetSection("Ffmpeg"));
85 builder.Services.Configure<LandingPagePublishOptions>(
86 builder.Configuration.GetSection("CardHybrid:Publish"));
87 builder.Services.Configure<List<StreamConfig>>(
88 builder.Configuration.GetSection("Streams"));
89 builder.Services.AddHostedService<SitePreviewService>();
90
91 builder.Services.AddDreamineBlazorServer<AppShell>(options =>
92 {
93 options.Port = serverPort;
94 options.ListenAnyIp = listenAnyIp;
95 options.SharedServiceTypes.Add(typeof(CardHybridSession));
96 options.SharedServiceTypes.Add(typeof(ICardProfileStore));
97 options.ConfigureServices = services =>
98 {
99 services.AddScoped<CardHybridCircuitSession>();
100
101 // DreamineBlazorServerHostedService는 Blazor 서버용으로 완전히 별도의
102 // DI 컨테이너를 새로 만들기 때문에, 위에서 바깥쪽 호스트에 등록한
103 // Configure<Contact.MailSettings>(...)가 전달되지 않는다(SharedServiceTypes에
104 // 명시한 것만 복사됨). 그래서 Contact.razor가 주입받는 IOptions<MailSettings>가
105 // 항상 기본값(빈 문자열)이 되어 "Host/User/Password 값이 비어 있습니다" 가드가
106 // appsettings.json 내용과 무관하게 항상 걸렸다. 여기서 다시 등록해서 고친다.
107 services.Configure<Contact.MailSettings>(builder.Configuration.GetSection("MailSettings"));
108 };
109 options.SharedServiceTypes.Add(typeof(IQrSvgGenerator));
110 options.SharedServiceTypes.Add(typeof(ImageBackgroundRemover));
111 options.SharedServiceTypes.Add(typeof(LandingPageExporter));
112 options.SharedServiceTypes.Add(typeof(LandingPagePublisher));
113 options.SharedServiceTypes.Add(typeof(CardProfileImporter));
114 options.ConfigurePipeline = app => app.Use(next =>
115 ctx => OgTagMiddleware(ctx, next));
116
117 options.AddDreamineIdentity(authOptions, usersDbPath);
118 });
119
120
121 builder.Build().RunDreamineWpfApp<App>();
122 }
123
124 // 크롤러(카카오톡·네이버·구글 봇 등)가 접근하면 경로별 OG 태그가 포함된 최소 HTML을 반환합니다.
133 private static readonly Dictionary<string, (string Title, string Desc, string Image, string Url)> OgMeta = new()
134 {
135 ["/"] = (
136 "CodeMaru - 명함 QR · 청첩장 · 감사장 · 가족 앨범 · CCTV · 쇼핑몰",
137 "명함 QR, 모바일 청첩장, 감사장, 가족 앨범, CCTV 원격 모니터링, 쇼핑몰까지 CodeMaru 계정 하나로 사용하는 디지털 서비스 플랫폼입니다.",
138 "https://codemaru.co.kr/img/codemaru_og.png",
139 "https://codemaru.co.kr/"),
140 ["/cardhybrid"] = (
141 "CardHybrid — 무료 명함 QR · vCard · 랜딩 페이지 | CodeMaru",
142 "QR 코드 하나로 내 정보를 전달합니다. 스캔만 하면 모바일 랜딩 페이지가 열리고 vCard로 연락처 저장까지. 앞뒤 명함 디자인 편집, AI 배경 제거, SVG 내보내기 무료 제공.",
143 "https://codemaru.co.kr/img/cardhybrid_og.png",
144 "https://codemaru.co.kr/cardhybrid"),
145 ["/guide"] = (
146 "이용 설명서 | CodeMaru",
147 "CardHybrid 명함, Wedding 청첩장, ThankYou 감사장, Families 가족 앨범, CCTV Viewer, Shop Store, Portfolio, Dreamine 프레임워크의 상세 이용 가이드를 제공합니다.",
148 "https://codemaru.co.kr/img/codemaru_og.png",
149 "https://codemaru.co.kr/guide"),
150 ["/guide/cardhybrid"] = (
151 "CardHybrid 이용 설명서 — QR 명함 · vCard · 랜딩 페이지 | CodeMaru",
152 "CardHybrid 시작하기부터 QR 생성, vCard 저장, 명함 디자인 편집, AI 배경 제거, SVG 내보내기까지 단계별 사용 방법을 안내합니다.",
153 "https://codemaru.co.kr/img/cardhybrid_og.png",
154 "https://codemaru.co.kr/guide/cardhybrid"),
155 ["/guide/wedding"] = (
156 "Wedding 이용 설명서 — 디지털 청첩장 만들기 | CodeMaru",
157 "5분이면 완성되는 디지털 청첩장. 지도·갤러리·방명록·배경음악·계좌 안내까지 설정하는 방법을 단계별로 안내합니다.",
158 "https://codemaru.co.kr/img/codemaru_og.png",
159 "https://codemaru.co.kr/guide/wedding"),
160 ["/guide/thankyou"] = (
161 "ThankYou 이용 설명서 — 모바일 감사장 만들기 | CodeMaru",
162 "결혼식 이후 하객에게 전하는 감사 인사 페이지. 사진, 인사말, 계좌 안내, 공유 링크를 정리하는 방법을 안내합니다.",
163 "https://codemaru.co.kr/img/codemaru_og.png",
164 "https://codemaru.co.kr/guide/thankyou"),
165 ["/guide/families"] = (
166 "Families 이용 설명서 — 가족 앨범 플랫폼 | CodeMaru",
167 "가족끼리만 공유하는 비공개 앨범·타임라인. 포스트 작성, 앨범 폴더, 이모지 반응, 댓글 기능 사용법을 안내합니다.",
168 "https://codemaru.co.kr/img/codemaru_og.png",
169 "https://codemaru.co.kr/guide/families"),
170 ["/guide/cctv"] = (
171 "CCTV Viewer 이용 설명서 — 실시간 카메라 | CodeMaru",
172 "DreamineVMS 에이전트 설치부터 RTSP 카메라 등록, 공개 라이브 링크 발급까지 단계별 사용 방법을 안내합니다.",
173 "https://codemaru.co.kr/img/codemaru_og.png",
174 "https://codemaru.co.kr/guide/cctv"),
175 ["/guide/shop"] = (
176 "Shop Store 이용 설명서 — 직영 쇼핑몰 | CodeMaru",
177 "농산물·소프트웨어·개발 용역 구매 방법, 장바구니, 주문·환불 정책 안내.",
178 "https://codemaru.co.kr/img/codemaru_og.png",
179 "https://codemaru.co.kr/guide/shop"),
180 ["/guide/portfolio"] = (
181 "Portfolio 이용 설명서 — 개발자 포트폴리오 | CodeMaru",
182 "장민수 개발자의 프로젝트·이력서·기술 스택 포트폴리오 사이트 구성과 이용 방법을 안내합니다.",
183 "https://codemaru.co.kr/img/codemaru_og.png",
184 "https://codemaru.co.kr/guide/portfolio"),
185 ["/guide/dreamine"] = (
186 "Dreamine 이용 설명서 — MVVM 프레임워크 | CodeMaru",
187 "WPF·Blazor 하이브리드를 위한 오픈소스 MVVM 프레임워크 Dreamine의 패키지 구성과 빠른 시작 방법을 안내합니다.",
188 "https://codemaru.co.kr/img/codemaru_og.png",
189 "https://codemaru.co.kr/guide/dreamine"),
190 ["/contact"] = (
191 "문의하기 | CodeMaru",
192 "CodeMaru 서비스에 대한 문의, 제안, 협업 요청을 남겨주세요. 24시간 이내에 답변드립니다.",
193 "https://codemaru.co.kr/img/codemaru_og.png",
194 "https://codemaru.co.kr/contact"),
195 ["/privacy"] = (
196 "개인정보처리방침 | CodeMaru",
197 "CodeMaru가 수집·이용·보관하는 개인정보에 대한 처리방침을 안내합니다.",
198 "https://codemaru.co.kr/img/codemaru_og.png",
199 "https://codemaru.co.kr/privacy"),
200 ["/terms"] = (
201 "이용약관 | CodeMaru",
202 "CodeMaru 서비스 이용에 관한 약관을 안내합니다.",
203 "https://codemaru.co.kr/img/codemaru_og.png",
204 "https://codemaru.co.kr/terms"),
205 };
206
239 private static async Task OgTagMiddleware(HttpContext context, RequestDelegate next)
240 {
241 var ua = context.Request.Headers.UserAgent.ToString();
242 // 카카오 인앱브라우저(KAKAOTALK/x.x)는 브라우저이므로 제외 — 크롤러만 감지
243 bool isBot = ua.Contains("Kakaotalk-Scrap", StringComparison.OrdinalIgnoreCase)
244 || ua.Contains("kakaostory-og-reader", StringComparison.OrdinalIgnoreCase)
245 || ua.Contains("Naver", StringComparison.OrdinalIgnoreCase)
246 || ua.Contains("Googlebot", StringComparison.OrdinalIgnoreCase)
247 || ua.Contains("Mediapartners-Google", StringComparison.OrdinalIgnoreCase)
248 || ua.Contains("AdsBot-Google", StringComparison.OrdinalIgnoreCase)
249 || ua.Contains("facebookexternalhit", StringComparison.OrdinalIgnoreCase)
250 || ua.Contains("Twitterbot", StringComparison.OrdinalIgnoreCase)
251 || ua.Contains("Discordbot", StringComparison.OrdinalIgnoreCase)
252 || ua.Contains("Slackbot", StringComparison.OrdinalIgnoreCase)
253 || ua.Contains("LinkedInBot", StringComparison.OrdinalIgnoreCase)
254 || ua.Contains("Baiduspider", StringComparison.OrdinalIgnoreCase);
255
256 var method = context.Request.Method;
257 if (!isBot || (!string.Equals(method, "GET", StringComparison.OrdinalIgnoreCase)
258 && !string.Equals(method, "HEAD", StringComparison.OrdinalIgnoreCase)))
259 {
260 await next(context);
261 return;
262 }
263
264 var path = context.Request.Path.Value ?? "/";
265 var normalizedPath = path == "/" ? "/" : path.TrimEnd('/');
266 if (!OgMeta.TryGetValue(normalizedPath, out var og))
267 {
268 if (ShouldReturnBotNotFound(normalizedPath))
269 {
270 await WriteBotNotFoundAsync(context, normalizedPath);
271 return;
272 }
273
274 await next(context);
275 return;
276 }
277
278 context.Response.ContentType = "text/html; charset=utf-8";
279 await context.Response.WriteAsync($"""
280 <!DOCTYPE html>
281 <html lang="ko">
282 <head>
283 <meta charset="utf-8" />
284 <title>{og.Title}</title>
285 <meta name="description" content="{og.Desc}" />
286 <meta property="og:title" content="{og.Title}" />
287 <meta property="og:description" content="{og.Desc}" />
288 <meta property="og:image" content="{og.Image}" />
289 <meta property="og:url" content="{og.Url}" />
290 <meta property="og:type" content="website" />
291 <meta property="og:site_name" content="CodeMaru" />
292 <meta property="og:locale" content="ko_KR" />
293 <meta name="twitter:card" content="summary_large_image" />
294 <meta name="twitter:title" content="{og.Title}" />
295 <meta name="twitter:description" content="{og.Desc}" />
296 <meta name="twitter:image" content="{og.Image}" />
297 </head>
298 <body></body>
299 </html>
300 """);
301 }
302
327 private static bool ShouldReturnBotNotFound(string path)
328 {
329 if (string.IsNullOrWhiteSpace(path) || path == "/")
330 {
331 return false;
332 }
333
334 if (Path.HasExtension(path))
335 {
336 return false;
337 }
338
339 return !path.StartsWith("/card/", StringComparison.OrdinalIgnoreCase);
340 }
341
374 private static async Task WriteBotNotFoundAsync(HttpContext context, string path)
375 {
376 string encodedPath = HtmlEncoder.Default.Encode(path);
377
378 context.Response.StatusCode = StatusCodes.Status404NotFound;
379 context.Response.ContentType = "text/html; charset=utf-8";
380 await context.Response.WriteAsync($"""
381 <!DOCTYPE html>
382 <html lang="ko">
383 <head>
384 <meta charset="utf-8" />
385 <title>페이지를 찾을 수 없습니다 | CodeMaru</title>
386 <meta name="robots" content="noindex, follow" />
387 <link rel="canonical" href="https://codemaru.co.kr/404" />
388 </head>
389 <body>
390 <h1>페이지를 찾을 수 없습니다</h1>
391 <p>{encodedPath} 경로는 CodeMaru에서 제공하지 않는 페이지입니다.</p>
392 <p><a href="https://codemaru.co.kr/">CodeMaru 홈</a></p>
393 </body>
394 </html>
395 """);
396 }
397
438 private static int GetInt(IConfiguration configuration, string key, int fallback)
439 {
440 string? value = configuration[key];
441
442 return int.TryParse(value, out int parsed)
443 ? parsed
444 : fallback;
445 }
446
487 private static bool GetBool(IConfiguration configuration, string key, bool fallback)
488 {
489 string? value = configuration[key];
490
491 return bool.TryParse(value, out bool parsed)
492 ? parsed
493 : fallback;
494 }
495}