Handler.cs 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367
  1. using Application.Abstractions.Messaging;
  2. using Application.Abstractions.Crypto;
  3. using Application.Abstractions.Data;
  4. using Application.Abstractions.Cache;
  5. using Domain.Entities.Common;
  6. using SharedKernel.Storage;
  7. using Microsoft.AspNetCore.Http;
  8. using Microsoft.EntityFrameworkCore;
  9. namespace Application.Features.Config.Update;
  10. public sealed class Handler(IAppDbContext db, IFileStorage storage, IEditorImageService editorImage, ICacheService cache, IFieldEncryptor encryptor) : ICommandHandler<Command>
  11. {
  12. public async Task Handle(Command request, CancellationToken ct)
  13. {
  14. var config = await db.Config.OrderByDescending(x => x.ID).FirstOrDefaultAsync(ct);
  15. if (config is null)
  16. {
  17. config = Domain.Entities.Common.Config.Create();
  18. db.Config.Add(config);
  19. }
  20. var basic = new BasicConfig { };
  21. var images = new ImagesConfig { };
  22. var meta = new MetaConfig { };
  23. var company = new CompanyConfig { };
  24. var account = new AccountConfig { };
  25. var emailTemplate = new EmailTemplateConfig { };
  26. var external = new ExternalApiConfig { };
  27. if (request.Basic != null)
  28. {
  29. basic.SiteName = request.Basic.SiteName;
  30. basic.SiteURL = request.Basic.SiteURL;
  31. basic.RootID = request.Basic.RootID;
  32. basic.FromEmail = request.Basic.FromEmail;
  33. basic.FromName = request.Basic.FromName;
  34. basic.SmtpServer = request.Basic.SmtpServer;
  35. basic.SmtpPort = request.Basic.SmtpPort;
  36. basic.SmtpEnableSSL = request.Basic.SmtpEnableSSL;
  37. basic.SmtpUsername = request.Basic.SmtpUsername;
  38. basic.SmtpPassword = request.Basic.SmtpPassword;
  39. basic.AdminWhiteIPList = request.Basic.AdminWhiteIPList;
  40. basic.FrontWhiteIPList = request.Basic.FrontWhiteIPList;
  41. basic.BlockAlertTitle = request.Basic.BlockAlertTitle;
  42. basic.BlockAlertContent = await SaveBytesAsync(request.Basic.BlockAlertContent, UploadFolder.Basic, UploadAddition.BlockAlertContent, ct);
  43. basic.IsMaintenance = request.Basic.IsMaintenance;
  44. basic.MaintenanceContent = await SaveBytesAsync(request.Basic.MaintenanceContent, UploadFolder.Basic, UploadAddition.MaintenanceContent, ct);
  45. basic.NoteDailySendLimit = request.Basic.NoteDailySendLimit;
  46. } else {
  47. basic = config.Basic;
  48. }
  49. if (request.Images != null)
  50. {
  51. var del = request.ImagesDelete ?? new Command.ImagesDeleteFlags();
  52. images.Favicon = await ApplyImageAsync(del.Favicon, config.Images.Favicon, request.Images.FaviconFile, [".ico"], ct);
  53. images.LogoSquare = await ApplyImageAsync(del.LogoSquare, config.Images.LogoSquare, request.Images.LogoSquareFile, [".jpg", ".jpeg", ".png", ".gif", ".webp", ".svg"], ct);
  54. images.LogoHorizontal = await ApplyImageAsync(del.LogoHorizontal, config.Images.LogoHorizontal, request.Images.LogoHorizontalFile, [".jpg", ".jpeg", ".png", ".gif", ".webp", ".svg"], ct);
  55. images.OgDefault = await ApplyImageAsync(del.OgDefault, config.Images.OgDefault, request.Images.OgDefaultFile, [".jpg", ".jpeg", ".png", ".gif", ".webp"], ct);
  56. images.TwitterImage = await ApplyImageAsync(del.TwitterImage, config.Images.TwitterImage, request.Images.TwitterImageFile, [".jpg", ".jpeg", ".png", ".gif", ".webp"], ct);
  57. images.AppleTouchIcon = await ApplyImageAsync(del.AppleTouchIcon, config.Images.AppleTouchIcon, request.Images.AppleTouchIconFile, [".jpg", ".jpeg", ".png", ".gif", ".webp"], ct);
  58. images.AppIcon_192 = await ApplyImageAsync(del.AppIcon192, config.Images.AppIcon_192, request.Images.AppIcon192File, [".jpg", ".jpeg", ".png", ".gif", ".webp"], ct);
  59. images.AppIcon_512 = await ApplyImageAsync(del.AppIcon512, config.Images.AppIcon_512, request.Images.AppIcon512File, [".jpg", ".jpeg", ".png", ".gif", ".webp"], ct);
  60. } else {
  61. images = config.Images;
  62. }
  63. if (request.Meta != null)
  64. {
  65. meta.Keywords = request.Meta.Keywords;
  66. meta.Description = request.Meta.Description;
  67. meta.Author = request.Meta.Author;
  68. meta.Viewport = request.Meta.Viewport;
  69. meta.ApplicationName = request.Meta.ApplicationName;
  70. meta.Generator = request.Meta.Generator;
  71. meta.Robots = request.Meta.Robots;
  72. meta.Adds = request.Meta.Adds;
  73. }
  74. else
  75. {
  76. meta = config.Meta;
  77. }
  78. if (request.Company != null)
  79. {
  80. company.Name = request.Company.Name;
  81. company.RegNo = request.Company.RegNo;
  82. company.Address = request.Company.Address;
  83. company.ZipCode = request.Company.ZipCode;
  84. company.Owner = request.Company.Owner;
  85. company.Tel = request.Company.Tel;
  86. company.Fax = request.Company.Fax;
  87. company.RetailSaleNo = request.Company.RetailSaleNo;
  88. company.AddedSaleNo = request.Company.AddedSaleNo;
  89. company.Hosting = request.Company.Hosting;
  90. company.AdminName = request.Company.AdminName;
  91. company.AdminEmail = request.Company.AdminEmail;
  92. company.SiteUrl = request.Company.SiteUrl;
  93. company.BankCode = request.Company.BankCode;
  94. company.BankOwner = request.Company.BankOwner;
  95. company.BankNumber = request.Company.BankNumber;
  96. } else {
  97. company = config.Company;
  98. }
  99. if (request.Account != null)
  100. {
  101. account.IsRegisterBlock = request.Account.IsRegisterBlock;
  102. account.IsRegisterEmailAuth = request.Account.IsRegisterEmailAuth;
  103. account.PasswordMinLength = request.Account.PasswordMinLength;
  104. account.PasswordUppercaseLength = request.Account.PasswordUppercaseLength;
  105. account.PasswordNumbersLength = request.Account.PasswordNumbersLength;
  106. account.PasswordSpecialcharsLength = request.Account.PasswordSpecialcharsLength;
  107. account.DeniedEmailList = request.Account.DeniedEmailList;
  108. account.DeniedNameList = request.Account.DeniedNameList;
  109. account.ChangeEmailDay = request.Account.ChangeEmailDay;
  110. account.ChangeNameDay = request.Account.ChangeNameDay;
  111. account.ChangeSummaryDay = request.Account.ChangeSummaryDay;
  112. account.ChangeIntroDay = request.Account.ChangeIntroDay;
  113. account.ChangePasswordDay = request.Account.ChangePasswordDay;
  114. account.IsLoginEmailVerifiedOnly = request.Account.IsLoginEmailVerifiedOnly;
  115. account.MaxLoginTryCount = request.Account.MaxLoginTryCount;
  116. account.MaxLoginTryLimitSecond = request.Account.MaxLoginTryLimitSecond;
  117. } else {
  118. account = config.Account;
  119. }
  120. if (request.EmailTemplate != null)
  121. {
  122. emailTemplate.RegisterEmailFormTitle = request.EmailTemplate.RegisterEmailFormTitle;
  123. emailTemplate.RegisterEmailFormContent = await SaveBytesAsync(request.EmailTemplate.RegisterEmailFormContent, UploadFolder.EmailTemplate, UploadAddition.RegisterEmailFormContent, ct);
  124. emailTemplate.RegistrationEmailFormTitle = request.EmailTemplate.RegistrationEmailFormTitle;
  125. emailTemplate.RegistrationEmailFormContent = await SaveBytesAsync(request.EmailTemplate.RegistrationEmailFormContent, UploadFolder.EmailTemplate, UploadAddition.RegistrationEmailFormContent, ct);
  126. emailTemplate.ResetPasswordEmailFormTitle = request.EmailTemplate.ResetPasswordEmailFormTitle;
  127. emailTemplate.ResetPasswordEmailFormContent = await SaveBytesAsync(request.EmailTemplate.ResetPasswordEmailFormContent, UploadFolder.EmailTemplate, UploadAddition.ResetPasswordEmailFormContent, ct);
  128. emailTemplate.ChangedPasswordEmailFormTitle = request.EmailTemplate.ChangedPasswordEmailFormTitle;
  129. emailTemplate.ChangedPasswordEmailFormContent = await SaveBytesAsync(request.EmailTemplate.ChangedPasswordEmailFormContent, UploadFolder.EmailTemplate, UploadAddition.ChangedPasswordEmailFormContent, ct);
  130. emailTemplate.WithdrawEmailFormTitle = request.EmailTemplate.WithdrawEmailFormTitle;
  131. emailTemplate.WithdrawEmailFormContent = await SaveBytesAsync(request.EmailTemplate.WithdrawEmailFormContent, UploadFolder.EmailTemplate, UploadAddition.WithdrawEmailFormContent, ct);
  132. emailTemplate.WithdrawVerifyEmailFormTitle = request.EmailTemplate.WithdrawVerifyEmailFormTitle;
  133. emailTemplate.WithdrawVerifyEmailFormContent = await SaveBytesAsync(request.EmailTemplate.WithdrawVerifyEmailFormContent, UploadFolder.EmailTemplate, UploadAddition.WithdrawVerifyEmailFormContent, ct);
  134. emailTemplate.EmailVerifyFormTitle = request.EmailTemplate.EmailVerifyFormTitle;
  135. emailTemplate.EmailVerifyFormContent = await SaveBytesAsync(request.EmailTemplate.EmailVerifyFormContent, UploadFolder.EmailTemplate, UploadAddition.EmailVerifyFormContent, ct);
  136. emailTemplate.ChangedEmailFormTitle = request.EmailTemplate.ChangedEmailFormTitle;
  137. emailTemplate.ChangedEmailFormContent = await SaveBytesAsync(request.EmailTemplate.ChangedEmailFormContent, UploadFolder.EmailTemplate, UploadAddition.ChangedEmailFormContent, ct);
  138. } else {
  139. emailTemplate = config.EmailTemplate;
  140. }
  141. if (request.External != null)
  142. {
  143. external.YouTubeApiKeyEnc = request.External.YouTubeApiKeyEnc;
  144. external.YouTubeApiName = request.External.YouTubeApiName;
  145. external.GoogleClientId = request.External.GoogleClientId;
  146. external.GoogleClientSecretEnc = request.External.GoogleClientSecretEnc;
  147. external.GoogleAppId = request.External.GoogleAppId;
  148. // 소셜 로그인 on/off (체크박스)
  149. external.NaverLoginEnabled = request.External.NaverLoginEnabled;
  150. external.KakaoLoginEnabled = request.External.KakaoLoginEnabled;
  151. external.GoogleLoginEnabled = request.External.GoogleLoginEnabled;
  152. // 소셜 로그인(OAuth) — ID/JS 키는 직접 저장, Secret 은 ProtectOrKeep(빈 값=유지, 입력=암호화)
  153. external.NaverClientId = request.External.NaverClientId;
  154. external.NaverClientSecretEnc = ProtectOrKeep(request.External.NaverClientSecret, config.External.NaverClientSecretEnc);
  155. external.KakaoRestApiKeyEnc = ProtectOrKeep(request.External.KakaoRestApiKey, config.External.KakaoRestApiKeyEnc);
  156. external.KakaoJavascriptKey = request.External.KakaoJavascriptKey;
  157. external.KakaoAdminKeyEnc = ProtectOrKeep(request.External.KakaoAdminKey, config.External.KakaoAdminKeyEnc);
  158. external.NaverSearchClientId = request.External.NaverSearchClientId;
  159. external.NaverSearchClientSecretEnc = ProtectOrKeep(request.External.NaverSearchClientSecret, config.External.NaverSearchClientSecretEnc);
  160. // 토스 필드는 결제 설정 화면(request.Toss) 소관 — External 저장 시 보존
  161. external.TossPayMode = config.External.TossPayMode;
  162. external.TossTestClientKeyEnc = config.External.TossTestClientKeyEnc;
  163. external.TossTestSecretKeyEnc = config.External.TossTestSecretKeyEnc;
  164. external.TossLiveClientKeyEnc = config.External.TossLiveClientKeyEnc;
  165. external.TossLiveSecretKeyEnc = config.External.TossLiveSecretKeyEnc;
  166. } else {
  167. external = config.External;
  168. }
  169. if (request.Toss != null)
  170. {
  171. // 키는 빈 값 = 기존 유지, 입력 시 암호화 저장 (이미 암호문 포맷이면 이중 암호화 방지 위해 그대로 저장)
  172. external.TossPayMode = request.Toss.TossPayMode;
  173. external.TossTestClientKeyEnc = ProtectOrKeep(request.Toss.TestClientKey, external.TossTestClientKeyEnc);
  174. external.TossTestSecretKeyEnc = ProtectOrKeep(request.Toss.TestSecretKey, external.TossTestSecretKeyEnc);
  175. external.TossLiveClientKeyEnc = ProtectOrKeep(request.Toss.LiveClientKey, external.TossLiveClientKeyEnc);
  176. external.TossLiveSecretKeyEnc = ProtectOrKeep(request.Toss.LiveSecretKey, external.TossLiveSecretKeyEnc);
  177. }
  178. var attendance = new AttendanceConfig { };
  179. if (request.Attendance != null)
  180. {
  181. attendance.IsEnabled = request.Attendance.IsEnabled;
  182. attendance.BaseExp = request.Attendance.BaseExp;
  183. attendance.BasePoint = request.Attendance.BasePoint;
  184. attendance.UseStreakBonus = request.Attendance.UseStreakBonus;
  185. attendance.StreakBonusPerDay = request.Attendance.StreakBonusPerDay;
  186. attendance.StreakBonusPointPerDay = request.Attendance.StreakBonusPointPerDay;
  187. attendance.StreakBonusMaxDays = request.Attendance.StreakBonusMaxDays;
  188. attendance.UseRankBonus = request.Attendance.UseRankBonus;
  189. attendance.RankBonusConfig = request.Attendance.RankBonusConfig;
  190. } else {
  191. attendance = config.Attendance;
  192. }
  193. var signupReward = new SignupRewardConfig { };
  194. if (request.SignupReward != null)
  195. {
  196. signupReward.Enabled = request.SignupReward.Enabled;
  197. signupReward.CoinAmount = request.SignupReward.CoinAmount;
  198. signupReward.CashAmount = request.SignupReward.CashAmount;
  199. signupReward.ExpAmount = request.SignupReward.ExpAmount;
  200. } else {
  201. signupReward = config.SignupReward;
  202. }
  203. config.Update(
  204. basic,
  205. images,
  206. meta,
  207. company,
  208. account,
  209. emailTemplate,
  210. external,
  211. config.Crypto,
  212. attendance,
  213. signupReward
  214. );
  215. // 보상 엔진 설정은 별도 메서드로 갱신 — 다른 섹션 저장 시 null 덮어쓰기 방지 (ChatExp/Paper 패턴)
  216. if (request.Reward != null)
  217. {
  218. config.UpdateReward(new RewardConfig
  219. {
  220. PostExp = request.Reward.PostExp,
  221. PostPoint = request.Reward.PostPoint,
  222. CommentExp = request.Reward.CommentExp,
  223. CommentPoint = request.Reward.CommentPoint,
  224. LikeGivenExp = request.Reward.LikeGivenExp,
  225. LikeReceivedPoint = request.Reward.LikeReceivedPoint,
  226. MinPostLength = request.Reward.MinPostLength,
  227. MinCommentLength = request.Reward.MinCommentLength,
  228. NewMemberHoldDays = request.Reward.NewMemberHoldDays,
  229. PostDailyCount = request.Reward.PostDailyCount,
  230. PostDailyExp = request.Reward.PostDailyExp,
  231. PostDailyPoint = request.Reward.PostDailyPoint,
  232. CommentDailyCount = request.Reward.CommentDailyCount,
  233. CommentDailyExp = request.Reward.CommentDailyExp,
  234. CommentDailyPoint = request.Reward.CommentDailyPoint,
  235. LikeGivenDailyCount = request.Reward.LikeGivenDailyCount,
  236. LikeGivenDailyExp = request.Reward.LikeGivenDailyExp,
  237. LikeReceivedDailyCount = request.Reward.LikeReceivedDailyCount,
  238. LikeReceivedDailyPoint = request.Reward.LikeReceivedDailyPoint,
  239. ChatDailyCount = request.Reward.ChatDailyCount,
  240. ChatDailyExp = request.Reward.ChatDailyExp,
  241. CheerFeePercent = request.Reward.CheerFeePercent,
  242. CheerMinAmount = request.Reward.CheerMinAmount,
  243. CheerDailyMax = request.Reward.CheerDailyMax
  244. });
  245. }
  246. // 모의투자 설정도 별도 메서드로 갱신 — 부분 저장 시 null 덮어쓰기 방지 (d4 M4)
  247. if (request.Paper != null)
  248. {
  249. config.UpdatePaper(new PaperConfig
  250. {
  251. Enabled = request.Paper.Enabled,
  252. FeeRateBp = request.Paper.FeeRateBp,
  253. TaxRateBp = request.Paper.TaxRateBp,
  254. MinDeposit = request.Paper.MinDeposit,
  255. MaxHolding = request.Paper.MaxHolding,
  256. WithdrawProfitBurnBp = request.Paper.WithdrawProfitBurnBp,
  257. OrderMaxPctBp = request.Paper.OrderMaxPctBp,
  258. MinFillsForRank = request.Paper.MinFillsForRank
  259. });
  260. }
  261. // 데이터 수집 API 키 + 수집기 활성화 — 별도 메서드(부분 저장 시 null 덮어쓰기 방지). 키는 ProtectOrKeep(빈 값=유지).
  262. if (request.DataCollection != null)
  263. {
  264. config.UpdateDataCollection(new DataCollectionConfig
  265. {
  266. DataGoKrServiceKeyEnc = ProtectOrKeep(request.DataCollection.DataGoKrServiceKey, config.DataCollection.DataGoKrServiceKeyEnc),
  267. KrxApiKeyEnc = ProtectOrKeep(request.DataCollection.KrxApiKey, config.DataCollection.KrxApiKeyEnc),
  268. OpenDartApiKeyEnc = ProtectOrKeep(request.DataCollection.OpenDartApiKey, config.DataCollection.OpenDartApiKeyEnc),
  269. KoreaEximExchangeKeyEnc = ProtectOrKeep(request.DataCollection.KoreaEximExchangeKey, config.DataCollection.KoreaEximExchangeKeyEnc),
  270. KoreaEximLoanRateKeyEnc = ProtectOrKeep(request.DataCollection.KoreaEximLoanRateKey, config.DataCollection.KoreaEximLoanRateKeyEnc),
  271. KoreaEximIntlRateKeyEnc = ProtectOrKeep(request.DataCollection.KoreaEximIntlRateKey, config.DataCollection.KoreaEximIntlRateKeyEnc),
  272. SeibroApiKeyEnc = ProtectOrKeep(request.DataCollection.SeibroApiKey, config.DataCollection.SeibroApiKeyEnc),
  273. KosisApiKeyEnc = ProtectOrKeep(request.DataCollection.KosisApiKey, config.DataCollection.KosisApiKeyEnc),
  274. StockDataMasterSync = request.DataCollection.StockDataMasterSync,
  275. StockDataDailyPriceSync = request.DataCollection.StockDataDailyPriceSync,
  276. KrxIndexSync = request.DataCollection.KrxIndexSync,
  277. KrxBondIndexSync = request.DataCollection.KrxBondIndexSync,
  278. KrxStockSync = request.DataCollection.KrxStockSync,
  279. KrxEtpSync = request.DataCollection.KrxEtpSync,
  280. KrxWarrantSync = request.DataCollection.KrxWarrantSync,
  281. KrxBondSync = request.DataCollection.KrxBondSync,
  282. KrxDerivativeSync = request.DataCollection.KrxDerivativeSync,
  283. KrxCommoditySync = request.DataCollection.KrxCommoditySync,
  284. KrxEsgSync = request.DataCollection.KrxEsgSync,
  285. OpenDartDisclosureSync = request.DataCollection.OpenDartDisclosureSync,
  286. KoreaEximMacroSync = request.DataCollection.KoreaEximMacroSync,
  287. KosisSync = request.DataCollection.KosisSync,
  288. WorldIndexEnabled = request.DataCollection.WorldIndexEnabled,
  289. MarketQuoteEnabled = request.DataCollection.MarketQuoteEnabled,
  290. SeibroIssuerSync = request.DataCollection.SeibroIssuerSync,
  291. SeibroDividendSync = request.DataCollection.SeibroDividendSync,
  292. SeibroSupplySync = request.DataCollection.SeibroSupplySync,
  293. SeibroCorpActionSync = request.DataCollection.SeibroCorpActionSync,
  294. SeibroBondSync = request.DataCollection.SeibroBondSync,
  295. SeibroDerivSync = request.DataCollection.SeibroDerivSync,
  296. SeibroForeignSync = request.DataCollection.SeibroForeignSync
  297. });
  298. }
  299. await db.SaveChangesAsync(ct);
  300. await cache.RemoveAsync(CacheKeys.Config, ct);
  301. }
  302. /// <summary>키 입력값이 비어있으면 기존 값 유지, 입력됐으면 암호화 저장. 이미 암호문 포맷이면 재암호화하지 않는다.</summary>
  303. private string? ProtectOrKeep(string? input, string? current)
  304. {
  305. if (string.IsNullOrWhiteSpace(input))
  306. {
  307. return current;
  308. }
  309. var trimmed = input.Trim();
  310. return EncryptedConfigValue.IsProtected(trimmed) ? trimmed : EncryptedConfigValue.Protect(encryptor, trimmed);
  311. }
  312. private async Task<string?> ApplyImageAsync(bool deleteRequested, string? currentUrl, IFormFile? newFile, string[] allowedExtensions, CancellationToken ct)
  313. {
  314. if (deleteRequested)
  315. {
  316. storage.DeleteByUrl(currentUrl);
  317. return null;
  318. }
  319. var uploaded = await SaveFileAsync(newFile, allowedExtensions, ct);
  320. return uploaded ?? currentUrl;
  321. }
  322. private async Task<string?> SaveFileAsync(IFormFile? file, string[] allowedExtensions, CancellationToken ct)
  323. {
  324. var path = new FileStoragePath(UploadTarget.Upload, UploadFolder.Basic, null, null);
  325. var result = await storage.SaveFileAsync(file, path, allowedExtensions, ct);
  326. return result?.Url;
  327. }
  328. private async Task<string?> SaveBytesAsync(string? html, UploadFolder folder, UploadAddition? addition = null, CancellationToken ct = default)
  329. {
  330. var path = new FileStoragePath(UploadTarget.Editor, folder, null, addition);
  331. return await editorImage.UploadAsync(html, path, ct);
  332. }
  333. }