index.vue 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631
  1. <template>
  2. <div class="roulette--container--wrappers">
  3. <div class="roulette--wrapper">
  4. <div class="title">
  5. <div>미리 포인트로 즐기는 행운의 찬스!</div>
  6. <div class="main-title"><span>룰렛</span><span>을 돌려라!</span></div>
  7. </div>
  8. <div class="roulette-container-wrap">
  9. <div class="roulette-container">
  10. <div class="pointer"></div>
  11. <div ref="wheelRef" class="wheel"></div>
  12. <div ref="textLayerRef" class="text-layer"></div>
  13. <button
  14. id="agreeButton"
  15. type="button"
  16. class="center-button"
  17. @click="activeLayer(1)"
  18. ></button>
  19. <!-- <button
  20. id="spinButton"
  21. type="button"
  22. class="center-button"
  23. @click="trySpinRoulette"
  24. ></button> -->
  25. </div>
  26. </div>
  27. <div class="bottom-text">100% 당첨! 룰렛 돌리기</div>
  28. <div class="sub-text">미리톡에서 추후 시 500P 사용하면 보너 2개!</div>
  29. <div class="buttons">
  30. <button type="button" class="button secondary">응모권 확인하기</button>
  31. <button type="button" class="button primary">나의 포인트 전환</button>
  32. </div>
  33. <div class="probability-display">
  34. <div>당첨 확률: <span id="probabilityDisplay">0.2</span>%</div>
  35. </div>
  36. </div>
  37. <!-- 레이어 입력 폼 -->
  38. <div
  39. id="randomBoxAuth-layer01"
  40. class="layer-popup bottom-sheet-wrap event"
  41. role="dialog"
  42. aria-hidden="false"
  43. tabindex="0"
  44. style="z-index: 101"
  45. :class="{ show: activeLayer1 }"
  46. >
  47. <div class="layer-popup-item evt-reservation jan-roulette-pop">
  48. <div class="popup-header">
  49. <strong class="txt-main">룰렛 이벤트 신청</strong>
  50. </div>
  51. <div class="popup-body">
  52. <div class="page-desc">
  53. <h2>
  54. 이벤트 신청을 위해서 본인 인증이 필요합니다.<br />본인 인증을 진행해 주세요.
  55. </h2>
  56. <!-- <p>이벤트 기간 내 가입한 010 신규가입자도 룰렛 이벤트에 참여 가능합니다.</p>-->
  57. </div>
  58. <!-- <div class="box-btn">-->
  59. <!-- <button class="btns md-ripples ripples-light gtm-tracking" type="button" data-gtm-tracking-category="랜덤박스 팝업_PC" data-gtm-tracking-action="본인인증 시도_PC" data-gtm-tracking-label="인증하기 버튼" id="randomBoxAuthBtn">본인 인증 하기</button>-->
  60. <!-- <button id="randomBoxChkBtn" class="btns md-ripples ripples-light" type="button" disabled="" style="display: none;">인증 완료</button>-->
  61. <!-- </div>-->
  62. <div class="phone-certification mt45">
  63. <h2>신청자 정보</h2>
  64. <div class="box-input mt--45">
  65. <label for="userName" class="input-label">이름</label>
  66. <div class="input-wrap">
  67. <input
  68. id="userName"
  69. type="text"
  70. placeholder="이름을 입력해주세요."
  71. class="input-default is-delete"
  72. />
  73. </div>
  74. </div>
  75. <div class="box-input mt45">
  76. <label for="userPhone" class="input-label">휴대폰 번호</label>
  77. <div class="input-wrap">
  78. <input
  79. id="userPhone"
  80. type="text"
  81. placeholder="휴대폰 번호를 입력해주세요."
  82. class="input-default is-delete"
  83. />
  84. </div>
  85. <input id="randomBoxInput3" type="hidden" disabled="" value="" />
  86. </div>
  87. <div class="rq-form">
  88. <div class="agree-wrap">
  89. <!-- 전체 동의 -->
  90. <div class="btn-box btn-check btn-text-line">
  91. <input
  92. type="checkbox"
  93. id="agreeAll"
  94. v-model="agreeAll"
  95. @change="onAgreeAllChange"
  96. />
  97. <label for="agreeAll">
  98. <span class="ico-check"></span>전체 동의 (필수)
  99. </label>
  100. </div>
  101. <div class="agree-group">
  102. <div class="btn-box btn-check">
  103. <input
  104. type="checkbox"
  105. class="agreeReq"
  106. id="randomBoxAgree1"
  107. v-model="agree1"
  108. @change="onAgreeChange"
  109. />
  110. <label for="randomBoxAgree1">
  111. <span class="ico-check"></span>이벤트 참여 및 전화 상담을 위한
  112. 개인정보 수집 및 이용 동의 (필수)
  113. </label>
  114. <a @click="activeLayer(2)" class="ico-arrow-right agreeActions"
  115. >더보기</a
  116. >
  117. </div>
  118. <div class="btn-box btn-check">
  119. <input
  120. type="checkbox"
  121. class="agreeReq"
  122. id="randomBoxAgree2"
  123. v-model="agree2"
  124. @change="onAgreeChange"
  125. />
  126. <label for="randomBoxAgree2">
  127. <span class="ico-check"></span>고객 혜택 정보 및 광고 수신 동의
  128. (필수)
  129. </label>
  130. <a @click="activeLayer(2)" class="ico-arrow-right agreeActions"
  131. >더보기</a
  132. >
  133. </div>
  134. </div>
  135. </div>
  136. </div>
  137. </div>
  138. </div>
  139. <div class="popup-footer">
  140. <div class="btn-group">
  141. <button
  142. class="btns w-sm lightgray md-ripples ripples-dark popup-close"
  143. type="button"
  144. >
  145. 취소
  146. </button>
  147. <button
  148. id="randomBoxAplyBtn"
  149. class="btns w-sm md-ripples ripples-light gtm-tracking"
  150. data-gtm-tracking-category="랜덤박스 팝업_PC"
  151. data-gtm-tracking-action="랜덤박스 열기_PC"
  152. data-gtm-tracking-label="박스열기 버튼"
  153. type="button"
  154. @click="trySpinRoulette(useSeq)"
  155. >
  156. 룰렛 돌리기
  157. </button>
  158. </div>
  159. <button
  160. @click="activeLayer1 = !activeLayer1"
  161. type="button"
  162. id="layer-close01"
  163. class="btn-close-x popup-close"
  164. >
  165. <svg
  166. xmlns="http://www.w3.org/2000/svg"
  167. width="37.657"
  168. height="37.657"
  169. viewBox="0 0 37.657 37.657"
  170. >
  171. <path
  172. data-name="선 392"
  173. transform="translate(2.828 2.828)"
  174. style="fill: none; stroke: #000; stroke-linecap: round; stroke-width: 4px"
  175. d="m0 0 32 32"
  176. />
  177. <path
  178. data-name="선 393"
  179. transform="translate(2.828 2.828)"
  180. style="fill: none; stroke: #000; stroke-linecap: round; stroke-width: 4px"
  181. d="M32 0 0 32"
  182. />
  183. </svg>
  184. </button>
  185. </div>
  186. </div>
  187. </div>
  188. <div
  189. id="randomBoxAgree-layer02"
  190. class="layer-popup bottom-sheet-wrap terms"
  191. role="dialog"
  192. aria-hidden="false"
  193. tabindex="0"
  194. style="z-index: 101"
  195. :class="{ show: activeLayer2 }"
  196. >
  197. <div class="layer-popup-item">
  198. <div class="popup-header">
  199. <strong class="txt-main">약관 및 동의 내용 보기</strong>
  200. </div>
  201. <div class="popup-body">
  202. <div class="agree-cont">
  203. <div class="agree-box">
  204. <p>
  205. 귀사가 고객 혜택정보 및 광고 수신동의(필수) 항목에서 수집한 개인정보,
  206. 고객세분화정보,<br />
  207. 선호도 및 라이프스타일 정보, 전산조회이력정보 및 상담이력정보, 고객 간
  208. 관계에 관한 예측 정보 및 이 정보들에 대한 통계·분석데이터를 해지 시까지
  209. 수집·이용·분석하여 각종 서비스<br />
  210. ·상품(주)미디어로그가 제공하는 이동통신, 금융서비스, 결합·제휴상품,
  211. 스토리지 등<br />
  212. 데이터·콘텐츠서비스, 부가서비스, 전자상거래서비스, 위치정보서비스,
  213. it솔루션, Smart health서비스, 신규서비스·상품 포함), 제휴사와 결합된
  214. 서비스 및 제휴사의 서비스에 대하여 홍보, 가입권유, 프로모션, 생활정보,
  215. 멤버십정보, 이벤트, 해외로밍 안내(공항 또는 항만 위치 시 로밍 이용방법,
  216. 진행중인 이벤트 등 안내) 및 설문조사 목적으로 수집·이용·활용하는 것,
  217. 본인에게 혜택정보, 광고정보를 각종 통신방식[전화, SMS, LMS, MMS, WAP
  218. Push,이메일, 우편, APP안내 및 팝업, APP PUSH]으로 전송하는 것에
  219. 동의합니다.
  220. </p>
  221. </div>
  222. </div>
  223. </div>
  224. <div class="popup-footer">
  225. <!-- <div class="btn-group">
  226. <button
  227. class="btns w-sm lightgray md-ripples ripples-dark popup-close"
  228. type="button"
  229. >
  230. 취소
  231. </button>
  232. <button class="btns w-sm md-ripples ripples-light popup-close" type="button">
  233. 확인
  234. </button>
  235. </div> -->
  236. <button
  237. @click="activeLayer2 = !activeLayer2"
  238. class="btn-close-x popup-close"
  239. id="layer-close02"
  240. type="button"
  241. >
  242. <svg
  243. xmlns="http://www.w3.org/2000/svg"
  244. width="37.657"
  245. height="37.657"
  246. viewBox="0 0 37.657 37.657"
  247. >
  248. <path
  249. data-name="선 392"
  250. transform="translate(2.828 2.828)"
  251. style="fill: none; stroke: #000; stroke-linecap: round; stroke-width: 4px"
  252. d="m0 0 32 32"
  253. />
  254. <path
  255. data-name="선 393"
  256. transform="translate(2.828 2.828)"
  257. style="fill: none; stroke: #000; stroke-linecap: round; stroke-width: 4px"
  258. d="M32 0 0 32"
  259. />
  260. </svg>
  261. </button>
  262. </div>
  263. </div>
  264. </div>
  265. </div>
  266. </template>
  267. <script setup>
  268. import { ref, onMounted, nextTick } from "vue";
  269. definePageMeta({
  270. layout: "roulette",
  271. });
  272. const pageId = ref("ROULETTE");
  273. const { $eventBus } = useNuxtApp();
  274. const winProbability = ref(100.0);
  275. const savedName = ref("");
  276. const savedPhone = ref("");
  277. const isSpinning = ref(false);
  278. const activeLayer1 = ref(false);
  279. const activeLayer2 = ref(false);
  280. const wheelRef = ref(null);
  281. const textLayerRef = ref(null);
  282. const agreeAll = ref(false);
  283. const agree1 = ref(false);
  284. const agree2 = ref(false);
  285. /************************************************************************
  286. | 스토어
  287. ************************************************************************/
  288. const useDtStore = useDetailStore();
  289. const useSeq = ref();
  290. const setWinProbability = (probability) => {
  291. winProbability.value = Math.max(0, Math.min(100, parseFloat(probability)));
  292. return winProbability.value;
  293. };
  294. const activeLayer = (idx) => {
  295. if (idx === 1) {
  296. activeLayer1.value = !activeLayer1.value;
  297. } else if (idx === 2) {
  298. activeLayer2.value = !activeLayer2.value;
  299. }
  300. };
  301. const createRoulette = (numSections, itemName) => {
  302. nextTick(() => {
  303. const $wheel = wheelRef.value;
  304. const $textLayer = textLayerRef.value;
  305. if (!$wheel || !$textLayer) return;
  306. // 초기화
  307. $wheel.style.background = "";
  308. $textLayer.innerHTML = "";
  309. const items = ["당첨", "꽝", "꽝", "꽝", "꽝", "꽝", "꽝", "꽝"];
  310. const sectionCount = numSections;
  311. const sectionAngle = 360 / sectionCount;
  312. const colors = ["#f5c4c3", "#fdf195", "#F0FFF0"];
  313. // conic-gradient
  314. let conicGradient = "conic-gradient(";
  315. for (let i = 0; i < sectionCount; i++) {
  316. const startAngle = i * sectionAngle;
  317. const endAngle = (i + 1) * sectionAngle;
  318. const color = colors[i % 3];
  319. conicGradient += `${color} ${startAngle}deg ${endAngle}deg`;
  320. if (i < sectionCount - 1) conicGradient += ", ";
  321. }
  322. conicGradient += ")";
  323. $wheel.style.background = conicGradient;
  324. // 룰렛 크기 계산
  325. const wheelRect = $wheel.getBoundingClientRect();
  326. const radius = wheelRect.width / 2;
  327. for (let i = 0; i < sectionCount; i++) {
  328. const textDiv = document.createElement("div");
  329. textDiv.className = "section-text";
  330. textDiv.innerText = items[i % items.length];
  331. const angle = i * sectionAngle + sectionAngle / 2;
  332. const distance = radius * 0.75;
  333. const radians = angle * (Math.PI / 180);
  334. const centerX = radius;
  335. const centerY = radius;
  336. const x = centerX + Math.sin(radians) * distance;
  337. const y = centerY - Math.cos(radians) * distance;
  338. textDiv.style.position = "absolute";
  339. textDiv.style.left = `${x}px`;
  340. textDiv.style.top = `${y}px`;
  341. textDiv.style.transform = `rotate(${angle}deg)`;
  342. $textLayer.appendChild(textDiv);
  343. }
  344. // 당첨 섹션 강조
  345. const winSectionIndex = 4;
  346. const $winText = $textLayer.children[winSectionIndex];
  347. if ($winText) {
  348. $winText.style.color = "#E91E63";
  349. $winText.style.fontWeight = "bolder";
  350. }
  351. });
  352. };
  353. onMounted(() => {
  354. useSeq.value = useDtStore.boardInfo.seq;
  355. winnerCheck(useSeq.value);
  356. /*
  357. TODO :
  358. */
  359. });
  360. const handleWin = (name, phone, EVT_SEQ) => {
  361. let __req = {
  362. name: name,
  363. phone: phone,
  364. seq: EVT_SEQ,
  365. };
  366. useAxios()
  367. .post("/winner/reg", __req)
  368. .then((res) => {
  369. if (res.data.rank > 0) {
  370. let param = {
  371. id: pageId,
  372. title: "시스템 메시지",
  373. content: `축하합니다.<br/>${res.data.rank}등에 당첨되었습니다!`,
  374. yes: {
  375. text: "확인",
  376. isProc: false,
  377. },
  378. no: {
  379. text: "취소",
  380. isProc: false,
  381. },
  382. reload: true,
  383. };
  384. $eventBus.emit("OPEN_CONFIRM_POP_UP", param);
  385. } else {
  386. let param = {
  387. id: pageId,
  388. title: "시스템 메시지",
  389. content: `당첨에 실패하였습니다.<br/>다음 기회에 도전해보세요!`,
  390. yes: {
  391. text: "확인",
  392. isProc: false,
  393. },
  394. no: {
  395. text: "취소",
  396. isProc: false,
  397. },
  398. reload: true,
  399. };
  400. $eventBus.emit("OPEN_CONFIRM_POP_UP", param);
  401. }
  402. })
  403. .catch((error) => {})
  404. .finally(() => {});
  405. };
  406. const spinRoulette = (name, phone, EVT_SEQ) => {
  407. if (isSpinning.value) return;
  408. isSpinning.value = true;
  409. const spinDuration = 5000;
  410. const spinRounds = 5; // 룰렛 회전 횟수
  411. const sectionCount = 8; // 룰렛 섹션 개수
  412. const sectionAngle = 360 / sectionCount;
  413. const winSectionIndex = 0; //당첨 섹션 인덱스 : crateRoulette()에서 설정한 인덱스와 동일해야 함
  414. const winSectionStart = winSectionIndex * sectionAngle; // 당첨 섹션 시작 각도
  415. const winSectionEnd = (winSectionIndex + 1) * sectionAngle; // 당첨 섹션 끝 각도
  416. const isWin = Math.random() * 100 < winProbability.value;
  417. let stopAngle;
  418. if (isWin) {
  419. stopAngle = winSectionStart + Math.random() * (winSectionEnd - winSectionStart);
  420. } else {
  421. const loseSections = [];
  422. for (let i = 0; i < sectionCount; i++) {
  423. if (i !== winSectionIndex) loseSections.push(i);
  424. }
  425. const loseIndex = loseSections[Math.floor(Math.random() * loseSections.length)];
  426. const loseStart = loseIndex * sectionAngle;
  427. const loseEnd = (loseIndex + 1) * sectionAngle;
  428. stopAngle = loseStart + Math.random() * (loseEnd - loseStart);
  429. }
  430. const rotationAmount = spinRounds * 360 + (360 - stopAngle);
  431. nextTick(() => {
  432. const $wheel = wheelRef.value;
  433. const $textLayer = textLayerRef.value;
  434. if (!$wheel || !$textLayer) return;
  435. $wheel.style.transition = "none";
  436. $wheel.style.transform = "rotate(0deg)";
  437. $textLayer.style.transition = "none";
  438. $textLayer.style.transform = "rotate(0deg)";
  439. void $wheel.offsetWidth;
  440. $wheel.style.transition = `transform ${spinDuration}ms cubic-bezier(0.2, 0.8, 0.3, 0.9)`;
  441. $wheel.style.transform = `rotate(${rotationAmount}deg)`;
  442. $textLayer.style.transition = `transform ${spinDuration}ms cubic-bezier(0.2, 0.8, 0.3, 0.9)`;
  443. $textLayer.style.transform = `rotate(${rotationAmount}deg)`;
  444. setTimeout(() => {
  445. isSpinning.value = false;
  446. const finalAngle = (360 - (rotationAmount % 360)) % 360;
  447. const sectionIndex = Math.floor(finalAngle / sectionAngle);
  448. //console.log(sectionIndex, winSectionIndex);
  449. if (sectionIndex === winSectionIndex) {
  450. handleWin(name, phone, EVT_SEQ);
  451. } else {
  452. handleWin(name, phone, EVT_SEQ);
  453. }
  454. }, spinDuration);
  455. });
  456. };
  457. const trySpinRoulette = (EVT_SEQ) => {
  458. const name = document.getElementById("userName").value.trim();
  459. const phone = document.getElementById("userPhone").value.trim();
  460. if (!name) {
  461. let param = {
  462. id: pageId,
  463. title: "시스템 메시지",
  464. content: "이름을 입력하세요.",
  465. yes: {
  466. text: "확인",
  467. isProc: false,
  468. },
  469. no: {
  470. text: "취소",
  471. isProc: false,
  472. },
  473. };
  474. $eventBus.emit("OPEN_CONFIRM_POP_UP", param);
  475. document.getElementById("userName").focus();
  476. return;
  477. }
  478. if (!phone) {
  479. let param = {
  480. id: pageId,
  481. title: "시스템 메시지",
  482. content: "휴대폰 번호를 입력하세요.",
  483. yes: {
  484. text: "확인",
  485. isProc: false,
  486. },
  487. no: {
  488. text: "취소",
  489. isProc: false,
  490. },
  491. };
  492. $eventBus.emit("OPEN_CONFIRM_POP_UP", param);
  493. document.getElementById("userPhone").focus();
  494. return;
  495. }
  496. const phoneRegex = /^010-\d{4}-\d{4}$/;
  497. if (!phoneRegex.test(phone)) {
  498. let param = {
  499. id: pageId,
  500. title: "시스템 메시지",
  501. content: "휴대폰 번호를 형식에 맞게 입력하세요. 예시: 010-1234-5678",
  502. yes: {
  503. text: "확인",
  504. isProc: false,
  505. },
  506. no: {
  507. text: "취소",
  508. isProc: false,
  509. },
  510. };
  511. $eventBus.emit("OPEN_CONFIRM_POP_UP", param);
  512. document.getElementById("userPhone").focus();
  513. return;
  514. }
  515. if (!agreeAll.value) {
  516. let param = {
  517. id: pageId,
  518. title: "시스템 메시지",
  519. content:
  520. "이벤트 참여 및 전화 상담을 위한 개인정보 수집 및\n이용 동의 (필수) 에 동의해주세요.",
  521. yes: {
  522. text: "확인",
  523. isProc: false,
  524. },
  525. no: {
  526. text: "취소",
  527. isProc: false,
  528. },
  529. };
  530. $eventBus.emit("OPEN_CONFIRM_POP_UP", param);
  531. return;
  532. }
  533. savedName.value = name;
  534. savedPhone.value = phone;
  535. activeLayer1.value = !activeLayer1.value;
  536. spinRoulette(savedName.value, savedPhone.value, EVT_SEQ);
  537. };
  538. const rouletteCnt = (EVT_SEQ) => {
  539. let __req = {
  540. seq: EVT_SEQ,
  541. };
  542. useAxios()
  543. .post("/winner/itemcount", __req)
  544. .then((res) => {
  545. //createRoulette(res.data.count, res.data.items);
  546. /*
  547. TODO :
  548. 룰렛 생성 어떤 형태로 진행할지 기획 조율 필요
  549. */
  550. createRoulette(8, res.data.items);
  551. })
  552. .catch((error) => {})
  553. .finally(() => {});
  554. };
  555. // 전체 동의 체크 시 하위 동의도 같이 변경
  556. const onAgreeAllChange = () => {
  557. agree1.value = agreeAll.value;
  558. agree2.value = agreeAll.value;
  559. };
  560. // 하위 동의 체크 시 전체 동의 상태도 동기화
  561. const onAgreeChange = () => {
  562. agreeAll.value = agree1.value && agree2.value;
  563. };
  564. const winnerCheck = (EVT_SEQ) => {
  565. let params = {
  566. seq: EVT_SEQ,
  567. };
  568. useAxios()
  569. .post("/winner/winnerchk", params)
  570. .then((res) => {
  571. //console.log(res.data.status);
  572. rouletteCnt(EVT_SEQ);
  573. if (res.data.status == "closed") {
  574. setWinProbability(0);
  575. } else {
  576. setWinProbability(100); // 당첨 확률 몇 퍼센트로 할지 관리자 설정값으로 진행할지 여부 확인 필요
  577. }
  578. })
  579. .catch((error) => {
  580. //$log.debug("[equipMgmtReg][fnGetTenantList][error]");
  581. //useErrorHandler().fnSetCommErrorHandle(error, fnGetTenantList);
  582. })
  583. .finally(() => {
  584. //$log.debug("[equipMgmtReg][fnGetTenantList][finished]");
  585. //objSlt.value.tenantNameList = _cloneDeep(temp);
  586. });
  587. };
  588. </script>