Quellcode durchsuchen

feat(config): 수집기/OAuth 키 Admin 복호화 표시 + /api/config 암호문 유지 + External UI 통일

- 공개 /api/config(GetConfig): 키/시크릿을 암호문(enc) 그대로 반환 — 복호화 안 함(보안 유지)
- Admin /Config/External: PageModel 이 IFieldEncryptor 로 로컬 복호화해 실제 키 표시
- Request DTO init→set (PageModel prefill 용)
- External.cshtml: 데이터 수집 6개 섹션을 금융위 패턴(키행 + API 라벨행 + 체크박스 인라인 + hr)으로 통일, 마스킹 placeholder 제거
- 빌드 0에러, 461 테스트 그린

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
KIM-JINO5 vor 2 Wochen
Ursprung
Commit
1cdc66f2e1

+ 155 - 113
Admin/Pages/Config/External.cshtml

@@ -5,7 +5,7 @@
     ViewData["Title"] = "API 설정";
 }
 
-<div class="container">
+<hr class="container">
     <h3>@ViewData["Title"]</h3>
     <hr />
 
@@ -90,11 +90,11 @@
             </div>
             <div class="row mb-2">
                 <label asp-for="Input.External.NaverClientSecret" class="col-sm-3 col-form-label">네이버 Client Secret</label>
-                <div class="col-sm-9"><input asp-for="Input.External.NaverClientSecret" type="text" class="form-control" autocomplete="off" placeholder="저장된 키 유지 — 변경 시에만 입력" /></div>
+                <div class="col-sm-9"><input asp-for="Input.External.NaverClientSecret" type="text" class="form-control" autocomplete="off" /></div>
             </div>
             <div class="row mb-2">
                 <label asp-for="Input.External.KakaoRestApiKey" class="col-sm-3 col-form-label">카카오 REST API 키</label>
-                <div class="col-sm-9"><input asp-for="Input.External.KakaoRestApiKey" type="text" class="form-control" autocomplete="off" placeholder="저장된 키 유지 — 변경 시에만 입력" /></div>
+                <div class="col-sm-9"><input asp-for="Input.External.KakaoRestApiKey" type="text" class="form-control" autocomplete="off" /></div>
             </div>
             <div class="row mb-2">
                 <label asp-for="Input.External.KakaoJavascriptKey" class="col-sm-3 col-form-label">카카오 JavaScript 키</label>
@@ -102,7 +102,7 @@
             </div>
             <div class="row mb-2">
                 <label asp-for="Input.External.KakaoAdminKey" class="col-sm-3 col-form-label">카카오 Admin 키</label>
-                <div class="col-sm-9"><input asp-for="Input.External.KakaoAdminKey" type="text" class="form-control" autocomplete="off" placeholder="저장된 키 유지 — 변경 시에만 입력" /></div>
+                <div class="col-sm-9"><input asp-for="Input.External.KakaoAdminKey" type="text" class="form-control" autocomplete="off" /></div>
             </div>
             <div class="row mb-2">
                 <label asp-for="Input.External.NaverSearchClientId" class="col-sm-3 col-form-label">네이버검색 Client ID</label>
@@ -110,7 +110,7 @@
             </div>
             <div class="row mb-2">
                 <label asp-for="Input.External.NaverSearchClientSecret" class="col-sm-3 col-form-label">네이버검색 Client Secret</label>
-                <div class="col-sm-9"><input asp-for="Input.External.NaverSearchClientSecret" type="text" class="form-control" autocomplete="off" placeholder="저장된 키 유지 — 변경 시에만 입력" /></div>
+                <div class="col-sm-9"><input asp-for="Input.External.NaverSearchClientSecret" type="text" class="form-control" autocomplete="off" /></div>
             </div>
         </details>
 
@@ -123,142 +123,184 @@
                 키는 빈 값 저장 시 기존 값 유지(변경 시에만 입력). 활성화 체크를 켜면 재배포 없이 다음 예정 시각에 해당 수집기가 동작합니다. (세계지수·주요종목은 무키 — Yahoo)
             </div>
 
-            <h6 class="mt-3">금융위 (data.go.kr)</h6>
+            <hr/>
+            <h6 class="fw-bold mt-3">금융위 (data.go.kr)</h6>
             <div class="row mb-2">
-                <label asp-for="Input.DataCollection.DataGoKrServiceKey" class="col-sm-3 col-form-label">ServiceKey</label>
-                <div class="col-sm-9"><input asp-for="Input.DataCollection.DataGoKrServiceKey" type="text" class="form-control" autocomplete="off" placeholder="저장된 키 유지 — 변경 시에만 입력" /></div>
+                <label asp-for="Input.DataCollection.DataGoKrServiceKey" class="col-sm-2 col-form-label">ServiceKey</label>
+                <div class="col-sm-9"><input asp-for="Input.DataCollection.DataGoKrServiceKey" type="text" class="form-control" autocomplete="off" /></div>
             </div>
-            <div class="form-check form-check-inline">
-                <input asp-for="Input.DataCollection.StockDataMasterSync" class="form-check-input" type="checkbox" />
-                <label asp-for="Input.DataCollection.StockDataMasterSync" class="form-check-label">종목 마스터</label>
-            </div>
-            <div class="form-check form-check-inline">
-                <input asp-for="Input.DataCollection.StockDataDailyPriceSync" class="form-check-input" type="checkbox" />
-                <label asp-for="Input.DataCollection.StockDataDailyPriceSync" class="form-check-label">일별 시세</label>
+            <div class="row mb-2 align-items-center">
+                <label asp-for="Input.DataCollection.DataGoKrServiceKey" class="col-sm-2 col-form-label">API</label>
+                <div class="col-sm-10">
+                    <div class="form-check form-check-inline">
+                        <input asp-for="Input.DataCollection.StockDataMasterSync" class="form-check-input" type="checkbox" />
+                        <label asp-for="Input.DataCollection.StockDataMasterSync" class="form-check-label">종목 마스터</label>
+                    </div>
+                    <div class="form-check form-check-inline">
+                        <input asp-for="Input.DataCollection.StockDataDailyPriceSync" class="form-check-input" type="checkbox" />
+                        <label asp-for="Input.DataCollection.StockDataDailyPriceSync" class="form-check-label">일별 시세</label>
+                    </div>
+                </div>
             </div>
+            <hr/>
 
-            <h6 class="mt-4">KRX 한국거래소</h6>
+            <h6 class="fw-bold mt-3">KRX 한국거래소</h6>
             <div class="row mb-2">
-                <label asp-for="Input.DataCollection.KrxApiKey" class="col-sm-3 col-form-label">AUTH_KEY</label>
-                <div class="col-sm-9"><input asp-for="Input.DataCollection.KrxApiKey" type="text" class="form-control" autocomplete="off" placeholder="저장된 키 유지 — 변경 시에만 입력" /></div>
-            </div>
-            <div class="form-check form-check-inline">
-                <input asp-for="Input.DataCollection.KrxIndexSync" class="form-check-input" type="checkbox" />
-                <label asp-for="Input.DataCollection.KrxIndexSync" class="form-check-label">지수</label>
-            </div>
-            <div class="form-check form-check-inline">
-                <input asp-for="Input.DataCollection.KrxBondIndexSync" class="form-check-input" type="checkbox" />
-                <label asp-for="Input.DataCollection.KrxBondIndexSync" class="form-check-label">채권지수</label>
-            </div>
-            <div class="form-check form-check-inline">
-                <input asp-for="Input.DataCollection.KrxStockSync" class="form-check-input" type="checkbox" />
-                <label asp-for="Input.DataCollection.KrxStockSync" class="form-check-label">주식(마스터+일별)</label>
-            </div>
-            <div class="form-check form-check-inline">
-                <input asp-for="Input.DataCollection.KrxEtpSync" class="form-check-input" type="checkbox" />
-                <label asp-for="Input.DataCollection.KrxEtpSync" class="form-check-label">ETF/ETN/ELW</label>
-            </div>
-            <div class="form-check form-check-inline">
-                <input asp-for="Input.DataCollection.KrxWarrantSync" class="form-check-input" type="checkbox" />
-                <label asp-for="Input.DataCollection.KrxWarrantSync" class="form-check-label">신주인수권</label>
+                <label asp-for="Input.DataCollection.KrxApiKey" class="col-sm-2 col-form-label">AUTH_KEY</label>
+                <div class="col-sm-9"><input asp-for="Input.DataCollection.KrxApiKey" type="text" class="form-control" autocomplete="off" /></div>
             </div>
