달력을 화면에 넣으면 예약 기능이 생긴 것처럼 보일 수 있다. 그러나 선택한 날짜와 시간이 요청으로 이어지고, 그 요청을 실제로 받았는지 확인하는 길이 필요하다. 나는 예약 소스와 교체 엔진의 조건을 다시 읽고 화면 뒤의 상태를 작은 모듈로 정리했다.

직접 선택해 볼 수 있는 예시는 넣되 실제 예약을 받지는 않는다. 코드가 무엇을 확인하는지, 서버에 연결하려면 무엇을 더 정해야 하는지, 응답을 잃었을 때 어떤 상태를 남겨야 하는지 함께 설명한다. 방문자에게 보여주는 완료 문구가 실제 근거보다 앞서지 않게 하는 기준이다.

날짜 선택과 실제 접수는 다른 단계다

확인한 예약 소스에는 달력과 시간 버튼뿐 아니라 입력 검증, 접수 중 표시, 서버 결과 확인, 실패 안내가 있다. 교체 엔진의 미리보기는 실제 접수를 막고 연결이 준비되지 않은 공개 예약도 배포 전에 확인하도록 한다. 달력이 보인다는 사실을 접수 기능의 완료로 처리하지 않는 조건이다.

나는 예약을 설명할 때 선택한 값, 보낼 요청, 서버가 수락한 요청을 구별한다. 수락 응답이 있어도 실제 방문 시간의 최종 확정과 이메일 전달까지 자동으로 증명하는 것은 아니다. 예약 가능 여부를 정하는 운영 정책과 전달 결과는 별도의 근거가 필요하다.

업무 시간대의 날짜와 가능한 시간을 전달한다

현재 교체 엔진은 현장별 시간 목록과 예약 가능 기간을 설정할 수 있게 만든다. 새 모듈은 최소·최대 날짜와 가능한 시간 목록, 업무 기준의 오늘과 지난 시간 기준을 입력받는다. 날짜 문자열의 실제 달력 유효성도 확인해 존재하지 않는 날짜를 범위 안 값처럼 사용하지 않는다.

전세계에서 읽는 홈페이지라면 방문자의 PC 날짜를 사업장의 기준으로 곧바로 쓰지 않도록 해야 한다. 실제 운영에서는 업무 시간대와 서버의 일정 정책을 확인하고 그 값으로 선택과 재검사를 연결한다. 아래 예시의 일정은 설명을 위한 고정 합성 값이며 실제 예약 가능한 시각을 보여주는 자료가 아니다.

입력 오류와 미연결 상태를 구별한다

날짜나 시간을 고르지 않은 경우와 실제 접수 어댑터가 없는 경우는 다른 문제다. 전자는 어떤 입력을 고쳐야 하는지 알려주고 후자는 지금 접수할 연결이 없다는 상태를 보여줘야 한다. 새 함수는 입력 확인 뒤 연결 여부를 보고 실제 요청 없이 미연결 결과를 돌려준다.

예시 확인 체크박스도 문자열이 아닌 실제 true여야 한다. 이는 예시의 입력 조건이며 법적 동의 양식을 완성했다는 뜻이 아니다. 실제 폼에서 필요한 개인정보 항목과 안내, 수집 목적을 정하는 일은 그 사이트의 별도 설계다. 여기서는 개인정보를 받거나 전달하지 않는 날짜·시간 흐름만 다룬다.

접수 중과 수락 뒤의 재클릭을 관리한다

현재 폼 소스는 처리 중 버튼을 비활성화하고 서버 결과의 명시적인 수락 조건을 확인한 뒤 완료 표시를 한다. 새 모듈도 처리 중 새 호출을 만들지 않고 수락된 요청에는 명시적인 초기화가 있어야 다음 예시를 보낼 수 있게 했다. 눌렀다는 사실만으로 완료 표시를 만들지 않는다.

이 클라이언트 가드는 서버의 중복 방지와 같다거나 시간 좌석을 확보했다고 설명할 수 없다. 실제 운영에서는 요청 식별자, 서버의 중복 처리, 일정의 최신 확인을 함께 검토해야 한다. 예시의 식별자는 흐름을 이어가기 위한 값이며 공개 홈페이지의 실제 예약 저장소와 연결되어 있지 않다.

응답을 잃었으면 실패로 단정하고 바로 보내지 않는다

서버가 처리한 뒤 응답만 사라질 수도 있다. 이때 모든 통신 오류를 미접수라고 단정하고 다시 보내면 중복 요청이 될 수 있다. 새 모듈은 명시적인 거절과 결과 미확인을 나누고 미확인 상태에서는 새 전송과 초기화를 막는다. 결과를 알 수 없다는 사실을 상태로 남긴다.

실제 적용에서는 같은 요청 식별자로 서버 상태를 조회해 수락 또는 미접수를 확인하는 연결이 필요하다. 이 함수의 대조 메서드는 같은 식별자와 명시적인 결과를 요구하지만 조회 자체를 수행하지 않는다. 아래 버튼은 합성 조회 결과를 넣는 예시이며 실제 서버의 답을 확인했다고 표시하지 않는다.

