$(window).on('load', function () { // DOM 로드 후 initDatePickers 를 실행합니다. // setTimeout을 짧게 둔 이유: duet-date-picker 같은 웹컴포넌트가 // 브라우저에서 hydrate(초기화) 되는 시점과 맞추기 위함입니다. setTimeout(initDatePickers, 10); }); // =================================================================== // initDatePickers // - 페이지 내 모든 .startDate / .endDate 요소를 찾아 순번 붙이고 // duet-date-picker 관련 기능(포맷, 로컬, 키보드 입력, validation 등)을 바인딩합니다. // - 동적으로 요소가 추가된 경우(예: AJAX) initDatePickers()를 // 다시 호출하면 새로 추가된 요소에도 자동 적용됩니다. // =================================================================== function initDatePickers() { // start / end picker들을 쿼리합니다. const startPickers = $(".startDate"); const endPickers = $(".endDate"); // --------------------------------------------------------------- // 1) 각 start/end에 1부터 순번 부여 // .startDate -> .startDate1, .startDate2 ... // 내부 duet-date__input에도 id(startDate1, ...)를 부여 (필요시) // --------------------------------------------------------------- startPickers.each(function (idx, itm) { $(itm).removeClass("startDate").addClass("startDate" + (idx + 1)); // 내부 input id 셋팅: 스크린리더 또는 라벨 연결에 유용 $(itm).find(".duet-date__input").attr("id", "startDate" + (idx + 1)); $(itm).find("input[type=hidden]").attr("name","startDate"+(idx+1)+"_submit"); }); endPickers.each(function (idx, itm) { $(itm).removeClass("endDate").addClass("endDate" + (idx + 1)); $(itm).find(".duet-date__input").attr("id", "endDate" + (idx + 1)); $(itm).find("input[type=hidden]").attr("name","startDate"+(idx+1)+"_submit"); }); // 총 페어 개수는 start / end 중 큰 쪽 기준으로 반복합니다. const total = Math.max(startPickers.length, endPickers.length); // 각 인덱스별로 duet-date-picker 쌍에 기능을 적용합니다. for (let i = 1; i <= total; i++) { const startEl = document.querySelector(".startDate" + i); const endEl = document.querySelector(".endDate" + i); // ---------------------------------------------------------------- // 날짜 포맷 설정 (duet 라이브러리의 dateAdapter에 연결) // - parse: 문자열 -> Date // - format: Date -> "YYYY.MM.DD" // ---------------------------------------------------------------- function setDateAdapter(target) { if (!target) return; target.dateAdapter = { parse(value = "", createDate) { const parts = value.split("."); if (parts.length !== 3) return null; // 포맷이 아니면 null const [y, m, d] = parts.map(Number); return createDate(y, m - 1, d); // month는 0-base }, format(date) { return `${date.getFullYear()}.${String(date.getMonth() + 1).padStart(2, "0")}.${String(date.getDate()).padStart(2, "0")}`; }, }; } // ---------------------------------------------------------------- // 한글 로컬라이제이션 설정 // ---------------------------------------------------------------- function setLocalization(target, type) { if (!target) return; target.localization = { placeholder: type === "start" ? "시작일 선택" : "종료일 선택", buttonLabel: "달력 열기", selectedDateMessage: "선택된 날짜:", prevMonthLabel: "이전 달", nextMonthLabel: "다음 달", monthSelectLabel: "월 선택", yearSelectLabel: "연도 선택", closeLabel: "닫기", calendarHeading: "날짜 선택", dayNames: ["일", "월", "화", "수", "목", "금", "토"], monthNames: ["1월","2월","3월","4월","5월","6월","7월","8월","9월","10월","11월","12월"], monthNamesShort: ["1월","2월","3월","4월","5월","6월","7월","8월","9월","10월","11월","12월"], }; } // ---------------------------------------------------------------- // 키보드 입력 지원 (숫자만 받아서 자동으로 yyyy.mm.dd 포맷으로 만듦) // - duet-date-picker.value, .duet-date__input.value, hidden.value 를 모두 동기화 // - 입력 중에는 포맷을 점진적으로 적용(input 이벤트) // - blur 시 유효성 검사를 통해 값 확정 // ---------------------------------------------------------------- function enableKeyboardInput(target) { if (!target) return; const input = target.querySelector(".duet-date__input"); const hidden = target.querySelector('input[type="hidden"]'); if (!input) return; // 입력 도중 포맷 해주는 로직 input.addEventListener("input", function (e) { let val = e.target.value.replace(/[^0-9]/g, ""); // 숫자만 // 자동으로 20251107 -> 2025.11.07 형태로 변환 if (val.length > 4 && val.length <= 6) { val = val.replace(/(\d{4})(\d+)/, "$1.$2"); } else if (val.length > 6) { val = val.replace(/(\d{4})(\d{2})(\d+)/, "$1.$2.$3"); } e.target.value = val; // 형식이 완성되면 duet 컴포넌트 value + hidden 동기화 if (/^\d{4}\.\d{2}\.\d{2}$/.test(val)) { if (hidden) hidden.value = val; // duet-date-picker 자체 value에 반영 (컴포넌트에 따라 내부 업데이트 트리거) try { target.value = val; } catch (err) { /* 안정성: 일부 환경에서 읽기전용일 수 있음 */ } } }); // blur 시 최종 확인(잘못된 형식이면 초기화) input.addEventListener("blur", function (e) { const val = e.target.value; if (/^\d{4}\.\d{2}\.\d{2}$/.test(val)) { if (hidden) hidden.value = val; try { target.value = val; } catch (err) {} } else if (val.trim() !== "") { // 비어있지 않은데 형식이 맞지 않으면 사용자에게 알리고 초기화 alert("날짜 형식은 YYYY.MM.DD 입니다."); e.target.value = ""; if (hidden) hidden.value = ""; try { target.value = ""; } catch (err) {} } }); } // ---------------------------------------------------------------- // duetClose 이벤트 바인딩 // - 달력에서 선택 후 닫힐 때 실행됨 // - 이때도 input + hidden 동기화 // ---------------------------------------------------------------- function bindCloseEvent(target) { if (!target) return; const input = target.querySelector(".duet-date__input"); const hidden = target.querySelector('input[type="hidden"]'); // duet 컴포넌트이므로 커스텀 이벤트 duetClose를 사용 target.addEventListener("duetClose", function (e) { const val = e.target.value; if (val) { if (input) input.value = val; if (hidden) hidden.value = val; } }); } // ---------------------------------------------------------------- // 시작/종료 유효성 검사 // - endEl 이 없으면 검사 스킵 // - duetChange 시 (값이 확정될 때) 검사 // ---------------------------------------------------------------- function bindDateValidation(startEl, endEl) { if (!startEl || !endEl) return; const startInput = startEl.querySelector(".duet-date__input"); const endInput = endEl.querySelector(".duet-date__input"); if (startEl) { startEl.addEventListener("duetChange", function () { const sVal = (startInput?.value || "").replace(/\./g, ""); const eVal = (endInput?.value || "").replace(/\./g, ""); if (sVal && eVal && sVal > eVal) { alert("시작일은 종료일보다 클 수 없습니다."); if (startInput) startInput.value = ""; try { startEl.value = ""; } catch (err) {} } }); } if (endEl) { endEl.addEventListener("duetChange", function () { const sVal = (startInput?.value || "").replace(/\./g, ""); const eVal = (endInput?.value || "").replace(/\./g, ""); if (sVal && eVal && eVal < sVal) { alert("종료일은 시작일보다 작을 수 없습니다."); if (endInput) endInput.value = ""; try { endEl.value = ""; } catch (err) {} } }); } } // ---------------------------------------------------------------- // 모든 기능 적용 // - setDateAdapter / setLocalization 은 duet 컴포넌트 내부 동작을 위해 필수 // - enableKeyboardInput / bindCloseEvent 은 우리가 추가한 동기화 로직 // - bindDateValidation 은 쌍이 있을 때만 적용 // ---------------------------------------------------------------- setDateAdapter(startEl); setDateAdapter(endEl); setLocalization(startEl, "start"); setLocalization(endEl, "end"); enableKeyboardInput(startEl); enableKeyboardInput(endEl); bindCloseEvent(startEl); bindCloseEvent(endEl); bindDateValidation(startEl, endEl); } } // =================================================================== // 사용/주의사항 // 1) 동적 추가: AJAX나 JS로 duet-date-picker 를 추가한 경우, // 추가 후 initDatePickers() 를 다시 호출하면 자동으로 바인딩됩니다. // 2) duet-date-picker 내부 구조가 달라지면 (예: .duet-date__input 클래스 변경) // 선택자(input / hidden)들을 그에 맞게 수정해야 합니다. // 3) monthNames 배열은 12개로 반드시 채워야 합니다 (렌더링 에러 방지). // 4) 일부 duet-date-picker 구현은 target.value 가 읽기전용일 수 있습니다. // 그 경우 try/catch로 보호해두었습니다(오류 무시). // ===================================================================