-            <div class="form-check form-check-inline">
-                <input asp-for="Input.DataCollection.KrxBondSync" class="form-check-input" type="checkbox" />
-                <label asp-for="Input.DataCollection.KrxBondSync" class="form-check-label">채권</label>
-            </div>
-            <div class="form-check form-check-inline">
-                <input asp-for="Input.DataCollection.KrxDerivativeSync" class="form-check-input" type="checkbox" />
-                <label asp-for="Input.DataCollection.KrxDerivativeSync" class="form-check-label">파생상품</label>
-            </div>
-            <div class="form-check form-check-inline">
-                <input asp-for="Input.DataCollection.KrxCommoditySync" class="form-check-input" type="checkbox" />
-                <label asp-for="Input.DataCollection.KrxCommoditySync" class="form-check-label">일반상품</label>
-            </div>
-            <div class="form-check form-check-inline">
-                <input asp-for="Input.DataCollection.KrxEsgSync" class="form-check-input" type="checkbox" />
-                <label asp-for="Input.DataCollection.KrxEsgSync" class="form-check-label">ESG</label>
+            <div class="row mb-2 align-items-center">
+                <label class="col-sm-2 col-form-label">API</label>
+                <div class="col-sm-10">
+                    <div class="form-check form-check-inline">
+                        <input asp-for="Input.DataCollection.KrxIndexSync" class="form-check-input" type="checkbox" />
+                        <label asp-for="Input.DataCollection.KrxIndexSync" class="form-check-label">지수</label>
+                    </div>
+                    <div class="form-check form-check-inline">
+                        <input asp-for="Input.DataCollection.KrxBondIndexSync" class="form-check-input" type="checkbox" />
+                        <label asp-for="Input.DataCollection.KrxBondIndexSync" class="form-check-label">채권지수</label>
+                    </div>
+                    <div class="form-check form-check-inline">
+                        <input asp-for="Input.DataCollection.KrxStockSync" class="form-check-input" type="checkbox" />
+                        <label asp-for="Input.DataCollection.KrxStockSync" class="form-check-label">주식(마스터+일별)</label>
+                    </div>
+                    <div class="form-check form-check-inline">
+                        <input asp-for="Input.DataCollection.KrxEtpSync" class="form-check-input" type="checkbox" />
+                        <label asp-for="Input.DataCollection.KrxEtpSync" class="form-check-label">ETF/ETN/ELW</label>
+                    </div>
+                    <div class="form-check form-check-inline">
+                        <input asp-for="Input.DataCollection.KrxWarrantSync" class="form-check-input" type="checkbox" />
+                        <label asp-for="Input.DataCollection.KrxWarrantSync" class="form-check-label">신주인수권</label>
+                    </div>
+                    <div class="form-check form-check-inline">
+                        <input asp-for="Input.DataCollection.KrxBondSync" class="form-check-input" type="checkbox" />
+                        <label asp-for="Input.DataCollection.KrxBondSync" class="form-check-label">채권</label>
+                    </div>
+                    <div class="form-check form-check-inline">
+                        <input asp-for="Input.DataCollection.KrxDerivativeSync" class="form-check-input" type="checkbox" />
+                        <label asp-for="Input.DataCollection.KrxDerivativeSync" class="form-check-label">파생상품</label>
+                    </div>
+                    <div class="form-check form-check-inline">
+                        <input asp-for="Input.DataCollection.KrxCommoditySync" class="form-check-input" type="checkbox" />
+                        <label asp-for="Input.DataCollection.KrxCommoditySync" class="form-check-label">일반상품</label>
+                    </div>
+                    <div class="form-check form-check-inline">
+                        <input asp-for="Input.DataCollection.KrxEsgSync" class="form-check-input" type="checkbox" />
+                        <label asp-for="Input.DataCollection.KrxEsgSync" class="form-check-label">ESG</label>
+                    </div>
+                </div>
             </div>
+            <hr/>
 
-            <h6 class="mt-4">전자공시 (OpenDART)</h6>
+            <h6 class="fw-bold mt-3">전자공시 (OpenDART)</h6>
             <div class="row mb-2">
-                <label asp-for="Input.DataCollection.OpenDartApiKey" class="col-sm-3 col-form-label">crtfc_key</label>
-                <div class="col-sm-9"><input asp-for="Input.DataCollection.OpenDartApiKey" type="text" class="form-control" autocomplete="off" placeholder="저장된 키 유지 — 변경 시에만 입력" /></div>
+                <label asp-for="Input.DataCollection.OpenDartApiKey" class="col-sm-2 col-form-label">crtfc_key</label>
+                <div class="col-sm-9"><input asp-for="Input.DataCollection.OpenDartApiKey" type="text" class="form-control" autocomplete="off" /></div>
             </div>
-            <div class="form-check form-check-inline">
-                <input asp-for="Input.DataCollection.OpenDartDisclosureSync" class="form-check-input" type="checkbox" />
-                <label asp-for="Input.DataCollection.OpenDartDisclosureSync" class="form-check-label">공시 수집</label>
+            <div class="row mb-2 align-items-center">
+                <label class="col-sm-2 col-form-label">API</label>
+                <div class="col-sm-10">
+                    <div class="form-check form-check-inline">
+                        <input asp-for="Input.DataCollection.OpenDartDisclosureSync" class="form-check-input" type="checkbox" />
+                        <label asp-for="Input.DataCollection.OpenDartDisclosureSync" class="form-check-label">공시 수집</label>
+                    </div>
+                </div>
             </div>
+            <hr/>
 
-            <h6 class="mt-4">거시·환율 (한국수출입은행)</h6>
+            <h6 class="fw-bold mt-3">거시·환율 (한국수출입은행)</h6>
             <div class="row mb-2">
-                <label asp-for="Input.DataCollection.KoreaEximExchangeKey" class="col-sm-3 col-form-label">환율(AP01) authkey</label>
-                <div class="col-sm-9"><input asp-for="Input.DataCollection.KoreaEximExchangeKey" type="text" class="form-control" autocomplete="off" placeholder="저장된 키 유지 — 변경 시에만 입력" /></div>
+                <label asp-for="Input.DataCollection.KoreaEximExchangeKey" class="col-sm-2 col-form-label">환율(AP01)</label>
+                <div class="col-sm-9"><input asp-for="Input.DataCollection.KoreaEximExchangeKey" type="text" class="form-control" autocomplete="off" /></div>
             </div>
             <div class="row mb-2">
-                <label asp-for="Input.DataCollection.KoreaEximLoanRateKey" class="col-sm-3 col-form-label">대출금리(AP02) authkey</label>
-                <div class="col-sm-9"><input asp-for="Input.DataCollection.KoreaEximLoanRateKey" type="text" class="form-control" autocomplete="off" placeholder="저장된 키 유지 — 변경 시에만 입력" /></div>
+                <label asp-for="Input.DataCollection.KoreaEximLoanRateKey" class="col-sm-2 col-form-label">대출금리(AP02)</label>
+                <div class="col-sm-9"><input asp-for="Input.DataCollection.KoreaEximLoanRateKey" type="text" class="form-control" autocomplete="off" /></div>
             </div>
             <div class="row mb-2">
-                <label asp-for="Input.DataCollection.KoreaEximIntlRateKey" class="col-sm-3 col-form-label">국제금리(AP03) authkey</label>
-                <div class="col-sm-9"><input asp-for="Input.DataCollection.KoreaEximIntlRateKey" type="text" class="form-control" autocomplete="off" placeholder="저장된 키 유지 — 변경 시에만 입력" /></div>
+                <label asp-for="Input.DataCollection.KoreaEximIntlRateKey" class="col-sm-2 col-form-label">국제금리(AP03)</label>
+                <div class="col-sm-9"><input asp-for="Input.DataCollection.KoreaEximIntlRateKey" type="text" class="form-control" autocomplete="off" /></div>
             </div>
-            <div class="form-check form-check-inline">
-                <input asp-for="Input.DataCollection.KoreaEximMacroSync" class="form-check-input" type="checkbox" />
-                <label asp-for="Input.DataCollection.KoreaEximMacroSync" class="form-check-label">거시·환율 수집</label>
+            <div class="row mb-2 align-items-center">
+                <label class="col-sm-2 col-form-label">API</label>
+                <div class="col-sm-10">
+                    <div class="form-check form-check-inline">
+                        <input asp-for="Input.DataCollection.KoreaEximMacroSync" class="form-check-input" type="checkbox" />
+                        <label asp-for="Input.DataCollection.KoreaEximMacroSync" class="form-check-label">거시·환율 수집</label>
+                    </div>
+                </div>
             </div>
+            <hr/>
 
-            <h6 class="mt-4">KOSIS 국가통계</h6>
+            <h6 class="fw-bold mt-3">KOSIS 국가통계</h6>
             <div class="row mb-2">
-                <label asp-for="Input.DataCollection.KosisApiKey" class="col-sm-3 col-form-label">apiKey</label>
-                <div class="col-sm-9"><input asp-for="Input.DataCollection.KosisApiKey" type="text" class="form-control" autocomplete="off" placeholder="저장된 키 유지 — 변경 시에만 입력" /></div>
+                <label asp-for="Input.DataCollection.KosisApiKey" class="col-sm-2 col-form-label">apiKey</label>
+                <div class="col-sm-9"><input asp-for="Input.DataCollection.KosisApiKey" type="text" class="form-control" autocomplete="off" /></div>
             </div>
-            <div class="form-check form-check-inline">
-                <input asp-for="Input.DataCollection.KosisSync" class="form-check-input" type="checkbox" />
-                <label asp-for="Input.DataCollection.KosisSync" class="form-check-label">거시지표 수집</label>
+            <div class="row mb-2 align-items-center">
+                <label class="col-sm-2 col-form-label">API</label>
+                <div class="col-sm-10">
+                    <div class="form-check form-check-inline">
+                        <input asp-for="Input.DataCollection.KosisSync" class="form-check-input" type="checkbox" />
+                        <label asp-for="Input.DataCollection.KosisSync" class="form-check-label">거시지표 수집</label>
+                    </div>
+                </div>
             </div>
+            <hr/>
 
-            <h6 class="mt-4">SEIBro 한국예탁결제원</h6>
+            <h6 class="fw-bold mt-3">SEIBro 한국예탁결제원</h6>
             <div class="row mb-2">
