Write.cshtml 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  1. @page
  2. @model Admin.Pages.Channel.List.WriteModel
  3. @{
  4. ViewData["Title"] = "채널 등록";
  5. }
  6. <div class="container">
  7. <h3>@ViewData["Title"]</h3>
  8. <hr />
  9. <partial name="_StatusMessage" />
  10. <div class="alert alert-success" role="alert">
  11. 채널 등록 시 YouTube API를 통해 자동 수집된 정보입니다.<br />
  12. 관리자가 직접 등록 시 잘못된 결과가 발생할 수 있습니다.
  13. </div>
  14. <form id="fAdminWrite" method="post" accept-charset="utf-8" autocomplete="off">
  15. <div class="row mb-2">
  16. <label class="col-sm-2 col-form-label"><span class="text-danger">*</span> 회원(소유자)</label>
  17. <div class="col-sm-10">
  18. <input type="hidden" asp-for="Input.MemberID" id="memberIdHidden" />
  19. <div id="memberSelected" class="mb-1" style="display:none;">
  20. <span class="badge bg-primary fs-6 fw-normal" id="memberBadge">
  21. <span id="memberBadgeText"></span>
  22. <button type="button" class="btn-close btn-close-white ms-2" aria-label="제거" id="memberRemoveBtn" style="font-size:0.6em;vertical-align:middle;"></button>
  23. </span>
  24. </div>
  25. <div class="position-relative" id="memberSearchWrap">
  26. <div class="input-group">
  27. <span class="input-group-text"><i class="bi bi-search"></i></span>
  28. <input type="text" class="form-control" id="memberSearchInput" placeholder="회원 ID, SID, 이메일로 검색" autocomplete="off" />
  29. </div>
  30. <div class="list-group position-absolute w-100 shadow" id="memberSearchResults" style="z-index:1050;display:none;max-height:300px;overflow-y:auto;"></div>
  31. </div>
  32. </div>
  33. </div>
  34. <div class="row mb-2">
  35. <label asp-for="Input.SID" class="col-sm-2 col-form-label"><span class="text-danger">*</span> SID</label>
  36. <div class="col-sm-10">
  37. <input type="text" asp-for="Input.SID" class="form-control" required minlength="24" maxlength="24" placeholder="중복 시 등록이 불가합니다. 24자" />
  38. <div class="text-muted form-text">
  39. YouTube 채널 고유 ID (예: UCxxxxxxxxxxxxxxxxxxxxxx)
  40. </div>
  41. </div>
  42. </div>
  43. <div class="row mb-2">
  44. <label asp-for="Input.Name" class="col-sm-2 col-form-label"><span class="text-danger">*</span> 이름</label>
  45. <div class="col-sm-10">
  46. <input type="text" asp-for="Input.Name" class="form-control" required maxlength="200" />
  47. </div>
  48. </div>
  49. <div class="row mb-2">
  50. <label asp-for="Input.Handle" class="col-sm-2 col-form-label">핸들</label>
  51. <div class="col-sm-10">
  52. <div class="input-group">
  53. <span class="input-group-text">@@</span>
  54. <input type="text" asp-for="Input.Handle" class="form-control" maxlength="30" />
  55. </div>
  56. </div>
  57. </div>
  58. <div class="row mb-2">
  59. <label asp-for="Input.YouTubeUrl" class="col-sm-2 col-form-label"><span class="text-danger">*</span> YouTube 주소</label>
  60. <div class="col-sm-10">
  61. <input type="url" asp-for="Input.YouTubeUrl" class="form-control" required maxlength="255" />
  62. <div class="text-muted form-text">
  63. YouTube 채널 주소 (예: https://www.youtube.com/channel/UCxxxxxxxxxxxxxxxxxxxxxx)
  64. </div>
  65. </div>
  66. </div>
  67. <div class="row mb-2">
  68. <label asp-for="Input.DonationCode" class="col-sm-2 col-form-label">후원 코드</label>
  69. <div class="col-sm-10">
  70. <div class="row">
  71. <div class="col col-md-auto">
  72. <div class="input-group">
  73. <input type="text" asp-for="Input.DonationCode" id="channelDonationCode" class="form-control" maxlength="7" autocomplete="off" />
  74. </div>
  75. </div>
  76. </div>
  77. <small class="form-text text-muted">4~7자 영문/숫자 (UNIQUE, 대문자). 비워두면 후원 코드를 사용하지 않습니다.</small>
  78. <span asp-validation-for="Input.DonationCode" class="text-danger"></span>
  79. </div>
  80. </div>
  81. <div class="row mb-2">
  82. <label asp-for="Input.PlatformFeeRate" class="col-sm-2 col-form-label"><span class="text-danger">*</span> 후원 수수료(%)</label>
  83. <div class="col-sm-10">
  84. <div class="row">
  85. <div class="col col-md-auto">
  86. <div class="input-group">
  87. <input type="number" asp-for="Input.PlatformFeeRate" class="form-control" required min="0" max="100" step="any" />
  88. <span class="input-group-text">%</span>
  89. </div>
  90. </div>
  91. </div>
  92. <small class="form-text text-muted">후원 받은 금액 중 dpot 이 가져가는 비율 (채널주는 100-PlatformFee% 보유)</small>
  93. <span asp-validation-for="Input.PlatformFeeRate" class="text-danger"></span>
  94. </div>
  95. </div>
  96. <div class="row mb-2">
  97. <label asp-for="Input.StoreCommissionRate" class="col-sm-2 col-form-label"><span class="text-danger">*</span> 상점 보상률(%)</label>
  98. <div class="col-sm-10">
  99. <div class="row">
  100. <div class="col col-md-auto">
  101. <div class="input-group">
  102. <input type="number" asp-for="Input.StoreCommissionRate" class="form-control" required min="0" max="100" step="any" />
  103. <span class="input-group-text">%</span>
  104. </div>
  105. </div>
  106. </div>
  107. <small class="form-text text-muted">[비활성] 채널 보상 적립 제거됨(2026-07-03) — 과거 설정값 표시용</small>
  108. <span asp-validation-for="Input.StoreCommissionRate" class="text-danger"></span>
  109. </div>
  110. </div>
  111. <div class="row mb-2">
  112. <label asp-for="Input.IsVerified" class="col-sm-2 col-form-label">인증 여부</label>
  113. <div class="col-sm-10 align-content-center">
  114. <div class="form-check form-check-inline">
  115. <input type="checkbox" asp-for="Input.IsVerified" class="form-check-input" />
  116. <label class="form-check-label" asp-for="Input.IsVerified">인증합니다.</label>
  117. </div>
  118. </div>
  119. </div>
  120. <div class="row mb-2">
  121. <label asp-for="Input.IsActive" class="col-sm-2 col-form-label">사용 여부</label>
  122. <div class="col-sm-10 align-content-center">
  123. <div class="form-check form-check-inline">
  124. <input type="checkbox" asp-for="Input.IsActive" class="form-check-input" />
  125. <label class="form-check-label" asp-for="Input.IsActive">사용합니다.</label>
  126. </div>
  127. </div>
  128. </div>
  129. <hr />
  130. <div class="d-grid gap-2 text-center d-md-block">
  131. <button type="submit" class="btn btn-success">저장</button>
  132. <a href="/Channel/List" class="btn btn-secondary">취소</a>
  133. </div>
  134. <br />
  135. </form>
  136. </div>
  137. @section Scripts {
  138. <script>
  139. $(function () {
  140. let timer = null;
  141. const $input = $('#memberSearchInput');
  142. const $results = $('#memberSearchResults');
  143. const $hidden = $('#memberIdHidden');
  144. const $selected = $('#memberSelected');
  145. const $badgeText = $('#memberBadgeText');
  146. const $searchWrap = $('#memberSearchWrap');
  147. function showSelected(id, email, name, sid) {
  148. const display = id + ' (' + email + ')' + (name ? ' ' + name : '') + (sid ? ' ' + sid : '');
  149. $badgeText.text(display);
  150. $hidden.val(id);
  151. $selected.show();
  152. $searchWrap.hide();
  153. $results.hide();
  154. }
  155. function clearSelected() {
  156. $hidden.val('');
  157. $selected.hide();
  158. $searchWrap.show();
  159. $input.val('').focus();
  160. }
  161. $('#memberRemoveBtn').on('click', function () {
  162. clearSelected();
  163. });
  164. $input.on('input', function () {
  165. clearTimeout(timer);
  166. const val = $(this).val().trim();
  167. if (val.length < 1) {
  168. $results.hide().empty();
  169. return;
  170. }
  171. timer = setTimeout(function () {
  172. $.getJSON('/Channel/List/Write?handler=SearchMember&keyword=' + encodeURIComponent(val), function (data) {
  173. $results.empty();
  174. if (data.length === 0) {
  175. $results.append('<div class="list-group-item text-muted">검색 결과가 없습니다.</div>');
  176. } else {
  177. $.each(data, function (i, m) {
  178. const text = m.id + ' (' + m.email + ')' + (m.name ? ' ' + m.name : '') + (m.sid ? ' ' + m.sid : '');
  179. $results.append(
  180. $('<a href="#" class="list-group-item list-group-item-action"></a>')
  181. .text(text)
  182. .on('click', function (e) {
  183. e.preventDefault();
  184. showSelected(m.id, m.email, m.name, m.sid);
  185. })
  186. );
  187. });
  188. }
  189. $results.show();
  190. });
  191. }, 300);
  192. });
  193. $(document).on('click', function (e) {
  194. if (!$(e.target).closest('#memberSearchWrap').length) {
  195. $results.hide();
  196. }
  197. });
  198. $input.on('focus', function () {
  199. if ($results.children().length > 0) {
  200. $results.show();
  201. }
  202. });
  203. // 폼 제출 시 회원 선택 여부 확인
  204. $('#fAdminWrite').on('submit', function (e) {
  205. if (!$hidden.val() || $hidden.val() === '0') {
  206. e.preventDefault();
  207. alert('회원(소유자)을 선택해주세요.');
  208. $input.focus();
  209. }
  210. });
  211. });
  212. </script>
  213. }