2using System.Text.Encodings.Web;
4using Codemaru.Blazor.Pages;
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;
30public static class Program
41 public static void Main()
43 HostApplicationBuilder builder = Host.CreateApplicationBuilder();
48 builder.Configuration.AddUserSecrets(
"codemaru-oauth-2ba4e1b2");
50 int serverPort = GetInt(builder.Configuration,
"CodemaruServer:Port", 5040);
51 bool listenAnyIp = GetBool(builder.Configuration,
"CodemaruServer:ListenAnyIp",
true);
53 AuthOptions authOptions = builder.Configuration
54 .GetSection(AuthOptions.SectionName)
55 .Get<AuthOptions>() ??
new AuthOptions();
57 string usersDbPath = Path.Combine(AppContext.BaseDirectory,
"App_Data",
"codemaru.db");
59 builder.Services.AddDreamineHybridWpf();
60 builder.Services.AddDreamineIdentityWpfHost();
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>();
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>();
91 builder.Services.AddDreamineBlazorServer<AppShell>(options =>
93 options.Port = serverPort;
94 options.ListenAnyIp = listenAnyIp;
95 options.SharedServiceTypes.Add(typeof(CardHybridSession));
96 options.SharedServiceTypes.Add(typeof(ICardProfileStore));
97 options.ConfigureServices = services =>
99 services.AddScoped<CardHybridCircuitSession>();
107 services.Configure<Contact.MailSettings>(builder.Configuration.GetSection(
"MailSettings"));
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));
117 options.AddDreamineIdentity(authOptions, usersDbPath);
121 builder.Build().RunDreamineWpfApp<App>();
133 private static readonly Dictionary<string, (
string Title,
string Desc,
string Image,
string Url)> OgMeta =
new()
136 "CodeMaru - 명함 QR · 청첩장 · 감사장 · 가족 앨범 · CCTV · 쇼핑몰",
137 "명함 QR, 모바일 청첩장, 감사장, 가족 앨범, CCTV 원격 모니터링, 쇼핑몰까지 CodeMaru 계정 하나로 사용하는 디지털 서비스 플랫폼입니다.",
138 "https://codemaru.co.kr/img/codemaru_og.png",
139 "https://codemaru.co.kr/"),
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"),
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"),
171 "CCTV Viewer 이용 설명서 — 실시간 카메라 | CodeMaru",
172 "DreamineVMS 에이전트 설치부터 RTSP 카메라 등록, 공개 라이브 링크 발급까지 단계별 사용 방법을 안내합니다.",
173 "https://codemaru.co.kr/img/codemaru_og.png",
174 "https://codemaru.co.kr/guide/cctv"),
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"),
192 "CodeMaru 서비스에 대한 문의, 제안, 협업 요청을 남겨주세요. 24시간 이내에 답변드립니다.",
193 "https://codemaru.co.kr/img/codemaru_og.png",
194 "https://codemaru.co.kr/contact"),
196 "개인정보처리방침 | CodeMaru",
197 "CodeMaru가 수집·이용·보관하는 개인정보에 대한 처리방침을 안내합니다.",
198 "https://codemaru.co.kr/img/codemaru_og.png",
199 "https://codemaru.co.kr/privacy"),
202 "CodeMaru 서비스 이용에 관한 약관을 안내합니다.",
203 "https://codemaru.co.kr/img/codemaru_og.png",
204 "https://codemaru.co.kr/terms"),
239 private static async Task OgTagMiddleware(HttpContext context, RequestDelegate next)
241 var ua = context.Request.Headers.UserAgent.ToString();
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);
256 var method = context.Request.Method;
257 if (!isBot || (!
string.Equals(method,
"GET", StringComparison.OrdinalIgnoreCase)
258 && !
string.Equals(method,
"HEAD", StringComparison.OrdinalIgnoreCase)))
264 var path = context.Request.Path.Value ??
"/";
265 var normalizedPath = path ==
"/" ?
"/" : path.TrimEnd(
'/');
266 if (!OgMeta.TryGetValue(normalizedPath, out var og))
268 if (ShouldReturnBotNotFound(normalizedPath))
270 await WriteBotNotFoundAsync(context, normalizedPath);
278 context.Response.ContentType =
"text/html; charset=utf-8";
279 await context.Response.WriteAsync($
"""
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}
" />
327 private static bool ShouldReturnBotNotFound(
string path)
329 if (
string.IsNullOrWhiteSpace(path) || path ==
"/")
334 if (Path.HasExtension(path))
339 return !path.StartsWith(
"/card/", StringComparison.OrdinalIgnoreCase);
374 private static async Task WriteBotNotFoundAsync(HttpContext context,
string path)
376 string encodedPath = HtmlEncoder.Default.Encode(path);
378 context.Response.StatusCode = StatusCodes.Status404NotFound;
379 context.Response.ContentType =
"text/html; charset=utf-8";
380 await context.Response.WriteAsync($
"""
384 <meta charset="utf-8
" />
385 <title>페이지를 찾을 수 없습니다 | CodeMaru</title>
386 <meta name="robots
" content="noindex, follow
" />
387 <link rel="canonical
" href="https:
390 <h1>페이지를 찾을 수 없습니다</h1>
391 <p>{encodedPath} 경로는 CodeMaru에서 제공하지 않는 페이지입니다.</p>
392 <p><a href=
"https://codemaru.co.kr/">CodeMaru 홈</a></p>
438 private static int GetInt(IConfiguration configuration, string key, int fallback)
440 string? value = configuration[key];
442 return int.TryParse(value, out int parsed)
487 private static bool GetBool(IConfiguration configuration, string key, bool fallback)
489 string? value = configuration[key];
491 return bool.TryParse(value, out bool parsed)