선택과 접수 상태를 연결하는 구현

다음 코드는 선택 정책을 검사하고 주입한 접수 어댑터의 결과로 상태를 바꾼다. 실제 endpoint나 수신 설정은 들어 있지 않다. 네트워크와 개인정보 처리를 연결하는 쪽은 검토된 별도 서버 계약을 사용해야 한다. 코드를 글 안에 넣는 일과 실제 고객 예약을 활성화하는 일을 구별한다.

이번에는 합성 달력과 로컬 어댑터로 12개 검사를 실행했다. 날짜와 시간·확인 입력, 지난 시간, 미연결, 중복 클릭과 수락 시점, 명시적 거절과 응답 유실, 잘못된 수락 값, 식별자 대조, 입력 복사, 수락 뒤 초기화, 잘못된 정책을 확인했다. 실제 예약·이메일·브라우저 달력 조작 검사는 아니다.

코드와 구현 살펴보기

JavaScript · 날짜 선택에서 접수 결과까지

function validDate(value){
 if(typeof value!=='string'||!/^\d{4}-\d{2}-\d{2}$/.test(value))return false;
 const d=new Date(value+'T00:00:00Z');return Number.isFinite(d.valueOf())&&d.toISOString().slice(0,10)===value;
}
export function validateBookingChoice(choice,policy){
 if(!validDate(policy?.minDate)||!validDate(policy?.maxDate)||policy.minDate>policy.maxDate||!Array.isArray(policy.timeSlots)||policy.timeSlots.some(x=>!/^([01]\d|2[0-3]):[0-5]\d$/.test(x)))throw new TypeError('Valid business-calendar policy required');
 if((policy.businessToday!==undefined||policy.cutoffTime!==undefined)&&(!validDate(policy.businessToday)||typeof policy.cutoffTime!=='string'||!/^([01]\d|2[0-3]):[0-5]\d$/.test(policy.cutoffTime)))throw new TypeError('Valid paired business-date and cutoff required');
 const issues=[];
 if(!validDate(choice?.date)||choice.date<policy.minDate||choice.date>policy.maxDate)issues.push('date');
 if(!policy.timeSlots.includes(choice?.time))issues.push('time');
 if(choice?.date===policy.businessToday&&typeof policy.cutoffTime==='string'&&choice.time<=policy.cutoffTime)issues.push('elapsed-time');
 if(choice?.agreed!==true)issues.push('agreement');
 return [...new Set(issues)];
}
export function createBookingFlow({policy,send,notify=()=>{},makeId=()=>crypto.randomUUID()}){
 let status='idle',request=null,issues=[],busy=false;
 const snapshot=()=>({status,requestId:request?.id||null,issues:[...issues],busy});
 function update(next){status=next;notify(snapshot());return snapshot();}
 return {snapshot,
  async submit(choice){
   if(busy||status==='uncertain'||status==='accepted')return snapshot();
   issues=validateBookingChoice(choice,policy);if(issues.length)return update('invalid');
   if(typeof send!=='function')return update('unconnected');
   const requestId=makeId();if(typeof requestId!=='string'||!requestId.trim())throw new TypeError('Request identifier required');
   request={id:requestId,date:choice.date,time:choice.time,agreed:true};
   busy=true;update('submitting');
   try{
    const result=await send({...request});
    if(result?.ok===true&&result.accepted===true)status='accepted';
    else if(result?.rejected===true&&result.accepted===false)status='rejected';
    else status='uncertain';
   }catch{status='uncertain';}
   finally{busy=false;notify(snapshot());}
   return snapshot();
  },
  reconcile(result){
   if(status!=='uncertain'||result?.requestId!==request?.id)throw new Error('Matching uncertain request required');
   if(result.accepted===true)return update('accepted');
   if(result.accepted===false&&result.rejected===true)return update('rejected');
   throw new Error('Authoritative acceptance or rejection required');
  },
  reset(){if(busy||status==='uncertain')throw new Error('Resolve the outstanding result first');request=null;issues=[];return update('idle');}
 };
}

미연결과 모의 결과를 직접 바꾸는 예시

아래 예시는 2030년 5월의 합성 날짜와 시간을 쓴다. 미연결, 모의 수락, 모의 거절, 모의 응답 유실을 선택하도록 작성했다. 실제 접수와 이메일, 개인정보 전송은 없다. 선택한 조건이 처리 상태와 다시 실행할 수 있는 조건을 어떻게 바꾸는지 보여준다.

공개 HTML의 실행 프레임과 스크립트 문법을 확인하지만 실제 PC·모바일에서 달력과 버튼을 조작한 결과는 아니다. 이 홈페이지의 연락 방식은 이메일로 유지한다. 예약 모듈은 필요할 때 별도 검토와 서버 연결을 거쳐 적용할 수 있는 구현 재료로 남긴다.