-                <label asp-for="Input.DataCollection.SeibroApiKey" class="col-sm-3 col-form-label">key</label>
-                <div class="col-sm-9"><input asp-for="Input.DataCollection.SeibroApiKey" type="text" class="form-control" autocomplete="off" placeholder="저장된 키 유지 — 변경 시에만 입력" /></div>
-            </div>
-            <div class="form-check form-check-inline">
-                <input asp-for="Input.DataCollection.SeibroIssuerSync" class="form-check-input" type="checkbox" />
-                <label asp-for="Input.DataCollection.SeibroIssuerSync" class="form-check-label">발행회사번호</label>
-            </div>
-            <div class="form-check form-check-inline">
-                <input asp-for="Input.DataCollection.SeibroDividendSync" class="form-check-input" type="checkbox" />
-                <label asp-for="Input.DataCollection.SeibroDividendSync" class="form-check-label">배당·권리</label>
-            </div>
-            <div class="form-check form-check-inline">
-                <input asp-for="Input.DataCollection.SeibroSupplySync" class="form-check-input" type="checkbox" />
-                <label asp-for="Input.DataCollection.SeibroSupplySync" class="form-check-label">수급 이벤트</label>
-            </div>
-            <div class="form-check form-check-inline">
-                <input asp-for="Input.DataCollection.SeibroCorpActionSync" class="form-check-input" type="checkbox" />
-                <label asp-for="Input.DataCollection.SeibroCorpActionSync" class="form-check-label">기업행위·총회</label>
+                <label asp-for="Input.DataCollection.SeibroApiKey" class="col-sm-2 col-form-label">key</label>
+                <div class="col-sm-9"><input asp-for="Input.DataCollection.SeibroApiKey" type="text" class="form-control" autocomplete="off" /></div>
             </div>
-            <div class="form-check form-check-inline">
-                <input asp-for="Input.DataCollection.SeibroBondSync" class="form-check-input" type="checkbox" />
-                <label asp-for="Input.DataCollection.SeibroBondSync" class="form-check-label">채권·단기금융</label>
-            </div>
-            <div class="form-check form-check-inline">
-                <input asp-for="Input.DataCollection.SeibroDerivSync" class="form-check-input" type="checkbox" />
-                <label asp-for="Input.DataCollection.SeibroDerivSync" class="form-check-label">파생결합(ELS/DLS)</label>
-            </div>
-            <div class="form-check form-check-inline">
-                <input asp-for="Input.DataCollection.SeibroForeignSync" class="form-check-input" type="checkbox" />
-                <label asp-for="Input.DataCollection.SeibroForeignSync" class="form-check-label">외화증권</label>
+            <div class="row mb-2 align-items-center">
+                <label class="col-sm-2 col-form-label">API</label>
+                <div class="col-sm-10">
+                    <div class="form-check form-check-inline">
+                        <input asp-for="Input.DataCollection.SeibroIssuerSync" class="form-check-input" type="checkbox" />
+                        <label asp-for="Input.DataCollection.SeibroIssuerSync" class="form-check-label">발행회사번호</label>
+                    </div>
+                    <div class="form-check form-check-inline">
+                        <input asp-for="Input.DataCollection.SeibroDividendSync" class="form-check-input" type="checkbox" />
+                        <label asp-for="Input.DataCollection.SeibroDividendSync" class="form-check-label">배당·권리</label>
+                    </div>
+                    <div class="form-check form-check-inline">
+                        <input asp-for="Input.DataCollection.SeibroSupplySync" class="form-check-input" type="checkbox" />
+                        <label asp-for="Input.DataCollection.SeibroSupplySync" class="form-check-label">수급 이벤트</label>
+                    </div>
+                    <div class="form-check form-check-inline">
+                        <input asp-for="Input.DataCollection.SeibroCorpActionSync" class="form-check-input" type="checkbox" />
+                        <label asp-for="Input.DataCollection.SeibroCorpActionSync" class="form-check-label">기업행위·총회</label>
+                    </div>
+                    <div class="form-check form-check-inline">
+                        <input asp-for="Input.DataCollection.SeibroBondSync" class="form-check-input" type="checkbox" />
+                        <label asp-for="Input.DataCollection.SeibroBondSync" class="form-check-label">채권·단기금융</label>
+                    </div>
+                    <div class="form-check form-check-inline">
+                        <input asp-for="Input.DataCollection.SeibroDerivSync" class="form-check-input" type="checkbox" />
+                        <label asp-for="Input.DataCollection.SeibroDerivSync" class="form-check-label">파생결합(ELS/DLS)</label>
+                    </div>
+                    <div class="form-check form-check-inline">
+                        <input asp-for="Input.DataCollection.SeibroForeignSync" class="form-check-input" type="checkbox" />
+                        <label asp-for="Input.DataCollection.SeibroForeignSync" class="form-check-label">외화증권</label>
+                    </div>
+                </div>
             </div>
+            <hr/>
 
-            <h6 class="mt-4">Yahoo Finance (무키)</h6>
-            <div class="form-check form-check-inline">
-                <input asp-for="Input.DataCollection.WorldIndexEnabled" class="form-check-input" type="checkbox" />
-                <label asp-for="Input.DataCollection.WorldIndexEnabled" class="form-check-label">세계지수</label>
-            </div>
-            <div class="form-check form-check-inline">
-                <input asp-for="Input.DataCollection.MarketQuoteEnabled" class="form-check-input" type="checkbox" />
-                <label asp-for="Input.DataCollection.MarketQuoteEnabled" class="form-check-label">주요종목·상품 시세</label>
+            <h6 class="fw-bold mt-3">Yahoo Finance (무키)</h6>
+            <div class="row mb-2 align-items-center">
+                <label class="col-sm-2 col-form-label">API</label>
+                <div class="col-sm-10">
+                    <div class="form-check form-check-inline">
+                        <input asp-for="Input.DataCollection.WorldIndexEnabled" class="form-check-input" type="checkbox" />
+                        <label asp-for="Input.DataCollection.WorldIndexEnabled" class="form-check-label">세계지수</label>
+                    </div>
+                    <div class="form-check form-check-inline">
+                        <input asp-for="Input.DataCollection.MarketQuoteEnabled" class="form-check-input" type="checkbox" />
+                        <label asp-for="Input.DataCollection.MarketQuoteEnabled" class="form-check-label">주요종목·상품 시세</label>
+                    </div>
+                </div>
             </div>
         </details>
 

+ 22 - 1
Admin/Pages/Config/External.cshtml.cs

@@ -1,10 +1,11 @@
+using Application.Abstractions.Crypto;
 using Application.Abstractions.Messaging;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.AspNetCore.Mvc.RazorPages;
 
 namespace Admin.Pages.Config;
 
-public class ExternalModel(IMediator mediator) : PageModel
+public class ExternalModel(IMediator mediator, IFieldEncryptor encryptor) : PageModel
 {
     [BindProperty]
     public InputModel Input { get; set; } = new();
@@ -15,9 +16,29 @@ public class ExternalModel(IMediator mediator) : PageModel
         if (config is not null)
         {
             Input = InputModel.From(config);
+
+            // 공개(/api/config) 응답엔 암호문(enc) 그대로 — Admin 화면에서만 복호화해 실제 키 표시
+            Input.DataCollection.DataGoKrServiceKey = Dec(config.DataCollection.DataGoKrServiceKey);
+            Input.DataCollection.KrxApiKey = Dec(config.DataCollection.KrxApiKey);
+            Input.DataCollection.OpenDartApiKey = Dec(config.DataCollection.OpenDartApiKey);
+            Input.DataCollection.KoreaEximExchangeKey = Dec(config.DataCollection.KoreaEximExchangeKey);
+            Input.DataCollection.KoreaEximLoanRateKey = Dec(config.DataCollection.KoreaEximLoanRateKey);
+            Input.DataCollection.KoreaEximIntlRateKey = Dec(config.DataCollection.KoreaEximIntlRateKey);
+            Input.DataCollection.SeibroApiKey = Dec(config.DataCollection.SeibroApiKey);
+            Input.DataCollection.KosisApiKey = Dec(config.DataCollection.KosisApiKey);
+            Input.External.NaverClientSecret = Dec(config.External.NaverClientSecret);
+            Input.External.KakaoRestApiKey = Dec(config.External.KakaoRestApiKey);
+            Input.External.KakaoAdminKey = Dec(config.External.KakaoAdminKey);
+            Input.External.NaverSearchClientSecret = Dec(config.External.NaverSearchClientSecret);
         }
     }
 
