File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
$(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));
});
endPickers.each(function (idx, itm) {
$(itm).removeClass("endDate").addClass("endDate" + (idx + 1));
$(itm).find(".duet-date__input").attr("id", "endDate" + (idx + 1));
});
// 총 페어 개수는 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로 보호해두었습니다(오류 무시).
// ===================================================================