PageSourceSearch

https://clubdoasis.com/hBook/bookMainV2/19110001?lang=ko

html clubdoasis.com collected 2026-10-04 19:40:43 UTC 190,988 bytes, 4,307 lines download raw bytes

1<!-- [클럽디 오아시스] 예약 1단계 '날짜/티켓/수량' 선택 -->
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16<html lang="ko">
17<head>
18	<meta charset="utf-8">
19	
20	<meta content="width=device-width, initial-scale=1.0" name="viewport">
21	
22	<title>클럽디 오아시스 </title>
23	<meta name="description" content="해운대 프리미엄 스파 & 워터파크 클럽디 오아시스">
24	<meta name="keywords" content="클럽디오아시스, 클럽디 오아시스, 부산워터파크, 부산스파, 해운대워터파크, 해운대스파, 워터클럽">
25
26    <!-- Open Graph -->
27    <meta property="og:title" content="클럽디오아시스" />
28    <meta property="og:description" content="해운대 프리미엄 스파 & 워터파크 클럽디 오아시스" />
29    <meta property="og:image" content="https://www.clubdoasis.com/img/orimage2.jpg" />
30    <meta property="og:url" content="https://www.clubdoasis.com/" />
31    <meta property="og:site_name" content="클럽디오아시스" />
32
33    <!-- Twitter Card -->
34    <meta name="twitter:card" content="summary_large_image" />
35    <meta name="twitter:title" content="클럽디오아시스" />
36    <meta name="twitter:description" content="해운대 프리미엄 스파 & 워터파크 클럽디 오아시스" />
37    <meta name="twitter:image" content="https://www.clubdoasis.com/img/orimage2.jpg" />
38    <meta name="twitter:url" content="https://www.clubdoasis.com/" />
39
40    <!-- Favicons -->
41    <link rel="shortcut icon" href="/img/favicon.ico">
42    <link href="/img/apple-touch-icon.png" rel="apple-touch-icon">
43
44    <!-- 지원 모바일 브라우저 Safari 15~ / 삼성 인터넷 6.2~ / 크롬 94~ -->
45    <meta name="theme-color" content="#ffffff">
46
47    <!--iOS Safari 전용-->
48    <meta name="apple-mobile-web-app-status-bar-style" content="default">
49
50    <link rel="stylesheet" href="/css/bootstrap-icons.css"> <!-- bootstrap-icons -->
51	<link rel="stylesheet" href="/css/bootstrap.min.css"> <!-- bootstrap-css -->
52    <link rel="stylesheet" href="/css/fonts.css?now=Mon Oct 05 04:40:42 KST 2026">
53    <link rel="stylesheet" href="/css/base.css?now=Mon Oct 05 04:40:42 KST 2026">
54    <link rel="stylesheet" href="/css/gnb.css?now=Mon Oct 05 04:40:42 KST 2026">
55    <link rel="stylesheet" href="/css/sub.css?now=Mon Oct 05 04:40:42 KST 2026">
56
57	<link rel="stylesheet" href="/css/animate.css"><!--움직임-->
58	<link rel="stylesheet" href="/css/owl.carousel.min.css"><!--슬라이드-->
59	<link rel="stylesheet" href="/css/member.css?now=Mon Oct 05 04:40:42 KST 2026">
60	<link rel="stylesheet" href="/css/style.css?now=Mon Oct 05 04:40:42 KST 2026">
61	<link rel="stylesheet" href="/css/common.css?now=Mon Oct 05 04:40:42 KST 2026">
62    <link rel="stylesheet" href="/css/hBook.css?now=Mon Oct 05 04:40:42 KST 2026">
63
64	<link rel="stylesheet" href="/css/aos.css">
65	<!--스크롤aos-->
66	<link rel="stylesheet" href="/css/swiper.css" />
67	<!--swiper-->
68	<link rel="stylesheet" href="/vendor/jquery-ui.min.css">
69	<link rel="stylesheet" href="/css/festival.css?now=Mon Oct 05 04:40:42 KST 2026">
70	<link rel="stylesheet" href="/css/main.css?now=Mon Oct 05 04:40:42 KST 2026">
71	<link rel="stylesheet" href="/css/main-popup.css?now=Mon Oct 05 04:40:42 KST 2026">
72	<link rel="stylesheet" href="/css/secret.css?now=Mon Oct 05 04:40:42 KST 2026">
73
74	<!--폰트 아이콘-->
75
76	<link rel="preconnect" href="https://fonts.googleapis.com">
77	<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
78	<link
79		href="https://fonts.googleapis.com/css2?family=Nanum+Pen+Script&display=swap"
80		rel="stylesheet">
81	<link rel="preconnect" href="https://fonts.googleapis.com">
82	<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
83	<link
84		href="https://fonts.googleapis.com/css2?family=Gamja+Flower&display=swap"
85		rel="stylesheet">
86	
86<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
86
87	<!-- date picker CSS -->
88	<link rel="stylesheet" href="/css/flatpickr/flatpickr.css">
89	<link rel="stylesheet" href="/css/flatpickr/confetti.css">
90
91    <!-- Naver -->
92    <meta name="naver-site-verification" content="edd9e8eecb6c3c4fbd7ef0bb269872bf737d344a" />
93    
93<script> 
vendor: 127 bytes, line 93
93window.dataLayer = window.dataLayer || []; function gtag(){dataLayer.push(arguments);} gtag('js', new Date()); gtag('config', '
93G-QNL6YK7B8E
vendor: 4 bytes, line 93
93'); 
93</script>
93
94    
94<script type="application/ld+json"> { "@context": "http://schema.org", "@type": "Person", "name": "CLUBD OASIS", "url": "https://clubdoasis.com/", "sameAs": [ "https://www.instagram.com/clubdoasis/", "https://www.youtube.com/@clubdoasis" ] } </script>
94
95    <!-- Google tag (gtag.js) -->
vendor: 65 bytes, lines 95-96
95
96	<script async src="https://www.googletagmanager.com/gtag/js?id=
96G-QNL6YK7B8E
vendor: 13 bytes, lines 96-97
96"></script>
97	
97<!-- 네이버 서치어드바이저 태그 -->
98	<meta name="naver-site-verification" content="3d5d33e0c622de8473586655e70b98e7a7a8dfd4" />
99	<!-- kakao -->
100	
100<script type="text/javascript" charset="UTF-8" src="//t1.daumcdn.net/kas/static/kp.js"></script>
100
101	
101<script type="text/javascript"> kakaoPixel('7350261284392647385').pageView(); </script>
101
102	<!-- Meta Pixel Code -->
103	
103<script>
vendor: 425 bytes, lines 103-112
103
104		!function(f,b,e,v,n,t,s)
105		{if(f.fbq)return;n=f.fbq=function(){n.callMethod?
106				n.callMethod.apply(n,arguments):n.queue.push(arguments)};
107			if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';
108			n.queue=[];t=b.createElement(e);t.async=!0;
109			t.src=v;s=b.getElementsByTagName(e)[0];
110			s.parentNode.insertBefore(t,s)}(window, document,'script',
111				'https://connect.facebook.net/en_US/fbevents.js');
112		fbq('init', '
1121675250166477047
vendor: 33 bytes, lines 112-114
112');
113		fbq('track', 'PageView');
114	
114</script>
114
115	<noscript><img height="1" width="1" style="display:none" src="https://www.facebook.com/tr?id=1675250166477047&ev=PageView&noscript=1" /></noscript>
116
117
118    
119    
120    
121</head>
122
123
124<body class="nonetop no-res-btn hbook-purchase-page">
125
126<!-- 상품조회 로딩 스피너 -->
127<div id="v2-loading-overlay" role="status" aria-live="polite" aria-label="상품을 조회 중입니다.">
128    <div class="v2-loading-box">
129        <div class="v2-loading-spinner" aria-hidden="true"></div>
130        <div class="v2-loading-text">상품을 조회 중입니다.</div>
131    </div>
132</div>
133
134<!-- 상단 메뉴 -->
135
136
137
138
139
140    
141    
142    
143    
144    
145        
146
147
148
149
150	
151
152<header id="fh5co-header-section" role="banner">
153	
154
155<section class="header-utility" aria-label="Quick navigation">
156	<div class="top">
157		<div class="top-left">
158			<button type="button" class="txt_global" aria-haspopup="true" aria-expanded="false" aria-controls="header-language-menu">
159				<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-globe2" viewBox="0 0 16 16" aria-hidden="true">
160					<path d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8m7.5-6.923c-.67.204-1.335.82-1.887 1.855q-.215.403-.395.872c.705.157 1.472.257 2.282.287zM4.249 3.539q.214-.577.481-1.078a7 7 0 0 1 .597-.933A7 7 0 0 0 3.051 3.05q.544.277 1.198.49zM3.509 7.5c.036-1.07.188-2.087.436-3.008a9 9 0 0 1-1.565-.667A6.96 6.96 0 0 0 1.018 7.5zm1.4-2.741a12.3 12.3 0 0 0-.4 2.741H7.5V5.091c-.91-.03-1.783-.145-2.591-.332M8.5 5.09V7.5h2.99a12.3 12.3 0 0 0-.399-2.741c-.808.187-1.681.301-2.591.332zM4.51 8.5c.035.987.176 1.914.399 2.741A13.6 13.6 0 0 1 7.5 10.91V8.5zm3.99 0v2.409c.91.03 1.783.145 2.591.332.223-.827.364-1.754.4-2.741zm-3.282 3.696q.18.469.395.872c.552 1.035 1.218 1.65 1.887 1.855V11.91c-.81.03-1.577.13-2.282.287zm.11 2.276a7 7 0 0 1-.598-.933 9 9 0 0 1-.481-1.079 8.4 8.4 0 0 0-1.198.49 7 7 0 0 0 2.276 1.522zm-1.383-2.964A13.4 13.4 0 0 1 3.508 8.5h-2.49a6.96 6.96 0 0 0 1.362 3.675c.47-.258.995-.482 1.565-.667m6.728 2.964a7 7 0 0 0 2.275-1.521 8.4 8.4 0 0 0-1.197-.49 9 9 0 0 1-.481 1.078 7 7 0 0 1-.597.933M8.5 11.909v3.014c.67-.204 1.335-.82 1.887-1.855q.216-.403.395-.872A12.6 12.6 0 0 0 8.5 11.91zm3.555-.401c.57.185 1.095.409 1.565.667A6.96 6.96 0 0 0 14.982 8.5h-2.49a13.4 13.4 0 0 1-.437 3.008M14.982 7.5a6.96 6.96 0 0 0-1.362-3.675c-.47.258-.995.482-1.565.667.248.92.4 1.938.437 3.008zM11.27 2.461q.266.502.482 1.078a8.4 8.4 0 0 0 1.196-.49 7 7 0 0 0-2.275-1.52c.218.283.418.597.597.932m-.488 1.343a8 8 0 0 0-.395-.872C9.835 1.897 9.17 1.282 8.5 1.077V4.09c.81-.03 1.577-.13 2.282-.287z"/>
161				</svg>
162				한국어(KOR)
163			</button>
164			<ul class="ul_global text-center" id="header-language-menu">
165				<li><a href="/?lang=ko">한국어</a></li>
166				<li><a href="/en?lang=en">English</a></li>
167				<li><a href="/jp?lang=ja">日本語</a></li>
168				<li><a href="/cn?lang=zh_CN">简体中文</a></li>
169				
170			</ul>
171		</div>
172
173		<div class="logo" role="link" tabindex="0" aria-label="CLUBD OASIS" data-home-url="/"></div>
174
175		<div class="top-right">
176			<a class="header-ticket-link" href="/hBook/bookMainV2/19110001?lang=ko">
177				<i class="bi bi-ticket-perforated-fill" aria-hidden="true"></i>
178				<span>TICKETS</span>
179			</a>
180			
181				
182				
183				
184					
185						<a href="/join/loginView"><i class="bi bi-person-fill" aria-hidden="true"></i><span>LOGIN</span></a>
186					
187					
188				
189			
190		</div>
191	</div>
192</section>
193<script>
194	(function () {
195		var headerUtility = document.currentScript.previousElementSibling;
196		var logo = headerUtility.querySelector('.logo');
197		var languageButton = headerUtility.querySelector('.txt_global');
198		var languageMenu = headerUtility.querySelector('.ul_global');
199		var header = headerUtility.closest('#fh5co-header-section');
200		var goHome = function () { window.location.href = logo.getAttribute('data-home-url'); };
201		var setLanguageMenuOpen = function (isOpen) {
202			languageMenu.style.display = isOpen ? 'block' : 'none';
203			languageButton.setAttribute('aria-expanded', String(isOpen));
204			if (header) {
205				header.classList.toggle('language-menu-open', isOpen);
206			}
207		};
208
209		setLanguageMenuOpen(false);
210		languageButton.addEventListener('click', function () {
211			var isOpen = languageMenu.style.display !== 'none';
212			setLanguageMenuOpen(!isOpen);
213		});
214		document.addEventListener('click', function (event) {
215			if (!headerUtility.contains(event.target)) {
216				setLanguageMenuOpen(false);
217			}
218		});
219		headerUtility.addEventListener('keydown', function (event) {
220			if (event.key === 'Escape') {
221				setLanguageMenuOpen(false);
222				languageButton.focus();
223			}
224		});
225
226		logo.addEventListener('click', goHome);
227		logo.addEventListener('keydown', function (event) {
228			if (event.key === 'Enter' || event.key === ' ') {
229				event.preventDefault();
230				goHome();
231			}
232		});
233	}());
234</script>
234
235
236	<!-- <div class="event-button-zone">
237		<p class="mb-2">2024 HAEUNDAE WATER WARS</p>
238		<a href="/waterwars" class="mr-2 event-button">이용권 예매</a>
239		<a href="/highest" class="event-button">풀파티 예매</a>
240	</div>	  -->
241	
242	
243	<section>
244		
245		<div class="border">
246		<div class="container">
247			<nav id="fh5co-menu-wrap" role="navigation">
248				<div class="gnb">
249					<ul>
250						<li><a href="/brand/brand">BRAND</a>
251							<ul class="depth2">
252								<li><a href="/brand/brand">브랜드소개</a></li>
253								<li><a href="/brand/boyang">보양온천</a></li>
254								<li><a href="/brand/well-luv">K-OGAM(웰니스)</a></li>
255								<li><a href="/brand/pr">홍보영상</a></li>
256							</ul>
257						</li>
258						<li><a href="/enjoy/sand">SPA & WATERPARK</a>
259							<ul class="depth2">
260								<li><a class="" href="/enjoy/sand">THE SAND</a></li>
261								<li><a class="" href="/enjoy/horizon">THE HORIZON</a></li>
262								<li><a class="" href="/enjoy/mirage">THE MIRAGE</a></li>
263								<li><a class="" href="/enjoy/palm">THE PALM</a></li>
264								<li><a class="" href="/enjoy/oasis">THE OASIS</a></li>
265							</ul>
266						</li>
267						<li><a href="/guide/guideV2">GUIDE</a>
268							<ul class="depth2">
269								<li><a class="" href="/guide/guideV2">이용안내</a></li>
270								<li><a class="" href="/guide/charge">이용요금</a></li>
271                                <li><a class="" href="/guide/floor">층별안내</a></li>
272								<li><a class="" href="/guide/rental">렌탈용품 </a></li>
273								<li><a class="" href="/guide/cbnEtc">카바나/면역공방 </a></li>
274								<li><a class="" href="/guide/location">교통/주차안내</a></li>
275								<li><a class="" href="/guide/faq">자주묻는질문</a></li>
276							</ul>
277						</li>
278						<li><a href="/facility/service">FACILITY</a>
279							<ul class="depth2">
280								<li><a class="" href="/facility/service">편의시설</a></li>
281								
282
283								
284								<li><a class="" href="/facility/food">식음시설 </a></li>
285								<li><a class="" href="/facility/3dmap">3D ë§µ </a></li>
286							</ul>
287						</li>
288						<li><a href="/hBook/bookMainV2/19110001">TICKETS</a>
289							<ul class="depth2">
290								<li><a href="/hBook/bookMainV2/19110001">티켓 구매하기</a></li>
291								<li><a href="/join/loginView?mode=ticket">티켓조회</a></li>
292								<li><a href="/hBook/inquiryGroup">
292단체예약문의</a></li>
293								<li><a href="/hBook/inquiryPtnrsp">대관/제휴문의</a></li>
294                                <li onClick="openModal('code')" style="cursor: pointer; line-height: 40px; font-size: 15px;">제휴회원</li>
295							</ul>
296						</li>
297						<li><a href="/customer/board?bbstype=1">NOTICE</a>
298							<ul class="depth2">
299								<li><a class="" href="/customer/board?bbstype=1">이벤트</a></li>
300								<li><a class="" href="/customer/board?bbstype=0">공지사항</a></li>
301								<li><a class="" href="/customer/region">제휴업체</a></li>
302								<li><a class="" href="/voc">고객의소리</a></li>
303							</ul>
304						</li>
305					</ul>
306				</div>
307			</nav>
308		</div>
309		</div>
310		<!-- Waves -->
311		<svg class="waves" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 24 150 28" preserveAspectRatio="none" shape-rendering="auto">
312	    	<defs>
313	            <path id="gentle-wave" d="M-160 44c30 0 58-18 88-18s 58 18 88 18 58-18 88-18 58 18 88 18 v44h-352z"/>
314	        </defs>
315	        <g class="parallax">
316	            <use xlink:href="#gentle-wave" x="48" y="0" fill="rgba(0, 0, 0, 0.6" stroke-width="1" stroke="rgba(221, 221, 221, 0.6)"/>
317	        </g>
318	    </svg>
319	</section>
320	
321	<div class="container gnb_container">
322		<!-- START #fh5co-menu-wrap  -->
323		<nav id="fh5co-menu-wrap" role="navigation">
324			<!--모바일용에만 보이는  메뉴-->
325			<div class="mobile_gnb" style="display: none;">
326				<div id="fh5co-primary-menu">
327					<div class="login">
328						<div class="mobile-account-actions">
329							
330								
331									<a href="/join/loginView"><i class="bi bi-person-fill" aria-hidden="true"></i> LOGIN</a>
332								
333								
334							
335						</div>
336						<a href="javascript:closeWin()" id="fh5co-mobile-menu-btn" class="mr-0"><svg xmlns="http://www.w3.org/2000/svg" width="21" height="21" fill="currentColor" class="bi bi-x-lg" viewBox="0 0 16 16">
337  <path d="M2.146 2.854a.5.5 0 1 1 .708-.708L8 7.293l5.146-5.147a.5.5 0 0 1 .708.708L8.707 8l5.147 5.146a.5.5 0 0 1-.708.708L8 8.707l-5.146 5.147a.5.5 0 0 1-.708-.708L7.293 8z"/>
338</svg></a>
339					</div>
340					<div class="ticket">
341						<a href="/hBook/bookMainV2/19110001"><i class="bi bi-ticket-perforated-fill" aria-hidden="true"></i> 티켓구매하기</a>
342					</div>
343					<ul class="mobile sf-menu">
344						<li class="mobile_depth">
345							<h3>BRAND<i class="fas fa-sort-down ml10"></i></h3>
346							<ul class="mobile_depth2">
347								<li><a href="/brand/brand">브랜드소개</a></li>
348								<li><a href="/brand/boyang">보양온천</a></li>
349								<li><a href="/brand/well-luv">K-OGAM(웰니스)</a></li>
350								<li><a href="/brand/pr">홍보영상</a></li>
351							</ul>
352						</li>
353						<li class="mobile_depth">
354							<h3>SPA & WATERPARK<i class="fas fa-sort-down ml10"></i></h3>
355							<ul class="mobile_depth2">
356								<li><a class="" href="/enjoy/sand">THE SAND</a></li>
357								<li><a class="" href="/enjoy/horizon">THE HORIZON</a></li>
358								<li><a class="" href="/enjoy/mirage">THE MIRAGE</a></li>
359								<li><a class="" href="/enjoy/palm">THE PALM</a></li>
360								<li><a class="" href="/enjoy/oasis">THE OASIS</a></li>
361							</ul>
362						</li>
363	
364						<li class="mobile_depth">
365							<h3>GUIDE<i class="fas fa-sort-down ml10"></i></h3>
366							<ul class="mobile_depth2">
367								<li><a class="" href="/guide/guideV2">이용안내</a></li>
368								<li><a class="" href="/guide/charge">이용요금</a></li>
369                                <li><a class="" href="/guide/floor">층별안내</a></li>
370								<li><a class="" href="/guide/rental">렌탈용품 </a></li>
371								<li><a class="" href="/guide/cbnEtc">카바나/면역공방 </a></li>
372								<li><a class="" href="/guide/location">교통/주차안내</a></li>
373								<li><a class="" href="/guide/faq">자주묻는질문</a></li>
374							</ul>
375						</li>
376	
377						<li class="mobile_depth">
378							<h3>FACILITY<i class="fas fa-sort-down ml10"></i></h3>
379							<ul class="mobile_depth2">
380								<li><a class="" href="/facility/service">
380편의시설</a></li>
381								<!-- <li style="background: url(/img/event/cats/bg-event.jpg);"><a class="orange" href="/facility/catssoup"><b>클럽디 오아시스 x 고양이와 스프</b></a> -->
382
383								
384								<li><a class="" href="/facility/food">식음시설 </a></li>
385								<li><a class="" href="/facility/3dmap">3D ë§µ </a></li>
386							</ul>
387						</li>
388						<li class="mobile_depth">
389							<h3>TICKETS<i class="fas fa-sort-down ml10"></i></h3>
390							<ul class="mobile_depth2">
391								<li><a href="/hBook/bookMainV2/19110001">티켓구매하기</a></li>
392								<li><a href="/join/loginView?mode=ticket">티켓조회</a></li>
393								<li><a href="/hBook/inquiryGroup">단체예약문의</a></li>
394								<li><a href="/hBook/inquiryPtnrsp">대관/제휴문의</a></li>
395                                <li onClick="openModal('code')" style="cursor: pointer; line-height: 3em;">제휴회원</li>
396							</ul>
397						</li>
398						<li class="mobile_depth">
399							<h3>NOTICE <i class="fas fa-sort-down ml10"></i></h3>
400							<ul class="mobile_depth2">
401								<li><a class="" href="/customer/board?bbstype=1">이벤트</a></li>
402								<li><a class="" href="/customer/board?bbstype=0">공지사항</a></li>
403								<li><a class="" href="/customer/region">제휴업체</a></li>
404								<li><a class="" href="/voc">고객의소리</a></li>
405							</ul>
406						</li>
407						
408						
409						
410		
411		
412						<!-- 다국어선택 -->
413						<li class="mobile_depth mobile-language-menu mb20">
414							<h3>
415								<div class="mobile-language-current"><i class="fas fa-globe-asia fs14"></i> 한국어(KOR)</div> <i class="fas fa-sort-down ml10"></i>
416							</h3>
417							<ul class="mobile_depth2">
418								<li><a href="/en?lang=en">English</a></li>
419								<li><a href="/cn?lang=zh_CN">简体中文</a></li>
420								<li><a href="/tw?lang=zh_TW">繁體中文</a></li>
421								<li><a href="/jp?lang=ja">日本語</a></li>
422							</ul>
423						</li>
424						<!-- //다국어선택 -->
425					</ul>
426				</div>
427			</div>
428			<!--//gnb-->
429
430		</nav>
431	</div>
432</header>
433
434<div class="modal fade" id="code" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1" aria-labelledby="codeLabel" aria-hidden="true">
435    <div class="modal-dialog modal-dialog-centered">
436        <div class="modal-content">
437            <div class="modal-header">
438                <h5 class="modal-title" id="staticBackdropLabel">제휴회원 코드 입력</h5>
439            </div>
440            <div class="modal-body">
441                <input type="text" class="form-control" id="pCode" placeholder="제휴회원 코드를 입력해주세요.">
442            </div>
443            <div class="modal-footer">
444                <button type="button" class="btn btn-secondary"	onClick="closeModal('code')">취소</button>
445                <button type="button" class="btn btn-primary" onClick="fnNext()">입력</button>
446            </div>
447        </div>
448    </div>
449</div>
450
451<script type="text/javascript">
452	function checkLogout() {
453		Swal.fire({
454			title: '로그아웃 하시겠습니까?',
455			icon: 'question',
456			showCancelButton: true,
457			confirmButtonText: '확인',
458			cancelButtonText: '아니오',
459			confirmButtonColor: '#ff710d',
460			cancelButtonColor: '#d9d9d9',
461			heightAuto: false
462		}).then(function(result) {
463			if (result.isConfirmed) location.href = "/succ-logout";
464		});
465	}
466</script>
vendor: 2 bytes, line 466
466
467<script>
468
469    let gubun 	= ""; //  1: 풀파티 패키지 예약 페이지로 이동하기 위한 값
470
471    function goBook() {
472        if (gubun == "1") location.href='/hBook/pakageBook/1911001';	// 풀파티 패키지 예약페이지
473        // else location.href='/hBook/bookMain/19110001';	// 일반 예약페이지
474        else location.href='/hBook/bookMainV2/19110001';	// 일반 예약페이지
475    }
476
477    function openModal(modalName) {
478        $('#'+modalName).addClass('menu-active show bgDarkly');
479        $('#'+modalName).css('display', 'block');
480    }
481
482    function closeModal(modalName) {
483        $('#'+modalName).removeClass('menu-active show bgDarkly');
484        $('#'+modalName).css('display', 'none');
485    }
486
487
488    //다음단계
489    function fnNext() {
490
491        let pCode = $('#pCode').val().trim();
492
493        if (pCode == '') {
494            hBookSwalAlert('제휴회원 코드를 입력해주세요.').then(function() {
495                $('#pCode').focus();
496            });
497            return false;
498        }
499
500        $.ajax({
501            url: "/hBook/validatePromotionCode/"+pCode,
502            type: "post",
503            dataType: 'json',
504            contentType:"application/json; charset=utf-8",
505            success:function(data){
506
507                if (data.code == '0000') {
508                    var vData = data.data;
509                    location.href="/hBook/bookMainV2/19110001?pCode=" + pCode + "&startDt=" + vData.StartDt + "&endDt=" + vData.EndDt;
510                } else {
511                    hBookSwalAlert(data.message, { icon: 'warning' });
512                }
513            }
514        });
515    }
516</script>
516
517
518    
519
520
521
522<div class="hBook v2-book-main">
523    <div class="container">
524        <div class="row">
525            <!-- 왼쪽: 상품 목록 -->
526            <div class="col-lg-8 col-12">
527                <nav class="v2-language-switcher" id="v2ProductLanguageSwitcher" aria-label="Language selection">
528                    <span class="v2-language-switcher__label" aria-hidden="true">
529                        <i class="bi bi-globe2"></i>
530                        <span>LANGUAGE</span>
531                    </span>
532                    <div class="v2-language-switcher__buttons" role="group" aria-label="Language selection">
533                        <button type="button"
534                                class="v2-language-button is-active"
535                                data-booking-lang="ko" aria-pressed="true"
536                                title="한국어">
537                            <span class="v2-language-code">KR</span><span class="v2-language-name">한국어</span>
538                        </button>
539                        <button type="button"
540                                class="v2-language-button "
541                                data-booking-lang="en" aria-pressed="false"
542                                title="English">
543                            <span class="v2-language-code">EN</span><span class="v2-language-name">English</span>
544                        </button>
545                        <button type="button"
546                                class="v2-language-button "
547                                data-booking-lang="ja" aria-pressed="false"
548                                title="日本語">
549                            <span class="v2-language-code">JP</span><span class="v2-language-name">日本語</span>
550                        </button>
551                        <button type="button"
552                                class="v2-language-button "
553                                data-booking-lang="zh_CN" aria-pressed="false"
554                                title="简体中文">
555                            <span class="v2-language-code">CN</span><span class="v2-language-name">简体中文</span>
556                        </button>
557                        <button type="button"
558                                class="v2-language-button "
559                                data-booking-lang="zh_TW" aria-pressed="false"
560                                title="繁體中文">
561                            <span class="v2-language-code">TW</span><span class="v2-language-name">繁體中文</span>
562                        </button>
563                    </div>
564                </nav>
565
566                <ul class="nav nav-tabs" id="v2TabNav">
567                    <li class="nav-item"><a class="nav-link active" data-tab="special" href="#">특가</a></li>
568                    <li class="nav-item"><a class="nav-link" data-tab="ticket" href="#">입장티켓</a></li>
569                    <li class="nav-item"><a class="nav-link" data-tab="facility" href="#">부대시설</a></li>
570                    <li class="nav-item"><a class="nav-link" data-tab="member" href="#">시즌/제휴</a></li>
571                </ul>
572
573                <div class="tab-content">
574                    <div class="tab-pane" id="v2TabSpecial">
575                        <div id="v2ProductListSpecial"></div>
576                    </div>
577                    <div class="tab-pane" id="v2TabTicket">
578                        <div id="v2ProductListTicket"></div>
579                    </div>
580                    <div class="tab-pane" id="v2TabFacility">
581                        <div id="v2FacilityList"></div>
582                    </div>
583                    <div class="tab-pane" id="v2TabMember">
584                        <div id="v2ProductListMember"></div>
585                    </div>
586                </div>
587
588                <!-- hidden DOM -->
589                <input type="hidden" name="bookDt" id="bookDt" />
590                <select id="selectTicket1"></select>
591                <select id="selectTicket2"></select>
592                <select id="selectSpNo"></select>
593                <select id="selectSpTime"></select>
594                <div id="timeTable"></div>
595                <div id="hiddenCnt2"></div>
596            </div>
597
598            <!-- 오른쪽: 선택 내역 + 유의사항 -->
599            <div class="col-lg-4 col-12" id="v2SidebarCol">
600                <div class="sticky-sidebar">
601                    <div class="card" id="v2TicketPanel">
602                        <h4 class="card-title">장바구니</h4>
603                        <div class="card-scroll pt-2">
604                            <div class="facility-hold-status" id="v2FacilityHoldStatus" role="timer" aria-live="off">
605                                <div class="facility-hold-status__label">
606                                    <i class="bi bi-clock" aria-hidden="true"></i>
607                                    <span>부대시설 임시선점</span>
608                                </div>
609                                <strong class="facility-hold-status__time" id="v2FacilityHoldCountdown">20:00</strong>
610                                <small>남은 시간 안에 결제를 완료해 주세요.</small>
611                            </div>
612                            <div class="option_area">
613                                <div class="text-center py-5" id="v2EmptyMsg">티켓을 선택해주세요!</div>
614                                <div class="selectedTicket1"></div>
615                                <div class="selectedTicket2"></div>
616                            </div>
617                            <div class="row totalAmtInfo">
618                                <hr class="hr_2border">
619                                <div class="col-lg-6"><p class="title-single">합계금액</p></div>
620                                <div class="col-lg-6">
621                                    <nav class="breadcrumb-box d-flex justify-content-lg-end">
622                                        <h4 class="totalAmt"></h4>
623                                    </nav>
624                                </div>
625                            </div>
626                            <button type="button" class="btn btn-orange btn-next" onclick="fnNext()">결제하기</button>
627                        </div>
628                    </div>
629
630                    <div class="card" id="v2NoticeWrap">
631                        <h4 class="card-title">유의사항</h4>
632                        <div class="card-scroll pt-2">
633                            <ul class="description" id="v2NoticeList"></ul>
634                        </div>
635                    </div>
636
637                    <div id="hiddenCnt"></div>
638                </div>
639            </div>
640        </div>
641    </div>
642    <!-- 모바일 하단 고정 바 -->
643    <div class="mobile-bar" id="v2MobileBar">
644        <div class="mobile-cart-summary">
645            <button type="button" class="mobile-cart-toggle" id="v2MobileCartToggle"
646                    aria-expanded="true" aria-controls="v2MobileCartContent" aria-label="장바구니 접기"
647                    onclick="v2ToggleMobileCart()">
648                <i class="bi bi-chevron-down mobile-cart-chevron" aria-hidden="true"></i>
649            </button>
650            <div class="mobile-cart-total-row">
651                <span class="mobile-cart-total-label">합계금액</span>
652                <strong class="mobile-total mobile-cart-toggle-total">0 원</strong>
653            </div>
654            <div class="mobile-facility-hold-status" id="v2MobileFacilityHoldStatus" role="timer" aria-live="off">
655                <span><i class="bi bi-clock" aria-hidden="true"></i> 부대시설 결제 남은 시간</span>
656                <strong id="v2MobileFacilityHoldCountdown">20:00</strong>
657            </div>
658        </div>
659        <div class="mobile-bar-content" id="v2MobileCartContent">
660            <button type="button" class="btn btn-orange mobile-cart-pay-btn" onclick="fnNext()">결제하기 →</button>
661            <button type="button" class="btn mobile-cart-detail-btn" onclick="v2ScrollToPanel()">
662                선택 내역 확인하기 ↓
663            </button>
664        </div>
665    </div>
666</div>
667
668
669
670
671
672
673
674    
675    
676    
677    
678    
679        
680
681
682<section class="section-footer" id="footer">
683	<div class="footer-link">
684		<nav class="container footer-nav" aria-label="하단 메뉴">
685			<a href="/hBook/inquiryGroup">
685단체예약문의</a>
686			<a href="/hBook/inquiryPtnrsp">대관/제휴문의</a>
687			<a href="/customer/region">제휴업체</a>
688			<a href="/privacy/cookie">이용약관</a>
689			<a href="/privacy/privacy2">영상정보처리기기 운영관리방침</a>
690			<a href="/privacy/privacy" class="is-emphasis">개인정보처리방침</a>
691		</nav>
692	</div>
693	<div class="container footer-content">
694		<div class="footer-contact">
695			<a class="footer-phone" href="tel:1566-8007">
696				<span>대표전화</span>
697				<strong>1566-8007</strong>
698			</a>
699			<p class="footer-hours">
700				<strong>고객센터 기본 운영시간</strong>
701				연중무휴 10:00~18:00 · 점심시간 11:30~13:00<br>
702				시설 운영시간에 따라 변동될 수 있습니다.
703			</p>
704		</div>
705		<dl class="footer-company-info">
706			<div class="footer-info-item">
707				<dt>사업자주소지</dt>
708				<dd>부산광역시 해운대구 달맞이길 30, 4, 5층(중동, 엘시티)</dd>
709                </br>
710				<dt>통신판매업신고번호</dt>
711				<dd>제2023-부산해운대-0834호</dd>
712			</div>
713
714			<div class="footer-info-item">
715				<dt>상호</dt>
716				<dd>이도클럽디해운대 주식회사</dd>
717				</br>
718				<dt>사업자등록번호</dt>
719				<dd>133-86-02980</dd>
720			</div>
721			<div class="footer-info-item">
722				<dt>대표자명</dt>
723				<dd>민치연</dd>
724			</div>
725		</dl>
726		<p class="footer-copyright">Copyright ⓒ YIDOCLUBDHAEUNDAE Co., Ltd. All rights reserved.</p>
727	</div>
728</section>
729
730<!-- 예약하기 -->
731
732<a href="/hBook/bookMainV2/19110001" class="reservation" id="ftBookBtn">
733	<img src="/img/tickte.svg"> 티켓구매하기
734</a>
735
736<!-- 인스타그램
737<a href="https://www.instagram.com/clubdoasis/" target="_blank;" class="insta"></a>
738
739<div id="preloader"></div> -->
740
741<!-- 찾았을 때 뜨는 모달 -->
742<div class="modal fade" id="catFoundModalSuccess" tabindex="-1" role="dialog" aria-hidden="true">
743	<div class="modal-dialog modal-dialog-centered" role="document">
744		<div class="modal-content p-5">
745			<div class="modal-body text-center">
746				<h5 class="mb-2">축하합니다!</h5>
747				<p id="catFoundMsg" class="mb-0">고양이를 찾으셨습니다.</p>
748				<div class="mt-3">
749					<button type="button" class="btn" onclick="location.href='/facility/catssoup#pills-2'">내 스탬프 확인하기</button>
750					<button type="button" class="btn" data-dismiss="modal" aria-label="Close">계속 찾기</button>
751				</div>
752			</div>
753		</div>
754	</div>
755</div>
756
757<div class="modal fade" id="catFoundModalDuplicate" tabindex="-1" role="dialog" aria-hidden="true">
758	<div class="modal-dialog modal-dialog-centered" role="document">
759		<div class="modal-content p-5">
760			<div class="modal-body text-center">
761				<h5 class="mb-2">이미 참여하신 이벤트입니다.</h5>
762				<div class="mt-3">
763					<button type="button" class="btn" onclick="location.href='/facility/catssoup#pills-2'">내 스탬프 확인하기</button>
764					<button type="button" class="btn" data-dismiss="modal" aria-label="Close">계속 찾기</button>
765				</div>
766			</div>
767		</div>
768	</div>
769</div>
770<script>
771	function fnCatFount (catName) {
772		$.ajax({
773			url: "/facility/foundCat",
774			type: "POST",
775			data :  JSON.stringify({ catName: catName }),
776			contentType: "application/json; charset=UTF-8",
777			dataType: "json", //
778			success: function(data) {
779				if (data.resultCode == '0000') {
780					// 새로 찾음
781					$('#catFoundModalSuccess').modal('show');
782				} else if (data.resultCode == '0001') {
783					// 이미 찾음
784					$('#catFoundModalDuplicate').modal('show');
785				} else {
786					// 에러 처리
787					console.error("에러 발생:", data.message);
788				}
789			}, error: function (err) {
790				console.error("에러:", err);
791			}
792		});
793	}
794</script>
794
795
796    
797
798
799
800
801
802
803
804
805
806
807<script src="/js/jquery.min.js"></script>
vendor: 1 bytes, line 807
807
808<script src="https://code.jquery.com/ui/1.13.2/jquery-ui.js"></script>
vendor: 1 bytes, line 808
808
809<script src="/js/bootstrap.bundle.min.js"></script>
809<!--부트스트랩-->
810<script src="https://cdn.jsdelivr.net/npm/@popperjs/[email protected]/dist/umd/popper.min.js" integrity="sha384-I7E8VVD/ismYTF4hNIPjVp/Zjvgyol6VFvRkX/vR+Vc4jQkC+hVqc2pM8ODewa9r" crossorigin="anonymous"></script>
810
811
812<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
812
813<!-- 마지막으로 활성화되는 jQuery에 QR 플러그인을 연결해야 한다. -->
814<script src="/js/jquery.qrcode.min.js"></script>
814
815<!-- v4일 때 Popper 필수 -->
816<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/umd/popper.min.js"></script>
vendor: 1 bytes, line 816
816
817<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.min.js"></script>
817
818
819
820
821<script src="/js/jquery.easing.min.js"></script>
821<!--움직임-->
822<!--
822<script src="/js/validate.js"></script>
822<!--폼알럿-->
823
824<script src="/js/owl.carousel.js"></script>
824<!--슬라이드-->
825<script src="/js/aos.js"></script>
825<!--스크롤-->
826<script src="/js/mobilegnb.js?now=Mon Oct 05 04:40:42 KST 2026"></script>
826<!--모바일-->
827<script src="/js/swiper.js"></script>
827
828
829<!-- 날짜 관련 라이브러리 -->
830<script src="/js/moment.min.js"></script>
830
831
832<!-- date picker -->
833<script src="/js/flatpickr/flatpickr.js"></script>
vendor: 1 bytes, line 833
833
834<script src="/js/flatpickr/flatpickr.min.js"></script>
vendor: 1 bytes, line 834
834
835<script src="/js/flatpickr/ko.js"></script>
vendor: 1 bytes, line 835
835
836<script src="/js/flatpickr/ja.js"></script>
vendor: 1 bytes, line 836
836
837<script src="/js/flatpickr/zh.js"></script>
837
838
839
840<script src="https://pay.billgate.net/paygate/plugin/gx_web_client.js" ></script>
840
841
842<script src="/js/main.js?now=Mon Oct 05 04:40:42 KST 2026"></script>
vendor: 1 bytes, line 842
842
843<script src="/js/tools.js?now=Mon Oct 05 04:40:42 KST 2026"></script>
843
844
845<script src="/js/pay.js?now=Mon Oct 05 04:40:42 KST 2026"></script>
845
846
847
848<script src="/datatables/datatables.min.js"></script>
vendor: 1 bytes, line 848
848
849<script type="text/javascript" src="https://unpkg.com/tabulator-tables/dist/js/tabulator.min.js"></script>
849
850
851<script>
852	/**
853	 * 예매 화면 공통 단일 확인 모달.
854	 *
855	 * 기본 alert()와 달리 비동기로 닫히므로, 확인 후 이동이나 포커스가 필요한
856	 * 호출부는 반환 Promise의 then()에서 후속 동작을 수행한다.
857	 */
858	window.hBookSwalAlert = function(message, options) {
859		options = options || {};
860		var documentLanguage = (document.documentElement.lang || 'ko').toLowerCase();
861		var labels;
862
863		if (documentLanguage.indexOf('zh-hant') === 0) {
864			labels = { title: '提示', confirm: '確認' };
865		} else if (documentLanguage.indexOf('zh') === 0) {
866			labels = { title: '提示', confirm: '确认' };
867		} else if (documentLanguage.indexOf('ja') === 0) {
868			labels = { title: 'ご案内', confirm: '確認' };
869		} else if (documentLanguage.indexOf('en') === 0) {
870			labels = { title: 'Notice', confirm: 'OK' };
871		} else {
872			labels = { title: '안내', confirm: '확인' };
873		}
874
875		return Swal.fire({
876			icon: options.icon || 'warning',
877			title: options.title || labels.title,
878			text: message == null ? '' : String(message),
879			confirmButtonText: options.confirmButtonText || labels.confirm,
880			confirmButtonColor: options.confirmButtonColor || '#ff710d',
881			allowOutsideClick: options.allowOutsideClick === true,
882			returnFocus: options.returnFocus !== false,
883			heightAuto: false
884		});
885	};
886</script>
886
887
888<script>
889	AOS.init();
890
891	const rootPath = "https://www.clubdoasis.com";
892
893	const boardSwiper = new Swiper(".boardSwiper", {
894		pagination: {
895			el: ".swiper-pagination",
896			clickable: true,
897		},
898		navigation: {
899	        nextEl: ".swiper-button-next",
900	        prevEl: ".swiper-button-prev",
901	    },
902	});
903
904	const eventSwiper = new Swiper(".eventSwiper", {
905		pagination: {
906			el: ".swiper-pagination",
907			clickable: true,
908		},
909	});
910
911	const mobileSwiper = new Swiper(".mobileSwiper", {
912		autoplay: {
913			delay: 2000,
914		},
915		loop: true,
916		pagination: {
917			el: ".swiper-pagination",
918			clickable: true,
919		},
920	});
921
922	const mobileSwiper2 = new Swiper(".mobileSwiper2", {
923		pagination: {
924			el: ".swiper-pagination",
925			clickable: true,
926		},
927		navigation: {
928	        nextEl: ".swiper-button-next",
929	        prevEl: ".swiper-button-prev",
930	    },
931	});
932
933	const mySwiper = new Swiper(".mySwiper", {
934		spaceBetween: 10,
935		slidesPerView: 4,
936		freeMode: true,
937		watchSlidesProgress: true,
938	});
939
940	const mySwiper2 = new Swiper(".mySwiper2", {
941		spaceBetween: 10,
942		autoplay: {
943			delay: 5000,
944			disableOnInteraction: false,
945		},
946		navigation: {
947			nextEl: ".swiper-button-next",
948			prevEl: ".swiper-button-prev",
949		},
950		thumbs: {
951			swiper: mySwiper,
952		},
953	});
954</script>
954
955
956<script>
957	document.addEventListener('DOMContentLoaded', function () {
958		setupSlides();
959	});
960
961	function setupSlides() {
962		const slides = document.querySelectorAll('.flex-slide');
963
964		slides.forEach(slide => {
965			slide.addEventListener('mouseenter', function () {
966				slides.forEach(otherSlide => {
967					const otherFlex = otherSlide.querySelector('.flex');
968					if (otherSlide !== slide) {
969						otherFlex.style.opacity = '0'; // 나머지 슬라이드의 내용을 숨김
970					}
971				});
972			});
973
974			slide.addEventListener('mouseleave', function () {
975				slides.forEach(otherSlide => {
976					const otherFlex = otherSlide.querySelector('.flex');
977					otherFlex.style.opacity = '1'; // 다시 모든 슬라이드의 내용을 보이게 함
978				});
979			});
980		});
981	}
982</script>
982
983
984<script>
985	$('#close').on("click", function() {
986		$('#layer_pop').hide();
987	});
988
989	function setCookie(name, value, expirehours) {
990		var todayDate = new Date();
991		todayDate.setHours(todayDate.getHours() + expirehours);
992		document.cookie = name + "=" + escape(value) + "; path=/; expires="
993				+ todayDate.toGMTString() + ";"
994	}
995	function closeWin() {
996		if (document.getElementById("pop_today").checked) {
997			setCookie("ncookie", "done", 24);
998		}
999		document.getElementById('layer_pop').style.display = "none";
1000	}
1001
1002	/*메인에서만 gnb 1280px*/
1003	$('.gnb_container').addClass('main_container_1280')
1004	$(".main").addClass("on"); //sub header 이미지 scale 변경. 클래스on이 추가되면 이미지가 축소.
1005</script>
1005
1006
1007<!--GNB -->
1008<script>  
1009	// 페이지 내부 서브메뉴 및 콘텐츠 hover 효과
1010    $(document).ready(function () { 
1011
1012	//서브메뉴gnb
1013    $('.submenu_open > div > ul > li > div ul').hide();    //흰바탕 bg와 gnb항목을 hide 한다.         
1014	$('.submenu_1').mouseenter(function () {
1015		$('.submenu_1 > div ul').stop().slideDown(450);   
1016	});
1017	$('.submenu_1').mouseleave(function () {
1018		$('.submenu_1 > div ul').stop().slideUp(450);    
1019	});
1020	$('.submenu_2').mouseenter(function () {
1021		$('.submenu_2 > div ul').stop().slideDown(450);   
1022	});
1023	$('.submenu_2').mouseleave(function () {
1024		$('.submenu_2 > div ul').stop().slideUp(450);    
1025	});
1026	
1027	
1028	/*news 박스 hover 액션*/ 
1029	$(".main_box_news").each(function () {
1030					$(this).mouseenter(function () {
1031						$(this).addClass("on");
1032						$(this).siblings().removeClass("on"); //각각의 자식요소는 on클래스 삭제
1033					});
1034				 	$(this).mouseleave(function () { 
1035						$(this).removeClass("on");  // 마우스를 leave하면 on클래스 삭제
1036					});
1037				});
1038
1039	});  
1040	
1041	//티켓조회
1042    $('.month_pick button').click(function(){
1043           $(this).addClass('on');    
1044           $(this).siblings().removeClass('on');
1045       });
1046	
1047</script>
1047
1048
1049
1050<script>
1051
1052    const bookingI18n = {
1053        confirm: '확인',
1054        cancel: '취소',
1055        login: '로그인',
1056        join: '회원가입',
1057        loading: '상품을 조회 중입니다.',
1058        cartCollapse: '장바구니 접기',
1059        cartExpand: '장바구니 펼치기',
1060        loginRequiredTitle: '로그인 후 상품을 확인할 수 있습니다',
1061        loginRequiredDescription: '로그인하고 회원가를 확인해 보세요. 회원 대상 다양한 프로모션을 제공합니다.',
1062        partnerCodeTitle: '제휴할인 코드 입력',
1063        partnerCodePlaceholder: '제휴할인 코드를 입력해 주세요.',
1064        apply: '적용',
1065        partnerCodeChangeTitle: '제휴할인 코드 변경',
1066        partnerCodeChangeDescription: '기존 선택 상품이 초기화됩니다.',
1067        continueText: '계속',
1068        partnerCodeChecking: '제휴할인 코드를 확인 중입니다.',
1069        partnerCodeInvalid: '유효하지 않은 코드입니다.',
1070        partnerCodeError: '제휴할인 코드 확인 중 오류가 발생했습니다.',
1071        codeCheck: '코드 확인',
1072        openSoon: '오픈예정',
1073        onSale: '판매중',
1074        saleEnded: '판매종료',
1075        emptySpecial: '진행 중인 특가가 없습니다.',
1076        groupReserved: '날짜지정 티켓',
1077        groupPeriod: '날짜미지정 티켓',
1078        groupOther: '기타',
1079        emptyTicket: '판매 가능한 티켓이 없습니다.',
1080        partnerProducts: '제휴상품',
1081        partnerCodeChange: '코드 변경',
1082        partnerCodeEnter: '제휴코드를 입력해 주세요',
1083        partnerCodeDescription: '제휴상품은 로그인 없이 제휴코드만 입력하면 확인하고 구매할 수 있습니다.',
1084        partnerCodeButton: '제휴코드 입력',
1085        partnerCodeApplied: '적용된 제휴코드',
1086        partnerLoadFailed: '제휴상품을 불러오지 못했습니다',
1087        retrySupport: '잠시 후 다시 시도해 주세요. 문제가 계속되면 고객센터로 문의해 주세요.',
1088        partnerEmpty: '해당되는 제휴상품이 없습니다',
1089        partnerEmptyDescription: '입력한 코드로 현재 판매 중인 상품이 없습니다. 제휴코드를 다시 확인해 주세요.',
1090        membershipProducts: '연회원권\/시즌권',
1091        membershipLoginTitle: '로그인하고 회원가를 확인하세요',
1092        membershipLoginDescription: '회원 대상 다양한 프로모션을 제공합니다. 상품 선택 시 로그인할 수 있습니다.',
1093        membershipEmpty: '현재 상품을 준비중입니다.',
1094        membershipEmptyDescription: '새로운 연회원권\/시즌권 상품을 준비하고 있습니다.',
1095        productLoadFailed: '상품 목록을 불러오지 못했습니다.',
1096        salePrice: '판매가',
1097        priceUnavailable: '가격 조회 불가',
1098        dateUnavailable: '선택일 예약 불가',
1099        detailFailed: '상세 정보를 불러올 수 없습니다.',
1100        more: '더보기 ▼',
1101        close: '접기 ▲',
1102        detailImage: '상세이미지',
1103        dateSelect: '날짜 선택',
1104        datePlaceholder: '날짜를 선택하세요',
1105        seatSelect: '자리 선택',
1106        seatPlaceholder: '자리를 선택해주세요',
1107        reservationDateSelect: '예약일 선택',
1108        dateNoProduct: '선택한 날짜에 예약 가능한 상품이 없습니다.',
1109        productEmpty: '선택 가능한 상품이 없습니다.',
1110        dateGuide: '예약일을 선택하면 이용 가능한 상품과 가격이 표시됩니다.',
1111        todayUnavailable: '당일 예약 불가',
1112        priceFailed: '가격 조회 실패',
1113        loadError: '상품 조회 중 오류가 발생했습니다.',
1114        seatEmpty: '구매 가능한 자리가 없습니다.',
1115        error: '오류가 발생했습니다.',
1116        timeLoading: '시간대 조회 중...',
1117        timeEmpty: '예약 가능한 시간이 없습니다.',
1118        timePlaceholder: '시간을 선택해주세요',
1119        timeAvailable: '예약가능',
1120        timeOnsite: '현장판매기간',
1121        timeClosed: '마감',
1122        timeSelect: '시간 선택',
1123        timeError: '시간 조회 중 오류가 발생했습니다.',
1124        selectionRequired: '선택 필요',
1125        dateFirst: '예약일을 먼저 선택해주세요.',
1126        quantityFirst: '수량을 1 이상 선택해주세요.',
1127        holdExpiredTitle: '선점 시간 만료',
1128        holdConfirmTitle: '부대시설을 임시로 확보했어요',
1129        dateCheckTitle: '날짜 확인',
1130        dateCheckDescription: '동일 날짜의 상품만 함께 예약할 수 있습니다. 기존 선택 내역을 비우고 다시 선택해 주세요.',
1131        cartUnavailable: '담기 불가',
1132        cartMixInvalid: '날짜지정권과 날짜미지정\/이벤트\/시즌권은 함께 결제할 수 없습니다. 부대시설은 날짜지정권과 함께 결제할 수 있습니다.',
1133        info: '알림',
1134        notice: '안내',
1135        cartRestoreTitle: '장바구니 복원 안내',
1136        cartRestoreExpired: '선점 시간이 만료되었거나 확인할 수 없는 부대시설은 장바구니에서 제외했습니다.',
1137        cartRestoreUnavailable: '부대시설 선점 상태를 확인하지 못해 복원에서 제외했습니다. 잠시 후 새로고침해 다시 확인해 주세요.',
1138        cartRestoreLanguageUnavailable: '언어 변경 후 현재 상품정보를 확인할 수 없는 항목은 장바구니에서 제외했습니다.',
1139        facilityHoldFailed: '부대시설을 선점하지 못했습니다.',
1140        facilityHoldError: '부대시설 선점 중 오류가 발생했습니다. 다시 시도해 주세요.',
1141        facilityReleaseFailed: '부대시설 선점을 해제하지 못했습니다.',
1142        facilityReleaseError: '부대시설 선점 해제 중 오류가 발생했습니다. 다시 시도해 주세요.',
1143        facilityHoldExpiredDescription: '부대시설 임시선점 {0}분이 지나 장바구니에서 해제되었습니다. 다시 선택해 주세요.',
1144        holdConfirmDescription: '선택한 부대시설은 {0}분 동안 임시선점됩니다. 장바구니에 표시된 남은 시간 안에 결제를 완료해 주세요.',
1145        facilityTimeChangeError: '시간 변경 오류',
1146        processing: '처리 중입니다. 잠시 후 다시 시도해 주세요.',
1147        reservationProcessing: '예약 정보를 처리 중입니다.',
1148        reservationError: '예약 처리 중 오류가 발생했습니다.',
1149        paymentNone: '결제할 금액이 없습니다. 상품을 선택해 주세요.',
1150        ticketInvalid: '이용권 선택 정보가 올바르지 않습니다.',
1151        facilityInvalid: '부대시설 선택 정보가 올바르지 않습니다.',
1152        facilityTimeInvalid: '부대시설 예약 시간 선택 정보를 확인해 주세요.',
1153        discountSuffix: '할인',
1154        operationNotice: '운영 안내',
1155        dateUnavailableSelected: '선택하신 날짜는 이용이 불가합니다.'
1156        , noticeCapacity: '방문고객이 많을 경우 쾌적한 이용을 위한 입장이 제한될 수 있습니다.'
1157        , noticeDateRefund: '사용일자 당일 또는 날짜가 지난 티켓에 대해서는 취소 및 환불\/교환이 불가합니다.'
1158        , noticeAdvance: '날짜지정권 티켓 및 부대시설 당일 예약은 불가합니다. 최소 1일 전 예약 부탁드립니다.'
1159        , noticeUsedNoRefund: '사용한 티켓 및 부대시설은 환불이 불가능합니다.'
1160        , noticeUnusedRefund: '유효기간 경과 전 미사용티켓은 100% 환불 가능합니다.'
1161        , noticeAutoRefund: '유효기간 경과 후 영업일 기준 2-3일 내 미사용 티켓 100% 자동 환불 처리 예정, 다만 오류로 인해 별도 처리 되지 않는 경우가 있을 수 있어 혹 취소가 되지 않을 시 고객센터로 연락 부탁드립니다.'
1162        , noticeNoChange: '티켓 수신 후 권종 및 수량 변경은 불가하며, 취소 후 재구매해야 합니다.'
1163        , noticeUsedTicketNoRefund: '사용한 티켓은 환불이 불가능합니다.'
1164    };
1165    const bookingLanguage = 'ko';
1166    const bookingLanguageParam = 'ko';
1167    const v2SupportedLanguageParams = ['ko', 'en', 'ja', 'zh_CN', 'zh_TW'];
1168    const membershipProductsEnabled = true;
1169    const bookingCalendarLocale = bookingLanguage === 'ja' ? 'ja'
1170        : (bookingLanguage === 'zh' ? 'zh' : (bookingLanguage === 'en' ? 'en' : 'ko'));
1171
1172    /* ================================================================
1173       전역 변수
1174    ================================================================ */
1175    let prevBookDt      = "";
1176    let agentCode       = "19110001";
1177    let backPageYn      = "";
1178    let mbId            = "";
1179    let mbNo            = "";
1180    let pCode           = "";
1181    let pStartDt        = "";
1182    let pEndDt          = "";
1183    let invalidPCodeYn  = "N";
1184
1185    function v2AppendQueryParam(url, name, value) {
1186        if (!value) return url;
1187        return url + (url.indexOf('?') >= 0 ? '&' : '?')
1188            + encodeURIComponent(name) + '=' + encodeURIComponent(value);
1189    }
1190
1191    function v2AppendLanguage(url) {
1192        return v2AppendQueryParam(url, 'lang', bookingLanguageParam);
1193    }
1194
1195    function v2AppendPartnerCode(url) {
1196        return v2AppendQueryParam(v2AppendLanguage(url), 'pCode', pCode);
1197    }
1198
1199    function v2EscapeHtml(value) {
1200        return $('<div>').text(value == null ? '' : String(value)).html();
1201    }
1202
1203    function v2BuildBookMainUrl(extraParams) {
1204        const extra = extraParams || {};
1205        const params = [];
1206        const requestedLanguage = Object.prototype.hasOwnProperty.call(extra, 'language')
1207            ? v2NormalizeBookingLanguage(extra.language) : bookingLanguageParam;
1208        const code = Object.prototype.hasOwnProperty.call(extra, 'pCode') ? extra.pCode : pCode;
1209        const startDt = Object.prototype.hasOwnProperty.call(extra, 'startDt') ? extra.startDt : pStartDt;
1210        const endDt = Object.prototype.hasOwnProperty.call(extra, 'endDt') ? extra.endDt : pEndDt;
1211
1212        if (requestedLanguage) params.push('lang=' + encodeURIComponent(requestedLanguage));
1213        if (code) params.push('pCode=' + encodeURIComponent(code));
1214        if (startDt) params.push('startDt=' + encodeURIComponent(startDt));
1215        if (endDt) params.push('endDt=' + encodeURIComponent(endDt));
1216        if (extra.serialNo) params.push('serialNo=' + encodeURIComponent(extra.serialNo));
1217
1218        return '/hBook/bookMainV2/' + agentCode + (params.length ? '?' + params.join('&') : '');
1219    }
1220
1221    function v2NormalizeBookingLanguage(language) {
1222        const normalized = String(language || '').trim();
1223        return v2SupportedLanguageParams.indexOf(normalized) >= 0 ? normalized : '';
1224    }
1225
1226    function v2GetLanguageFromLink(link) {
1227        if (!link) return '';
1228        try {
1229            return v2NormalizeBookingLanguage(new URL(link.href, location.origin).searchParams.get('lang'));
1230        } catch (e) {
1231            const matched = String(link.getAttribute('href') || '').match(/[?&]lang=([^&#]+)/);
1232            return matched ? v2NormalizeBookingLanguage(decodeURIComponent(matched[1])) : '';
1233        }
1234    }
1235
1236    function v2GetActiveTab() {
1237        return String($('#v2TabNav .nav-link.active').data('tab') || v2GetRequestedTab() || '');
1238    }
1239
1240    function v2BuildBookingLanguageUrl(language) {
1241        const serialNo = v2GetQuerySerialNo() || v2CheckoutSerialNo || '';
1242        const activeTab = v2GetActiveTab();
1243        const targetUrl = v2BuildBookMainUrl({ language: language, serialNo: serialNo });
1244        return targetUrl + (activeTab ? '#' + encodeURIComponent(activeTab) : '');
1245    }
1246
1247    const v2HeaderLanguageLinkSelector = [
1248        '#header-language-menu a',
1249        '#fh5co-mobile-menu .mobile-language-menu a',
1250        '#fh5co-logo-mobile-wrap .mobile-language-selector .dropdown-item'
1251    ].join(', ');
1252
1253    function v2SyncHeaderLanguageLinks() {
1254        $(v2HeaderLanguageLinkSelector).each(function() {
1255            const language = v2GetLanguageFromLink(this);
1256            if (language) this.setAttribute('href', v2BuildBookingLanguageUrl(language));
1257        });
1258    }
1259
1260    function v2ChangeBookingLanguage(language) {
1261        const normalizedLanguage = v2NormalizeBookingLanguage(language);
1262        if (!normalizedLanguage) return;
1263
1264        if (normalizedLanguage === bookingLanguageParam) {
1265            v2SyncHeaderLanguageLinks();
1266            return;
1267        }
1268
1269        v2PersistCartState();
1270        location.href = v2BuildBookingLanguageUrl(normalizedLanguage);
1271    }
1272
1273    function v2GetPartnerLookupDate() {
1274        const today = getToDay('-');
1275        if (pStartDt && today < pStartDt) return pStartDt;
1276        if (pEndDt && today > pEndDt) return pEndDt;
1277        return today;
1278    }
1279
1280    function v2BuildDatePickerOptions(options, partnerYn) {
1281        const today = getToDay('-');
1282        const usePartnerPeriod = partnerYn === true;
1283        const minDate = usePartnerPeriod && pStartDt && pStartDt > today ? pStartDt : 'today';
1284        return $.extend({
1285            locale: bookingCalendarLocale,
1286            dateFormat: 'Y-m-d',
1287            minDate: minDate,
1288            maxDate: usePartnerPeriod ? (pEndDt || null) : null,
1289            disableMobile: true
1290        }, options || {});
1291    }
1292
1293    let selectedDate    = "";   // 현재 선택된 예약일자
1294    let selectedSpNo    = "";   // 현재 선택된 상품번호
1295    let selectedPlaceNo = "";   // 현재 선택된 자리번호
1296    let selectedCavanaType = ""; // 카바나 타입 (C: 카바나, V: 면역공방)
1297
1298    // V1 호환용 전역 변수 (v2SyncToV1 에서 덮어씀)
1299    let bookList   = [];
1300    let cBookList  = [];
1301    let totalAmt   = 0;
1302    let oriTotAmt  = 0;
1303
1304    // V2 전용 전역 변수
1305    let v2BookData             = {};
1306    let v2BookList             = [];
1307    let v2CBookList            = [];
1308    let v2TotalAmt             = 0;
1309    let v2OriTotAmt            = 0;
1310    let v2SelectedCavanaAmt    = 0;
1311    let v2SelectedCavanaTotAmt = 0;
1312    let v2SelectedTimeList     = '';
1313    let v2BookIdx              = 0;
1314    let v2NextInProgress       = false;
1315    let v2CheckoutBuyerNo      = '';
1316    let v2CheckoutSerialNo     = '';
1317    let v2FacilityHoldClockOffsetMs = 0;
1318    let v2FacilityHoldTimerId  = null;
1319    let v2FacilityHoldNoticeShown = false;
1320    let v2FacilityHoldExpiryHandling = false;
1321    let v2FacilityHoldRequestCount = 0;
1322    const V2_CART_STORAGE_PREFIX = 'oasis:v2Cart:';
1323    const V2_CART_STORAGE_VERSION = 1;
1324    const V2_CART_STORAGE_MAX_AGE_MS = 2 * 60 * 60 * 1000;
1325    let v2CartRestoreState = null;
1326    let v2CartRestorePending = false;
1327    let v2CartRestoreInProgress = false;
1328
1329    // 유의사항: 현재 열어본 상품 기준 유형 (장바구니 비어있을 때 사용)
1330    let v2ActiveNoticeType     = '';
1331
1332    // 부대시설 데이터 캐시
1333    let v2FacilityDataMap = {};
1334
1335    // v2 모드 플래그 (기존 본문 로직 우회용)
1336    window.__V2_MODE__ = true;
1337
1338    function v2GoLoginWithReturnUrl() {
1339        var returnUrl = location.pathname + location.search + '#member';
1340        location.href = '/join/loginView?returnUrl=' + encodeURIComponent(returnUrl);
1341    }
1342
1343    function v2GoJoinWithReturnUrl() {
1344        var returnUrl = location.pathname + location.search + '#member';
1345        location.href = '/join/join02?returnUrl=' + encodeURIComponent(returnUrl);
1346    }
1347
1348    function v2PromptMemberLogin() {
1349        Swal.fire({
1350            icon: 'info',
1351            title: bookingI18n.loginRequiredTitle,
1352            html: bookingI18n.loginRequiredDescription,
1353            showCancelButton: true,
1354            showDenyButton: true,
1355            confirmButtonText: bookingI18n.login,
1356            denyButtonText: bookingI18n.join,
1357            cancelButtonText: bookingI18n.cancel,
1358            confirmButtonColor: '#ff710d',
1359            denyButtonColor: '#6c757d',
1360            heightAuto: false
1361        }).then(function(result) {
1362            if (result.isConfirmed) v2GoLoginWithReturnUrl();
1363            else if (result.isDenied) v2GoJoinWithReturnUrl();
1364        });
1365    }
1366
1367    function v2GetRequestedTab() {
1368        var tab = String(location.hash || '').replace(/^#/, '');
1369        return ['special', 'ticket', 'facility', 'member'].indexOf(tab) >= 0 ? tab : '';
1370    }
1371
1372    function v2PromptPartnerCode() {
1373        Swal.fire({
1374            icon: 'info',
1375            title: bookingI18n.partnerCodeTitle,
1376            input: 'text',
1377            inputValue: pCode || '',
1378            inputPlaceholder: bookingI18n.partnerCodePlaceholder,
1379            showCancelButton: true,
1380            confirmButtonText: bookingI18n.apply,
1381            cancelButtonText: bookingI18n.cancel,
1382            confirmButtonColor: '#ff710d',
1383            heightAuto: false,
1384            inputValidator: function(value) {
1385                if (!value || !String(value).trim()) return bookingI18n.partnerCodePlaceholder;
1386            }
1387        }).then(function(result) {
1388            if (!result.isConfirmed) return;
1389
1390            const nextCode = String(result.value || '').trim();
1391            const needsResetNotice = (pCode && nextCode !== pCode) || v2BookList.length > 0 || v2CBookList.length > 0;
1392            const confirmReset = needsResetNotice
1393                ? Swal.fire({
1394                    icon: 'warning',
1395                    title: bookingI18n.partnerCodeChangeTitle,
1396                    text: bookingI18n.partnerCodeChangeDescription,
1397                    showCancelButton: true,
1398                    confirmButtonText: bookingI18n.continueText,
1399                    cancelButtonText: bookingI18n.cancel,
1400                    confirmButtonColor: '#ff710d',
1401                    heightAuto: false
1402                })
1403                : Promise.resolve({ isConfirmed: true });
1404
1405            confirmReset.then(function(confirmResult) {
1406                if (!confirmResult.isConfirmed) return;
1407                v2ValidateAndApplyPartnerCode(nextCode);
1408            });
1409        });
1410    }
1411
1412    function v2ValidateAndApplyPartnerCode(nextCode) {
1413        $.ajax({
1414            url: '/hBook/validatePromotionCode/' + encodeURIComponent(nextCode),
1415            type: 'post',
1416            dataType: 'json',
1417            contentType: 'application/json; charset=utf-8',
1418            beforeSend: function() { v2ShowLoading(bookingI18n.partnerCodeChecking); },
1419            complete: v2HideLoading,
1420            success: function(data) {
1421                if (!data || data.code !== '0000' || !data.data) {
1422                    v2SwalError(data && data.message ? data.message : bookingI18n.partnerCodeInvalid, bookingI18n.codeCheck);
1423                    return;
1424                }
1425
1426                const startDt = data.data.StartDt || data.data.startDt || '';
1427                const endDt = data.data.EndDt || data.data.endDt || '';
1428                v2ReleaseAllFacilityHolds(function() {
1429                    location.href = v2BuildBookMainUrl({ pCode: nextCo
1429de, startDt: startDt, endDt: endDt });
1430                });
1431            },
1432            error: function() {
1433                v2SwalError(bookingI18n.partnerCodeError, bookingI18n.codeCheck);
1434            }
1435        });
1436    }
1437
1438    /* 로그인 체크 비활성화 — 비로그인도 장바구니 담기 가능 */
1439    function v2IsMemberOnlyProduct(item) {
1440        if (!item) return false;
1441        return String(item.purchaseContext || 'regular') !== 'partner'
1442            && String(item.saleType || '').toUpperCase() === 'S';
1443    }
1444
1445    function v2HasMemberOnlyProduct() {
1446        return v2BookList.some(v2IsMemberOnlyProduct) || v2CBookList.some(v2IsMemberOnlyProduct);
1447    }
1448
1449    function v2CreateBuyerNo() {
1450        if (mbNo && String(mbNo).trim() !== '') return String(mbNo).trim();
1451        if (window.crypto && typeof window.crypto.getRandomValues === 'function') {
1452            const values = new Uint32Array(10);
1453            window.crypto.getRandomValues(values);
1454            let generated = String((values[0] % 9) + 1);
1455            for (let i = 1; i < values.length; i++) generated += String(values[i] % 10);
1456            return generated;
1457        }
1458        return String(Date.now()).slice(-8) + String(Math.floor(Math.random() * 90) + 10);
1459    }
1460
1461    function v2EnsureCheckoutIdentity() {
1462        if (!v2CheckoutBuyerNo) v2CheckoutBuyerNo = v2CreateBuyerNo();
1463        if (!v2CheckoutSerialNo) {
1464            v2CheckoutSerialNo = getToDay('') + getNowTime('') + v2CheckoutBuyerNo;
1465            history.replaceState('', 'bookMain', v2BuildBookMainUrl({ serialNo: v2CheckoutSerialNo }));
1466        }
1467        return { buyerNo: v2CheckoutBuyerNo, serialNo: v2CheckoutSerialNo };
1468    }
1469
1470    function v2GetQuerySerialNo() {
1471        try {
1472            return new URLSearchParams(location.search).get('serialNo') || '';
1473        } catch (e) {
1474            const matched = String(location.search || '').match(/[?&]serialNo=([^&]+)/);
1475            return matched ? decodeURIComponent(matched[1]) : '';
1476        }
1477    }
1478
1479    function v2CartStorageKey(serialNo) {
1480        return V2_CART_STORAGE_PREFIX + agentCode + ':' + String(serialNo || '');
1481    }
1482
1483    function v2RemoveCartStorage(serialNo) {
1484        if (!serialNo) return;
1485        try { sessionStorage.removeItem(v2CartStorageKey(serialNo)); } catch (e) {}
1486    }
1487
1488    function v2ReadCartRestoreState(serialNo) {
1489        if (!serialNo) return null;
1490        try {
1491            const raw = sessionStorage.getItem(v2CartStorageKey(serialNo));
1492            if (!raw) return null;
1493            const state = JSON.parse(raw);
1494            const invalid = !state
1495                || Number(state.version) !== V2_CART_STORAGE_VERSION
1496                || String(state.agentCode || '') !== String(agentCode)
1497                || String(state.serialNo || '') !== String(serialNo)
1498                || !Array.isArray(state.tickets)
1499                || !Array.isArray(state.facilities)
1500                || !Number(state.savedAt)
1501                || Date.now() - Number(state.savedAt) > V2_CART_STORAGE_MAX_AGE_MS
1502                || (mbNo && String(mbNo).trim() && String(state.buyerNo || '') !== String(mbNo).trim());
1503            if (invalid || (!state.tickets.length && !state.facilities.length)) {
1504                v2RemoveCartStorage(serialNo);
1505                return null;
1506            }
1507            return state;
1508        } catch (e) {
1509            v2RemoveCartStorage(serialNo);
1510            return null;
1511        }
1512    }
1513
1514    function v2PrepareCartRestore() {
1515        const serialNo = v2GetQuerySerialNo();
1516        const state = v2ReadCartRestoreState(serialNo);
1517        if (!state) return;
1518
1519        v2CheckoutSerialNo = serialNo;
1520        v2CheckoutBuyerNo = mbNo && String(mbNo).trim()
1521            ? String(mbNo).trim() : String(state.buyerNo || '');
1522        v2CartRestoreState = state;
1523        v2CartRestorePending = true;
1524    }
1525
1526    function v2SerializableCartItems(items) {
1527        return (items || []).map(function(item) {
1528            const copy = $.extend(true, {}, item);
1529            delete copy.releaseInProgress;
1530            return copy;
1531        });
1532    }
1533
1534    function v2PersistCartState() {
1535        if (v2CartRestorePending || v2CartRestoreInProgress) return;
1536        if (!v2BookList.length && !v2CBookList.length) {
1537            v2RemoveCartStorage(v2CheckoutSerialNo);
1538            return;
1539        }
1540
1541        const identity = v2EnsureCheckoutIdentity();
1542        const state = {
1543            version: V2_CART_STORAGE_VERSION,
1544            savedAt: Date.now(),
1545            agentCode: agentCode,
1546            serialNo: identity.serialNo,
1547            buyerNo: identity.buyerNo,
1548            partnerCode: pCode || '',
1549            language: bookingLanguageParam,
1550            tickets: v2SerializableCartItems(v2BookList),
1551            facilities: v2SerializableCartItems(v2CBookList)
1552        };
1553        try { sessionStorage.setItem(v2CartStorageKey(identity.serialNo), JSON.stringify(state)); } catch (e) {}
1554    }
1555
1556    function v2FacilityRestoreKey(item) {
1557        const times = String(item && item.bkTimeList || '').split(',').map(function(value) {
1558            return String(value || '').trim();
1559        }
1559).filter(Boolean).sort();
1560        return String(item && item.bkDate || '').replace(/-/g, '') + '|'
1561            + String(item && item.spNo || '').trim() + '|'
1562            + String(item && item.bkSeq || '').trim() + '|'
1563            + times.join(',');
1564    }
1565
1566    function v2RestoreTicketEntry(source) {
1567        const item = $.extend(true, {}, source || {});
1568        const qty = Number(item.qty || 0);
1569        const spAmt = Number(item.spAmt || 0);
1570        const finalAmt = Number(item.dcSpAmt || item.spAmt || 0);
1571        const saleType = String(item.saleType || '').toUpperCase();
1572        if (!item.spNo || qty <= 0 || spAmt <= 0 || finalAmt <= 0) return false;
1573        if (saleType === 'R' && (!item.bkDate || String(item.bkDate) < getToDay('-'))) return false;
1574
1575        v2BookIdx++;
1576        item.bookIdx = v2BookIdx;
1577        item.qty = qty;
1578        v2BookList.push(item);
1579        v2TotalAmt += finalAmt * qty;
1580        v2OriTotAmt += spAmt * qty;
1581        v2TicketDivCnt(item.bookIdx,
1582            String(item.spName || '') + (item.bkDate ? ' (' + item.bkDate + ')' : ''),
1583            spAmt, finalAmt, finalAmt < spAmt, item.dcType || '', item.dcValue || item.dcAmt || 0, qty);
1584        return true;
1585    }
1586
1587    function v2RestoreFacilityEntry(source, status) {
1588        const item = $.extend(true, {}, source || {});
1589        const times = String(item.bkTimeList || '').split(',').map(function(value) {
1590            return String(value || '').trim();
1591        }).filter(Boolean);
1592        const spAmt = Number(item.spAmt || 0);
1593        const finalAmt = Number(item.dcSpAmt || item.spAmt || 0);
1594        const expiry = Number(status && status.expiresAtEpochMs || 0);
1595        if (!item.spNo || !item.bkSeq || !times.length || spAmt <= 0 || finalAmt <= 0 || expiry <= v2FacilityHoldNow()) {
1596            return false;
1597        }
1598
1599        v2BookIdx++;
1600        item.bookIdx = v2BookIdx;
1601        item.qty = times.length;
1602        item.holdExpiresAtEpochMs = expiry;
1603        item.holdSerialNo = v2CheckoutSerialNo;
1604        item.holdMinutes = Number(item.holdMinutes) || 20;
1605        item.spPlaceValue = item.spPlaceValue || (String(item.spNo) + String(item.bkSeq));
1606        item.displayTxt = item.displayTxt || v2BuildSpPlaceName(item.spPlaceName || item.spName || '', item.bkTimeList);
1607        v2CBookList.push(item);
1608        v2TotalAmt += finalAmt * item.qty;
1609        v2OriTotAmt += spAmt * item.qty;
1610        v2CavanaDivCnt(item.bookIdx, item.displayTxt, item.spPlaceValue,
1611            finalAmt * item.qty, item.dcCode || '', item.dcType || '', item.dcValue || item.dcAmt || 0,
1612            spAmt * item.qty);
1613        return true;
1614    }
1615
1616    function v2ApplyRestoredCart(tickets, facilities, statusMap, options) {
1617        options = options || {};
1618        v2BookList = [];
1619        v2CBookList = [];
1620        v2TotalAmt = 0;
1621        v2OriTotAmt = 0;
1622        v2BookIdx = 0;
1623
1624        (tickets || []).forEach(v2RestoreTicketEntry);
1625        let invalidFacilityCount = 0;
1626        (facilities || []).forEach(function(item) {
1627            const status = statusMap[v2FacilityRestoreKey(item)];
1628            if (!status || status.valid !== true || !v2RestoreFacilityEntry(item, status)) {
1629                invalidFacilityCount++;
1630            }
1631        });
1632
1633        const firstDateItem = v2CBookList[0] || v2BookList.find(function(item) { return item.bkDate; });
1634        const cartDate = firstDateItem ? String(firstDateItem.bkDate || '') : '';
1635        if (cartDate) {
1636            $('#bookDt').val(cartDate);
1637            if (v2CBookList.length) selectedDate = cartDate;
1638        }
1639
1640        $('.totalAmt').html(toLocaleString(v2TotalAmt));
1641        $('.mobile-total').text(toLocaleString(v2TotalAmt));
1642        if (v2BookList.length || v2CBookList.length) {
1643            $('.totalAmtInfo').css('display', 'flex');
1644            $('.btn-next').show();
1645            $('#v2EmptyMsg, #v2MobileEmptyMsg').hide();
1646            if (window.innerWidth <= 768) $('#v2MobileTotalInfo').show();
1647        } else {
1648            $('.totalAmtInfo, .btn-next, #v2MobileTotalInfo').hide();
1649            $('#v2EmptyMsg, #v2MobileEmptyMsg').show();
1650        }
1651
1652        v2SyncFacilityHoldTimer();
1653        v2RenderNotice();
1654        v2SyncToV1();
1655        v2CartRestoreInProgress = false;
1656        v2CartRestorePending = false;
1657        v2CartRestoreState = null;
1658        if (options.persist !== false) v2PersistCartState();
1659
1660        const restoreWarnings = [];
1661        if (invalidFacilityCount > 0 && options.suppressInvalidNotice !== true) {
1662            restoreWarnings.push(bookingI18n.cartRestoreExpired);
1663        }
1664        if (Number(options.languageRemovedCount || 0) > 0) {
1665            restoreWarnings.push(bookingI18n.cartRestoreLanguageUnavailable);
1666        }
1667        if (restoreWarnings.length) {
1668            v2SwalWarning(restoreWarnings.join('\n'), bookingI18n.cartRestoreTitle);
1669        }
1670        if (typeof options.done === 'function') options.done();
1671    }
1672
1673    function v2LocalizedTicketName(detail, item) {
1674        const children = detail && Array.isArray(detail.childProducts)
1675            ? detail.childProducts : [];
1676        const matched = children.find(function(child) {
1677            return String(child.spNo || '') === String(item.spNo || '');
1678        });
1679        if (matched && String(matched.spName || '').trim()) {
1680            return String(matched.spName).trim();
1681        }
1682        if (detail && String(detail.spNo || '') === String(item.spNo || '')) {
1683            return String(detail.spName || detail.displayTitle || '').trim();
1684        }
1685        return '';
1686    }
1687
1688    function v2RelocalizeFacilityCartItem(item) {
1689        const localized = v2FacilityDataMap[String(item.spNo || '')]
1690            || v2FacilityDataMap[String(item.displayId || '')];
1691        const localizedName = localized
1692            ? String(localized.spName || localized.displayTitle || '').trim() : '';
1693        if (!localizedName) return;
1694
1695        const oldName = String(item.spName || '');
1696        const oldPlaceName = String(item.spPlaceName || oldName);
1697        const placeSuffix = oldName && oldPlaceName.indexOf(oldName) === 0
1698            ? oldPlaceName.substring(oldName.length) : '';
1699        const localizedPlaceName = localizedName + placeSuffix;
1700        const oldDisplayText = String(item.displayTxt || oldPlaceName);
1701        const displaySuffix = oldPlaceName && oldDisplayText.indexOf(oldPlaceName) === 0
1702            ? oldDisplayText.substring(oldPlaceName.length) : '';
1703
1704        item.spName = localizedName;
1705        item.spPlaceName = localizedPlaceName;
1706        item.displayTxt = localizedPlaceName + displaySuffix;
1707    }
1708
1709    function v2RelocalizeCartState(state, done) {
1710        const result = {
1711            tickets: v2SerializableCartItems(state.tickets || []),
1712            facilities: v2SerializableCartItems(state.facilities || []),
1713            removedCount: 0
1714        };
1715        const savedLanguage = v2NormalizeBookingLanguage(state.language);
1716        if (savedLanguage && savedLanguage === bookingLanguageParam) {
1717            done(result);
1718            return;
1719        }
1720
1721        result.facilities.forEach(v2RelocalizeFacilityCartItem);
1722        if (!result.tickets.length) {
1723            done(result);
1724            return;
1725        }
1726
1727        const keep = result.tickets.map(function() { return true; });
1728        const groups = {};
1729        result.tickets.forEach(function(item, index) {
1730            const displayId = String(item.displayId || '').trim();
1731            if (!displayId) {
1732                keep[index] = false;
1733                return;
1734            }
1735            const purchaseContext = String(item.purchaseContext || 'regular');
1736            const detailDate = String(item.bkDate || '').trim()
1737                || (purchaseContext === 'partner' ? v2GetPartnerLookupDate() : getToDay('-'));
1738            const key = purchaseContext + '|' + displayId + '|' + detailDate;
1739            if (!groups[key]) {
1740                groups[key] = {
1741                    displayId: displayId,
1742                    purchaseContext: purchaseContext,
1743                    detailDate: detailDate,
1744                    indexes: []
1745                };
1746            }
1747            groups[key].indexes.push(index);
1748        });
1749
1750        const groupList = Object.keys(groups).map(function(key) { return groups[key]; });
1751        if (!groupList.length) {
1752            result.tickets = [];
1753            result.removedCount = keep.length;
1754            done(result);
1755            return;
1756        }
1757
1758        let remaining = groupList.length;
1759        function finishGroup() {
1760            remaining--;
1761            if (remaining > 0) return;
1762            result.removedCount = keep.filter(function(value) { return !value; }).length;
1763            result.tickets = result.tickets.filter(function(item, index) { return keep[index]; });
1764            done(result);
1765        }
1766
1767        groupList.forEach(function(group) {
1768            const detailPath = '/hBook/v2/display/detail/'
1769                + encodeURIComponent(group.displayId)
1770                + '?bkDate=' + encodeURIComponent(group.detailDate);
1771            $.ajax({
1772                url: group.purchaseContext === 'partner'
1773                    ? v2AppendPartnerCode(detailPath)
1774                    : v2AppendLanguage(detailPath),
1775                type: 'get',
1776                dataType: 'json',
1777                success: function(detail) {
1778                    group.indexes.forEach(function(index) {
1779                        const localizedName = v2LocalizedTicketName(
1780                            detail, result.tickets[index]);
1781                        if (localizedName) result.tickets[index].spName = localizedName;
1782                        else keep[index] = false;
1783                    });
1784                },
1785                error: function() {
1786                    group.indexes.forEach(function(index) { keep[index] = false; });
1787                },
1788                complete: finishGroup
1789            });
1790        });
1791    }
1792
1793    function v2RestoreSavedCart(done) {
1794        const state = v2CartRestoreState;
1795        if (!v2CartRestorePending || !state) {
1796            v2CartRestorePending = false;
1797            if (typeof done === 'function') done();
1798            return;
1799        }
1800
1801        v2CartRestoreInProgress = true;
1802        v2RelocalizeCartState(state, function(localized) {
1803            const tickets = localized.tickets;
1804            const facilities = localized.facilities;
1805            const restoreOptions = function(extra) {
1806                return $.extend({
1807                    done: done,
1808                    languageRemovedCount: localized.removedCount
1809                }, extra || {});
1810            };
1811            if (!facilities.length) {
1812                v2ApplyRestoredCart(tickets, [], {}, restoreOptions());
1813                return;
1814            }
1815
1816            const bookingDates = facilities.map(function(item) { return String(item.bkDate || ''); })
1817                .filter(Boolean).filter(function(value, index, list) { return list.indexOf(value) === index; });
1818            if (bookingDates.length !== 1) {
1819                v2ApplyRestoredCart(tickets, facilities, {}, restoreOptions());
1820                return;
1821            }
1822
1823            $.ajax({
1824                url: '/hBook/v2/facility/hold/status/' + encodeURIComponent(agentCode)
1825                    + '/' + encodeURIComponent(bookingDates[0])
1826                    + '/' + encodeURIComponent(v2CheckoutSerialNo),
1827                type: 'post',
1828                dataType: 'json',
1829                contentType: 'application/json; charset=utf-8',
1830                timeout: 10000,
1831                data: JSON.stringify(facilities.map(v2FacilityHoldPayload)),
1832                success: function(data) {
1833                    const statusMap = {};
1834                    if (data && data.code === '0000' && Array.isArray(data.facilities)) {
1835                        if (Number(data.serverNowEpochMs)) {
1836                            v2FacilityHoldClockOffsetMs = Number(data.serverNowEpochMs) - Date.now();
1837                        }
1838                        data.facilities.forEach(function(status) {
1839                            statusMap[String(status.key || '')] = status;
1840                        });
1841                    }
1842                    v2ApplyRestoredCart(tickets, facilities, statusMap, restoreOptions());
1843                },
1844                error: function() {
1845                    v2ApplyRestoredCart(tickets, facilities, {}, restoreOptions({
1846                        persist: false,
1847                        suppressInvalidNotice: true,
1848                        done: function() {
1849                            v2SwalWarning(bookingI18n.cartRestoreUnavailable, bookingI18n.cartRestoreTitle);
1850                            if (typeof done === 'function') done();
1851                        }
1852                    }));
1853                }
1854            });
1855        });
1856    }
1857
1858    function v2RequireLoginBeforeCart(item) {
1859        const requiresLogin = item ? v2IsMemberOnlyProduct(item) : v2HasMemberOnlyProduct();
1860        if (!requiresLogin) return true;
1861        if (mbNo && String(mbNo).trim() !== '') return true;
1862        v2PromptMemberLogin();
1863        return false;
1864    }
1865
1866    function v2SelectDisplayProduct(displayId, viewKey, partnerYn, saleType) {
1867        const isGuestSeason = partnerYn !== true
1868            && String(saleType || '').toUpperCase() === 'S'
1869            && (!mbNo || !String(mbNo).trim());
1870        if (isGuestSeason) {
1871            v2PromptMemberLogin();
1872            return;
1873        }
1874        v2ToggleDetail(displayId, viewKey, partnerYn, saleType);
1875    }
1876
1877    /* ================================================================
1878       V1 동기화 헬퍼
1879    ================================================================ */
1880    function v2SyncToV1() {
1881        try {
1882            cBookList = v2CBookList;
1883            bookList  = v2BookList;
1884            totalAmt  = v2TotalAmt;
1885            oriTotAmt = v2OriTotAmt;
1886        } catch(e) {}
1887        v2PersistCartState();
1888    }
1889
1890    /* ================================================================
1891       상품조회 로딩 스피너 헬퍼
1892    ================================================================ */
1893    var v2LoadingCount     = 0;
1894    var v2LoadingStartAt   = 0;
1895    var v2LoadingMinMs     = 600;
1896    var v2LoadingHideTimer = null;
1897
1898    function v2ShowLoading(message) {
1899        var loadingText = (typeof message === 'string' && message.trim())
1900            ? message
1901            : bookingI18n.loading;
1902
1903        v2LoadingCount++;
1904
1905        if (v2LoadingHideTimer) {
1906            clearTimeout(v2LoadingHideTimer);
1907            v2LoadingHideTimer = null;
1908        }
1909
1910        if (v2LoadingCount === 1) {
1911            v2LoadingStartAt = Date.now();
1912
1913            $('#v2-loading-overlay .v2-loading-text').text(loadingText);
1914
1915            $('#v2-loading-overlay')
1916                .appendTo('body')
1917                .stop(true, true)
1918                .css({
1919                    display: 'flex',
1920                    opacity: 1
1921                });
1922        }
1923    }
1924
1925    function v2HideLoading() {
1926        v2LoadingCount = Math.max(0, v2LoadingCount - 1);
1927
1928        if (v2LoadingCount > 0) {
1929            return;
1930        }
1931
1932        var elapsed = Date.now() - v2LoadingStartAt;
1933        var remain  = Math.max(0, v2LoadingMinMs - elapsed);
1934
1935        v2LoadingHideTimer = setTimeout(function() {
1936            if (v2LoadingCount === 0) {
1937                $('#v2-loading-overlay').hide();
1938                $('#v2-loading-overlay .v2-loading-text').text(bookingI18n.loading);
1939            }
1940            v2LoadingHideTimer = null;
1941        }, remain);
1942    }
1943
1944    function v2ResetLoading() {
1945        v2LoadingCount = 0;
1946        v2LoadingStartAt = 0;
1947
1948        if (v2LoadingHideTimer) {
1949            clearTimeout(v2LoadingHideTimer);
1950            v2LoadingHideTimer = null;
1951        }
1952
1953        $('#v2-loading-overlay').hide();
1954        $('#v2-loading-overlay .v2-loading-text').text(bookingI18n.loading);
1955    }
1956
1957    /* ================================================================
1958       초기화
1959    ================================================================ */
1960    function v2InitPage() {
1961        if (invalidPCodeYn === 'Y') {
1962            v2SwalError(bookingI18n.partnerCodeInvalid, bookingI18n.codeCheck);
1963            return;
1964        }
1965
1966        v2ShowLoading();
1967
1968        $.when(
1969            v2LoadDisplayList(),
1970            v2LoadFacilityList()
1971        ).always(function() {
1972            v2RestoreSavedCart(v2HideLoading);
1973        });
1974
1975        v2RenderNotice();
1976    }
1977
1978    /* ================================================================
1979       상단 탭 전환
1980    ================================================================ */
1981    function v2SetTabVisibility(tab, visible) {
1982        $('#v2TabNav .nav-link[data-tab="' + tab + '"]').closest('.nav-item').toggle(!!visible);
1983    }
1984
1985    function v2IsTabVisible(tab) {
1986        const $item = $('#v2TabNav .nav-link[data-tab="' + tab + '"]').closest('.nav-item');
1987        return $item.length > 0 && $item.css('display') !== 'none';
1988    }
1989
1990    function v2FirstVisibleTab(preferred) {
1991        if (preferred && v2IsTabVisible(preferred)) {
1992            return preferred;
1993        }
1994
1995        const tabOrder = ['special', 'ticket', 'facility', 'member'];
1996        for (let i = 0; i < tabOrder.length; i++) {
1997            if (v2IsTabVisible(tabOrder[i])) {
1998                return tabOrder[i];
1999            }
2000        }
2001        return null;
2002    }
2003
2004    function v2SwitchTab(tab) {
2005        const nextTab = v2FirstVisibleTab(tab);
2006        if (!nextTab) {
2007            return;
2008        }
2009
2010        $('#v2TabNav .nav-link').removeClass('active');
2011        $('#v2TabNav .nav-link[data-tab="' + nextTab + '"]').addClass('active');
2012        $('.tab-pane').hide();
2013        $('#v2Tab' + nextTab.charAt(0).toUpperCase() + nextTab.slice(1)).show();
2014    }
2015
2016    function v2EnsureVisibleTab(preferred) {
2017        const activeTab = $('#v2TabNav .nav-link.active').data('tab');
2018        v2SwitchTab(v2IsTabVisible(activeTab) ? activeTab : preferred);
2019    }
2020
2021    /* ================================================================
2022       유의사항 동적 노출 — 장바구니 + 현재 선택 상품 기준
2023       ================================================================
2024       우선순위: 장바구니 기준 > 현재 선택 상품 기준 > 기본값(PERIOD)
2025       RESERVE : 날짜지정권(R) / 이벤트권(E) / 부대시설 포함 시
2026       PERIOD  : 날짜미지정권(P) / 시즌권(S) 만 선택 시
2027                 ※ S(시즌권)는 정책 확정 전까지 P와 동일 문구 적용
2028    ================================================================ */
2029
2030    /** 장바구니 기준 유의사항 유형 반환 */
2031    function v2GetCartNoticeType() {
2032        var hasFacility = v2CBookList.length > 0;
2033        var hasReserve  = v2BookList.some(function(item) {
2034            var st = String(item.saleType || '').toUpperCase();
2035            return st === 'R';
2036        });
2037        var hasPeriod = v2BookList.some(function(item) {
2038            var st = String(item.saleType || '').toUpperCase();
2039            return st === 'P' || st === 'E' || st === 'S';
2040        });
2041        if (hasFacility || hasReserve) return 'RESERVE';
2042        if (hasPeriod) return 'PERIOD';
2043        return '';
2044    }
2045
2046    /** 최종 유의사항 유형 반환 (장바구니 > 현재 선택 상품 > 기본값) */
2047    function v2ResolveNoticeType() {
2048        var cartType = v2GetCartNoticeType();
2049        if (cartType) return cartType;
2050        if (v2ActiveNoticeType) return v2ActiveNoticeType;
2051        return 'PERIOD';
2052    }
2053
2054    function v2RenderNotice() {
2055        var type = v2ResolveNoticeType();
2056        var $wrap = $('#v2NoticeWrap');
2057        var $list = $('#v2NoticeList');
2058
2059        if (!type) {
2060            $wrap.hide();
2061            $list.empty();
2062            return;
2063        }
2064
2065        var items = [];
2066        if (type === 'RESERVE') {
2067            items = [
2068                bookingI18n.noticeCapacity,
2069                bookingI18n.noticeDateRefund,
2070                bookingI18n.noticeAdvance,
2071                bookingI18n.noticeUsedNoRefund
2072            ];
2073        } else {
2074            items = [
2075                bookingI18n.noticeCapacity,
2076                bookingI18n.noticeUnusedRefund,
2077                bookingI18n.noticeAutoRefund,
2078                bookingI18n.noticeNoChange,
2079                bookingI18n.noticeUsedTicketNoRefund
2080            ];
2081        }
2082
2083        var html = '';
2084        for (var i = 0; i < items.length; i++) {
2085            html += '<li>' + items[i] + '</li>';
2086        }
2087        $list.html(html);
2088        $wrap.show();
2089    }
2090
2091    function v2LoadDisplayList() {
2092        const regularListUrl = "/hBook/v2/display/list/" + agentCode + "/" 
2092+ getToDay('-');
2093        const partnerListUrl = "/hBook/v2/display/list/" + agentCode + "/" + v2GetPartnerLookupDate();
2094        let regularFailed = false;
2095        let partnerFailed = false;
2096
2097        const regularRequest = $.ajax({
2098            url: v2AppendLanguage(regularListUrl),
2099            type: "get",
2100            dataType: "json"
2101        }).then(function(list) {
2102            return list || [];
2103        }, function() {
2104            regularFailed = true;
2105            return [];
2106        });
2107
2108        const partnerRequest = pCode ? $.ajax({
2109            url: v2AppendPartnerCode(partnerListUrl),
2110            type: "get",
2111            dataType: "json"
2112        }).then(function(list) {
2113            return list || [];
2114        }, function() {
2115            partnerFailed = true;
2116            return [];
2117        }) : $.Deferred().resolve([]).promise();
2118
2119        return $.when(regularRequest, partnerRequest).done(function(regularList, partnerList) {
2120            v2RenderProductList(regularList, partnerList, regularFailed, partnerFailed);
2121        });
2122    }
2123
2124    function v2LoadFacilityList() {
2125        return $.ajax({
2126            url: v2AppendLanguage("/hBook/v2/facility/list/" + agentCode + "/" + getToDay('-')),
2127            type: "get",
2128            dataType: "json",
2129            success: function(list) { v2RenderFacilityList(list || []); },
2130            error:   function() { $('#v2FacilityList').html('<div class="alert alert-light">' + bookingI18n.productLoadFailed + '</div>'); }
2131        });
2132    }
2133
2134    function v2CalculateFinalPrice(price, dcType, dcAmt) {
2135        const originalPrice = Number(price || 0);
2136        const discountType = Number(dcType || 0);
2137        const discountAmount = Number(dcAmt || 0);
2138
2139        if (discountAmount > 0 && discountType === 1) {
2140            return originalPrice - discountAmount;
2141        }
2142        if (discountAmount > 0 && discountType === 2) {
2143            return Math.round(originalPrice * (1 - discountAmount / 100));
2144        }
2145        return originalPrice;
2146    }
2147
2148    function v2NormalizeProductPriceRange(minPrice, maxPrice) {
2149        if (minPrice == null || maxPrice == null || minPrice === '' || maxPrice === '') {
2150            return null;
2151        }
2152
2153        const min = Number(minPrice);
2154        const max = Number(maxPrice);
2155        if (!isFinite(min) || !isFinite(max)) return null;
2156        return { min: Math.min(min, max), max: Math.max(min, max) };
2157    }
2158
2159    function v2GetProductSummaryPriceRange(item, purchaseContext) {
2160        item = item || {};
2161        if (purchaseContext === 'partner') {
2162            const ownPrice = v2CalculateFinalPrice(item.price, item.dcType, item.dcAmt);
2163            return v2NormalizeProductPriceRange(ownPrice, ownPrice);
2164        }
2165        return v2NormalizeProductPriceRange(
2166            item.childMinFinalPrice, item.childMaxFinalPrice);
2167    }
2168
2169    function v2GetDetailSummaryPriceRange(data, partnerYn) {
2170        data = data || {};
2171        if (partnerYn === true) {
2172            const ownPrice = v2CalculateFinalPrice(data.price, data.dcType, data.dcAmt);
2173            return v2NormalizeProductPriceRange(ownPrice, ownPrice);
2174        }
2175
2176        const children = Array.isArray(data.childProducts) ? data.childProducts : [];
2177        if (!children.length) return null;
2178
2179        let min = null;
2180        let max = null;
2181        children.forEach(function(child) {
2182            const finalPrice = v2CalculateFinalPrice(
2183                child.spAmt, child.dcType, child.dcAmt);
2184            min = min == null ? finalPrice : Math.min(min, finalPrice);
2185            max = max == null ? finalPrice : Math.max(max, finalPrice);
2186        });
2187        return v2NormalizeProductPriceRange(min, max);
2188    }
2189
2190    function v2FormatProductPriceRange(range) {
2191        if (!range) return '';
2192        return range.min === range.max
2193            ? toLocaleString(range.min)
2194            : toLocaleString(range.min) + ' ~ ' + toLocaleString(range.max);
2195    }
2196
2197    function v2BuildProductSummaryPriceHtml(item, purchaseContext, viewKey) {
2198        const priceText = v2FormatProductPriceRange(
2199            v2GetProductSummaryPriceRange(item, purchaseContext));
2200        return '<p class="product-summary-price' + (priceText ? '' : ' is-hidden')
2201            + '" id="v2ProductSummaryPrice_' + viewKey + '">'
2202            + '<span class="product-summary-price-label">' + bookingI18n.salePrice + '</span>'
2203            + '<strong>' + priceText + '</strong></p>';
2204    }
2205
2206    function v2UpdateProductSummaryPrice(viewKey, data, partnerYn) {
2207        const $price = $('#v2ProductSummaryPrice_' + viewKey);
2208        if (!$price.length) return;
2209
2210        const priceText = v2FormatProductPriceRange(
2211            v2GetDetailSummaryPriceRange(data, partnerYn));
2212        $price.find('strong').text(priceText);
2213        $price.toggleClass('is-hidden', !priceText);
2214    }
2215
2216    /* ================================================================
2217       티켓 목록 렌더링 — 특가(이벤트권) / 입장티켓 탭으로 분리
2218    ================================================================ */
2219    function v2RenderProductList(list, partnerList, regularFailed, partnerFailed) {
2220        list = Array.isArray(list) ? list : [];
2221        partnerList = Array.isArray(partnerList) ? partnerList : [];
2222        const reserveList = [], periodList = [], eventList = [], seasonList = [], unknownList = [];
2223        list.forEach(function(item) {
2224            const t = String(item.saleType || '').trim().toUpperCase();
2225            item.saleType = t;
2226            if      (t === 'R') reserveList.push(item);
2227            else if (t === 'P') periodList.push(item);
2228            else if (t === 'E') eventList.push(item);
2229            else if (t === 'S') seasonList.push(item);
2230            else                unknownList.push(item);
2231        });
2232
2233        v2SetTabVisibility('special', eventList.length > 0);
2234
2235        function buildCardHtml(item, purchaseContext) {
2236            const thumb        = (item.thumbUrl && item.thumbUrl.trim()) ? item.thumbUrl : '/img/event/event.png';
2237            const today        = getToDay('-');
2238            const isComingSoon = item.saleStartDate && item.saleStartDate > today;
2239            const isSaleEnded  = String(item.saleType || '').toUpperCase() === 'R'
2240                && item.saleEndDate && item.saleEndDate < today;
2241            const clickable    = !isComingSoon && !isSaleEnded;
2242            const isPartner    = purchaseContext === 'partner';
2243            const viewKey      = (isPartner ? 'partner-' : 'regular-') + item.displayId;
2244            const statusBadge  = isComingSoon
2245                ? ' <span class="badge badge-warning">' + bookingI18n.openSoon + '(' + item.saleStartDate + ')</span>'
2246                : isSaleEnded
2247                    ? ' <span class="badge badge-secondary">' + bookingI18n.saleEnded + '(' + item.saleEndDate + ')</span>'
2248                    : ' <span class="badge badge-success">' + bookingI18n.onSale + '</span>';
2249
2250            let h = '<div class="product-item" data-display-id="' + item.displayId + '" data-purchase-context="' + purchaseContext + '">';
2251            h += '<div class="product-summary' + (!clickable ? ' is-unavailable' : '') + '"'
2252                + (clickable ? ' onclick="v2SelectDisplayProduct(\'' + item.displayId + '\', \'' + viewKey + '\', ' + isPartner + ', \'' + (item.saleType || '') + '\')"' : '')
2253                + '>';
2254            h += '<img class="product-thumb" src="' + thumb + '" onerror="if(!this.dataset.fallback){this.dataset.fallback=\'1\';this.src=\'/img/event/event.png\';}" alt="thumb">';
2255            h += '<div class="product-info">';
2256            h += '<h4 class="product-title">' + (item.displayTitle || '') + statusBadge + '</h4>';
2257            h += '<p class="product-subtitle">' + (item.displaySubTitle || '') + '</p>';
2258            h += v2BuildProductSummaryPriceHtml(item, purchaseContext, viewKey);
2259            h += '</div></div>';
2260            if (clickable) h += '<div class="product-detail" id="v2-detail-' + viewKey + '"></div>';
2261            h += '</div>';
2262            return h;
2263        }
2264
2265        /* ── 특가 탭 = 이벤트권(E) 전용 ── */
2266        let specialHtml;
2267        if (eventList.length) {
2268            specialHtml = '<div class="card"><div class="card-scroll">';
2269            eventList.forEach(function(item) { specialHtml += buildCardHtml(item, 'regular'); });
2270            specialHtml += '</div></div>';
2271        } else {
2272            specialHtml = '<div class="alert alert-light">' + bookingI18n.emptySpecial + '</div>';
2273        }
2274        $('#v2ProductListSpecial').html(specialHtml);
2275
2276        /* ── 입장티켓 탭 = 날짜지정/날짜미지정/기타 ── */
2277        const groups = [
2278            { label: bookingI18n.groupPeriod, list: periodList },
2279            { label: bookingI18n.groupReserved, list: reserveList },
2280            { label: bookingI18n.groupOther, list: unknownList }
2281        ];
2282        let ticketHtml = '<div class="card"><div class="card-scroll">';
2283        let hasTicket = false;
2284        groups.forEach(function(g) {
2285            if (!g.list.length) return;
2286            hasTicket = true;
2287            ticketHtml += '<div class="subgroup">';
2288            ticketHtml += '<h5 class="subgroup-title">' + g.label + '</h5>';
2289            g.list.forEach(function(item) { ticketHtml += buildCardHtml(item, 'regular'); });
2290            ticketHtml += '</div>';
2291        });
2292        ticketHtml += '</div></div>';
2293        $('#v2ProductListTicket').html(hasTicket ? ticketHtml : '<div class="alert alert-light">' + bookingI18n.emptyTicket + '</div>');
2294
2295        function buildMemberState(iconClass, title, description, actionsHtml, errorState) {
2296            const centered = !iconClass;
2297            let state = '<div class="member-tab-state' + (centered ? ' member-tab-state--centered' : '')
2298                + (errorState ? ' member-tab-state--error' : '') + '" role="status">';
2299            if (iconClass) state += '<i class="bi ' + iconClass + '" aria-hidden="true"></i>';
2300            state += '<div class="member-tab-state-copy"><strong>' + title + '</strong>';
2301            state += '<p>' + description + '</p>';
2302            if (actionsHtml) state += '<div class="member-tab-actions">' + actionsHtml + '</div>';
2303            state += '</div></div>';
2304            return state;
2305        }
2306
2307        let memberContentHtml = '<div class="member-tab-sections">';
2308
2309        memberContentHtml += '<section class="member-product-section member-product-section--partner">';
2310        memberContentHtml += '<div class="member-section-header"><h5 class="member-section-title">' + bookingI18n.partnerProducts + '</h5>';
2311        if (pCode) {
2312            memberContentHtml += '<button type="button" class="btn btn-outline-secondary btn-sm" onclick="v2PromptPartnerCode()"><i class="bi bi-pencil-square" aria-hidden="true"></i> ' + bookingI18n.partnerCodeChange + '</button>';
2313        }
2314        memberContentHtml += '</div>';
2315
2316        if (!pCode) {
2317            memberContentHtml += buildMemberState(
2318                '',
2319                bookingI18n.partnerCodeEnter,
2320                bookingI18n.partnerCodeDescription,
2321                '<button type="button" class="btn btn-orange" onclick="v2PromptPartnerCode()"><i class="bi bi-key" aria-hidden="true"></i> ' + bookingI18n.partnerCodeButton + '</button>',
2322                false
2323            );
2324        } else {
2325            memberContentHtml += '<div class="partner-code-box"><p class="partner-code-status">' + bookingI18n.partnerCodeApplied + ' <strong>' + v2EscapeHtml(pCode) + '</strong></p></div>';
2326            if (partnerFailed) {
2327                memberContentHtml += buildMemberState(
2328                    'bi-exclamation-circle',
2329                    bookingI18n.partnerLoadFailed,
2330                    bookingI18n.retrySupport,
2331                    '',
2332                    true
2333                );
2334            } else if (partnerList.length) {
2335                partnerList.forEach(function(item) { memberContentHtml += buildCardHtml(item, 'partner'); });
2336            } else {
2337                memberContentHtml += buildMemberState(
2338                    'bi-info-circle',
2339                    bookingI18n.partnerEmpty,
2340                    bookingI18n.partnerEmptyDescription,
2341                    '',
2342                    false
2343                );
2344            }
2345        }
2346        memberContentHtml += '</section>';
2347
2348        if (membershipProductsEnabled) {
2349        memberContentHtml += '<section class="member-product-section member-product-section--season">';
2350        memberContentHtml += '<div class="member-section-header"><h5 class="member-section-title">' + bookingI18n.membershipProducts + '</h5></div>';
2351        const isGuest = !mbNo || !String(mbNo).trim();
2352        const memberActions = '<button type="button" class="btn btn-orange" onclick="v2GoLoginWithReturnUrl()"><i class="bi bi-box-arrow-in-right" aria-hidden="true"></i> ' + bookingI18n.login + '</button>'
2353            + '<button type="button" class="btn btn-outline-secondary" onclick="v2GoJoinWithReturnUrl()"><i class="bi bi-person-plus" aria-hidden="true"></i> ' + bookingI18n.join + '</button>';
2354        if (regularFailed) {
2355            memberContentHtml += buildMemberState(
2356                'bi-exclamation-circle',
2357                    bookingI18n.membershipEmpty,
2358                    bookingI18n.retrySupport,
2359                '',
2360                true
2361            );
2362        } else if (seasonList.length) {
2363            if (isGuest) {
2364                memberContentHtml += buildMemberState(
2365                    '',
2366                    bookingI18n.membershipLoginTitle,
2367                    bookingI18n.membershipLoginDescription,
2368                    memberActions,
2369                    false
2370                );
2371            }
2372            seasonList.forEach(function(item) { memberContentHtml += buildCardHtml(item, 'regular'); });
2373        } else if (isGuest) {
2374            memberContentHtml += buildMemberState(
2375                'bi-lock',
2376                bookingI18n.loginRequiredTitle,
2377                bookingI18n.membershipLoginDescription,
2378                memberActions,
2379                false
2380            );
2381        } else {
2382            memberContentHtml += buildMemberState(
2383                'bi-info-circle',
2384                bookingI18n.membershipEmpty,
2385                bookingI18n.membershipEmptyDescription,
2386                '',
2387                false
2388            );
2389        }
2390        memberContentHtml += '</section>';
2391        }
2392        memberContentHtml += '</div>';
2393
2394        const memberHtml = '<div class="card"><div class="card-scroll member-tab-scroll">' + memberContentHtml + '</div></div>';
2395        $('#v2ProductListMember').html(memberHtml);
2396
2397        if (regularFailed) {
2398            $('#v2ProductListSpecial').html('<div class="alert alert-light">' + bookingI18n.productLoadFailed + '</div>');
2399            $('#v2ProductListTicket').html('<div class="alert alert-light">' + bookingI18n.productLoadFailed + '</div>');
2400        }
2401
2402        const requestedTab = v2GetRequestedTab();
2403        const firstTab = requestedTab || (pCode ? 'member' : (eventList.length ? 'special' : (hasTicket ? 'ticket' : 'member')));
2404        if (requestedTab || pCode) v2SwitchTab(firstTab);
2405        else v2EnsureVisibleTab(firstTab);
2406    }
2407
2408    /* ================================================================
2409       부대시설 목록 렌더링
2410    ================================================================ */
2411    function v2BuildFacilityPriceHtml(price, dcType, dcAmt, minFinalPrice, maxFinalPrice) {
2412        const originalPrice = Number(price || 0);
2413        const discountType  = Number(dcType || 0);
2414        const discountValue = Number(dcAmt || 0);
2415        const hasPriceRange = minFinalPrice != null && maxFinalPrice != null
2416            && minFinalPrice !== '' && maxFinalPrice !== ''
2417            && isFinite(Number(minFinalPrice)) && isFinite(Number(maxFinalPrice));
2418        const hasDiscount   = discountValue > 0 && (discountType === 1 || discountType === 2);
2419
2420        if (hasPriceRange) {
2421            const rangeMin = Math.min(Number(minFinalPrice), Number(maxFinalPrice));
2422            const rangeMax = Math.max(Number(minFinalPrice), Number(maxFinalPrice));
2423            const rangeText = rangeMin === rangeMax
2424                ? toLocaleString(rangeMin)
2425                : toLocaleString(rangeMin) + ' ~ ' + toLocaleString(rangeMax);
2426            return '<span class="v2-price-original">'
2427                + toLocaleString(originalPrice) + '</span> '
2428                + '<span class="facility-final-price v2-price-discounted'
2429                + (rangeMin === rangeMax ? '' : ' facility-price-range')
2430                + '">' + rangeText + '</span>';
2431        }
2432
2433        if (!hasDiscount) return toLocaleString(originalPrice);
2434
2435        const finalPrice = discountType === 1
2436            ? originalPrice - discountValue
2437            : originalPrice - (originalPrice * discountValue / 100);
2438        return '<span class="v2-price-original">'
2439            + toLocaleString(originalPrice) + '</span> '
2440            + '<span class="v2-price-discounted">'
2441            + toLocaleString(finalPrice) + '</span>';
2442    }
2443
2444    function v2UpdateFacilityCardPrice(sp) {
2445        if (!sp || sp.spNo == null) return;
2446
2447        const spNo   = String(sp.spNo);
2448        const price  = Number(sp.spAmt || 0);
2449        const dcAmt  = Number(sp.dcAmt || 0);
2450        const dcType = Number(sp.dcType || 0);
2451        const cached = v2FacilityDataMap[spNo] || {};
2452
2453        cached.price = price;
2454        cached.dcAmt = dcAmt;
2455        cached.dcType = dcType;
2456        cached.sangpumId = sp.sangpumId || '';
2457        v2FacilityDataMap[spNo] = cached;
2458
2459        $('#v2FacCardPrice_' + spNo).html(v2BuildFacilityPriceHtml(price, dcType, dcAmt));
2460        $('#v2FacBox_' + spNo).attr('data-spamt', price);
2461    }
2462
2463    function v2SetFacilityCardPriceMessage(spNo, message) {
2464        $('#v2FacCardPrice_' + String(spNo)).text(message || bookingI18n.priceUnavailable);
2465    }
2466
2467    function v2SetAllFacilityCardPriceMessages(message) {
2468        Object.keys(v2FacilityDataMap).forEach(function(spNo) {
2469            v2SetFacilityCardPriceMessage(spNo, message);
2470        });
2471    }
2472
2473    function v2SyncFacilityCardPrices(list) {
2474        const selectedDateProducts = Array.isArray(list) ? list : [];
2475        Object.keys(v2FacilityDataMap).forEach(function(spNo) {
2476            const matched = selectedDateProducts.find(function(item) {
2477                return String(item.spNo) === String(spNo);
2478            });
2479            if (matched) v2UpdateFacilityCardPrice(matched);
2480            else v2SetFacilityCardPriceMessage(spNo, bookingI18n.dateUnavailable);
2481        });
2482    }
2483
2484    function v2RenderFacilityList(list) {
2485        if (!Array.isArray(list) || !list.length) {
2486            $('#v2FacilityList').html('');
2487            return;
2488        }
2489
2490        v2FacilityDataMap = {};
2491        list.forEach(function(item) { v2FacilityDataMap[item.spNo] = item; });
2492
2493        function buildFacilityCardHtml(item) {
2494            const thumb        = (item.thumbUrl && item.thumbUrl.trim()) ? item.thumbUrl : '/img/event/event.png';
2495            const today        = getToDay('-');
2496            const isComingSoon = item.saleStartDate && item.saleStartDate > today;
2497            const clickable    = !isComingSoon;
2498            const statusBadge  = isComingSoon
2499                ? ' <span class="badge badge-warning">' + bookingI18n.openSoon + '(' + item.saleStartDate + ')</span>'
2500                : ' <span class="badge badge-success">' + bookingI18n.onSale + '</span>';
2501            var startTime = v2FormatTimeHHmm(item.operStartTime);
2502            var endTime = v2FormatTimeHHmm(item.operEndTime);
2503            let h = '<div class="product-item" data-sp-no="' + item.spNo + '">';
2504            h += '<div class="product-summary' + (isComingSoon ? ' is-coming-soon' : '') + '"'
2505                + (clickable ? ' onclick="v2ToggleFacilityDetail(\'' + item.spNo + '\', this)"' : '')
2506                + '>';
2507            h += '<img class="product-thumb" src="' + thumb + '" onerror="if(!this.dataset.fallback){this.dataset.fallback=\'1\';this.src=\'/img/event/event.png\';}" alt="thumb">';
2508            h += '<div class="product-info">';
2509            h += '<h4 class="product-title">' + (item.displayTitle || '') + statusBadge + '</h4>';
2510            h += '<p class="product-subtitle">'
2511                + (startTime ? ' (' + startTime + '~' + endTime + ')' : '')
2512                + '</p>';
2513            h += '<p class="product-price" id="v2FacCardPrice_' + item.spNo + '">'
2514                + v2BuildFacilityPriceHtml(item.price, item.dcType, item.dcAmt,
2515                    item.facilityMinFinalPrice, item.facilityMaxFinalPrice)
2516                + '</p>';
2517            h += '</div></div>';
2518            if (clickable) h += '<div class="facility-detail" id="v2-detail-fac-' + item.spNo + '"></div>';
2519            h += '</div>';
2520            return h;
2521        }
2522
2523        let html = '<div class="card"><div class="card-scroll">';
2524        list.forEach(function(item) { html += buildFacilityCardHtml(item); });
2525        html += '</div></div>';
2526
2527        $('#v2FacilityList').html(html);
2528    }
2529    function v2FormatTimeHHmm(value) {
2530        if (value == null || value === '') return '';
2531
2532        var str = String(value).replace(/:/g, '').trim();
2533        while (str.length < 4) {
2534            str = '0' + str;
2535        }
2536
2537        return str.substring(0, 2) + ':' + str.substring(2, 4);
2538    }
2539
2540    /* ================================================================
2541       상세 토글
2542    ================================================================ */
2543    function v2ToggleFacilityDetail(spNo) {
2544        const $detail = $('#v2-detail-fac-' + spNo);
2545        const $card   = $detail.closest('.product-item');
2546        const isOpen  = $detail.is(':visible');
2547
2548        $('.facility-detail').not($detail).slideUp(200);
2549        $('.product-item').not($card).removeClass('is-open');
2550
2551        if (isOpen) {
2552            $detail.slideUp(200);
2553            $card.removeClass('is-open');
2554            return;
2555        }
2556        $card.addClass('is-open');
2557
2558        v2ActiveNoticeType = 'RESERVE';
2559        v2RenderNotice();
2560
2561        if (!$detail.data('loaded')) {
2562            const data = v2FacilityDataMap[spNo];
2563            if (!data) {
2564                $detail.html('<div class="alert alert-light">' + bookingI18n.detailFailed + '</div>');
2565            } else {
2566                const enriched = Object.assign({}, data, { displayId: spNo });
2567                $detail.html(v2BuildDetailHtml(enriched));
2568                v2InitFacilityDetail(spNo, enriched);
2569            }
2570            $detail.data('loaded', true);
2571        }
2572        $detail.stop(true, true).slideDown(200);
2573    }
2574
2575    function v2ToggleDetail(displayId, viewKey, partnerYn, summarySaleType) {
2576        viewKey = viewKey || ('regular-' + displayId);
2577        partnerYn = partnerYn === true;
2578        const $detail = $('#v2-detail-' + viewKey);
2579        const $card   = $detail.closest('.product-item');
2580        const isOpen  = $detail.is(':visible');
2581
2582        $('.product-detail').not($detail).slideUp(200);
2583        $('.product-item').not($card).removeClass('is-open');
2584
2585        if (isOpen) {
2586            $detail.slideUp(200);
2587            $card.removeClass('is-open');
2588            return;
2589        }
2590        $card.addClass('is-open');
2591
2592        if (!$detail.data('loaded')) {
2593            const cartEntryDate = v2IsDateRequiredSaleType(summarySaleType)
2594                ? v2GetEntryTicketCartDate()
2595                : '';
2596            const initialDetailDate = cartEntryDate || (partnerYn ? v2GetPartnerLookupDate() : getToDay('-'));
2597            $.ajax({
2598                url: partnerYn
2599                    ? v2AppendPartnerCode('/hBook/v2/display/detail/' + displayId + '?bkDate=' + initialDetailDate)
2600                    : v2AppendLanguage('/hBook/v2/display/detail/' + displayId + '?bkDate=' + initialDetailDate),
2601                type: 'get',
2602                dataType: 'json',
2603                beforeSend: v2ShowLoading,
2604                complete: v2HideLoading,
2605                success: function(data) {
2606                    data.__viewKey = viewKey;
2607                    data.__partnerYn = partnerYn;
2608                    v2UpdateProductSummaryPrice(viewKey, data, partnerYn);
2609                    const detailSaleType = String(data.saleType || summarySaleType || '').toUpperCase();
2610                    const autoSelectedDate = v2IsDateRequiredSaleType(detailSaleType) ? cartEntryDate : '';
2611                    if (autoSelectedDate) {
2612                        data.__selectedBkDate = autoSelectedDate;
2613                    }
2614                    $detail.html(v2BuildDetailHtml(data));
2615
2616                    var st = detailSaleType;
2617                    var spGb = String(data.spGbCode || '').padStart(4, '0');
2618                    if (spGb === '0051') {
2619                        v2ActiveNoticeType = 'RESERVE';
2620                    } else {
2621                        v2ActiveNoticeType = (st === 'R') ? 'RESERVE' : 'PERIOD';
2622                    }
2623                    $detail.data('cachedSaleType', v2ActiveNoticeType);
2624                    $detail.data('saleTypeCode', st);
2625                    $detail.data('selectedBkDate', autoSelectedDate);
2626                    $detail.data('cartDateApplied', !!autoSelectedDate);
2627                    $detail.data('detailData', data);
2628                    v2RenderNotice();
2629
2630                    if (spGb === '0051') {
2631                        v2InitFacilityDetail(viewKey, data);
2632                    } else {
2633                        const el = document.getElementById('v2TicketDate_' + viewKey);
2634                        if (el) {
2635                            flatpickr(el, v2BuildDatePickerOptions({
2636                                position: 'below',
2637                                defaultDate: autoSelectedDate || null,
2638                                onChange: function(dates, dateStr) {
2639                                    $('#bookDt').val(dateStr);
2640            v2ShowTicketDateState(viewKey, bookingI18n.loading);
2641                                    v2CheckOperation(dateStr, viewKey, data, function() {
2642                                        v2ReloadDetailByDate(displayId, viewKey, partnerYn, dateStr);
2643                                    });
2644                                }
2645                            }, partnerYn));
2646                        }
2647                        if (autoSelectedDate) {
2648                            $('#bookDt').val(autoSelectedDate);
2649                            v2CheckOperation(autoSelectedDate, viewKey, data);
2650                        }
2651                    }
2652                    $detail.data('loaded', true);
2653
2654                    $detail.stop(true, true).slideDown(200, function() {
2655                        const el = document.getElementById('v2TicketDate_' + viewKey);
2656                        if (!autoSelectedDate && window.innerWidth > 768 && el && el._flatpickr) {
2657                            el._flatpickr.open();
2658                        }
2659                    });
2660                },
2661                error: function() {
2662                    $detail.html('<div class="alert alert-light">' + bookingI18n.detailFailed + '</div>');
2663                    $detail.data('loaded', true);
2664                    $detail.stop(true, true).slideDown(200);
2665                }
2666            });
2667            return;
2668        }
2669
2670        const cartEntryDate = v2GetEntryTicketCartDate();
2671        const cachedSaleTypeCode = String($detail.data('saleTypeCode') || summarySaleType || '').toUpperCase();
2672        const cachedSelectedDate = String($detail.data('selectedBkDate') || $('#v2TicketDate_' + viewKey).val() || '');
2673        if (v2IsDateRequiredSaleType(cachedSaleTypeCode)
2674                && cartEntryDate
2675                && cachedSelectedDate !== cartEntryDate) {
2676            $detail.stop(true, true).slideDown(200);
2677            v2CheckOperation(cartEntryDate, viewKey, $detail.data('detailData') || {}, function() {
2678                v2ReloadDetailByDate(displayId, viewKey, partnerYn, cartEntryDate, true);
2679            });
2680            return;
2681        }
2682
2683        var cached = $detail.data('cachedSaleType') || '';
2684        if (cached) {
2685            v2ActiveNoticeType = cached;
2686            v2RenderNotice();
2687        }
2688
2689        $detail.stop(true, true).slideDown(200, function() {
2690            const el = document.getElementById('v2TicketDate_' + viewKey);
2691            if (window.innerWidth > 768 && el && el._flatpickr) {
2692                el._flatpickr.open();
2693            }
2694        });
2695    }
2696
2697    /* ================================================================
2698       상세 HTML 빌더
2699    ================================================================ */
2700    function v2ToggleMore(btn) {
2701        const $left = $(btn).closest('.detail-left');
2702        const isExpanded = $left.hasClass('is-expanded');
2703
2704        if (isExpanded) {
2705            $left.removeClass('is-expanded');
2706            $(btn).text(bookingI18n.more);
2707        } else {
2708            $left.addClass('is-expanded');
2709            $(btn).text(bookingI18n.close);
2710        }
2711    }
2712
2713    function v2BuildDetailHtml(data) {
2714        const spGb = String(data.spGbCode || '').padStart(4, '0');
2715
2716        function buildLeftCol(d, extraHtml) {
2717            const hasImg    = d.detailImageUrl && d.detailImageUrl.trim();
2718            const hasNotice = !!d.noticeHtml;
2719            const hasGuide  = !!d.guideHtml;
2720            const isEmpty   = !hasImg && !hasNotice && !hasGuide && !extraHtml;
2721
2722            let col = '<div class="detail-left col-7' + (isEmpty ? ' detail-left--empty' : '') + '">';
2723            if (hasImg)    col += '<img class="detail-image" src="' + d.detailImageUrl + '" onerror="if(!this.dataset.fallback){this.dataset.fallback=\'1\';this.src=\'/img/event/251209_nightPocha.jpg\';}" alt="' + bookingI18n.detailImage + '">';
2724            if (hasNotice) col += '<div class="mb-3">' + d.noticeHtml + '</div>';
2725            if (hasGuide)  col += '<div class="mb-3">' + d.guideHtml  + '</div>';
2726            if (extraHtml) col += extraHtml;
2727
2728            if (!isEmpty) {
2729                col += '<button type="button" class="more-btn" onclick="v2ToggleMore(this)">' + bookingI18n.more + '</button>';
2730            }
2731
2732            col += '</div>';
2733            return col;
2734        }
2735
2736        /* 부대시설 */
2737        if (spGb === '0051') {
2738            const did  = data.__viewKey || data.displayId;
2739            const spNo = data.spNo || '';
2740
2741            let right = '<div class="fac-box" id="v2FacBox_' + did + '" data-spno="' + spNo + '" data-spamt="' + (data.price || 0) + '">';
2742            right += '<div class="fac-row"><label class="fac-label">' + bookingI18n.dateSelect + '</label>'
2743                + '<input type="text" id="v2FacDate_' + did + '" class="form-control fac-date-input" placeholder="' + bookingI18n.datePlaceholder + '"></div>';
2744            right += '<div class="fac-row v2-facility-place-row" id="v2FacPlaceRow_' + did + '">'
2745                + '<label class="fac-label">' + bookingI18n.seatSelect + '</label>'
2746                + '<select id="v2FacPlace_' + did + '" class="form-control fac-place" onchange="v2ChangeFacilityPlace(\'' + did + '\', this.value)">'
2747                + '<option value="">' + bookingI18n.seatPlaceholder + '</option></select></div>';
2748            right += '<div class="v2-facility-time-table" id="v2FacTimeTable_' + did + '"></div>';
2749            right += '<div class="v2-facility-selected-area" id="v2FacSelectedArea_' + did + '"></div>';
2750            right += '<div id="v2FacHidden_' + did + '"></div>';
2751            right += '</div>';
2752
2753            return '<div class="detail-layout row">' + buildLeftCol(data, '') + '<div class="detail-right col-5">' + right + '</div></div>';
2754        }
2755
2756        /* 티켓 */
2757        const saleType = data.saleType || 'R';
2758        const childs   = data.childProducts || [];
2759        const isSingle = childs.length === 0;
2760        const parent   = isSingle && data.__partnerYn === true && String(data.parentPurchasableYn || 'N') === 'Y' ? {
2761            spNo: data.spNo || '',
2762            spName: data.spName || data.displayTitle || '',
2763            spAmt: data.price || 0,
2764            dcType: data.dcType || 0,
2765            dcAmt: data.dcAmt || 0,
2766            sangpumId: data.sangpumId || data.spNo || '',
2767            tkGubun: data.tkGubun || ''
2768        } : null;
2769        const did      = data.__viewKey || data.displayId;
2770        const purchaseContext = data.__partnerYn === true ? 'partner' : 'regular';
2771        const tk       = String(data.tkGubun || '');
2772
2773        const selectedBkDate = data.__selectedBkDate || '';
2774        const isReserve = v2IsDateRequiredSaleType(saleType);
2775        const shouldShowChildren = !isReserve || !!selectedBkDate;
2776
2777        let right = '';
2778
2779        if (isReserve) {
2780            right += '<div class="reservation-box">';
2781            right += '<div class="calendar-wrapper"><label class="fac-label">' + bookingI18n.reservationDateSelect + '</label>'
2782                + '<input type="text" class="form-control detail-date" id="v2TicketDate_' + did + '" placeholder="' + bookingI18n.datePlaceholder + '">'
2783                + '</div></div>';
2784        }
2785
2786        if (shouldShowChildren) {
2787            right += '<div class="child-list" data-did="' + did + '" data-saletype="' + saleType + '" data-tkgubun="' + tk + '" data-context="' + purchaseContext + '">';
2788            if (!isSingle) {
2789                childs.forEach(function(c) {
2790                    right += v2BuildChildItemHtml(c, null);
2791                });
2792            } else if (parent) {
2793                right += v2BuildChildItemHtml(parent, null);
2794            } else {
2795                if (isReserve && selectedBkDate) {
2796                    right += '<p class="text-center py-3 mb-0">' + bookingI18n.dateNoProduct + '</p>';
2797                } else {
2798                    right += '<p class="mt-4 text-center">' + bookingI18n.productEmpty + '</p>';
2799                }
2800            }
2801            right += '</div>';
2802            /* 티켓담기 버튼 제거 — +/- 클릭 시 즉시 장바구니 반영 */
2803        } else {
2804            right += '<p class="pkg-info-note pkg-info-note--spaced">' + bookingI18n.dateGuide + '</p>';
2805        }
2806
2807        return '<div class="detail-layout row">' + buildLeftCol(data) + '<div class="detail-right col-5">' + right + '</div></div>';
2808    }
2809
2810    function v2BuildChildItemHtml(c, pkgDid) {
2811        const price     = c.spAmt  || 0;
2812        const dcAmtVal  = Number(c.dcAmt  || 0);
2813        const dcTypeVal = Number(c.dcType || 0);
2814
2815        let priceHtml   = toLocaleString(price);
2816        let resultPrice = price;
2817        let savePrice   = 0;
2818
2819        if (dcAmtVal > 0) {
2820            if (dcTypeVal == 1) {
2821                resultPrice = price - dcAmtVal;
2822                savePrice   = dcAmtVal;
2823                priceHtml   = '<span class="v2-price-original">' + toLocaleString(price) + '</span>'
2824                    + ' <span class="v2-price-discounted">' + toLocaleString(resultPrice) + '</span>';
2825            } else if (dcTypeVal == 2) {
2826                resultPrice = Math.round(price * (1 - dcAmtVal / 100));
2827                savePrice   = dcAmtVal;
2828                priceHtml   = '<span class="v2-price-original">' + toLocaleString(price) + '</span>'
2829                    + ' <span class="v2-price-discounted">' + toLocaleString(resultPrice) + '</span>';
2830            }
2831        }
2832
2833        let hiddenHtml = '';
2834        if (dcAmtVal > 0 && (dcTypeVal == 1 || dcTypeVal == 2) && c.spNo) {
2835            const sId = c.sId || c.sangpumId || c.spNo;
2836            hiddenHtml += '<input type="hidden" class="v2-discount-id" value="' + sId         + '">';
2837            hiddenHtml += '<input type="hidden" class="v2-discount-amt" value="' + dcAmtVal    + '">';
2838            hiddenHtml += '<input type="hidden" class="v2-discount-type" value="' + dcTypeVal   + '">';
2839            hiddenHtml += '<input type="hidden" class="v2-save-price" value="' + savePrice   + '">';
2840            hiddenHtml += '<input type="hidden" class="v2-result-price" value="' + resultPrice + '">';
2841        }
2842
2843        let qtyHtml;
2844        if (pkgDid != null) {
2845            qtyHtml = '<div class="child-qty pkg-rep-qty">'
2846                +   '<button type="button" class="qty-minus" onclick="event.stopPropagation();v2PkgQtyChange(' + pkgDid + ', -1)">-</button>'
2847                +   '<span class="qty-display" id="v2PkgQty_' + pkgDid + '">1</span>'
2848                +   '<button type="button" class="qty-plus" onclick="event.stopPropagation();v2PkgQtyChange(' + pkgDid + ', 1)">+</button>'
2849                + '</div>';
2850        } else {
2851            qtyHtml = '<div class="child-qty">'
2852                +   '<button type="button" class="qty-minus">-</button>'
2853                +   '<span class="qty-display">0</span>'
2854                +   '<button type="button" class="qty-plus">+</button>'
2855                + '</div>';
2856        }
2857
2858        return '<div class="child-item" data-spno="' + (c.spNo || '') + '" data-spamt="' + price + '" data-adtgb="' + (c.adtGb || '') + '">'
2859            + '<p class="child-name">' + (c.spName || '') + '</p>'
2860            + '<div class="option">'
2861            +   '<p class="child-price">' + priceHtml + '</p>'
2862            +   qtyHtml
2863            + '</div>'
2864            + hiddenHtml
2865            + '</div>';
2866    }
2867
2868    /* ================================================================
2869       부대시설 날짜/자리/시간 조회
2870    ================================================================ */
2871    function v2InitFacilityDetail(did, data) {
2872        const el = document.getElementById('v2FacDate_' + did);
2873        if (!el) return;
2874        flatpickr(el, v2BuildDatePickerOptions({
2875            onChange: function(dates, dateStr) {
2876                selectedDate = dateStr;
2877                $('#bookDt').val(dateStr);
2878
2879                if (!v2CanAddCartDate(dateStr)) {
2880                    const elC = document.getElementById('v2FacDate_' + did);
2881                    if (elC && elC._flatpickr) elC._flatpickr.clear();
2882                    selectedDate = v2GetCartBookingDate() || '';
2883                    $('#bookDt').val(selectedDate);
2884                    return;
2885                }
2886
2887                v2CheckOperationFacility(dateStr, did, function() {
2888                    v2SearchFacilityDirect(did, dateStr);
2889                });
2890            }
2891        }, false));
2892    }
2893
2894    function v2SearchFacilityDirect(did, bkDate) {
2895        const $placeRow = $('#v2FacPlaceRow_' + did);
2896        const $timeDiv  = $('#v2FacTimeTable_' + did);
2897        const $hidden   = $('#v2FacHidden_' + did);
2898
2899        $placeRow.hide();
2900        $timeDiv.html('');
2901        $hidden.html('');
2902
2903        const cardSpNo = $('#v2FacBox_' + did).attr('data-spno') || '';
2904
2905        $.ajax({
2906            url: '/hBook/spSearch2/' + agentCode + '/' + bkDate,
2907            type: 'get', dataType: 'json',
2908            success: function(data) {
2909                v2SyncFacilityCardPrices(data);
2910                if (!data || !data.length) {
2911                    $timeDiv.html('<p class="text-center py-3 mb-0">' + bookingI18n.dateNoProduct + '</p>');
2912                    return;
2913                }
2914                const sp = data.find(function(d){ return String(d.spNo) === String(cardSpNo); });
2915                if (checkIsToday(bkDate)) {
2916                    v2SetFacilityCardPriceMessage(cardSpNo, bookingI18n.todayUnavailable);
2917                    $timeDiv.html('<p class="text-center py-3 mb-0">' + bookingI18n.todayUnavailable + '</p>');
2918                    return;
2919                }
2920                if (!sp) {
2921                    v2SetFacilityCardPriceMessage(cardSpNo, bookingI18n.dateUnavailable);
2922                    console.error('[V2][Facility] sp 매칭 실패 — cardSpNo가 응답에 없음', { cardSpNo: cardSpNo, responseSpNos: data.map(function(d){ return d.spNo; }) });
2923                    $timeDiv.html('<p class="text-center py-3 mb-0">' + bookingI18n.dateNoProduct + '</p>');
2924                    return;
2925                }
2926                const spNo  = sp.spNo;
2927                const spAmt = Number(sp.spAmt || 0);
2928                const dcAmt = Number(sp.dcAmt || 0);
2929                const dcType= Number(sp.dcType || 0);
2930                const sId   = sp.sangpumId;
2931
2932                let hiddenHtml = '<input type="hidden" id="v2amt_' + did + '_' + spNo + '" value="' + spAmt + '">';
2933                if (dcAmt > 0 && sId) {
2934                    const dcResult = calc(spAmt, String(dcType), dcAmt);
2935                    hiddenHtml += '<input type="hidden" id="v2sId_'        + did + '_' + spNo + '" value="' + sId + '">';
2936                    hiddenHtml += '<input type="hidden" id="v2dcAmt_'      + did + '_' + spNo + '" value="' + dcAmt + '">';
2937                    hiddenHtml += '<input type="hidden" id="v2dcType_'     + did + '_' + spNo + '" value="' + dcType + '">';
2938                    hiddenHtml += '<input type="hidden" id="v2savePrice_'  + did + '_' + spNo + '" value="' + dcResult.savePrice + '">';
2939                    hiddenHtml += '<input type="hidden" id="v2resultPrice_'+ did + '_' + spNo + '" value="' + dcResult.resultPrice + '">';
2940                }
2941                $hidden.html(hiddenHtml);
2942
2943                let $spSel = $('#v2FacSp_' + did);
2944                // 장바구니 표기는 이 옵션 텍스트를 그대로 쓰므로, spSearch2의 한글
2945                // 이름 대신 번역된 상품명을 우선 사용 (v2RelocalizeFacilityCartItem 과 동일 규칙)
2946                const facLocalized = v2FacilityDataMap[String(spNo)] || {};
2947                const facOptionName = String(facLocalized.spName || facLocalized.displayTitle || '').trim()
2948                    || sp.spName || spNo;
2949                if (!$spSel.length) {
2950                    $hidden.append('<select id="v2FacSp_' + did + '" class="v2-hidden-select"><option value="' + spNo + '" selected>' + facOptionName + '</option></select>');
2951                } else {
2952                    $spSel.html('<option value="' + spNo + '" selected>' + facOptionName + '</option>');
2953                }
2954                v2ChangeFacility(did, spNo);
2955            },
2956            error: function() {
2957                v2SetAllFacilityCardPriceMessages(bookingI18n.priceFailed);
2958                $timeDiv.html('<p class="text-center py-3 mb-0">' + bookingI18n.loadError + '</p>');
2959            }
2960        });
2961    }
2962    function checkIsToday(someDate) {
2963        const date = new Date(someDate);
2964        const today = new Date();
2965        return (
2966            date.getFullYear() === today.getFullYear() &&
2967            date.getMonth() === today.getMonth() &&
2968            date.getDate() === today.getDate()
2969        );
2970    }
2971    function v2ChangeFacility(did, spNo) {
2972        selectedSpNo = spNo;
2973        const $placeRow = $('#v2FacPlaceRow_' + did);
2974        const $placeSel = $('#v2FacPlace_' + did);
2975        const $timeDiv  = $('#v2FacTimeTable_' + did);
2976
2977        $timeDiv.html('');
2978        $placeRow.hide();
2979        $placeSel.html('<option value="">' + bookingI18n.seatPlaceholder + '</option>');
2980
2981        $.ajax({
2982            url: '/hBook/spPlaceSearch/' + spNo,
2983            type: 'get', dataType: 'json',
2984            success: function(data) {
2985                if (!data || !data.length) {
2986                    $placeSel.html('<option value="">' + bookingI18n.seatEmpty + '</option>');
2987                    $placeRow.show();
2988                    return;
2989                }
2990                if (data.length === 1) {
2991                    selectedCavanaType = 'V';
2992                    $placeSel.html('<option value="' + data[0].cdCode + '">' + data[0].cdTitle1 + '</option>');
2993                    v2ChangeFacilityPlace(did, data[0].cdCode);
2994                } else {
2995                    selectedCavanaType = 'C';
2996                    let opts = '<option value="">' + bookingI18n.seatPlaceholder + '</option>';
2997                    data.forEach(function(d) { opts += '<option value="' + d.cdCode + '">' + d.cdTitle1 + '</option>'; });
2998                    $placeSel.html(opts);
2999                    $placeRow.show();
3000                }
3001            },
3002            error: function() { $placeSel.html('<option value="">' + bookingI18n.error + '</option>'); $placeRow.show(); }
3003        });
3004    }
3005
3006    /* ── 부대시설 시간 선택: select 방식 ── */
3007    function v2ChangeFacilityPlace(did, placeNo) {
3008        selectedPlaceNo = placeNo;
3009        if (!placeNo || !selectedDate) return;
3010
3011        const bkDay    = selectedDate.replace(/-/g, '');
3012        const $timeDiv = $('#v2FacTimeTable_' + did);
3013
3014        v2InitTimeBox(did);
3015        $timeDiv.html('<p class="text-muted v2-time-feedback">' + bookingI18n.timeLoading + '</p>');
3016
3017        $.ajax({
3018            url: '/hBook/spMarkSearch/' + bkDay + '/' + selectedSpNo + '/' + placeNo,
3019            type: 'post', dataType: 'json', contentType: 'application/json; charset=utf-8',
3020            success: function(data) {
3021                if (!data || !data.length) { $timeDiv.html('<p class="text-muted v2-time-feedback">' + bookingI18n.timeEmpty + '</p>'); return; }
3022
3023                function toHHmm(t) { let s = String(t || '').replace(':',''); while(s.length<4)s='0'+s; return s.slice(0,2)+':'+s.slice(2,4); }
3024                function addMins(hhmm, m) { const p=hhmm.split(':'); const tot=+p[0]*60+ +p[1]+(m||240); const h=Math.floor(tot/60)%24; const mm=tot%60; return (h<10?'0':'')+h+':'+(mm<10?'0':'')+mm; }
3025
3026                let opts = '<option value="">' + bookingI18n.timePlaceholder + '</option>';
3027                let hiddenTimeTxt = '';
3028                data.forEach(function(raw) {
3029                    const bkTime  = String(raw.bkTime || raw.bk_time || '');
3030                    let strtTm    = raw.strtTm || raw.strt_tm || '';
3031                    let endTm     = raw.endTm  || raw.end_tm  || '';
3032                    if (!strtTm && bkTime) strtTm = toHHmm(bkTime);
3033                    if (!endTm  && strtTm) endTm  = addMins(strtTm, parseInt(raw.bkUseTime || raw.bk_use_time || 240, 10));
3034                    const tmTxt   = strtTm ? strtTm + '~' + endTm : bkTime;
3035                    const bkState = raw.bkState || raw.bk_state || 'Y';
3036
3037                    hiddenTimeTxt += '<input type="hidden" id="timeTxt_' + bkTime + '" value="' + tmTxt + '">';
3038
3039                    if (bkState === 'Y') {
3040                        opts += '<option value="' + bkTime + '">' + tmTxt + ' (' + bookingI18n.timeAvailable + ')</option>';
3041                    } else {
3042                        opts += '<option value="' + bkTime + '" disabled>' + tmTxt + ' (' + (bkState === 'B' ? bookingI18n.timeOnsite : bookingI18n.timeClosed) + ')</option>';
3043                    }
3044                });
3045
3046                $('#v2FacHidden_' + did).append(hiddenTimeTxt);
3047
3048                $timeDiv.html(
3049                    '<label class="fac-label">' + bookingI18n.timeSelect + '</label>' +
3050                    '<select id="v2FacTime_' + did + '" class="form-control fac-time-select" onchange="v2TimeSelectChange(\'' + did + '\', this.value)">' + opts + '</select>'
3051                );
3052            },
3053            error: function() { $timeDiv.html('<p class="text-muted">' + bookingI18n.timeError + '</p>'); }
3054        });
3055    }
3056
3057    /* ================================================================
3058       시간 선택/해제 (select 단일선택 방식)
3059    ================================================================ */
3060    function v2InitTimeBox(did) {
3061        var $sel = $('#v2FacTime_' + did);
3062        if ($sel.length) $sel.val('');
3063        v2SelectedTimeList = '';
3064        v2SelectedCavanaAmt = 0;
3065        v2SelectedCavanaTotAmt = 0;
3066    }
3067
3068    function v2FacilityHoldPayload(item) {
3069        return {
3070            spNo: item.spNo,
3071            bkSeq: item.bkSeq,
3072            bkTimeList: item.bkTimeList,
3073            bkDate: item.bkDate,
3074            cavanaYn: 'Y'
3075        };
3076    }
3077
3078    function v2SetFacilityControlBusy(did, busy) {
3079        if (!did) return;
3080        $('#v2FacSp_' + did + ', #v2FacPlace_' + did + ', #v2FacTime_' + did + ', #v2FacDate_' + did)
3081            .prop('disabled', !!busy);
3082    }
3083
3084    function v2AcquireFacilityHold(item, onSuccess, onFailure) {
3085        const identity = v2EnsureCheckoutIdentity();
3086        v2FacilityHoldRequestCount++;
3087        $.ajax({
3088            url: '/hBook/v2/facility/hold/' + encodeURIComponent(agentCode)
3089                + '/' + encodeURIComponent(item.bkDate)
3090                + '/' + encodeURIComponent(identity.serialNo),
3091            type: 'post',
3092            dataType: 'json',
3093            contentType: 'application/json; charset=utf-8',
3094            timeout: 10000,
3095            data: JSON.stringify(v2FacilityHoldPayload(item)),
3096            success: function(data) {
3097                if (!data || data.code !== '0000' || !Number(data.expiresAtEpochMs)) {
3098                if (typeof onFailure === 'function') onFailure(data && data.message ? data.message : bookingI18n.facilityHoldFailed);
3099                    return;
3100                }
3101                if (Number(data.serverNowEpochMs)) {
3102                    v2FacilityHoldClockOffsetMs = Number(data.serverNowEpochMs) - Date.now();
3103                }
3104                if (typeof onSuccess === 'function') onSuccess(data, identity);
3105            },
3106            error: function() {
3107            if (typeof onFailure === 'function') onFailure(bookingI18n.facilityHoldError);
3108            },
3109            complete: function() {
3110                v2FacilityHoldRequestCount = Math.max(0, v2FacilityHoldRequestCount - 1);
3111            }
3112        });
3113    }
3114
3115    function v2ReleaseFacilityHold(item, onSuccess, onFailure) {
3116        const serialNo = item && item.holdSerialNo ? item.holdSerialNo : v2CheckoutSerialNo;
3117        if (!item || !serialNo) {
3118            if (typeof onSuccess === 'function') onSuccess({ code: '0000', releasedCount: 0 });
3119            return;
3120        }
3121
3122        v2FacilityHoldRequestCount++;
3123        $.ajax({
3124            url: '/hBook/v2/facility/hold/release/' + encodeURIComponent(agentCode)
3125                + '/' + encodeURIComponent(item.bkDate)
3126                + '/' + encodeURIComponent(serialNo),
3127            type: 'post',
3128            dataType: 'json',
3129            contentType: 'application/json; charset=utf-8',
3130            timeout: 10000,
3131            data: JSON.stringify(v2FacilityHoldPayload(item)),
3132            success: function(data) {
3133                if (!data || data.code !== '0000') {
3134                if (typeof onFailure === 'function') onFailure(data && data.message ? data.message : bookingI18n.facilityReleaseFailed);
3135                    return;
3136                }
3137                if (typeof onSuccess === 'function') onSuccess(data);
3138            },
3139            error: function() {
3140            if (typeof onFailure === 'function') onFailure(bookingI18n.facilityReleaseError);
3141            },
3142            complete: function() {
3143                v2FacilityHoldRequestCount = Math.max(0, v2FacilityHoldRequestCount - 1);
3144            }
3145        });
3146    }
3147
3148    function v2ReleaseAllFacilityHolds(done) {
3149        const holds = v2CBookList.slice();
3150        if (!holds.length) {
3151            if (typeof done === 'function') done();
3152            return;
3153        }
3154
3155        let remaining = holds.length;
3156        const finishOne = function() {
3157            remaining--;
3158            if (remaining <= 0 && typeof done === 'function') done();
3159        };
3160        holds.forEach(function(item) {
3161            v2ReleaseFacilityHold(item, finishOne, finishOne);
3162        });
3163    }
3164
3165    function v2FacilityHoldNow() {
3166        return Date.now() + v2FacilityHoldClockOffsetMs;
3167    }
3168
3169    function v2FormatFacilityHoldTime(milliseconds) {
3170        const totalSeconds = Math.max(0, Math.ceil(milliseconds / 1000));
3171        const minutes = Math.floor(totalSeconds / 60);
3172        const seconds = totalSeconds % 60;
3173        return (minutes < 10 ? '0' : '') + minutes + ':' + (seconds < 10 ? '0' : '') + seconds;
3174    }
3175
3176    function v2SyncFacilityHoldTimer() {
3177        const active = v2CBookList.filter(function(item) {
3178            return Number(item.holdExpiresAtEpochMs) > 0;
3179        });
3180        const hasHold = active.length > 0;
3181
3182        $('#v2FacilityHoldStatus, #v2MobileFacilityHoldStatus').toggleClass('is-active', hasHold);
3183        $('#v2MobileBar').toggleClass('has-facility-hold', hasHold);
3184        $('.hBook.v2-book-main').toggleClass('facility-hold-active', hasHold);
3185
3186        if (!hasHold) {
3187            if (v2FacilityHoldTimerId) window.clearInterval(v2FacilityHoldTimerId);
3188            v2FacilityHoldTimerId = null;
3189            return;
3190        }
3191
3192        v2UpdateFacilityHoldTimer();
3193        if (!v2FacilityHoldTimerId) {
3194            v2FacilityHoldTimerId = window.setInterval(v2UpdateFacilityHoldTimer, 1000);
3195        }
3196    }
3197
3198    function v2UpdateFacilityHoldTimer() {
3199        const expiries = v2CBookList.map(function(item) {
3200            return Number(item.holdExpiresAtEpochMs) || 0;
3201        }).filter(function(value) { return value > 0; });
3202        if (!expiries.length) {
3203            v2SyncFacilityHoldTimer();
3204            return;
3205        }
3206
3207        const remaining = Math.min.apply(null, expiries) - v2FacilityHoldNow();
3208        const text = v2FormatFacilityHoldTime(remaining);
3209        $('#v2FacilityHoldCountdown, #v2MobileFacilityHoldCountdown').text(text);
3210        $('#v2FacilityHoldStatus, #v2MobileFacilityHoldStatus').toggleClass('is-urgent', remaining <= 5 * 60 * 1000);
3211
3212        if (remaining <= 0) v2HandleFacilityHoldExpiry();
3213    }
3214
3215    function v2HasExpiredFacilityHold() {
3216        const now = v2FacilityHoldNow();
3217        return v2CBookList.some(function(item) {
3218            return Number(item.holdExpiresAtEpochMs) > 0 && Number(item.holdExpiresAtEpochMs) <= now;
3219        });
3220    }
3221
3222    function v2HandleFacilityHoldExpiry() {
3223        if (v2FacilityHoldExpiryHandling) return;
3224        if (v2NextInProgress) return;
3225        if (v2FacilityHoldRequestCount > 0) return;
3226        const now = v2FacilityHoldNow();
3227        const expired = v2CBookList.filter(function(item) {
3228            return Number(item.holdExpiresAtEpochMs) > 0 && Number(item.holdExpiresAtEpochMs) <= now;
3229        });
3230        if (!expired.length) return;
3231
3232        v2FacilityHoldExpiryHandling = true;
3233        expired.forEach(function(item) {
3234            v2ReleaseFacilityHold(item);
3235            v2RemoveCavanaLocal(item.bookIdx);
3236        });
3237        v2FacilityHoldExpiryHandling = false;
3238        v2SyncFacilityHoldTimer();
3239        const holdMinutes = Number(expired[0].holdMinutes) || 20;
3240        v2SwalWarning(bookingI18n.facilityHoldExpiredDescription.replace('{0}', holdMinutes), bookingI18n.holdExpiredTitle);
3241    }
3242
3243    function v2ShowFacilityHoldNotice(holdMinutes) {
3244        if (v2FacilityHoldNoticeShown) return;
3245        v2FacilityHoldNoticeShown = true;
3246        const minutes = Number(holdMinutes) > 0 ? Number(holdMinutes) : 20;
3247        Swal.fire({
3248            icon: 'info',
3249            title: bookingI18n.holdConfirmTitle,
3250            html: bookingI18n.holdConfirmDescription.replace('{0}', minutes),
3251            confirmButtonText: bookingI18n.confirm,
3252            confirmButtonColor: '#ff710d',
3253            heightAuto: false
3254        });
3255    }
3256
3257    function v2TimeSelectChange(did, selectedTime) {
3258        if (!selectedTime) {
3259            v2RemoveFacilitySelection(did);
3260            return;
3261        }
3262
3263        if (!v2RequireLoginBeforeCart()) {
3264            $('#v2FacTime_' + did).val('');
3265            return;
3266        }
3267
3268        const time      = getTimeFormat(selectedTime);
3269        const $spSel    = $('#v2FacSp_' + did);
3270        const spNo      = $spSel.val();
3271        if (!spNo) return;
3272
3273        const spName       = $spSel.find('option:selected').text();
3274        const $placeSel    = $('#v2FacPlace_' + did);
3275        const placeNo      = $placeSel.val() || '';
3276        const placeName    = selectedCavanaType === 'V' ? '' : ($placeSel.find('option:selected').text() || '');
3277        const spPlaceName  = selectedCavanaType === 'V' ? spName : (spName + ' / ' + placeName);
3278        const spPlaceValue = spNo + placeNo;
3279        const tkAmt        = Number($('#v2amt_' + did + '_' + spNo).val()) || 0;
3280
3281        if (!v2CanAddCartDate(selectedDate || '')) {
3282            $('#v2FacTime_' + did).val('');
3283            return;
3284        }
3285
3286        // hidden 동기화
3287        const $fh = $('#v2FacHidden_' + did);
3288        if (!$('#amt_' + spNo).length) $fh.append('<input type="hidden" id="amt_' + spNo + '" value="' + tkAmt + '">');
3289        else $('#amt_' + spNo).val(tkAmt);
3290        const rp = $('#v2resultPrice_' + did + '_' + spNo).val();
3291        const sp = $('#v2savePrice_'   + did + '_' + spNo).val();
3292        const dc = $('#v2sId_'         + did + '_' + spNo).val() || '';
3293        const da = $('#v2dcAmt_'       + did + '_' + spNo).val();
3294        const dt = $('#v2dcType_'      + did + '_' + spNo).val();
3295        if (rp && !$('#resultPrice_' + spNo).length) {
3296            $fh.append('<input type="hidden" id="resultPrice_' + spNo + '" value="' + rp + '">');
3297            $fh.append('<input type="hidden" id="savePrice_'   + spNo + '" value="' + sp + '">');
3298            $fh.append('<input type="hidden" id="sId_'         + spNo + '" value="' + dc + '">');
3299            $fh.append('<input type="hidden" id="dcAmt_'       + spNo + '" value="' + da + '">');
3300            $fh.append('<input type="hidden" id="dcType_'      + spNo + '" value="' + dt + '">');
3301        }
3302
3303        // 같은 자리는 한 시간만 유지하되, 새 시간 선점 성공 후 기존 선점을 해제한다.
3304        const existIdx = v2CBookList.findIndex(function(k){ return k.spPlaceName === spPlaceName; });
3305        const old = existIdx >= 0 ? v2CBookList[existIdx] : null;
3306        const candidate = {
3307            did: did,
3308            spNo: spNo,
3309            bkSeq: placeNo,
3310            bkTimeList: time,
3311            bkDate: selectedDate || '',
3312            cavanaYn: 'Y'
3313        };
3314
3315        v2SetFacilityControlBusy(did, true);
3316        v2AcquireFacilityHold(candidate, function(data, identity) {
3317            candidate.holdSerialNo = identity.serialNo;
3318            const sameSlot = old
3319                && String(old.spNo) === String(spNo)
3320                && String(old.bkSeq) === String(placeNo)
3321                && String(old.bkTimeList) === String(time);
3322
3323            if (sameSlot) {
3324                old.holdExpiresAtEpochMs = Number(data.expiresAtEpochMs);
3325                old.holdMinutes = Number(data.holdMinutes) || 20;
3326                old.holdSerialNo = identity.serialNo;
3327                v2SetFacilityControlBusy(did, false);
3328                v2SyncFacilityHoldTimer();
3329                v2PersistCartState();
3330                v2ShowFacilityHoldNotice(data.holdMinutes);
3331                return;
3332            }
3333
3334            const commitSelection = function() {
3335                if (old) v2RemoveCavanaLocal(old.bookIdx, true);
3336                v2BookIdx++;
3337                v2CavanaDiv('1', v2BookIdx, spPlaceValue, spNo, placeNo, spName, spPlaceName,
3338                    tkAmt, time, did, Number(data.expiresAtEpochMs), Number(data.holdMinutes), identity.serialNo);
3339                $('#v2FacTime_' + did).val(selectedTime);
3340                v2SetFacilityControlBusy(did, false);
3341                v2ShowFacilityHoldNotice(data.holdMinutes);
3342            };
3343
3344            if (!old) {
3345                commitSelection();
3346                return;
3347            }
3348
3349            v2ReleaseFacilityHold(old, commitSelection, function(message) {
3350                // 기존 시간 해제 실패 시 새 선점을 보상 해제하고 화면 선택도 원래 값으로 되돌린다.
3351                v2ReleaseFacilityHold(candidate);
3352                $('#v2FacTime_' + did).val(old.bkTimeList || '');
3353                v2SetFacilityControlBusy(did, false);
3354        v2SwalError(message, bookingI18n.facilityTimeChangeError);
3355            });
3356        }, function(message) {
3357            // 응답 타임아웃 직전에 서버 선점이 끝난 경우를 대비해 미확정 선점을 정리한다.
3358            const sameAsOld = old
3359                && String(old.spNo) === String(candidate.spNo)
3360                && String(old.bkSeq) === String(candidate.bkSeq)
3361                && String(old.bkTimeList) === String(candidate.bkTimeList);
3362            if (!sameAsOld) v2ReleaseFacilityHold(candidate);
3363            $('#v2FacTime_' + did).val(old ? old.bkTimeList : '');
3364            v2SetFacilityControlBusy(did, false);
3365        v2SwalError(message, bookingI18n.facilityHoldFailed);
3366        });
3367    }
3368
3369    function v2RemoveFacilitySelection(did) {
3370        const $spSel       = $('#v2FacSp_' + did);
3371        const spNo         = $spSel.val();
3372        const $placeSel    = $('#v2FacPlace_' + did);
3373        const placeNo      = $placeSel.val() || '';
3374        const placeName    = selectedCavanaType === 'V' ? '' : ($placeSel.find('option:selected').text() || '');
3375        const spName       = $spSel.find('option:selected').text();
3376        const spPlaceName  = selectedCavanaType === 'V' ? spName : (spName + ' / ' + placeName);
3377
3378        const existIdx = v2CBookList.findIndex(function(k){ return k.spPlaceName === spPlaceName; });
3379        if (existIdx < 0) return;
3380        v2DeleteCavana(v2CBookList[existIdx].bookIdx);
3381    }
3382
3383    function v2BuildSpPlaceName(spPlaceName, bkTimeList) {
3384        if (!bkTimeList) return spPlaceName;
3385        let txt = '';
3386        bkTimeList.split(',').sort().forEach(function(t) {
3387            const label = $('#timeTxt_' + t).val() || t;
3388            txt += (txt ? ' , ' : ' / ') + label;
3389        });
3390        return spPlaceName + txt;
3391    }
3392
3393    /* ================================================================
3394       티켓/부대시설 혼합 담기 검증 헬퍼
3395    ================================================================ */
3396    function v2HasSaleType(type) {
3397        var t = String(type || '').toUpperCase();
3398        return v2BookList.some(function(item) {
3399            return String(item.saleType || '').toUpperCase() === t;
3400        });
3401    }
3402
3403    function v2IsDateRequiredSaleType(saleType) {
3404        return String(saleType || '').toUpperCase() === 'R';
3405    }
3406
3407    function v2IsNoDateSaleType(saleType) {
3408        var st = String(saleType || '').toUpperCase();
3409        return st === 'P' || st === 'E' || st === 'S';
3410    }
3411
3412    function v2HasMixedDatePolicy(nextSaleType) {
3413        var next = String(nextSaleType || '').toUpperCase();
3414
3415        if (v2IsDateRequiredSaleType(next)) {
3416            return v2HasSaleType('P') || v2HasSaleType('E') || v2HasSaleType('S');
3417        }
3418
3419        if (v2IsNoDateSaleType(next)) {
3420            return v2HasSaleType('R');
3421        }
3422
3423        return false;
3424    }
3425
3426    function v2GetCartBookingDate() {
3427        var allItems = v2BookList.concat(v2CBookList);
3428        for (var i = 0; i < allItems.length; i++) {
3429            if (allItems[i] && allItems[i].bkDate) return allItems[i].bkDate;
3430        }
3431        return '';
3432    }
3433
3434    function v2GetEntryTicketCartDate() {
3435        for (var i = 0; i < v2BookList.length; i++) {
3436            var item = v2BookList[i];
3437            if (item && v2IsDateRequiredSaleType(item.saleType) && item.bkDate) {
3438                return item.bkDate;
3439            }
3440        }
3441        return '';
3442    }
3443
3444    function v2ResetAutoAppliedEntryTicketDates() {
3445        if (v2GetEntryTicketCartDate()) return;
3446
3447        $('.product-detail').each(function() {
3448            var $detail = $(this);
3449            if (!$detail.data('cartDateApplied')) return;
3450
3451            $detail.stop(true, true).hide().empty();
3452            $detail.removeData('loaded');
3453            $detail.removeData('cachedSaleType');
3454            $detail.removeData('saleTypeCode');
3455            $detail.removeData('selectedBkDate');
3456            $detail.removeData('cartDateApplied');
3457            $detail.removeData('detailData');
3458            $detail.closest('.product-item').removeClass('is-open');
3459        });
3460
3461        $('#bookDt').val(v2GetCartBookingDate() || '');
3462    }
3463
3464    function v2CanAddCartDate(nextDate) {
3465        if (!nextDate) return true;
3466        var existingDate = v2GetCartBookingDate();
3467        if (!existingDate || existingDate === nextDate) return true;
3468
3469        Swal.fire({
3470            icon: 'info',
3471            title: bookingI18n.dateCheckTitle,
3472            text: bookingI18n.dateCheckDescription,
3473            confirmButtonText: bookingI18n.confirm,
3474            confirmButtonColor: '#ff710d',
3475            heightAuto: false
3476        });
3477        return false;
3478    }
3479
3480    /* ================================================================
3481       티켓 담기 / 수량 / 삭제
3482    ================================================================ */
3483    function v2TicketDivCnt(idx, spName, tkOriAmt, tkAmt, dcCode, dcType, dcValue, quantity) {
3484        dcType = String(dcType || '0');
3485        let html = '<div class="border-bottom pb-4 mb-4 li_' + idx + '">';
3486        html += '<p class="badge badge-info"><i class="bi bi-x-circle" onclick="v2DeleteTicket(' + idx + ')"></i><span>' + spName + '</span></p>';
3487        html += '<div class="option">';
3488        if (tkOriAmt > tkAmt && tkAmt > 0) {
3489            const totalSaving = (tkOriAmt - tkAmt) * quantity;
3490            html += '<div>';
3491            html += '<p class="option_name_sale" id="selTotAmt_'   + idx + '">' + toLocaleString(tkOriAmt * quantity)      + '</p>';
3492            html += '<p class="option_name2 mb-0"     id="selDcAmt_'    + idx + '"> (-' + toLocaleString(totalSaving) + ' ' + bookingI18n.discountSuffix + ') </p>';
3493            html += '</div>';
3494            html += '<p class="option_name"      id="selDcTotAmt_' + idx + '">
3494' + toLocaleString(tkAmt    * quantity)      + '</p>';
3495        } else {
3496            html += '<p class="option_name" id="spPlaceAmt_' + idx + '">' + toLocaleString(tkAmt * quantity) + '</p>';
3497        }
3498        html += '</div>';
3499        html += '<div class="ticket_qty">';
3500        html += '<button type="button" class="qty-minus" onclick="v2ChangeTicketQty(' + idx + ',\'minus\')">-</button>';
3501        html += '<span class="qty_' + idx + '">' + quantity + '</span>';
3502        html += '<button type="button" class="qty-plus" onclick="v2ChangeTicketQty(' + idx + ',\'plus\')">+</button>';
3503        html += '</div></div>';
3504        $('.selectedTicket1').after(html);
3505        $('#v2EmptyMsg').hide();
3506
3507        if (window.innerWidth <= 768) {
3508            $('#v2MobileEmptyMsg').hide();
3509            $('#v2MobileTotalInfo').show();
3510        }
3511    }
3512
3513    function v2AddTicketSelection(did) {
3514        if (!v2RequireLoginBeforeCart()) {
3515            return;
3516        }
3517
3518        const $btn      = $('button[data-did="' + did + '"]');
3519        const saleType  = String($btn.data('saletype') || 'R').toUpperCase();
3520        const tkGubun   = String($btn.data('tkgubun') || '');
3521        const purchaseContext = String($('#v2-detail-' + did).find('.child-list').data('context') || 'regular');
3522        const isReserve = v2IsDateRequiredSaleType(saleType);
3523        const bkDate    = $('#v2TicketDate_' + did).val() || '';
3524        if (isReserve && !bkDate) { v2SwalInfo(bookingI18n.dateFirst, bookingI18n.selectionRequired); return; }
3525
3526        if (v2HasMixedDatePolicy(saleType)) {
3527            Swal.fire({
3528                icon: 'info', title: bookingI18n.cartUnavailable,
3529                html: bookingI18n.cartMixInvalid,
3530                confirmButtonText: bookingI18n.confirm, confirmButtonColor: '#ff710d', heightAuto: false
3531            });
3532            return;
3533        }
3534
3535        if (bkDate && !v2CanAddCartDate(bkDate)) {
3536            return;
3537        }
3538
3539        {
3540            const selected = [];
3541            $('#v2-detail-' + did).find('.child-item').each(function() {
3542                const qty = parseInt($(this).find('.qty-display').text(), 10) || 0;
3543                if (qty > 0) selected.push({ $item: $(this), spNo: $(this).data('spno') || '', spAmt: Number($(this).data('spamt')) || 0, spName: $(this).find('.child-name').text().trim(), qty });
3544            });
3545            if (!selected.length) { v2SwalInfo(bookingI18n.quantityFirst, bookingI18n.selectionRequired); return; }
3546
3547            selected.forEach(function(item) {
3548                const dupIdx = v2BookList.findIndex(function(b){ return b.spNo === item.spNo && b.bkDate === bkDate && String(b.purchaseContext || 'regular') === purchaseContext; });
3549                const dcCode    = item.$item.find('.v2-discount-id').val() || '';
3550                const dcAmt     = Number(item.$item.find('.v2-discount-amt').val()) || 0;
3551                const dcType    = item.$item.find('.v2-discount-type').val() || '0';
3552                const savePrice = Number(item.$item.find('.v2-save-price').val()) || 0;
3553                const resultPrice = Number(item.$item.find('.v2-result-price').val()) || item.spAmt;
3554                const hasDiscount = resultPrice > 0 && resultPrice < item.spAmt;
3555                const finalAmt  = hasDiscount ? resultPrice : item.spAmt;
3556
3557                if (dupIdx >= 0) {
3558                    const ex = v2BookList[dupIdx];
3559                    ex.qty += item.qty; ex.dcSpAmt = finalAmt; ex.saleType = ex.saleType || saleType;
3560                    v2TotalAmt += finalAmt * item.qty; v2OriTotAmt += item.spAmt * item.qty;
3561                    $('.qty_' + ex.bookIdx).html(ex.qty);
3562                    if (hasDiscount) {
3563                        $('#selTotAmt_'   + ex.bookIdx).text(toLocaleString(item.spAmt  * ex.qty));
3564                        $('#selDcTotAmt_' + ex.bookIdx).text(toLocaleString(finalAmt    * ex.qty));
3565        $('#selDcAmt_'    + ex.bookIdx).text('(-' + toLocaleString((item.spAmt - finalAmt) * ex.qty) + ' ' + bookingI18n.discountSuffix + ')');
3566                    }
3567                    else { $('#spPlaceAmt_' + ex.bookIdx).text(toLocaleStr
3567ing(finalAmt * ex.qty)); }
3568                } else {
3569                    v2BookIdx++;
3570                    v2TotalAmt += finalAmt * item.qty; v2OriTotAmt += item.spAmt * item.qty;
3571                    const entry = { bookIdx: v2BookIdx, displayId: String(did).replace(/^(regular|partner)-/, ''), spNo: item.spNo, spName: item.spName, spAmt: item.spAmt, dcSpAmt: finalAmt, qty: item.qty, bkDate: bkDate, saleType: saleType, purchaseContext: purchaseContext, partnerCode: purchaseContext === 'partner' ? pCode : '', discountTarget: mbNo ? 'M' : 'G', cavanaYn: 'N', dayPkgGb: 0, tkGubun: tkGubun, dcType: dcType, dcValue: dcAmt };
3572                    if (hasDiscount) { entry.dcCode = dcCode; entry.dcAmt = savePrice; entry.dcYn = 'Y'; }
3573                    v2BookList.push(entry);
3574                    v2TicketDivCnt(v2BookIdx, item.spName + (bkDate ? ' (' + bkDate + ')' : ''), item.spAmt, finalAmt, hasDiscount, dcType, dcAmt, item.qty);
3575                }
3576
3577                v2ResetTicketQtyDisplay(item.spNo, purchaseContext);
3578            });
3579        }
3580
3581        $('#bookDt').val(bkDate);
3582        $('.totalAmtInfo').css('display', 'flex');
3583        $('.btn-next').show();
3584        $('.totalAmt').html(toLocaleString(v2TotalAmt));
3585        v2SyncToV1();
3586        v2RenderNotice();
3587    }
3588
3589    function v2SyncChildItemToCart($item) {
3590        const $list     = $item.closest('.child-list');
3591        const did       = $list.data('did');
3592        const saleType  = String($list.data('saletype') || 'R').toUpperCase();
3593        const tkGubun   = String($list.data('tkgubun') || '');
3594        const purchaseContext = String($list.data('context') || 'regular');
3595        const isReserve = v2IsDateRequiredSaleType(saleType);
3596        const bkDate    = $('#v2TicketDate_' + did).val() || '';
3597
3598        const spNo   = $item.data('spno') || '';
3599        if (!spNo) return;
3600        const spAmt  = Number($item.data('spamt')) || 0;
3601        const spName = $item.find('.child-name').text().trim();
3602        const qty    = parseInt($item.find('.qty-display').text(), 10) || 0;
3603
3604        if (isReserve && !bkDate) {
3605            $item.find('.qty-display').text('0');
3606            v2SwalInfo(bookingI18n.dateFirst, bookingI18n.selectionRequired);
3607            return;
3608        }
3609
3610        /* ── DOM에 저장해둔 bookIdx 우선 사용 — spNo/bkDate 타입 비교 이슈 회피 ── */
3611        let storedBookIdx = $item.data('bookIdx');
3612        let idx = -1;
3613        if (storedBookIdx != null) {
3614            idx = v2BookList.findIndex(function(b){ return b.bookIdx === storedBookIdx; });
3615            if (idx < 0) {
3616                /* 이미 다른 경로(휴지통 등)로 삭제된 경우 — 저장값 초기화 */
3617                $item.removeData('bookIdx');
3618                storedBookIdx = null;
3619            }
3620        }
3621        if (idx < 0) {
3622            idx = v2BookList.findIndex(function(b){
3623                return String(b.spNo) === String(spNo)
3624                    && b.bkDate === bkDate
3625                    && String(b.purchaseContext || 'regular') === purchaseContext;
3626            });
3627        }
3628
3629        /* 신규 추가(0→양수) 시에만 정책 검증 */
3630        if (qty > 0 && idx < 0) {
3631            if (v2HasMixedDatePolicy(saleType)) {
3632                $item.find('.qty-display').text('0');
3633                Swal.fire({
3634                    icon: 'info', title: bookingI18n.cartUnavailable,
3635                    html: bookingI18n.cartMixInvalid,
3636                    confirmButtonText: bookingI18n.confirm, confirmButtonColor: '#ff710d', heightAuto: false
3637                });
3638                return;
3639            }
3640            if (bkDate && !v2CanAddCartDate(bkDate)) {
3641                $item.find('.qty-display').text('0');
3642                return;
3643            }
3644        }
3645
3646        const dcCode      = $item.find('.v2-discount-id').val() || '';
3647        const dcAmt       = Number($item.find('.v2-discount-amt').val()) || 0;
3648        const dcType      = $item.find('.v2-discount-type').val() || '0';
3649        const savePrice   = Number($item.find('.v2-save-price').val()) || 0;
3650        const resultPrice = Number($item.find('.v2-result-price').val()) || spAmt;
3651        const hasDiscount = resultPrice > 0 && resultPrice < spAmt;
3652        const finalAmt    = hasDiscount ? resultPrice : spAmt;
3653
3654        if (qty <= 0) {
3655            /* 0이 되면 장바구니에서 제거 */
3656            if (idx >= 0) {
3657                const entry = v2BookList[idx];
3658                v2TotalAmt  -= (entry.dcSpAmt || entry.spAmt) * entry.qty;
3659                v2OriTotAmt -= entry.spAmt * entry.qty;
3660                v2TotalAmt  = Math.max(0, v2TotalAmt);
3661                v2OriTotAmt = Math.max(0, v2OriTotAmt);
3662                v2BookList.splice(idx, 1);
3663                $('.li_' + entry.bookIdx).remove();
3664            }
3665            $item.removeData('bookIdx');
3666        } else if (idx >= 0) {
3667            /* 기존 항목 수량/금액 갱신 */
3668            const entry       = v2BookList[idx];
3669            const oldTotal     = (entry.dcSpAmt || entry.spAmt) * entry.qty;
3670            const oldOriTotal  = entry.spAmt * entry.qty;
3671            entry.qty     = qty;
3672            entry.dcSpAmt = finalAmt;
3673            v2TotalAmt  += (finalAmt * qty) - oldTotal;
3674            v2OriTotAmt += (spAmt    * qty) - oldOriTotal;
3675            $('.qty_' + entry.bookIdx).html(qty);
3676            if (hasDiscount) {
3677                $('#selTotAmt_'   + entry.bookIdx).text(toLocaleString(spAmt   * qty));
3678                $('#selDcTotAmt_' + entry.bookIdx).text(toLocaleString(finalAmt * qty));
3679        $('#selDcAmt_'    + entry.bookIdx).text('(-' + toLocaleString((spAmt - finalAmt) * qty) + ' ' + bookingI18n.discountSuffix + ')');
3680            } else {
3681                $('#spPlaceAmt_' + entry.bookIdx).text(toLocaleString(finalAmt * qty));
3682            }
3683            $item.data('bookIdx', entry.bookIdx);
3684        } else {
3685            /* 신규 항목 추가 */
3686            v2BookIdx++;
3687            v2TotalAmt  += finalAmt * qty;
3688            v2OriTotAmt += spAmt    * qty;
3689            const entry = { bookIdx: v2BookIdx, displayId: String(did).replace(/^(regular|partner)-/, ''), spNo, spName, spAmt, dcSpAmt: finalAmt, qty, bkDate, saleType, purchaseContext, partnerCode: purchaseContext === 'partner' ? pCode : '', discountTarget: mbNo ? 'M' : 'G', cavanaYn:'N', dayPkgGb:0, tkGubun, dcType, dcValue:dcAmt };
3690            if (hasDiscount) { entry.dcCode = dcCode; entry.dcAmt = savePrice; entry.dcYn = 'Y'; }
3691            v2BookList.push(entry);
3692            v2TicketDivCnt(v2BookIdx, spName + (bkDate ? ' (' + bkDate + ')' : ''), spAmt, finalAmt, hasDiscount, dcType, dcAmt, qty);
3693            $item.data('bookIdx', v2BookIdx);
3694        }
3695
3696        $('#bookDt').val(bkDate);
3697        if (v2BookList.length || v2CBookList.length) {
3698            $('.totalAmtInfo').css('display', 'flex');
3699            $('.btn-next').show();
3700        }
3701        $('.totalAmt').html(toLocaleString(v2TotalAmt));
3702        $('.mobile-total').text(toLocaleString(v2TotalAmt));
3703        v2SyncToV1();
3704        v2RenderNotice();
3705        v2ResetAutoAppliedEntryTicketDates();
3706    }
3707
3708    function v2PkgQtyChange(did, delta) {
3709        const $el = $('#v2PkgQty_' + did);
3710        const cur = parseInt($el.text(), 10) || 1;
3711        const next = cur + delta;
3712        if (next < 1) return;
3713        if (next > 99) return;
3714        $el.text(next);
3715    }
3716
3717    function v2ChangeTicketQty(bkIdx, type) {
3718        const idx = v2BookList.findIndex(function(b){ return b.bookIdx === bkIdx; });
3719        if (idx < 0) return;
3720        const item = v2BookList[idx];
3721        if (type === 'minus' && item.qty <= 1) return;
3722        const delta    = type === 'plus' ? 1 : -1;
3723        const newQty   = item.qty + delta;
3724        const finalAmt = item.dcSpAmt || item.spAmt;
3725        item.qty        = newQty;
3726        v2TotalAmt     += finalAmt    * delta;
3727        v2OriTotAmt    += item.spAmt  * delta;
3728        $('.qty_' + bkIdx).html(newQty);
3729        if (item.dcSpAmt && item.dcSpAmt < item.spAmt) {
3730            $('#selTotAmt_'   + bkIdx).text(toLocaleString(item.spAmt  * newQty));
3731            $('#selDcTotAmt_' + bkIdx).text(toLocaleString(finalAmt    * newQty));
3732        $('#selDcAmt_'    + bkIdx).text('(-' + toLocaleString((item.spAmt - finalAmt) * newQty) + ' ' + bookingI18n.discountSuffix + ')');
3733        }
3734        else { $('#spPlaceAmt_' + bkIdx).text(toLocaleString(finalAmt * newQty)); }
3735        $('.totalAmt').html(toLocaleString(v2TotalAmt));
3736        v2SyncToV1();
3737        v2RenderNotice();
3738    }
3739
3740    function v2ResetTicketQtyDisplay(spNo, purchaseContext) {
3741        if (!spNo) return;
3742
3743        $('.child-item').each(function() {
3744            var itemSpNo = String($(this).attr('data-spno') || '');
3745            var itemContext = String($(this).closest('.child-list').data('context') || 'regular');
3746            if (itemSpNo === String(spNo) && (!purchaseContext || itemContext === String(purchaseContext))) {
3747                $(this).find('.qty-display').text('0');
3748                $(this).removeData('bookIdx');
3749            }
3750        });
3751    }
3752
3753    function v2DeleteTicket(bkIdx) {
3754        const idx = v2BookList.findIndex(function(b){ return b.bookIdx === bkIdx; });
3755        if (idx < 0) return;
3756
3757        const item = v2BookList[idx];
3758        const qty = Number(item.qty || 0);
3759        const spAmt = Number(item.spAmt || 0);
3760        const finalAmt = Number(item.dcSpAmt || item.spAmt || 0);
3761
3762        v2TotalAmt  -= finalAmt * qty;
3763        v2OriTotAmt -= spAmt * qty;
3764
3765        v2TotalAmt = Math.max(0, v2TotalAmt);
3766        v2OriTotAmt = Math.max(0, v2OriTotAmt);
3767
3768        v2ResetTicketQtyDisplay(item.spNo, item.purchaseContext);
3769
3770        v2BookList.splice(idx, 1);
3771        v2ResetAutoAppliedEntryTicketDates();
3772        $('.li_' + bkIdx).remove();
3773        $('.totalAmt').html(toLocaleString(v2TotalAmt));
3774        $('.mobile-total').text(toLocaleString(v2TotalAmt));
3775        if (!v2BookList.length && !v2CBookList.length) {
3776            v2TotalAmt = 0;
3777            v2OriTotAmt = 0;
3778            $('.totalAmt').html(toLocaleString(0));
3779            $('.mobile-total').text(toLocaleString(0));
3780            $('.totalAmtInfo').hide();
3781            $('.btn-next').hide();
3782            $('#v2EmptyMsg').show();
3783        }
3784        v2SyncToV1();
3785        v2RenderNotice();
3786    }
3787
3788    /* ================================================================
3789       카바나/부대시설 담기 / 삭제
3790    ================================================================ */
3791    /* main.js getDcInfoTxt 는 "원 할인"이 한글 고정이라, 티켓 행과 동일하게
3792       bookingI18n.discountSuffix 를 쓰는 다국어 버전을 부대시설 장바구니에 사용 */
3793    function v2GetDcInfoTxt(dcType, dcAmt) {
3794        dcType = String(dcType || '0');
3795        if (dcType === '1') return '-' + toLocaleString(Number(dcAmt) || 0) + ' ' + bookingI18n.discountSuffix;
3796        if (dcType === '2') return '-' + dcAmt + '% ' + bookingI18n.discountSuffix;
3797        return '';
3798    }
3799
3800    function v2CavanaDivCnt(idx, spName, spPlaceValue, tkAmt, dcCode, dcType, dcValue, tkOriAmt) {
3801        dcType = String(dcType || '0');
3802        let html = '<div class="border-bottom pb-4 mb-4 li_' + idx + '" id="li_' + spPlaceValue + '">';
3803        html += '<p class="badge badge-info">'
3804            + '<i class="bi bi-x-circle" id="btnDel_' + spPlaceValue + '" onclick="v2DeleteCavana(' + idx + ')"></i>'
3805            + '<span id="spPlace_' + spPlaceValue + '">' + spName + '</span></p>';
3806        html += '<div class="option">';
3807        if (dcCode) {
3808            html += '<div>';
3809            html += '<p class="option_name_sale" id="selTotAmt_'   + spPlaceValue + '">' + toLocaleString(tkOriAmt) + '</p>';
3810            html += '<p class="option_name2 mb-0"     id="selDcAmt_'    + spPlaceValue + '"> (' + v2GetDcInfoTxt(dcType, dcValue) + ') </p>';
3811            html += '</div>';
3812            html += '<p class="option_name"      id="selDcTotAmt_' + spPlaceValue + '">' + toLocaleString(tkAmt)    + '</p>';
3813        } else {
3814            html += '<p class="option_name" id="spPlaceAmt_' + spPlaceValue + '">' + toLocaleString(tkAmt) + '</p>';
3815        }
3816        html += '</div></div>';
3817        if ($('.selectedTicket2').length) $('.selectedTicket2').append(html);
3818        $('#v2EmptyMsg').hide();
3819    }
3820
3821    function v2InitCavana() {
3822        v2TotalAmt  -= Number(v2SelectedCavanaTotAmt);
3823        v2OriTotAmt -= Number(v2SelectedCavanaAmt);
3824        v2SelectedCavanaAmt = 0; v2SelectedCavanaTotAmt = 0;
3825        $('.totalAmt').html(toLocaleString(v2TotalAmt));
3826        v2SyncToV1();
3827    }
3828
3829    function v2CavanaDiv(type, idx, spPlaceValue, spNo, placeNo, spName, spPlaceName, tkAmt,
3830                         timeList, did, holdExpiresAtEpochMs, holdMinutes, holdSerialNo) {
3831        tkAmt = Number(tkAmt);
3832        const tList = timeList.split(',').sort();
3833        let txtTimeList = '';
3834        tList.forEach(function(t){ const tTxt = $('#timeTxt_' + t).val() || t; txtTimeList += (txtTimeList ? ' , ' : ' / ') + tTxt; });
3835        const fullSpName = spPlaceName + txtTimeList;
3836
3837        let dcCode = '', dcValue = '', dcType = '', savePrice = 0, resultPrice = 0;
3838        const dcAmtEl = $('#dcAmt_' + spNo).val();
3839        if (dcAmtEl !== undefined && dcAmtEl !== '') {
3840            dcCode      = $('#sId_'          + spNo).val() || '';
3841            dcValue     = Number($('#dcAmt_' + spNo).val()) || 0;
3842            dcType      = $('#dcType_'       + spNo).val() || '';
3843            savePrice   = Number($('#savePrice_'   + spNo).val()) || 0;
3844            resultPrice = Number($('#resultPrice_' + spNo).val()) || 0;
3845        }
3846
3847        const oriTkAmt = Number($('#amt_' + spNo).val()) || tkAmt;
3848
3849        if (dcCode) { tkAmt = resultPrice; v2TotalAmt += resultPrice * tList.length; v2OriTotAmt += oriTkAmt * tList.length; v2SelectedCavanaAmt = oriTkAmt * tList.length; v2SelectedCavanaTotAmt = resultPrice * tList.length; }
3850        else         {                      v2TotalAmt += oriTkAmt;                   v2OriTotAmt += oriTkAmt;                v2SelectedCavanaAmt = oriTkAmt;                 v2SelectedCavanaTotAmt = oriTkAmt; }
3851        v2CavanaDivCnt(idx, fullSpName, spPlaceValue, v2SelectedCavanaTotAmt, dcCode, dcType, dcValue, v2SelectedCavanaAmt);
3852        $('.totalAmtInfo').css('display', 'flex'); $('.btn-next').show();
3853
3854        const delIdx = v2CBookList.findIndex(function(k){ return k.spPlaceName === spPlaceName; });
3855        if (delIdx >= 0) v2CBookList.splice(delIdx, 1);
3856
3857        const entry = { bookIdx: idx, did, displayId: String(did).replace(/^(regular|partner)-/, ''), purchaseContext: 'regular', partnerCode: '', discountTarget: mbNo ? 'M' : 'G', spPlaceValue, spNo, bkSeq: placeNo, spPlaceName, spName, displayTxt: fullSpName, bkTimeList: timeList, spAmt: oriTkAmt, qty: tList.length, cavanaYn: 'Y', dayPkgGb: 0, bkDate: selectedDate || '', holdExpiresAtEpochMs: Number(holdExpiresAtEpochMs) || 0, holdMinutes: Number(holdMinutes) || 20, holdSerialNo: holdSerialNo || v2CheckoutSerialNo, dcType: dcType, dcValue: dcValue };
3858        if (dcCode && savePrice != null) { entry.dcSpAmt = resultPrice; entry.dcAmt = savePrice; entry.dcYn = 'Y'; entry.dcCode = dcCode; }
3859        v2CBookList.push(entry);
3860
3861        $('.totalAmt').html(toLocaleString(v2TotalAmt));
3862        v2SyncToV1();
3863        v2RenderNotice();
3864        v2SyncFacilityHoldTimer();
3865    }
3866
3867    function v2DeleteCavana(bkIdx) {
3868        const idx = v2CBookList.findIndex(function(k){ return k.bookIdx === bkIdx; });
3869        if (idx < 0) return;
3870
3871        const item = v2CBookList[idx];
3872        if (item.releaseInProgress) return;
3873        item.releaseInProgress = true;
3874        v2SetFacilityControlBusy(item.did, true);
3875        v2ReleaseFacilityHold(item, function() {
3876            v2RemoveCavanaLocal(bkIdx);
3877            v2SetFacilityControlBusy(item.did, false);
3878        }, function(message) {
3879            item.releaseInProgress = false;
3880            if (item.did) $('#v2FacTime_' + item.did).val(item.bkTimeList || '');
3881            v2SetFacilityControlBusy(item.did, false);
3882        v2SwalError(message, bookingI18n.facilityReleaseError);
3883        });
3884    }
3885
3886    function v2RemoveCavanaLocal(bkIdx, keepControlValue) {
3887        const idx = v2CBookList.findIndex(function(k){ return k.bookIdx === bkIdx; });
3888        if (idx < 0) return;
3889
3890        const item = v2CBookList[idx];
3891        const qty = Number(item.qty || 0);
3892        const spAmt = Number(item.spAmt || 0);
3893        const finalAmt = Number(item.dcSpAmt || item.spAmt || 0);
3894
3895        v2TotalAmt  -= finalAmt * qty;
3896        v2OriTotAmt -= spAmt * qty;
3897
3898        v2TotalAmt = Math.max(0, v2TotalAmt);
3899        v2OriTotAmt = Math.max(0, v2OriTotAmt);
3900
3901        // select 초기화 (해당 facility의 시간 select 값 비우기)
3902        if (item.did && !keepControlValue) {
3903            const $sel = $('#v2FacTime_' + item.did);
3904            if ($sel.length) $sel.val('');
3905        }
3906
3907        v2CBookList.splice(idx, 1);
3908        $('.li_' + bkIdx).remove();
3909        v2SelectedTimeList = ''; v2SelectedCavanaAmt = 0; v2SelectedCavanaTotAmt = 0;
3910
3911        $('.totalAmt').html(toLocaleString(v2TotalAmt));
3912        $('.mobile-total').text(toLocaleString(v2TotalAmt));
3913
3914        if (!v2CBookList.length && !v2BookList.length) {
3915            v2TotalAmt = 0;
3916            v2OriTotAmt = 0;
3917            $('.totalAmt').html(toLocaleString(0));
3918            $('.mobile-total').text(toLocaleString(0));
3919            $('.totalAmtInfo').hide();
3920            $('.btn-next').hide();
3921            $('#v2EmptyMsg').show();
3922        }
3923        v2SyncToV1();
3924        v2RenderNotice();
3925        v2SyncFacilityHoldTimer();
3926    }
3927
3928    /* ================================================================
3929       다음단계 (fnNext)
3930    ================================================================ */
3931    function fnNext() {
3932        const valid = v2ValidateSelection();
3933        if (!valid.ok) {
3934        if (valid.msgType === 'error')        v2SwalError(valid.userMsg, bookingI18n.error);
3935        else if (valid.msgType === 'warning') v2SwalWarning(valid.userMsg, bookingI18n.error);
3936        else                                  v2SwalInfo(valid.userMsg, bookingI18n.selectionRequired);
3937            return false;
3938        }
3939        if (v2FacilityHoldRequestCount > 0) {
3940        v2SwalInfo(bookingI18n.processing, bookingI18n.info);
3941            return false;
3942        }
3943        if (v2HasExpiredFacilityHold()) {
3944            v2HandleFacilityHoldExpiry();
3945            return false;
3946        }
3947        if (v2NextInProgress) return false;
3948        v2NextInProgress = true;
3949
3950    v2ShowLoading(bookingI18n.reservationProcessing);
3951
3952        try {
3953            if (!v2RequireLoginBeforeCart()) {
3954                v2HideLoading();
3955                v2NextInProgress = false;
3956                return false;
3957            }
3958            const bkDate   = v2ResolveBkDateForSubmit();
3959            const identity = v2EnsureCheckoutIdentity();
3960            const serialNo = identity.serialNo;
3961            history.replaceState('', 'bookMain', v2BuildBookMainUrl({ serialNo: serialNo }));
3962            const payload  = v2BuildBookPayload(bkDate);
3963
3964            $.ajax({
3965                url: '/hBook/book/' + agentCode + '/' + bkDate + '/' + serialNo,
3966                type: 'post', dataType: 'json', contentType: 'application/json; charset=utf-8',
3967                data: JSON.stringify(payload),
3968                success: function(data) {
3969                    if (data && data.code === '0000') {
3970                        location.href = '/hBook/bookDetailConfirm/' + data.data
3971                            + '?lang=' + encodeURIComponent(bookingLanguageParam);
3972                        return;
3973                    }
3974
3975                    v2HideLoading();
3976                v2SwalError((data && data.message) ? data.message : bookingI18n.reservationError, bookingI18n.error)
3977                        .then(function(){
3978                            v2ReleaseAllFacilityHolds(function() {
3979                                location.href = v2BuildBookMainUrl();
3980                            });
3981                        });
3982                    v2NextInProgress = false;
3983                },
3984                error: function() {
3985                    v2HideLoading();
3986        v2SwalError(bookingI18n.reservationError, bookingI18n.error);
3987                    v2NextInProgress = false;
3988                }
3989            });
3990        } catch(e) {
3991            v2HideLoading();
3992        v2SwalError(bookingI18n.reservationError, bookingI18n.error);
3993            v2NextInProgress = false;
3994        }
3995        return false;
3996    }
3997
3998    function v2GetSaleTypeSummary() {
3999        const r = { reserveCnt:0, periodCnt:0, eventCnt:0, seasonCnt:0, unknownCnt:0, facilityCnt: v2CBookList.length };
4000        v2BookList.forEach(function(item){
4001            const t = String(item.saleType || '').toUpperCase();
4002            if (t==='R') r.reserveCnt++; else if(t==='P') r.periodCnt++; else if(t==='E') r.eventCnt++; else if(t==='S') r.seasonCnt++; else r.unknownCnt++;
4003        });
4004        return r;
4005    }
4006
4007    function v2NeedBookingDate() {
4008        const s = v2GetSaleTypeSummary();
4009        return s.facilityCnt > 0 || s.reserveCnt > 0;
4010    }
4011
4012    function v2ResolveBkDateForSubmit() {
4013        return ($('#bookDt').val() || '').trim() || getToDay('-');
4014    }
4015
4016    function v2ValidateSelection() {
4017        const bkDate = ($('#bookDt').val() || '').trim();
4018    if (v2NeedBookingDate() && !bkDate) return { ok: false, userMsg: bookingI18n.dateFirst, msgType: 'info' };
4019    if (!v2BookList.length && !v2CBookList.length) return { ok: false, userMsg: bookingI18n.productEmpty, msgType: 'info' };
4020    if (Number(v2TotalAmt || 0) <= 0) return { ok: false, userMsg: bookingI18n.paymentNone, msgType: 'warning' };
4021        for (let i = 0; i < v2BookList.length; i++) {
4022            const t = v2BookList[i] || {};
4023        if (!t.spNo || Number(t.qty || 0) <= 0) return { ok: false, userMsg: bookingI18n.ticketInvalid, msgType: 'error' };
4024        }
4025        for (let j = 0;
4025 j < v2CBookList.length; j++) {
4026            const c = v2CBookList[j] || {};
4027        if (!c.spNo || Number(c.qty || 0) <= 0) return { ok: false, userMsg: bookingI18n.facilityInvalid, msgType: 'error' };
4028        if (!c.bkTimeList || !c.bkTimeList.trim()) return { ok: false, userMsg: bookingI18n.facilityTimeInvalid, msgType: 'error' };
4029        }
4030        return { ok: true };
4031    }
4032
4033    function v2BuildBookPayload(bkDate) {
4034        const ticket = v2BookList.map(function(item){
4035            const itemBkDate = v2IsNoDateSaleType(item.saleType) ? '' : (item.bkDate || bkDate);
4036            return { bookIdx: Number(item.bookIdx||0), displayId: Number(item.displayId||0), purchaseContext: item.purchaseContext||'regular', partnerCode: item.partnerCode||'', discountTarget: item.discountTarget||(mbNo?'M':'G'), spNo: item.spNo||'', spName: item.spName||'', spAmt: Number(item.spAmt||0), dcSpAmt: Number(item.dcSpAmt||item.spAmt||0), qty: Number(item.qty||0), bkDate: itemBkDate, cavanaYn:'N', dayPkgGb: String(item.dayPkgGb!=null?item.dayPkgGb:0), dcCode: item.dcCode||'', dcAmt: Number(item.dcAmt||0), dcYn: item.dcYn||'N', displayTxt: item.displayTxt||'', cardDcNo: item.cardDcNo||'', expensiveYn: item.expensiveYn||'', saleType: item.saleType||'', tkGubun: item.tkGubun||'' };
4037        });
4038        const facility = v2CBookList.map(function(item){
4039            return { bookIdx: Number(item.bookIdx||0), displayId: Number(item.displayId||0), purchaseContext: 'regular', partnerCode: '', discountTarget: item.discountTarget||(mbNo?'M':'G'), spNo: item.spNo||'', bkSeq: item.bkSeq||'', spPlaceName: item.spPlaceName||'', spName: item.spName||'', displayTxt: item.displayTxt||'', bkTimeList: item.bkTimeList||'', spAmt: Number(item.spAmt||0), dcSpAmt: Number(item.dcSpAmt||item.spAmt||0), qty: Number(item.qty||0), bkDate: item.bkDate||bkDate, cavanaYn:'Y', dayPkgGb: String(item.dayPkgGb!=null?item.dayPkgGb:0), dcCode: item.dcCode||'', dcAmt: Number(item.dcAmt||0), dcYn: item.dcYn||'N', cardDcNo: item.cardDcNo||'', expensiveYn: item.expensiveYn||'' };
4040        });
4041        return ticket.concat(facility);
4042    }
4043
4044    /* ================================================================
4045       운영체크
4046    ================================================================ */
4047    function v2ResolveOperationCode(data) {
4048        if (!data) return 'WATERPARK';
4049        const title = (data.displayTitle || data.spName || '').toLowerCase();
4050        return (title.indexOf('찜질') >= 0 || title.indexOf('스파') >= 0) ? 'SPA' : 'WATERPARK';
4051    }
4052
4053    function v2CheckOperation(dateStr, viewKey, data, onAvailable) {
4054        const $detail = $('#v2-detail-' + viewKey);
4055        $detail.find('.v2-op-check-msg').remove();
4056        $.ajax({
4057            url: '/hBook/v2/operationCheck/' + v2ResolveOperationCode(data) + '/' + dateStr,
4058            type: 'get', dataType: 'json',
4059            success: function(res) {
4060                if (!res.available) {
4061        v2SwalError(res.message || bookingI18n.dateUnavailableSelected, bookingI18n.dateUnavailable).then(function(){
4062                        const el = document.getElementById('v2TicketDate_' + viewKey);
4063                        if (el && el._flatpickr) el._flatpickr.clear();
4064        v2ShowTicketDateState(viewKey, bookingI18n.dateGuide, true);
4065                        $detail.data('selectedBkDate', '');
4066                        $detail.data('cartDateApplied', false);
4067                        $('#bookDt').val(v2GetCartBookingDate() || '');
4068                    });
4069                } else {
4070                    if (res.warningOnly) {
4071        $detail.find('.reservation-box').after('<div class="v2-op-check-msg"><b>⚠ ' + bookingI18n.operationNotice + '</b><br>' + res.message + '</div>');
4072                    }
4073                    if (typeof onAvailable === 'function') onAvailable();
4074                }
4075            },
4076            error: function() {}
4077        });
4078    }
4079
4080    function v2ShowTicketDateState(viewKey, message, subtle) {
4081        var $right = $('#v2-detail-' + viewKey).find('.detail-right');
4082        $right.children('.child-list, .pkg-info-note, .v2-ticket-date-state').remove();
4083        var className = subtle
4084            ? 'pkg-info-note v2-ticket-date-state'
4085            : 'text-center py-3 mb-0 v2-ticket-date-state';
4086        $right.append('<p class="' + className + '">' + message + '</p>');
4087    }
4088
4089    function v2ReloadDetailByDate(displayId, viewKey, partnerYn, bkDate, cartDateApplied) {
4090        var $detail = $('#v2-detail-' + viewKey);
4091        $detail.data('pendingBkDate', bkDate);
4092
4093        function applyDetail(newData) {
4094            if (String($detail.data('pendingBkDate') || '') !== String(bkDate)) return;
4095
4096            newData.__selectedBkDate = bkDate;
4097            newData.__viewKey = viewKey;
4098            newData.__partnerYn = partnerYn === true;
4099            v2UpdateProductSummaryPrice(viewKey, newData, partnerYn === true);
4100
4101            $detail.html(v2BuildDetailHtml(newData));
4102
4103            var st = String(newData.saleType || '').toUpperCase();
4104            var spGb = String(newData.spGbCode || '').padStart(4, '0');
4105            if (spGb === '0051') {
4106                v2ActiveNoticeType = 'RESERVE';
4107            } else {
4108                v2ActiveNoticeType = (st === 'R') ? 'RESERVE' : 'PERIOD';
4109            }
4110            $detail.data('cachedSaleType', v2ActiveNoticeType);
4111            $detail.data('saleTypeCode', st);
4112            $detail.data('selectedBkDate', bkDate);
4113            $detail.data('cartDateApplied', cartDateApplied === true);
4114            $detail.data('detailData', newData);
4115            v2RenderNotice();
4116            $('#bookDt').val(bkDate);
4117
4118            if (spGb !== '0051') {
4119                var el = document.getElementById('v2TicketDate_' + viewKey);
4120                if (el) {
4121                    flatpickr(el, v2BuildDatePickerOptions({
4122                        position: 'below',
4123                        defaultDate: bkDate,
4124                        onChange: function(dates, dateStr) {
4125                            $('#bookDt').val(dateStr);
4126    v2ShowTicketDateState(viewKey, bookingI18n.loading);
4127                            v2CheckOperation(dateStr, viewKey, newData, function() {
4128                                v2ReloadDetailByDate(displayId, viewKey, partnerYn, dateStr);
4129                            });
4130                        }
4131                    }, partnerYn));
4132                }
4133            }
4134        }
4135
4136        $.ajax({
4137            url: partnerYn
4138                ? v2AppendPartnerCode('/hBook/v2/display/detail/' + displayId + '?bkDate=' + bkDate)
4139                : v2AppendLanguage('/hBook/v2/display/detail/' + displayId + '?bkDate=' + bkDate),
4140            type: 'get',
4141            dataType: 'json',
4142            beforeSend: v2ShowLoading,
4143            complete: v2HideLoading,
4144            success: function(newData) {
4145                applyDetail(newData);
4146            },
4147            error: function(xhr) {
4148                if (String($detail.data('pendingBkDate') || '') !== String(bkDate)) return;
4149
4150                if (xhr.status === 404) {
4151                    var emptyData = Object.assign({}, $detail.data('detailData') || {}, {
4152                        childProducts: [],
4153                        parentPurchasableYn: 'N'
4154                    });
4155                    applyDetail(emptyData);
4156                    return;
4157                }
4158        v2ShowTicketDateState(viewKey, bookingI18n.loadError);
4159            }
4160        });
4161    }
4162
4163    function v2CheckOperationFacility(dateStr, did, callback) {
4164        const $facBox = $('#v2FacBox_' + did);
4165        $facBox.find('.v2-op-check-msg').remove();
4166        $.ajax({
4167            url: '/hBook/v2/operationCheck/WATERPARK/' + dateStr,
4168            type: 'get', dataType: 'json',
4169            success: function(res) {
4170                if (!res.available) {
4171        v2SwalError(res.message || bookingI18n.dateUnavailableSelected, bookingI18n.dateUnavailable).then(function(){
4172                        const el = document.getElementById('v2FacDate_' + did);
4173                        if (el && el._flatpickr) el._flatpickr.clear();
4174                        $('#bookDt').val(''); selectedDate = '';
4175                    });
4176                } else {
4177    if (res.warningOnly) $facBox.find('.fac-row:first').after('<div class="v2-op-check-msg"><b>⚠ ' + bookingI18n.operationNotice + '</b><br>' + res.message + '</div>');
4178                    if (typeof callback === 'function') callback();
4179                }
4180            },
4181            error: function() { if (typeof callback === 'function') callback(); }
4182        });
4183    }
4184
4185    /* ================================================================
4186       SweetAlert2 헬퍼
4187    ================================================================ */
4188function v2SwalError(msg, title)   { return Swal.fire({ icon:'error',   title: title || bookingI18n.error, text: msg, confirmButtonText: bookingI18n.confirm, confirmButtonColor:'#ff710d', heightAuto:false }); }
4189function v2SwalWarning(msg, title) { return Swal.fire({ icon:'warning', title: title || bookingI18n.notice, text: msg, confirmButtonText: bookingI18n.confirm, confirmButtonColor:'#ff710d', heightAuto:false }); }
4190function v2SwalInfo(msg, title)    { return Swal.fire({ icon:'info',    title: title || bookingI18n.info, text: msg, confirmButtonText: bookingI18n.confirm, confirmButtonColor:'#ff710d', heightAuto:false }); }
4191
4192    // bfcache/뒤로가기 복원 시 남아있는 로딩/진행 상태를 강제 초기화한다.
4193    window.addEventListener('pageshow', function(event) {
4194        var navEntry = (performance && typeof performance.getEntriesByType === 'function')
4195            ? performance.getEntriesByType('navigation')[0]
4196            : null;
4197        var isBackForward = !!event.persisted || (navEntry && navEntry.type === 'back_forward');
4198
4199        if (!isBackForward) {
4200            return;
4201        }
4202
4203        v2NextInProgress = false;
4204        v2ResetLoading();
4205
4206        $('#v2-loading-overlay')
4207            .stop(true, true)
4208            .hide()
4209            .css({
4210                display: 'none',
4211                opacity: 1
4212            });
4213    });
4214
4215    document.addEventListener('visibilitychange', function() {
4216        if (!document.hidden && v2CBookList.length) v2UpdateFacilityHoldTimer();
4217    });
4218
4219    /* ================================================================
4220       Document Ready
4221    ================================================================ */
4222    $(function() {
4223        v2PrepareCartRestore();
4224        v2SyncHeaderLanguageLinks();
4225        v2SyncToV1();
4226        v2InitPage();
4227        v2SwitchTab('special'); // ← 추가: 초기 진입 시 특가 탭 인라인 표시 강제
4228
4229        /* 상단 탭 클릭 */
4230        $(document).on('click', '#v2TabNav .nav-link', function(e) {
4231            e.preventDefault();
4232            v2SwitchTab($(this).data('tab'));
4233        });
4234
4235        $(document).on('click', '#v2ProductLanguageSwitcher [data-booking-lang]', function() {
4236            v2ChangeBookingLanguage($(this).data('booking-lang'));
4237        });
4238
4239        $(document).on('click', v2HeaderLanguageLinkSelector, function(e) {
4240            const language = v2GetLanguageFromLink(this);
4241            if (!language) return;
4242            e.preventDefault();
4243            v2ChangeBookingLanguage(language);
4244        });
4245
4246        $(document).on('click', '.qty-plus', function(e) {
4247            e.stopPropagation();
4248            const $item   = $(this).closest('.child-item');
4249            if (!$item.length) return; // 부대시설 등 해당없는 영역은 무시
4250
4251            const $detail = $(this).closest('.product-detail, .facility-detail');
4252            const did     = $detail.attr('id') ? $detail.attr('id').replace('v2-detail-', '') : '';
4253            const $dateInput = $detail.find('#v2TicketDate_' + did);
4254
4255            if ($dateInput.length && !$dateInput.val()) {
4256    v2SwalInfo(bookingI18n.dateFirst, bookingI18n.selectionRequired);
4257                return;
4258            }
4259
4260            const $el = $(this).siblings('.qty-display');
4261            $el.text(parseInt($el.text()) + 1);
4262            v2SyncChildItemToCart($item);
4263        });
4264
4265        $(document).on('click', '.qty-minus', function(e) {
4266            e.stopPropagation();
4267            const $item = $(this).closest('.child-item');
4268            if (!$item.length) return;
4269
4270            const $el = $(this).siblings('.qty-display');
4271            const v   = parseInt($el.text());
4272            if (v <= 0) return;
4273
4274            $el.text(v - 1);
4275            v2SyncChildItemToCart($item);
4276        });
4277
4278        if (window.innerWidth <= 768) {
4279            const amtObserver = new MutationObserver(function() {
4280    $('.mobile-total').text($('.totalAmt').first().text() || '0');
4281            });
4282            document.querySelectorAll('.totalAmt').forEach(function(el) {
4283                amtObserver.observe(el, { childList: true, subtree: true, characterData: true });
4284            });
4285        }
4286
4287        window.v2ToggleMobileCart = function() {
4288            const $bar = $('#v2MobileBar');
4289            const willCollapse = !$bar.hasClass('is-collapsed');
4290            $bar.toggleClass('is-collapsed', willCollapse);
4291            $('.hBook').toggleClass('mobile-cart-collapsed', willCollapse);
4292            $('#v2MobileCartToggle')
4293                .attr('aria-expanded', String(!willCollapse))
4294                .attr('aria-label', willCollapse ? bookingI18n.cartExpand : bookingI18n.cartCollapse);
4295        };
4296
4297        window.v2ScrollToPanel = function() {
4298            $('html, body').animate({
4299                scrollTop: $('#v2SidebarCol').offset().top - 20
4300            }, 400);
4301        };
4302    });
4303
4304</script>
4304
4305
4306</body>
4307</html>

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.