WeddingPlatform.Web 1.0.0.0
지도, 갤러리, 방명록, 계좌 안내와 배경음악을 링크 하나에 담는 무료 모바일 청첩장 서비스입니다.
로딩중...
검색중...
일치하는것 없음
Program.cs
이 파일의 문서화 페이지로 가기
1using System.IO;
2using System.Net;
3using System.Net.Sockets;
4using Dreamine.Hybrid.Wpf.DependencyInjection;
5using Dreamine.Hybrid.Wpf.Hosting;
6using Dreamine.Identity;
7using Dreamine.Identity.Options;
8using Microsoft.AspNetCore.Components.Server;
9using Microsoft.Extensions.Configuration;
10using Microsoft.Extensions.DependencyInjection;
11using Microsoft.Extensions.Hosting;
12using WeddingPlatform.Blazor;
13using Microsoft.Extensions.Logging;
15
16namespace WeddingPlatform;
17
26public static class Program
27{
36 [STAThread]
37 public static void Main()
38 {
39 try
40 {
41 Run();
42 }
43 catch (Exception ex)
44 {
46 System.Windows.MessageBox.Show(
47 $"Wedding Platform 실행 중 오류가 발생했습니다.\n\n{ex.Message}\n\n자세한 내용은 로그를 확인하세요.",
48 "Wedding Platform",
49 System.Windows.MessageBoxButton.OK,
50 System.Windows.MessageBoxImage.Error);
51 }
52 }
53
62 private static void Run()
63 {
64 HostApplicationBuilder builder = Host.CreateApplicationBuilder();
65 builder.Configuration.AddUserSecrets("codemaru-oauth-2ba4e1b2");
66
67 int serverPort = GetInt(builder.Configuration, "WeddingServer:Port", 5050);
68 bool listenAnyIp = GetBool(builder.Configuration, "WeddingServer:ListenAnyIp", true);
69 bool useEmbeddedWebView = GetBool(builder.Configuration, "WeddingShell:UseEmbeddedWebView", true);
70 if (!IsTcpPortAvailable(serverPort, listenAnyIp))
71 {
72 serverPort = GetFreeLoopbackPort();
73 listenAnyIp = false;
74 }
75
76 AuthOptions authOptions =
77 builder.Configuration.GetSection(AuthOptions.SectionName).Get<AuthOptions>() ?? new AuthOptions();
78 string usersDbPath = ResolvePath(
79 builder.Configuration[$"{AuthOptions.SectionName}:UsersDbPath"],
80 Path.Combine(AppContext.BaseDirectory, "App_Data", "codemaru.db"));
81
82 builder.Services.AddDreamineHybridWpf();
83 builder.Services.AddDreamineIdentityWpfHost();
84
85 // ── Wedding 서비스 ─────────────────────────────────────────
86 var weddingOpts = WeddingOptions.From(builder.Configuration);
87 builder.Services.AddSingleton(weddingOpts);
88 builder.Services.AddSingleton<ITenantStore, JsonTenantStore>();
89 builder.Services.AddSingleton<IGuestbookStorage, CsvGuestbookStorage>();
90 builder.Services.AddSingleton<IGlobalSettingsStore, JsonGlobalSettingsStore>();
91 builder.Services.AddSingleton<IMediaQuotaPolicyResolver, MediaQuotaPolicyResolver>();
93 builder.Services.AddSingleton<IMediaMigrationService, JsonMediaMigrationService>();
94 builder.Services.AddSingleton<IMediaUsageQueryService, FileSystemMediaUsageQueryService>();
95 builder.Services.AddSingleton<ISuperAdminSessionTokenService, SuperAdminSessionTokenService>();
96 builder.Services.AddSingleton<IPhotoService, LocalPhotoService>();
97
98 builder.Services.AddSingleton<Views.MainWindow>();
99 builder.Services.AddHostedService<GhostAccountCleanupService>();
100
101 builder.Services.AddDreamineBlazorServer<AppShell>(options =>
102 {
103 options.Port = serverPort;
104 options.ListenAnyIp = listenAnyIp;
105 options.UseEmbeddedWebView = useEmbeddedWebView;
106 // Wedding 서비스 Blazor 호스트에 공유 (ViewModel은 AutoRegisterViewModels로 자동 등록)
107 options.SharedServiceTypes.Add(typeof(WeddingOptions));
108 options.SharedServiceTypes.Add(typeof(ITenantStore));
109 options.SharedServiceTypes.Add(typeof(IGuestbookStorage));
110 options.SharedServiceTypes.Add(typeof(IGlobalSettingsStore));
111 options.SharedServiceTypes.Add(typeof(IMediaQuotaPolicyResolver));
112 options.SharedServiceTypes.Add(typeof(IImageOptimizationService));
113 options.SharedServiceTypes.Add(typeof(IMediaMigrationService));
114 options.SharedServiceTypes.Add(typeof(IMediaUsageQueryService));
115 options.SharedServiceTypes.Add(typeof(ISuperAdminSessionTokenService));
116 options.SharedServiceTypes.Add(typeof(IPhotoService));
117 // 업로드된 사진을 /wedding-data/ URL로 제공
118 options.AddPhysicalStaticFiles(weddingOpts.ResolvedDataPath, "/wedding-data");
119
120 // InputFile(동영상 등 대용량 업로드)은 SignalR 회선을 통해 청크 단위로 전송되는데,
121 // 기본 SignalR 메시지 크기 제한(32KB)과 짧은 타임아웃 때문에 큰 파일은 매우 느리거나
122 // 응답 없이 멈춘 것처럼 보입니다. 업로드 용량 검증은 LocalPhotoService에서 별도로
123 // 하고 있으므로 전송 한도 자체는 풀어줍니다.
124 options.ConfigureServices = services =>
125 {
126 services.AddServerSideBlazor()
127 .AddHubOptions(o =>
128 {
129 o.MaximumReceiveMessageSize = null; // 무제한 (용량 검증은 LocalPhotoService에서 수행)
130 o.ClientTimeoutInterval = TimeSpan.FromMinutes(10);
131 o.HandshakeTimeout = TimeSpan.FromMinutes(2);
132 o.KeepAliveInterval = TimeSpan.FromSeconds(10);
133 o.EnableDetailedErrors = false;
134 });
135
136 // 삼성 인터넷 등 일부 브라우저가 SameSite 쿠키 정책에 엄격한 경우
137 // Blazor 회로 토큰(antiforgery 쿠키)이 차단될 수 있습니다.
138 // Lax + SameAsRequest 설정으로 호환성을 높입니다.
139 services.AddAntiforgery(o =>
140 {
141 o.Cookie.SameSite = Microsoft.AspNetCore.Http.SameSiteMode.Lax;
142 o.Cookie.SecurePolicy = Microsoft.AspNetCore.Http.CookieSecurePolicy.SameAsRequest;
143 });
144
145 // CircuitOptions: 회선 연결 실패 후 유지 시간 증가 (재접속 시 기존 상태 복원)
146 services.Configure<CircuitOptions>(o =>
147 {
148 o.DisconnectedCircuitMaxRetained = 100;
149 o.DisconnectedCircuitRetentionPeriod = TimeSpan.FromMinutes(3);
150 o.JSInteropDefaultCallTimeout = TimeSpan.FromMinutes(1);
151 o.MaxBufferedUnacknowledgedRenderBatches = 10;
152 });
153
154 services.AddScoped<WeddingUserContext>();
155 };
156
157 options.AddDreamineIdentity(authOptions, usersDbPath);
158 });
159
160 // OG 플랫폼 이미지 자동 생성 (없을 때만)
161 var wwwroot = Path.Combine(AppContext.BaseDirectory, "wwwroot");
162 try { OgImageGenerator.EnsureGenerated(wwwroot); } catch { /* 이미지 생성 실패해도 앱 계속 실행 */ }
163
164 builder.Build().RunDreamineWpfApp<App>();
165 }
166
199 private static bool IsTcpPortAvailable(int port, bool listenAnyIp)
200 {
201 if (port <= 0) return false;
202
203 TcpListener? listener = null;
204 try
205 {
206 listener = new TcpListener(listenAnyIp ? IPAddress.Any : IPAddress.Loopback, port);
207 listener.Start();
208 return true;
209 }
210 catch (SocketException)
211 {
212 return false;
213 }
214 finally
215 {
216 listener?.Stop();
217 }
218 }
219
236 private static int GetFreeLoopbackPort()
237 {
238 var listener = new TcpListener(IPAddress.Loopback, 0);
239 listener.Start();
240 try
241 {
242 return ((IPEndPoint)listener.LocalEndpoint).Port;
243 }
244 finally
245 {
246 listener.Stop();
247 }
248 }
249
266 private static void WriteStartupFailureLog(Exception ex)
267 {
268 try
269 {
270 var dir = Path.Combine(
271 Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
272 "WeddingPlatform",
273 "Logs");
274 Directory.CreateDirectory(dir);
275 var path = Path.Combine(dir, "startup-error.log");
276 File.AppendAllText(path,
277 $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] {ex}\r\n\r\n");
278 }
279 catch
280 {
281 // 마지막 안전망: 오류 로그 작성 실패가 앱 종료 원인이 되지 않게 둡니다.
282 }
283 }
284
325 private static int GetInt(IConfiguration cfg, string key, int fallback) =>
326 int.TryParse(cfg[key], out int v) ? v : fallback;
327
368 private static bool GetBool(IConfiguration cfg, string key, bool fallback) =>
369 bool.TryParse(cfg[key], out bool v) ? v : fallback;
370
403 private static string ResolvePath(string? configuredPath, string fallback)
404 {
405 if (string.IsNullOrWhiteSpace(configuredPath))
406 {
407 return fallback;
408 }
409
410 return Path.IsPathRooted(configuredPath)
411 ? configuredPath
412 : Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, configuredPath));
413 }
414}
static void Run()
Definition Program.cs:62
static bool GetBool(IConfiguration cfg, string key, bool fallback)
Definition Program.cs:368
static bool IsTcpPortAvailable(int port, bool listenAnyIp)
Definition Program.cs:199
static int GetFreeLoopbackPort()
Definition Program.cs:236
static int GetInt(IConfiguration cfg, string key, int fallback)
Definition Program.cs:325
static void Main()
Definition Program.cs:37
static void WriteStartupFailureLog(Exception ex)
Definition Program.cs:266
static string ResolvePath(string? configuredPath, string fallback)
Definition Program.cs:403
static void EnsureGenerated(string wwwrootPath)
static WeddingOptions From(IConfiguration configuration)