+    /// <summary>암호문(enc:)이면 복호화, 아니면 그대로/빈 값 반환 — Admin 화면 표시 전용.</summary>
+    private string? Dec(string? enc)
+    {
+        return string.IsNullOrWhiteSpace(enc) ? null : EncryptedConfigValue.Unprotect(encryptor, enc);
+    }
+
     public async Task<IActionResult> OnPostAsync(CancellationToken ct)
     {
         if (!ModelState.IsValid)

+ 10 - 6
Admin/Pages/Config/Payment/Index.cshtml

@@ -67,13 +67,17 @@
                         <span asp-validation-for="Input.Toss.LiveSecretKey" class="text-danger"></span>
                     </div>
                 </div>
+                
+                <br/>
 
-                <div class="form-text text-muted">
-                    <ul class="mb-0">
-                        <li>Client Key 는 결제위젯 호출용으로 프론트엔드에 전달됩니다.</li>
-                        <li>Secret Key 는 승인/취소/조회 API 인증에 사용됩니다. 외부에 노출되지 않도록 주의하세요.</li>
-                        <li>키 발급: 토스페이먼츠 개발자센터 → 내 개발정보 → API 키</li>
-                    </ul>
+                <div class="alert alert-info" role="alert">
+                    <div class="form-text text-muted">
+                        <ul class="mb-0">
+                            <li>Client Key 는 결제위젯 호출용으로 프론트엔드에 전달됩니다.</li>
+                            <li>Secret Key 는 승인/취소/조회 API 인증에 사용됩니다. 외부에 노출되지 않도록 주의하세요.</li>
+                            <li>키 발급: 토스페이먼츠 개발자센터 → 내 개발정보 → API 키</li>
+                        </ul>
+                    </div>
                 </div>
             </div>
         </details>

+ 2 - 2
Admin/Pages/NavMenu/Index.cshtml

@@ -30,7 +30,7 @@
                 <col style="width: 8%" />
                 <col />
                 <col />
-                <col style="width: 20%" />
+                <col />
             </colgroup>
             <thead>
                 <tr>
@@ -61,7 +61,7 @@
                             <td>@row.Href</td>
                             <td>@row.SortOrder</td>
                             <td>
-                                <form method="post" asp-page-handler="Toggle" class="form-check form-switch mb-0">
+                                <form method="post" asp-page-handler="Toggle" class="form-check form-switch mb-0 m-auto d-inline-block align-middle">
                                     @Html.AntiForgeryToken()
                                     <input type="hidden" name="id" value="@row.ID" />
                                     <input type="checkbox" class="form-check-input" role="switch" onchange="this.form.submit()" checked="@(row.IsVisible == 'Y')" title="노출 전환" />

+ 2 - 2
Admin/wwwroot/css/admin.css

@@ -1,8 +1,8 @@
 table{width:100%}table tr th,table tr td{-ms-word-wrap:inherit;word-wrap:inherit;overflow-wrap:break-word;text-align:center;vertical-align:middle}table thead tr th{background-color:#f7f7f6 !important}table thead tr th,table thead tr td{text-align:center}table tbody.striped:hover tr td{background-color:#f1f1f1 !important}#fAdminWrite div.row>label>span,#fAdminEdit div.row>label>span{color:red}.select-search-member{z-index:1050;position:relative;width:100%;top:115%;left:0;right:0;display:none;border-width:1px;border-color:red;overflow-y:auto}.select-search-member .list-group-item{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;cursor:pointer}.select-search-member .list-group-item.active{color:#fff}.select-search-member .list-group-item:hover{color:#ff0}.select-search-member .no-results{color:#6c757d}.field-validation-error{display:block;padding:7px 0 0 0}@media(max-width: 575.98px){#fAdminWrite .row>[class^=col-sm-]{margin-bottom:.5rem}#fAdminWrite .row.align-items-center>label.col-form-label{padding-bottom:0;font-weight:600}}@media(min-width: 768px){.search-form__label{min-width:7rem}}
 
 /* 접이식 섹션 (details/summary) — Admin 공용 컴포넌트 */
-.adm-section{border:1px solid var(--bs-border-color, #dee2e6);border-radius:var(--bs-border-radius, .375rem);margin-bottom:1rem;background-color:var(--bs-body-bg, #fff)}
-.adm-section>summary{padding:.5rem 1rem;font-weight:700;background-color:var(--bs-tertiary-bg, #f8f9fa);border-radius:calc(var(--bs-border-radius, .375rem) - 1px) calc(var(--bs-border-radius, .375rem) - 1px) 0 0;cursor:pointer;list-style:none;user-select:none}
+.adm-section{border-bottom:1px solid var(--bs-border-color, #dee2e6);margin-bottom:1rem;background-color:var(--bs-body-bg, #fff)}
+.adm-section>summary{padding:.5rem 1rem;font-weight:700;border-radius:calc(var(--bs-border-radius, .375rem) - 1px) calc(var(--bs-border-radius, .375rem) - 1px) 0 0;cursor:pointer;list-style:none;user-select:none}
 .adm-section:not([open])>summary{border-radius:inherit}
 .adm-section>summary::-webkit-details-marker{display:none}
 .adm-section>summary::before{content:"\25B8";display:inline-block;width:1em;color:var(--bs-secondary-color, #6c757d);transition:transform .15s ease}

+ 14 - 12
Application/Features/Config/Get/Handler.cs

@@ -138,12 +138,13 @@ public sealed class Handler(IAppDbContext db, ICacheService cache) : IQueryHandl
                 GoogleClientSecretEnc = config.External.GoogleClientSecretEnc,
                 GoogleAppId = config.External.GoogleAppId,
                 NaverClientId = config.External.NaverClientId,
-                HasNaverClientSecret = !string.IsNullOrEmpty(config.External.NaverClientSecretEnc),
-                HasKakaoRestApiKey = !string.IsNullOrEmpty(config.External.KakaoRestApiKeyEnc),
+                NaverClientSecret = config.External.NaverClientSecretEnc,
+                KakaoRestApiKey = config.External.KakaoRestApiKeyEnc,
                 KakaoJavascriptKey = config.External.KakaoJavascriptKey,
-                HasKakaoAdminKey = !string.IsNullOrEmpty(config.External.KakaoAdminKeyEnc),
+                KakaoAdminKey = config.External.KakaoAdminKeyEnc,
                 NaverSearchClientId = config.External.NaverSearchClientId,
-                HasNaverSearchClientSecret = !string.IsNullOrEmpty(config.External.NaverSearchClientSecretEnc)
+                NaverSearchClientSecret = config.External.NaverSearchClientSecretEnc
+                // 공개(/api/config)에는 암호문(enc) 그대로 — 복호화는 Admin PageModel 에서만
             },
 
             Toss = new Response.TossConfigDto
@@ -218,14 +219,15 @@ public sealed class Handler(IAppDbContext db, ICacheService cache) : IQueryHandl
 
             DataCollection = new Response.DataCollectionConfigDto
             {
-                HasDataGoKrServiceKey = !string.IsNullOrEmpty(config.DataCollection.DataGoKrServiceKeyEnc),
-                HasKrxApiKey = !string.IsNullOrEmpty(config.DataCollection.KrxApiKeyEnc),
-                HasOpenDartApiKey = !string.IsNullOrEmpty(config.DataCollection.OpenDartApiKeyEnc),
-                HasKoreaEximExchangeKey = !string.IsNullOrEmpty(config.DataCollection.KoreaEximExchangeKeyEnc),
-                HasKoreaEximLoanRateKey = !string.IsNullOrEmpty(config.DataCollection.KoreaEximLoanRateKeyEnc),
-                HasKoreaEximIntlRateKey = !string.IsNullOrEmpty(config.DataCollection.KoreaEximIntlRateKeyEnc),
-                HasSeibroApiKey = !string.IsNullOrEmpty(config.DataCollection.SeibroApiKeyEnc),
-                HasKosisApiKey = !string.IsNullOrEmpty(config.DataCollection.KosisApiKeyEnc),
+                // 공개(/api/config)에는 암호문(enc) 그대로 — 복호화는 Admin PageModel 에서만
+                DataGoKrServiceKey = config.DataCollection.DataGoKrServiceKeyEnc,
+                KrxApiKey = config.DataCollection.KrxApiKeyEnc,
+                OpenDartApiKey = config.DataCollection.OpenDartApiKeyEnc,
+                KoreaEximExchangeKey = config.DataCollection.KoreaEximExchangeKeyEnc,
+                KoreaEximLoanRateKey = config.DataCollection.KoreaEximLoanRateKeyEnc,
+                KoreaEximIntlRateKey = config.DataCollection.KoreaEximIntlRateKeyEnc,
+                SeibroApiKey = config.DataCollection.SeibroApiKeyEnc,
+                KosisApiKey = config.DataCollection.KosisApiKeyEnc,
                 StockDataMasterSync = config.DataCollection.StockDataMasterSync,
                 StockDataDailyPriceSync = config.DataCollection.StockDataDailyPriceSync,
                 KrxIndexSync = config.DataCollection.KrxIndexSync,

+ 18 - 14
Application/Features/Config/Get/Response.cs

@@ -328,30 +328,34 @@ public sealed class Response
         [DisplayName("Google - App ID")]
         public string? GoogleAppId { get; init; }
 
-        // OAuth — 비밀 키는 설정 여부(Has*)만 노출, ID/JS 키는 값 노출
+        // OAuth — 관리자 확인용으로 키/시크릿 원문(복호화)까지 노출 (사용자 승인 — /api/config 익명 공개 포함)
         [DisplayName("네이버 - Client ID")]
         public string? NaverClientId { get; init; }
-        public bool HasNaverClientSecret { get; init; }
-        public bool HasKakaoRestApiKey { get; init; }
+        [DisplayName("네이버 - Client Secret")]
+        public string? NaverClientSecret { get; init; }
+        [DisplayName("카카오 - REST API 키")]
+        public string? KakaoRestApiKey { get; init; }
         [DisplayName("카카오 - JavaScript 키")]
         public string? KakaoJavascriptKey { get; init; }
-        public bool HasKakaoAdminKey { get; init; }
+        [DisplayName("카카오 - Admin 키")]
+        public string? KakaoAdminKey { get; init; }
         [DisplayName("네이버검색 - Client ID")]
         public string? NaverSearchClientId { get; init; }
-        public bool HasNaverSearchClientSecret { get; init; }
+        [DisplayName("네이버검색 - Client Secret")]
+        public string? NaverSearchClientSecret { get; init; }
     }
 
-    // 데이터 수집 — 키는 설정 여부(Has*)만 노출, 활성화 플래그는 값 노출
+    // 데이터 수집 — 공개(/api/config)에는 암호문(enc) 그대로 노출 — 복호화 평문은 Admin 화면에서만
     public sealed class DataCollectionConfigDto
     {
-        public bool HasDataGoKrServiceKey { get; init; }
-        public bool HasKrxApiKey { get; init; }
-        public bool HasOpenDartApiKey { get; init; }
-        public bool HasKoreaEximExchangeKey { get; init; }
-        public bool HasKoreaEximLoanRateKey { get; init; }
-        public bool HasKoreaEximIntlRateKey { get; init; }
-        public bool HasSeibroApiKey { get; init; }
-        public bool HasKosisApiKey { get; init; }
+        public string? DataGoKrServiceKey { get; init; }
+        public string? KrxApiKey { get; init; }
+        public string? OpenDartApiKey { get; init; }
+        public string? KoreaEximExchangeKey { get; init; }
+        public string? KoreaEximLoanRateKey { get; init; }
+        public string? KoreaEximIntlRateKey { get; init; }
+        public string? SeibroApiKey { get; init; }
+        public string? KosisApiKey { get; init; }
 
         public bool StockDataMasterSync { get; init; }
         public bool StockDataDailyPriceSync { get; init; }

+ 196 - 196
Application/Features/Config/Update/Request.cs

@@ -5,96 +5,96 @@ using System.ComponentModel.DataAnnotations;
 namespace Application.Features.Config.Update;
 public sealed class Request
 {
-    public int ID { get; init; }
-
-    public BasicConfigDto Basic { get; init; } = new();
-    public ImagesConfigDto Images { get; init; } = new();
-    public MetaConfigDto Meta { get; init; } = new();
-    public CompanyConfigDto Company { get; init; } = new();
-    public AccountConfigDto Account { get; init; } = new();
-    public EmailTemplateConfigDto EmailTemplate { get; init; } = new();
-    public ExternalApiConfigDto External { get; init; } = new();
-    public TossConfigDto Toss { get; init; } = new();
-    public AttendanceConfigDto? Attendance { get; init; }
-    public SignupRewardConfigDto? SignupReward { get; init; }
-    public RewardConfigDto? Reward { get; init; }
-    public PaperConfigDto? Paper { get; init; }
-    public DataCollectionConfigDto? DataCollection { get; init; }
+    public int ID { get; set; }
+
+    public BasicConfigDto Basic { get; set; } = new();
+    public ImagesConfigDto Images { get; set; } = new();
+    public MetaConfigDto Meta { get; set; } = new();
+    public CompanyConfigDto Company { get; set; } = new();
+    public AccountConfigDto Account { get; set; } = new();
+    public EmailTemplateConfigDto EmailTemplate { get; set; } = new();
+    public ExternalApiConfigDto External { get; set; } = new();
+    public TossConfigDto Toss { get; set; } = new();
+    public AttendanceConfigDto? Attendance { get; set; }
+    public SignupRewardConfigDto? SignupReward { get; set; }
+    public RewardConfigDto? Reward { get; set; }
+    public PaperConfigDto? Paper { get; set; }
+    public DataCollectionConfigDto? DataCollection { get; set; }
 
     // 기본 설정
     public sealed class BasicConfigDto
     {
         [MaxLength(100)]
         [DisplayName("사이트 이름")]
-        public string? SiteName { get; init; }
+        public string? SiteName { get; set; }
 
         [MaxLength(100)]
         [DataType(DataType.Url)]
         [DisplayName("사이트 주소")]
-        public string? SiteURL { get; init; }
+        public string? SiteURL { get; set; }
 
         [DisplayName("최고 관리자 ID")]
-        public string? RootID { get; init; }
+        public string? RootID { get; set; }
 
         [MaxLength(100)]
         [DataType(DataType.EmailAddress)]
         [DisplayName("발송자 이메일")]
-        public string? FromEmail { get; init; }
+        public string? FromEmail { get; set; }
 
         [MaxLength(30)]
         [DisplayName("발송자 이름")]
-        public string? FromName { get; init; }
+        public string? FromName { get; set; }
 
         [MaxLength(200)]
         [DisplayName("SMTP Server")]
-        public string? SmtpServer { get; init; }
+        public string? SmtpServer { get; set; }
 
         [Range(1, 65535)]
         [DisplayName("SMTP Port")]
         public int? SmtpPort { get; set; }
 
         [DisplayName("SMTP Enable SSL")]
-        public bool SmtpEnableSSL { get; init; } = false;
+        public bool SmtpEnableSSL { get; set; } = false;
 
         [MaxLength(100)]
         [DisplayName("SMTP Username")]
-        public string? SmtpUsername { get; init; }
+        public string? SmtpUsername { get; set; }
 
         [MaxLength(200)]
         [DataType(DataType.Password)]
         [DisplayName("SMTP Password")]
-        public string? SmtpPassword { get; init; }
+        public string? SmtpPassword { get; set; }
 
         [MaxLength(200)]
         [DataType(DataType.MultilineText)]
         [DisplayName("관리자단 접근 가능 IP")]
-        public string? AdminWhiteIPList { get; init; }
+        public string? AdminWhiteIPList { get; set; }
 
         [MaxLength(200)]
         [DataType(DataType.MultilineText)]
         [DisplayName("사용자단 접근 가능 IP")]
-        public string? FrontWhiteIPList { get; init; }
+        public string? FrontWhiteIPList { get; set; }
 
         [MaxLength(200)]
         [DisplayName("차단 시 안내문 제목")]
-        public string? BlockAlertTitle { get; init; }
+        public string? BlockAlertTitle { get; set; }
 
         [MaxLength(5000)]
         [DataType(DataType.MultilineText)]
         [DisplayName("차단 시 안내문 내용")]
-        public string? BlockAlertContent { get; init; }
+        public string? BlockAlertContent { get; set; }
 
         [DisplayName("점검 여부")]
-        public bool IsMaintenance { get; init; } = false;
+        public bool IsMaintenance { get; set; } = false;
 
         [MaxLength(5000)]
         [DataType(DataType.MultilineText)]
         [DisplayName("점검 내용")]
-        public string? MaintenanceContent { get; init; }
+        public string? MaintenanceContent { get; set; }
 
         [Range(0, 100)]
         [DisplayName("쪽지 일일 발송 제한")]
-        public int NoteDailySendLimit { get; init; } = 3;
+        public int NoteDailySendLimit { get; set; } = 3;
     }
 
     // 이미지 경로 및 업로드 입력
@@ -103,68 +103,68 @@ public sealed class Request
         // ====== DB에 저장/표기할 경로(문자열) ======
         [MaxLength(255)]
         [DisplayName("Favicon")]
-        public string? FaviconPath { get; init; }
+        public string? FaviconPath { get; set; }
 
         [MaxLength(255)]
         [DisplayName("Logo-square")]
-        public string? LogoSquarePath { get; init; }
+        public string? LogoSquarePath { get; set; }
 
         [MaxLength(255)]
         [DisplayName("Logo-horizontal")]
-        public string? LogoHorizontalPath { get; init; }
+        public string? LogoHorizontalPath { get; set; }
 
         [MaxLength(255)]
         [DisplayName("og-default")]
-        public string? OgDefaultPath { get; init; }
+        public string? OgDefaultPath { get; set; }
 
         [MaxLength(255)]
         [DisplayName("Twitter-image")]
-        public string? TwitterImagePath { get; init; }
+        public string? TwitterImagePath { get; set; }
 
         [MaxLength(255)]
         [DisplayName("Apple-touch-icon")]
-        public string? AppleTouchIconPath { get; init; }
+        public string? AppleTouchIconPath { get; set; }
 
         [MaxLength(255)]
         [DisplayName("App-icon-192")]
-        public string? AppIcon192Path { get; init; }
+        public string? AppIcon192Path { get; set; }
 
         [MaxLength(255)]
         [DisplayName("App-icon-512")]
-        public string? AppIcon512Path { get; init; }
+        public string? AppIcon512Path { get; set; }
 
         // ====== 업로드 입력(새 업로드용) ======
         [DataType(DataType.Upload)]
         [DisplayName("Favicon 업로드")]
-        public IFormFile? FaviconFile { get; init; }
+        public IFormFile? FaviconFile { get; set; }
 
         [DataType(DataType.Upload)]
         [DisplayName("Logo-square 업로드")]
-        public IFormFile? LogoSquareFile { get; init; }
+        public IFormFile? LogoSquareFile { get; set; }
 
         [DataType(DataType.Upload)]
         [DisplayName("Logo-horizontal 업로드")]
-        public IFormFile? LogoHorizontalFile { get; init; }
+        public IFormFile? LogoHorizontalFile { get; set; }
 
         [DataType(DataType.Upload)]
         [DisplayName("og-default 업로드")]
-        public IFormFile? OgDefaultFile { get; init; }
+        public IFormFile? OgDefaultFile { get; set; }
 
         [DataType(DataType.Upload)]
         [DisplayName("Twitter-image 업로드")]
-        public IFormFile? TwitterImageFile { get; init; }
+        public IFormFile? TwitterImageFile { get; set; }
 
         [DataType(DataType.Upload)]
         [DisplayName("Apple-touch-icon 업로드")]
-        public IFormFile? AppleTouchIconFile { get; init; }
+        public IFormFile? AppleTouchIconFile { get; set; }
 
         [DataType(DataType.Upload)]
         [DisplayName("App-icon-192 업로드")]
-        public IFormFile? AppIcon192File { get; init; }
+        public IFormFile? AppIcon192File { get; set; }
 
         [DataType(DataType.Upload)]
         [DisplayName("App-icon-512 업로드")]
-        public IFormFile? AppIcon512File { get; init; }
+        public IFormFile? AppIcon512File { get; set; }
     }
 
     // 메타 태그
@@ -172,36 +172,36 @@ public sealed class Request
     {
         [MaxLength(255)]
         [DisplayName("Meta Keywords")]
-        public string? Keywords { get; init; }
+        public string? Keywords { get; set; }
 
         [MaxLength(255)]
         [DisplayName("Meta Description")]
-        public string? Description { get; init; }
+        public string? Description { get; set; }
 
         [MaxLength(255)]
         [DisplayName("Meta Author")]
-        public string? Author { get; init; }
+        public string? Author { get; set; }
 
         [MaxLength(255)]
         [DisplayName("Meta Viewport")]
-        public string? Viewport { get; init; }
+        public string? Viewport { get; set; }
 
         [MaxLength(255)]
         [DisplayName("Meta ApplicationName")]
-        public string? ApplicationName { get; init; }
+        public string? ApplicationName { get; set; }
 
         [MaxLength(255)]
         [DisplayName("Meta Generator")]
-        public string? Generator { get; init; }
+        public string? Generator { get; set; }
 
         [MaxLength(255)]
         [DisplayName("Meta Robots")]
-        public string? Robots { get; init; }
+        public string? Robots { get; set; }
 
         [MaxLength(3000)]
         [DataType(DataType.MultilineText)]
         [DisplayName("Meta Adds")]
-        public string? Adds { get; init; }
+        public string? Adds { get; set; }
     }
 
     // 회사 정보
@@ -209,135 +209,135 @@ public sealed class Request
     {
         [MaxLength(70)]
         [DisplayName("상호 명")]
-        public string? Name { get; init; }
+        public string? Name { get; set; }
 
         [MaxLength(100)]
         [DisplayName("사업자 등록 번호")]
-        public string? RegNo { get; init; }
+        public string? RegNo { get; set; }
 
         [MaxLength(255)]
         [DisplayName("사업장 소재지")]
-        public string? Address { get; init; }
+        public string? Address { get; set; }
 
         [MaxLength(8)]
         [DataType(DataType.PostalCode)]
         [DisplayName("우편번호")]
-        public string? ZipCode { get; init; }
+        public string? ZipCode { get; set; }
 
         [MaxLength(50)]
         [DisplayName("대표자 명")]
-        public string? Owner { get; init; }
+        public string? Owner { get; set; }
 
         [MaxLength(20)]
         [DisplayName("대표 전화번호")]
-        public string? Tel { get; init; }
+        public string? Tel { get; set; }
 
         [MaxLength(20)]
         [DisplayName("FAX")]
-        public string? Fax { get; init; }
+        public string? Fax { get; set; }
 
         [MaxLength(20)]
         [DisplayName("통신판매업 신고번호")]
-        public string? RetailSaleNo { get; init; }
+        public string? RetailSaleNo { get; set; }
 
         [MaxLength(20)]
         [DisplayName("부가통신 사업자번호")]
-        public string? AddedSaleNo { get; init; }
+        public string? AddedSaleNo { get; set; }
 
         [MaxLength(100)]
         [DisplayName("호스팅 서비스")]
-        public string? Hosting { get; init; }
+        public string? Hosting { get; set; }
 
         [MaxLength(70)]
         [DisplayName("정보관리책임자")]
-        public string? AdminName { get; init; }
+        public string? AdminName { get; set; }
 
         [MaxLength(100)]
         [DataType(DataType.EmailAddress)]
         [DisplayName("정보관리책임자 이메일")]
-        public string? AdminEmail { get; init; }
+        public string? AdminEmail { get; set; }
 
         [MaxLength(200)]
         [DataType(DataType.Url)]
         [DisplayName("사이트 주소")]
-        public string? SiteUrl { get; init; }
+        public string? SiteUrl { get; set; }
 
         [MaxLength(10)]
         [DisplayName("입금계좌 - 은행")]
-        public string? BankCode { get; init; }
+        public string? BankCode { get; set; }
 
         [MaxLength(70)]
         [DisplayName("입금계좌 - 예금주")]
-        public string? BankOwner { get; init; }
+        public string? BankOwner { get; set; }
 
         [MaxLength(100)]
         [DisplayName("입금계좌 - 계좌번호")]
-        public string? BankNumber { get; init; }
+        public string? BankNumber { get; set; }
     }
 
     // 회원 설정
     public sealed class AccountConfigDto
     {
         [DisplayName("회원가입 시 - 가입 차단")]
-        public bool IsRegisterBlock { get; init; } = false;
+        public bool IsRegisterBlock { get; set; } = false;
 
         [DisplayName("회원가입 시 - 이메일 인증 여부")]
-        public bool IsRegisterEmailAuth { get; init; } = false;
+        public bool IsRegisterEmailAuth { get; set; } = false;
 
         [Range(0, 100)]
         [DisplayName("회원가입 시 - 비밀번호 최소 길이")]
-        public ushort? PasswordMinLength { get; init; }
+        public ushort? PasswordMinLength { get; set; }
 
         [Range(0, 100)]
         [DisplayName("회원가입 시 - 비밀번호 대문자 최소 길이")]
-        public ushort? PasswordUppercaseLength { get; init; }
+        public ushort? PasswordUppercaseLength { get; set; }
 
         [Range(0, 100)]
         [DisplayName("회원가입 시 - 비밀번호 숫자 최소 길이")]
-        public ushort? PasswordNumbersLength { get; init; }
+        public ushort? PasswordNumbersLength { get; set; }
 
         [Range(0, 100)]
         [DisplayName("회원가입 시 - 비밀번호 특수문자 최소 길이")]
-        public ushort? PasswordSpecialcharsLength { get; init; }
+        public ushort? PasswordSpecialcharsLength { get; set; }
 
         [MaxLength(4000)]
         [DisplayName("회원가입 시 - 금지 이메일")]
-        public string? DeniedEmailList { get; init; }
+        public string? DeniedEmailList { get; set; }
 
         [MaxLength(4000)]
         [DisplayName("회원가입 시 - 금지 별명")]
-        public string? DeniedNameList { get; init; }
+        public string? DeniedNameList { get; set; }
 
         [Range(0, 365)]
         [DisplayName("회원가입 시 - 이메일 갱신 주기")]
-        public ushort? ChangeEmailDay { get; init; }
+        public ushort? ChangeEmailDay { get; set; }
 
         [Range(0, 365)]
         [DisplayName("회원가입 시 - 별명 갱신 주기")]
-        public ushort? ChangeNameDay { get; init; }
+        public ushort? ChangeNameDay { get; set; }
 
         [Range(0, 365)]
         [DisplayName("회원가입 시 - 한마디 갱신 주기")]
-        public ushort? ChangeSummaryDay { get; init; }
+        public ushort? ChangeSummaryDay { get; set; }
 
         [Range(0, 365)]
         [DisplayName("회원가입 시 - 자기소개 갱신 주기")]
-        public ushort? ChangeIntroDay { get; init; }
+        public ushort? ChangeIntroDay { get; set; }
 
         [Range(0, 365)]
         [DisplayName("회원가입 시 - 비밀번호 갱신 주기")]
-        public ushort? ChangePasswordDay { get; init; }
+        public ushort? ChangePasswordDay { get; set; }
 
         [DisplayName("로그인 시 - 이메일 인증 필요")]
-        public bool IsLoginEmailVerifiedOnly { get; init; } = false;
+        public bool IsLoginEmailVerifiedOnly { get; set; } = false;
 
         [Range(0, 100)]
         [DisplayName("로그인 시 - 로그인 시도(회)")]
-        public ushort? MaxLoginTryCount { get; init; }
+        public ushort? MaxLoginTryCount { get; set; }
 
         [Range(0, 86400)]
         [DisplayName("로그인 시 - 로그인 제한(초)")]
-        public ushort? MaxLoginTryLimitSecond { get; init; }
+        public ushort? MaxLoginTryLimitSecond { get; set; }
     }
 
     // 알림 발송 양식
@@ -345,67 +345,67 @@ public sealed class Request
     {
         [MaxLength(4000)]
         [DisplayName("회원가입 시 - 제목")]
-        public string? RegisterEmailFormTitle { get; init; }
+        public string? RegisterEmailFormTitle { get; set; }
 
         [MaxLength(4000)]
         [DisplayName("회원가입 시 - 내용")]
-        public string? RegisterEmailFormContent { get; init; }
+        public string? RegisterEmailFormContent { get; set; }
 
         [MaxLength(4000)]
         [DisplayName("회원가입 완료 - 제목")]
-        public string? RegistrationEmailFormTitle { get; init; }
+        public string? RegistrationEmailFormTitle { get; set; }
 
         [MaxLength(4000)]
         [DisplayName("회원가입 완료 - 내용")]
-        public string? RegistrationEmailFormContent { get; init; }
+        public string? RegistrationEmailFormContent { get; set; }
 
         [MaxLength(4000)]
         [DisplayName("비밀번호 재설정 - 제목")]
-        public string? ResetPasswordEmailFormTitle { get; init; }
+        public string? ResetPasswordEmailFormTitle { get; set; }
 
         [MaxLength(4000)]
         [DisplayName("비밀번호 재설정 - 내용")]
-        public string? ResetPasswordEmailFormContent { get; init; }
+        public string? ResetPasswordEmailFormContent { get; set; }
 
         [MaxLength(4000)]
         [DisplayName("비밀번호 변경 완료 - 제목")]
-        public string? ChangedPasswordEmailFormTitle { get; init; }
+        public string? ChangedPasswordEmailFormTitle { get; set; }
 
         [MaxLength(4000)]
         [DisplayName("비밀번호 변경 완료 - 내용")]
-        public string? ChangedPasswordEmailFormContent { get; init; }
+        public string? ChangedPasswordEmailFormContent { get; set; }
 
         [MaxLength(4000)]
         [DisplayName("회원탈퇴 시 - 제목")]
-        public string? WithdrawEmailFormTitle { get; init; }
+        public string? WithdrawEmailFormTitle { get; set; }
 
         [MaxLength(4000)]
         [DisplayName("회원탈퇴 시 - 내용")]
-        public string? WithdrawEmailFormContent { get; init; }
+        public string? WithdrawEmailFormContent { get; set; }
 
         [MaxLength(4000)]
         [DisplayName("회원탈퇴 인증 - 제목")]
-        public string? WithdrawVerifyEmailFormTitle { get; init; }
+        public string? WithdrawVerifyEmailFormTitle { get; set; }
 
         [MaxLength(4000)]
         [DisplayName("회원탈퇴 인증 - 내용")]
-        public string? WithdrawVerifyEmailFormContent { get; init; }
+        public string? WithdrawVerifyEmailFormContent { get; set; }
 
         [MaxLength(4000)]
         [DisplayName("이메일 변경 시 - 제목")]
-        public string? EmailVerifyFormTitle { get; init; }
+        public string? EmailVerifyFormTitle { get; set; }
 
         [MaxLength(4000)]
         [DisplayName("이메일 변경 시 - 내용")]
-        public string? EmailVerifyFormContent { get; init; }
+        public string? EmailVerifyFormContent { get; set; }
 
         [MaxLength(4000)]
         [DisplayName("이메일 변경 완료 - 제목")]
-        public string? ChangedEmailFormTitle { get; init; }
+        public string? ChangedEmailFormTitle { get; set; }
 
         [MaxLength(4000)]
         [DisplayName("이메일 변경 완료 - 내용")]
-        public string? ChangedEmailFormContent { get; init; }
+        public string? ChangedEmailFormContent { get; set; }
     }
 
     // API 정보
@@ -414,46 +414,46 @@ public sealed class Request
 
         [MaxLength(500)]
         [DisplayName("YouTube - API Name")]
-        public string? YouTubeApiKeyEnc { get; init; }
+        public string? YouTubeApiKeyEnc { get; set; }
 
         [MaxLength(500)]
         [DisplayName("YouTube - API Key")]
-        public string? YouTubeApiName { get; init; }
+        public string? YouTubeApiName { get; set; }
 
         [MaxLength(500)]
         [DisplayName("Google - Client ID")]
-        public string? GoogleClientId { get; init; }
+        public string? GoogleClientId { get; set; }
 
         [MaxLength(500)]
         [DisplayName("Google - Client Secret")]
-        public string? GoogleClientSecretEnc { get; init; }
+        public string? GoogleClientSecretEnc { get; set; }
 
         [MaxLength(500)]
         [DisplayName("Google - App ID")]
-        public string? GoogleAppId { get; init; }
+        public string? GoogleAppId { get; set; }
 
         // 소셜 로그인 (OAuth) — 비밀 키는 빈 값 제출 시 기존 유지(입력 시 암호화 저장), ID/JS 키는 직접 저장
         [MaxLength(500)]
         [DisplayName("네이버 - Client ID")]
-        public string? NaverClientId { get; init; }
+        public string? NaverClientId { get; set; }
         [MaxLength(500)]
         [DisplayName("네이버 - Client Secret")]
-        public string? NaverClientSecret { get; init; }
+        public string? NaverClientSecret { get; set; }
         [MaxLength(500)]
         [DisplayName("카카오 - REST API 키")]
-        public string? KakaoRestApiKey { get; init; }
+        public string? KakaoRestApiKey { get; set; }
         [MaxLength(500)]
         [DisplayName("카카오 - JavaScript 키")]
-        public string? KakaoJavascriptKey { get; init; }
+        public string? KakaoJavascriptKey { get; set; }
         [MaxLength(500)]
         [DisplayName("카카오 - Admin 키")]
-        public string? KakaoAdminKey { get; init; }
+        public string? KakaoAdminKey { get; set; }
         [MaxLength(500)]
         [DisplayName("네이버검색 - Client ID")]
-        public string? NaverSearchClientId { get; init; }
+        public string? NaverSearchClientId { get; set; }
         [MaxLength(500)]
         [DisplayName("네이버검색 - Client Secret")]
-        public string? NaverSearchClientSecret { get; init; }
+        public string? NaverSearchClientSecret { get; set; }
     }
 
     // 데이터 수집 API 키 + 수집기 활성화. 키는 빈 값 제출 시 기존 유지(입력 시 암호화 저장), 플래그는 수집기 on/off.
@@ -461,53 +461,53 @@ public sealed class Request
     {
         [MaxLength(1000)]
         [DisplayName("금융위 data.go.kr ServiceKey")]
-        public string? DataGoKrServiceKey { get; init; }
+        public string? DataGoKrServiceKey { get; set; }
         [MaxLength(1000)]
         [DisplayName("KRX AUTH_KEY")]
-        public string? KrxApiKey { get; init; }
+        public string? KrxApiKey { get; set; }
         [MaxLength(1000)]
         [DisplayName("OpenDART crtfc_key")]
-        public string? OpenDartApiKey { get; init; }
+        public string? OpenDartApiKey { get; set; }
         [MaxLength(1000)]
         [DisplayName("수출입은행 환율(AP01) authkey")]
-        public string? KoreaEximExchangeKey { get; init; }
+        public string? KoreaEximExchangeKey { get; set; }
         [MaxLength(1000)]
         [DisplayName("수출입은행 대출금리(AP02) authkey")]
-        public string? KoreaEximLoanRateKey { get; init; }
+        public string? KoreaEximLoanRateKey { get; set; }
         [MaxLength(1000)]
         [DisplayName("수출입은행 국제금리(AP03) authkey")]
-        public string? KoreaEximIntlRateKey { get; init; }
+        public string? KoreaEximIntlRateKey { get; set; }
         [MaxLength(1000)]
         [DisplayName("SEIBro key")]
-        public string? SeibroApiKey { get; init; }
+        public string? SeibroApiKey { get; set; }
         [MaxLength(1000)]
         [DisplayName("KOSIS apiKey")]
-        public string? KosisApiKey { get; init; }
+        public string? KosisApiKey { get; set; }
 
         // 수집기 활성화 플래그
-        public bool StockDataMasterSync { get; init; }
-        public bool StockDataDailyPriceSync { get; init; }
-        public bool KrxIndexSync { get; init; }
-        public bool KrxBondIndexSync { get; init; }
-        public bool KrxStockSync { get; init; }
-        public bool KrxEtpSync { get; init; }
-        public bool KrxWarrantSync { get; init; }
-        public bool KrxBondSync { get; init; }
-        public bool KrxDerivativeSync { get; init; }
-        public bool KrxCommoditySync { get; init; }
-        public bool KrxEsgSync { get; init; }
-        public bool OpenDartDisclosureSync { get; init; }
-        public bool KoreaEximMacroSync { get; init; }
-        public bool KosisSync { get; init; }
-        public bool WorldIndexEnabled { get; init; }
-        public bool MarketQuoteEnabled { get; init; }
-        public bool SeibroIssuerSync { get; init; }
-        public bool SeibroDividendSync { get; init; }
-        public bool SeibroSupplySync { get; init; }
-        public bool SeibroCorpActionSync { get; init; }
-        public bool SeibroBondSync { get; init; }
-        public bool SeibroDerivSync { get; init; }
-        public bool SeibroForeignSync { get; init; }
+        public bool StockDataMasterSync { get; set; }
+        public bool StockDataDailyPriceSync { get; set; }
+        public bool KrxIndexSync { get; set; }
+        public bool KrxBondIndexSync { get; set; }
+        public bool KrxStockSync { get; set; }
+        public bool KrxEtpSync { get; set; }
+        public bool KrxWarrantSync { get; set; }
+        public bool KrxBondSync { get; set; }
+        public bool KrxDerivativeSync { get; set; }
+        public bool KrxCommoditySync { get; set; }
+        public bool KrxEsgSync { get; set; }
+        public bool OpenDartDisclosureSync { get; set; }
+        public bool KoreaEximMacroSync { get; set; }
+        public bool KosisSync { get; set; }
+        public bool WorldIndexEnabled { get; set; }
+        public bool MarketQuoteEnabled { get; set; }
+        public bool SeibroIssuerSync { get; set; }
+        public bool SeibroDividendSync { get; set; }
+        public bool SeibroSupplySync { get; set; }
+        public bool SeibroCorpActionSync { get; set; }
+        public bool SeibroBondSync { get; set; }
+        public bool SeibroDerivSync { get; set; }
+        public bool SeibroForeignSync { get; set; }
     }
 
     // 토스페이먼츠 결제 설정 (결제 설정 화면 전용)
@@ -515,79 +515,79 @@ public sealed class Request
     public sealed class TossConfigDto
     {
         [DisplayName("토스 - 결제 환경")]
-        public string? TossPayMode { get; init; }
+        public string? TossPayMode { get; set; }
 
         [MaxLength(500)]
         [DisplayName("토스 - Test Client Key")]
-        public string? TestClientKey { get; init; }
+        public string? TestClientKey { get; set; }
 
         [MaxLength(500)]
         [DisplayName("토스 - Test Secret Key")]
-        public string? TestSecretKey { get; init; }
+        public string? TestSecretKey { get; set; }
 
         [MaxLength(500)]
         [DisplayName("토스 - Live Client Key")]
-        public string? LiveClientKey { get; init; }
+        public string? LiveClientKey { get; set; }
 
         [MaxLength(500)]
         [DisplayName("토스 - Live Secret Key")]
-        public string? LiveSecretKey { get; init; }
+        public string? LiveSecretKey { get; set; }
     }
 
     // 출석 설정
     public sealed class AttendanceConfigDto
     {
         [DisplayName("출석 기능 활성화")]
-        public bool IsEnabled { get; init; } = false;
+        public bool IsEnabled { get; set; } = false;
 
         [Range(0, 100000)]
         [DisplayName("기본 경험치")]
-        public int BaseExp { get; init; } = 0;
+        public int BaseExp { get; set; } = 0;
 
         [Range(0, 100000)]
         [DisplayName("기본 코인")]
-        public int BasePoint { get; init; } = 0;
+        public int BasePoint { get; set; } = 0;
 
         [DisplayName("연속 출석 가중치 사용")]
-        public bool UseStreakBonus { get; init; } = false;
+        public bool UseStreakBonus { get; set; } = false;
 
         [Range(0, 10000)]
         [DisplayName("1일당 추가 경험치")]
-        public int StreakBonusPerDay { get; init; } = 0;
+        public int StreakBonusPerDay { get; set; } = 0;
 
         [Range(0, 10000)]
         [DisplayName("1일당 추가 코인")]
-        public int StreakBonusPointPerDay { get; init; } = 0;
+        public int StreakBonusPointPerDay { get; set; } = 0;
 
         [Range(0, 365)]
         [DisplayName("가중치 최대 적용 일수")]
-        public int StreakBonusMaxDays { get; init; } = 0;
+        public int StreakBonusMaxDays { get; set; } = 0;
 
         [DisplayName("순위 보상 사용")]
-        public bool UseRankBonus { get; init; } = false;
+        public bool UseRankBonus { get; set; } = false;
 
         [MaxLength(2000)]
         [DisplayName("순위별 보상 설정 (JSON)")]
-        public string? RankBonusConfig { get; init; }
+        public string? RankBonusConfig { get; set; }
     }
 
     // 가입 축하 보상 설정
     public sealed class SignupRewardConfigDto
     {
         [DisplayName("가입 축하 보상 활성화")]
-        public bool Enabled { get; init; } = false;
+        public bool Enabled { get; set; } = false;
 
         [Range(0, 100000000)]
         [DisplayName("지급 코인")]
-        public int CoinAmount { get; init; } = 0;
+        public int CoinAmount { get; set; } = 0;
 
         [Range(0, 100000000)]
         [DisplayName("지급 캐시")]
-        public int CashAmount { get; init; } = 0;
+        public int CashAmount { get; set; } = 0;
 
         [Range(0, 100000)]
         [DisplayName("지급 경험치")]
-        public int ExpAmount { get; init; } = 0;
+        public int ExpAmount { get; set; } = 0;
     }
 
     // 보상 엔진 설정 (Reward) — d3 M2. ActivityToken 대체·통합.
@@ -595,134 +595,134 @@ public sealed class Request
     {
         [Range(0, 100000000)]
         [DisplayName("게시글 경험치")]
-        public int PostExp { get; init; } = 0;
+        public int PostExp { get; set; } = 0;
 
         [Range(0, 100000000)]
         [DisplayName("게시글 토큰")]
-        public int PostPoint { get; init; } = 0;
+        public int PostPoint { get; set; } = 0;
 
         [Range(0, 100000000)]
         [DisplayName("댓글 경험치")]
-        public int CommentExp { get; init; } = 0;
+        public int CommentExp { get; set; } = 0;
 
         [Range(0, 100000000)]
         [DisplayName("댓글 토큰")]
-        public int CommentPoint { get; init; } = 0;
+        public int CommentPoint { get; set; } = 0;
 
         [Range(0, 100000000)]
         [DisplayName("추천(누른 쪽) 경험치")]
-        public int LikeGivenExp { get; init; } = 0;
+        public int LikeGivenExp { get; set; } = 0;
 
         [Range(0, 100000000)]
         [DisplayName("추천(받은 쪽) 토큰")]
-        public int LikeReceivedPoint { get; init; } = 0;
+        public int LikeReceivedPoint { get; set; } = 0;
 
         [Range(0, 100000)]
         [DisplayName("최소 게시글 길이")]
-        public int MinPostLength { get; init; } = 0;
+        public int MinPostLength { get; set; } = 0;
 
         [Range(0, 100000)]
         [DisplayName("최소 댓글 길이")]
-        public int MinCommentLength { get; init; } = 0;
+        public int MinCommentLength { get; set; } = 0;
 
         [Range(0, 3650)]
         [DisplayName("신규 회원 보상 유예(일)")]
-        public int NewMemberHoldDays { get; init; } = 0;
+        public int NewMemberHoldDays { get; set; } = 0;
 
         [Range(0, 100000000)]
         [DisplayName("게시글 일일 횟수 상한")]
-        public int PostDailyCount { get; init; } = 0;
+        public int PostDailyCount { get; set; } = 0;
 
         [Range(0, 100000000)]
         [DisplayName("게시글 일일 경험치 상한")]
-        public int PostDailyExp { get; init; } = 0;
+        public int PostDailyExp { get; set; } = 0;
 
         [Range(0, 100000000)]
         [DisplayName("게시글 일일 토큰 상한")]
-        public int PostDailyPoint { get; init; } = 0;
+        public int PostDailyPoint { get; set; } = 0;
 
         [Range(0, 100000000)]
         [DisplayName("댓글 일일 횟수 상한")]
-        public int CommentDailyCount { get; init; } = 0;
+        public int CommentDailyCount { get; set; } = 0;
 
         [Range(0, 100000000)]
         [DisplayName("댓글 일일 경험치 상한")]
-        public int CommentDailyExp { get; init; } = 0;
+        public int CommentDailyExp { get; set; } = 0;
 
         [Range(0, 100000000)]
         [DisplayName("댓글 일일 토큰 상한")]
-        public int CommentDailyPoint { get; init; } = 0;
+        public int CommentDailyPoint { get; set; } = 0;
 
         [Range(0, 100000000)]
         [DisplayName("추천(누른 쪽) 일일 횟수 상한")]
-        public int LikeGivenDailyCount { get; init; } = 0;
+        public int LikeGivenDailyCount { get; set; } = 0;
 
         [Range(0, 100000000)]
         [DisplayName("추천(누른 쪽) 일일 경험치 상한")]
-        public int LikeGivenDailyExp { get; init; } = 0;
+        public int LikeGivenDailyExp { get; set; } = 0;
 
         [Range(0, 100000000)]
         [DisplayName("추천(받은 쪽) 일일 횟수 상한")]
-        public int LikeReceivedDailyCount { get; init; } = 0;
+        public int LikeReceivedDailyCount { get; set; } = 0;
 
         [Range(0, 100000000)]
         [DisplayName("추천(받은 쪽) 일일 토큰 상한")]
-        public int LikeReceivedDailyPoint { get; init; } = 0;
+        public int LikeReceivedDailyPoint { get; set; } = 0;
 
         [Range(0, 100000000)]
         [DisplayName("채팅 일일 횟수 상한")]
-        public int ChatDailyCount { get; init; } = 0;
+        public int ChatDailyCount { get; set; } = 0;
 
         [Range(0, 100000000)]
         [DisplayName("채팅 일일 경험치 상한")]
-        public int ChatDailyExp { get; init; } = 0;
+        public int ChatDailyExp { get; set; } = 0;
 
         [Range(0, 100)]
         [DisplayName("응원 수수료율(%)")]
-        public int CheerFeePercent { get; init; } = 20;
+        public int CheerFeePercent { get; set; } = 20;
 
         [Range(0, 100000000)]
         [DisplayName("응원 최소 금액")]
-        public int CheerMinAmount { get; init; } = 100;
+        public int CheerMinAmount { get; set; } = 100;
 
         [Range(0, 2000000000)]
         [DisplayName("응원 일일 발신 총액 상한(0=무제한)")]
-        public int CheerDailyMax { get; init; } = 0;
+        public int CheerDailyMax { get; set; } = 0;
     }
 
     // 모의투자 설정 (Paper) — d4 §④
     public sealed class PaperConfigDto
     {
         [DisplayName("모의투자 활성화")]
-        public bool Enabled { get; init; } = true;
+        public bool Enabled { get; set; } = true;
 
         [Range(0, 100000)]
         [DisplayName("매매 수수료(Bp)")]
-        public int FeeRateBp { get; init; } = 15;
+        public int FeeRateBp { get; set; } = 15;
 
         [Range(0, 100000)]
         [DisplayName("거래세(Bp)")]
-        public int TaxRateBp { get; init; } = 18;
+        public int TaxRateBp { get; set; } = 18;
 
         [Range(0, 1000000000)]
         [DisplayName("최소 입금 토큰")]
-        public int MinDeposit { get; init; } = 10000;
+        public int MinDeposit { get; set; } = 10000;
 
         [Range(0, 2000000000)]
         [DisplayName("최대 보유 토큰(0=무제한)")]
-        public int MaxHolding { get; init; } = 0;
+        public int MaxHolding { get; set; } = 0;
 
         [Range(0, 10000)]
         [DisplayName("출금 수익 소각(Bp, 0=사용안함)")]
-        public int WithdrawProfitBurnBp { get; init; } = 0;
+        public int WithdrawProfitBurnBp { get; set; } = 0;
 
         [Range(0, 10000)]
         [DisplayName("1주문 상한(Bp, 3000=30%)")]
-        public int OrderMaxPctBp { get; init; } = 3000;
+        public int OrderMaxPctBp { get; set; } = 3000;
 
         [Range(0, 100000)]
         [DisplayName("리더보드 등재 최소 체결수")]
-        public int MinFillsForRank { get; init; } = 3;
+        public int MinFillsForRank { get; set; } = 3;
     }
 
     public static Request From(Get.Response src) => new()