Nextjs 웹앱 시지 라이프 개발

매장 휴무일 설정 - 시지 라이프 Nextjs 웹앱 매장 등록 기능 구현 [8]

그랜파 개발자 2025. 7. 15. 17:45

시지 라이프 매장 등록 - 매장 휴무일 설정 

휴무일 등록은 참 어렵습니다.

1주일에 한번, 2주일에 한번, 한달에 한번, 한달에 두번 등 다양한 휴무일이 있습니다.

이들을 모두 처리하는 것이 쉽지가 않습니다.

 

영업시간과 휴무일은 별도로 설정이 되지만

휴무일의 영업 시간은 표기하지 말아야 합니다.

서로 연관이 있다는 뜻입니다.

 

https://next-tailwind-firebase-order-app.vercel.app/

 

지역 커뮤니티 - 시지 라이프

시지 라이프 시지 지역 커뮤니티 - 테스트 배포 중입니다. 로딩 중...

next-tailwind-firebase-order-app.vercel.app

 

매장 휴무일 설정

 

 

매장 영업 중
매장 영업 종료

 

StoreRegisterPage - 매장 등록 컴포넌트

'use client';

import { useState, useEffect } from 'react';
import { db } from '@/firebase/firebaseConfig';
import { collection, addDoc, serverTimestamp, query, where, getDocs } from 'firebase/firestore';
import { Store, BusinessHour, DayOfWeek, HolidayRule } from '@/types/store';
import Script from 'next/script';
import { useRouter } from 'next/navigation';
import { useUserStore } from '@/stores/userStore';
import BusinessHoursModal from '@/components/modals/BusinessHoursModal';
import HolidayRuleModal from '@/components/modals/HolidayRuleModal';

const categories = [
  '한식', '중식', '일식', '양식', '분식', '치킨', '피자', '패스트푸드',
  '고기/구이', '족발/보쌈', '찜/탕/찌개', '도시락', '야식', '해산물',
  '디저트', '베이커리', '카페', '커피/음료', '샐러드', '브런치', '기타',
];

const emptyBusinessHours: Record<DayOfWeek, BusinessHour> = {
  월: { opening: '', closing: '' },
  화: { opening: '', closing: '' },
  수: { opening: '', closing: '' },
  목: { opening: '', closing: '' },
  금: { opening: '', closing: '' },
  토: { opening: '', closing: '' },
  일: { opening: '', closing: '' },
};

const defaultHolidayRule: HolidayRule = {
  frequency: '매주',
  days: [],
};

export default function StoreRegisterPage() {
  const [form, setForm] = useState<Store>({
    category: '',
    name: '',
    description: '',
    zipcode: '',
    address: '',
    detailAddress: '',
    latitude: '',
    longitude: '',
    businessHours: emptyBusinessHours,
    holidayRule: defaultHolidayRule,
    admin: ''
  });

  const [showBusinessHoursModal, setShowBusinessHoursModal] = useState(false);
  const [showHolidayRuleModal, setShowHolidayRuleModal] = useState(false);
  const router = useRouter();
  const { userData } = useUserStore();

  const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
    const { name, value } = e.target;
    setForm(prev => ({ ...prev, [name]: value }));
  };

  const handleAddressSearch = () => {
    if (typeof window === 'undefined' || !(window as any).daum?.Postcode) return;
    new (window as any).daum.Postcode({
      oncomplete: (data: any) => {
        setForm(prev => ({
          ...prev,
          address: data.address,
          zipcode: data.zonecode,
        }));
      }
    }).open();
  };

  const handleOpenMap = () => {
    if (!form.name || !form.address) {
      alert('상호명과 주소를 입력해주세요.');
      return;
    }

    const query = new URLSearchParams({
      name: form.name,
      address: form.address,
      latitude: String(form.latitude),
      longitude: String(form.longitude),
    });

    const width = 800;
    const height = 600;
    const left = window.screenX + (window.outerWidth - width) / 2;
    const top = window.screenY + (window.outerHeight - height) / 2;

    window.open(
      `/naver-map-view?${query.toString()}`,
      'mapWindow',
      `width=${width},height=${height},left=${left},top=${top}`
    );
  };

  useEffect(() => {
    const messageHandler = (event: MessageEvent) => {
      if (event.origin !== window.location.origin) return;
      if (event.data?.type === 'coords') {
        setForm(prev => ({
          ...prev,
          latitude: event.data.lat.toString(),
          longitude: event.data.lng.toString(),
        }));
      }
    };
    window.addEventListener('message', messageHandler);
    return () => window.removeEventListener('message', messageHandler);
  }, []);

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();

    if (!userData?.userId) {
      alert('사용자 정보가 없습니다. 로그인 상태를 확인해주세요.');
      return;
    }

    const requiredFields = [
      { key: 'category', label: '업종' },
      { key: 'name', label: '상호명' },
      { key: 'description', label: '소개말' },
      { key: 'zipcode', label: '우편번호' },
      { key: 'address', label: '주소' },
      { key: 'latitude', label: '위도' },
      { key: 'longitude', label: '경도' },
    ];

    for (const field of requiredFields) {
      if (!form[field.key as keyof typeof form]) {
        alert(`${field.label}을(를) 입력해주세요.`);
        return;
      }
    }

    // 영업시간 하나라도 설정됐는지 체크
    const hasValidHours = Object.values(form.businessHours).some(
      (day) => day.opening && day.closing
    );
    if (!hasValidHours) {
      alert('영업시간을 설정해주세요.');
      return;
    }

    try {
      const storesRef = collection(db, 'stores');
      const duplicateQuery = query(
        storesRef,
        where('name', '==', form.name.trim()),
        where('address', '==', form.address.trim())
      );

      const snapshot = await getDocs(duplicateQuery);

      if (!snapshot.empty) {
        alert(`이미 등록된 매장입니다.\n\n상호명: ${form.name}\n주소: ${form.address}`);
        return;
      }

      await addDoc(storesRef, {
        ...form,
        latitude: parseFloat(form.latitude),
        longitude: parseFloat(form.longitude),
        admin: userData.userId,
        createdAt: serverTimestamp(),
      });

      alert('매장이 등록되었습니다!');
      router.push('/');
    } catch (error) {
      console.error(error);
      alert('등록 실패');
    }
  };

  return (
    <div className="max-w-xl mx-auto p-3">
      <h1 className="text-2xl font-bold mb-2 dark:text-white">매장 등록</h1>
      <form onSubmit={handleSubmit} className="space-y-4">

        {/* 업종 선택 */}
        <div>
          <label className="block font-semibold mb-2 dark:text-gray-200">업종 선택</label>
          <div className="flex flex-wrap gap-2">
            {categories.map(c => (
              <button
                key={c}
                type="button"
                onClick={() => setForm(prev => ({ ...prev, category: c }))}
                className={`px-3 py-1.5 rounded-full border text-xs transition
                  ${form.category === c
                    ? 'bg-blue-600 text-white border-blue-600'
                    : 'bg-white text-gray-700 border-gray-300 hover:bg-gray-100 dark:bg-gray-700 dark:text-gray-200 dark:border-gray-500 dark:hover:bg-gray-600'
                  }`}
              >
                {c}
              </button>
            ))}
          </div>
        </div>

        {/* 텍스트 입력 필드 예시 */}
        <input
          type="text"
          name="name"
          placeholder="상호명"
          value={form.name}
          onChange={handleChange}
          className="w-full p-2 border text-xs border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-800 text-black dark:text-white"
        />

        {/* textarea */}
        <textarea
          name="description"
          placeholder="소개말"
          value={form.description}
          onChange={handleChange}
          className="w-full p-2 border text-xs border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-800 text-black dark:text-white"
        />

        {/* 영업시간 버튼 */}
        <button
          type="button"
          onClick={() => setShowBusinessHoursModal(true)}
          className="w-full p-2 border text-xs rounded bg-gray-100 dark:bg-gray-700 dark:text-white"
        >
          영업시간 설정
        </button>

        {/* 설정된 영업시간 보기 */}
        <div className="text-xs text-gray-600">
          {Object.entries(form.businessHours).every(([_, h]) => !h.opening && !h.closing) ? (
            <span className="italic">영업시간 미설정</span>
          ) : (
            <ul className="mt-0 space-y-1">
              {Object.entries(form.businessHours).map(([day, h]) => (
                <li key={day}>
                  <span className="font-semibold">{day}</span>:&nbsp;
                  {h.opening && h.closing ? `${h.opening} ~ ${h.closing}` : '휴무'}
                </li>
              ))}
            </ul>
          )}
        </div>

        {/* 휴무일 설정 버튼 */}
        <button
          type="button"
          onClick={() => setShowHolidayRuleModal(true)}
          className="w-full p-2 border text-xs rounded bg-gray-100 dark:bg-gray-700 dark:text-white"
        >
          휴무일 설정
        </button>

        {/* 휴무 규칙 요약 */}
        <p className="mt-0 text-xs text-gray-600 dark:text-gray-300">
          {form.holidayRule.frequency} {form.holidayRule.days.join(', ')}
          {form.holidayRule.weeks?.length
            ? ` (매월 ${form.holidayRule.weeks.join(', ')}주차)`
            : ''} 휴무
        </p>

        
        {/* 우편번호 및 주소 */}
        <input
          type="text"
          name="zipcode"
          placeholder="우편번호 (클릭하여 검색)"
          value={form.zipcode}
          readOnly
          onClick={handleAddressSearch}
          className="w-full p-2 text-xs border border-gray-300 dark:border-gray-600
                    rounded bg-gray-100 dark:bg-gray-800 
                    text-black dark:text-white 
                    placeholder-gray-400 dark:placeholder-gray-500 
                    cursor-pointer"
        />

        <input
          type="text"
          name="address"
          placeholder="주소"
          value={form.address}
          readOnly
          onClick={handleAddressSearch}
          className="w-full p-2 text-xs border border-gray-300 dark:border-gray-600 
                    rounded bg-gray-100 dark:bg-gray-800 
                    text-black dark:text-white 
                    placeholder-gray-400 dark:placeholder-gray-500 
                    cursor-pointer"
        />

        <input
          type="text"
          name="detailAddress"
          placeholder="상세주소"
          value={form.detailAddress}
          onChange={handleChange}
          className="w-full p-2 text-xs border border-gray-300 dark:border-gray-600 
                    rounded bg-white dark:bg-gray-800 
                    text-black dark:text-white 
                    placeholder-gray-400 dark:placeholder-gray-500"
        />

        
        {/* 위도 / 경도 필드 */}
        <div className="flex gap-4">
          <input
            type="text"
            name="latitude"
            placeholder="위도"
            value={form.latitude}
            readOnly
            onClick={handleOpenMap}
            className="w-full p-2 border text-xs border-gray-300 dark:border-gray-600 
                      rounded bg-gray-100 dark:bg-gray-800 text-black dark:text-white"
          />
          <input
            type="text"
            name="longitude"
            placeholder="경도"
            value={form.longitude}
            readOnly
            onClick={handleOpenMap}
            className="w-full p-2 text-xs border border-gray-300 dark:border-gray-600 rounded
                       bg-gray-100 dark:bg-gray-800 text-black dark:text-white"
          />
        </div>

        {/* 등록 버튼 */}
        <button
          type="submit"
          className="w-full text-xs bg-blue-600 text-white py-2 rounded hover:bg-blue-700
                   dark:hover:bg-blue-500"
        >
          등록하기
        </button>
      </form>

      {showBusinessHoursModal && (
        <BusinessHoursModal
          defaultValue={form.businessHours}
          onSave={(updatedHours) => {
            setForm(prev => ({ ...prev, businessHours: updatedHours }));
            setShowBusinessHoursModal(false);
          }}
          onCancel={() => setShowBusinessHoursModal(false)}
        />
      )}

      {showHolidayRuleModal && (
        <HolidayRuleModal
          isOpen={true}
          defaultValue={form.holidayRule ?? defaultHolidayRule}
          onSave={(updatedRule) => {
            setForm(prev => ({ ...prev, holidayRule: updatedRule }));
            setShowHolidayRuleModal(false);
          }}
          onCancel={() => setShowHolidayRuleModal(false)}
        />
      )}

      <Script src="https://t1.daumcdn.net/mapjsapi/bundle/postcode/prod/postcode.v2.js" strategy="lazyOnload" />
    </div>
  );
}

 

매장 휴무일 설정 로직

매장 휴무일 설정 로직은 HolidayRuleModal을 통해 이루어지며,

사용자가 반복 주기(frequency), 요일(days), 주차(weeks)를 선택할 수 있도록 설계되어 있습니다. 

✅ 1. 휴무일 관련 데이터 구조

export interface HolidayRule {
  frequency: '매주' | '격주' | '한 달 1회' | '한 달 2회'; // 반복 주기
  days: DayOfWeek[];  // 휴무 요일들, 예: ['일', '수']
  weeks?: number[];   // 몇 주차에 적용할지, 예: [1, 3]
}


✅ 2. 휴무일 기본값 (defaultHolidayRule)

const defaultHolidayRule: HolidayRule = {
  frequency: '매주',
  days: [],
};
  • 모달을 처음 열 때의 기본 상태
  • frequency만 있고, days와 weeks는 비어 있음

✅ 3. 휴무일 설정 모달 트리거

<button
  type="button"
  onClick={() => setShowHolidayRuleModal(true)}
>
  휴무일 설정
</button>
  • 버튼 클릭 시 HolidayRuleModal이 열림

✅ 4. 휴무일 설정 저장 처리

<HolidayRuleModal
  isOpen={true}
  defaultValue={form.holidayRule ?? defaultHolidayRule}
  onSave={(updatedRule) => {
    setForm(prev => ({ ...prev, holidayRule: updatedRule }));
    setShowHolidayRuleModal(false);
  }}
  onCancel={() => setShowHolidayRuleModal(false)}
/>

 

여기서:

  • defaultValue는 현재 form.holidayRule
  • 모달 내에서 사용자가 설정한 값을 onSave로 전달하면 상태 업데이트
  • onCancel을 누르면 모달 닫기만 수행

✅ 5. 설정된 휴무일 요약 표시

<p className="mt-0 text-xs text-gray-600 dark:text-gray-300">
  {form.holidayRule.frequency} {form.holidayRule.days.join(', ')}
  {form.holidayRule.weeks?.length
    ? ` (매월 ${form.holidayRule.weeks.join(', ')}주차)`
    : ''} 휴무
</p>


예시 출력:

  • 매주 월,화 휴무
  • 격주 수,금 (매월 2,4주차) 휴무

✅ 6. 전체 흐름 요약

순서 설명
1. 사용자가 "휴무일 설정" 버튼 클릭
2. HolidayRuleModal 열림 (기존 데이터 전달됨)
3. 모달 내에서 주기/요일/주차 선택
4. "저장" 시 → 선택된 설정이 form.holidayRule에 저장됨
5. 설정 요약은 본문에 간단히 표시됨
6. 최종적으로 Firestore에 저장될 때 이 holidayRule도 함께 저장됨


✅ 7. 저장 예시 (Firestore에 저장될 데이터)

{
  "holidayRule": {
    "frequency": "한 달 2회",
    "days": ["일"],
    "weeks": [1, 3]
  }
}

 

HolidayRuleModal 컴포넌트

  • 매장 휴무일 설정을 위한 모달 UI입니다. 
  • 사용자는 주기(frequency), 요일(days), 그리고 몇째 주(weeks)를 선택하여 정기적인 휴무 규칙을 정의할 수 있습니다.
'use client';

import {
  Dialog,
  DialogPanel,
  DialogTitle,
  Transition,
  TransitionChild,
} from '@headlessui/react';
import { Fragment, useState } from 'react';
import type { HolidayRule, HolidayFrequency } from '@/types/store';

export type DayOfWeek = '월' | '화' | '수' | '목' | '금' | '토' | '일';

interface HolidayRuleModalProps {
  isOpen: boolean;
  defaultValue: HolidayRule;
  onSave: (rule: HolidayRule) => void;
  onCancel: () => void;
}

const allDays: DayOfWeek[] = ['월', '화', '수', '목', '금', '토', '일'];

const emptyRule: HolidayRule = {
  frequency: '매주',
  days: [],
  weeks: [],
};

export default function HolidayRuleModal({
  isOpen,
  defaultValue,
  onSave,
  onCancel,
}: HolidayRuleModalProps) {
  const [rule, setRule] = useState<HolidayRule>(defaultValue);

  const toggleDay = (day: DayOfWeek) => {
    setRule((prev) => ({
      ...prev,
      days: prev.days.includes(day)
        ? prev.days.filter((d) => d !== day)
        : [...prev.days, day],
    }));
  };

  const toggleWeek = (week: number) => {
    setRule((prev) => {
      const current = prev.weeks || [];

      if (prev.frequency === '매월 1회') {
        return { ...prev, weeks: [week] }; // 한 개만 선택 가능
      }

      // 매월 2회인 경우: 다중 선택 가능
      return {
        ...prev,
        weeks: current.includes(week)
          ? current.filter((w) => w !== week)
          : [...current, week],
      };
    });
  };

  const handleFrequencyChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
    const value = e.target.value as HolidayFrequency;
    setRule({
      frequency: value,
      days: [],
      weeks: value === '매월 1회' || value === '매월 2회' ? [] : undefined,
    });
  };

  const handleClear = () => {
    setRule(emptyRule);
  };

  return (
    <Transition appear show={isOpen} as={Fragment}>
      <Dialog as="div" className="relative z-50" onClose={onCancel}>
        <TransitionChild
          as={Fragment}
          enter="ease-out duration-200"
          enterFrom="opacity-0"
          enterTo="opacity-100"
          leave="ease-in duration-150"
          leaveFrom="opacity-100"
          leaveTo="opacity-0"
        >
          <div className="fixed inset-0 bg-black/30" />
        </TransitionChild>

        <div className="fixed inset-0 flex items-center justify-center p-4">
          <TransitionChild
            as={Fragment}
            enter="ease-out duration-200"
            enterFrom="opacity-0 scale-95"
            enterTo="opacity-100 scale-100"
            leave="ease-in duration-150"
            leaveFrom="opacity-100 scale-100"
            leaveTo="opacity-0 scale-95"
          >
            <DialogPanel className="w-full max-w-md bg-white rounded-xl p-6 shadow-xl">
              <DialogTitle className="text-lg font-semibold mb-4">휴무일 설정</DialogTitle>

              <div className="space-y-4">
                {/* 주기 선택 */}
                <div>
                  <label className="block text-sm font-medium mb-1">휴무 주기</label>
                  <select
                    className="w-full p-2 border rounded"
                    value={rule.frequency}
                    onChange={handleFrequencyChange}
                  >
                    <option value="매주">매주</option>
                    <option value="격주">격주</option>
                    <option value="매월 1회">매월 1회</option>
                    <option value="매월 2회">매월 2회</option>
                  </select>
                </div>

                {/* 요일 선택 */}
                <div>
                  <label className="block text-sm font-medium mb-1">요일 선택</label>
                  <div className="flex gap-2 flex-wrap">
                    {allDays.map((day) => (
                      <button
                        key={day}
                        type="button"
                        onClick={() => toggleDay(day)}
                        className={`px-3 py-1.5 rounded border text-sm transition ${
                          rule.days.includes(day)
                            ? 'bg-blue-600 text-white border-blue-600'
                            : 'bg-white text-gray-700 border-gray-300 hover:bg-gray-100'
                        }`}
                      >
                        {day}
                      </button>
                    ))}
                  </div>
                </div>

                {/* 주차 선택 */}
                {(rule.frequency === '매월 1회' || rule.frequency === '매월 2회') && (
                  <div>
                    <label className="block text-sm font-medium mb-1">몇째 주</label>
                    <div className="flex gap-2">
                      {[1, 2, 3, 4, 5].map((week) => (
                        <button
                          key={week}
                          type="button"
                          onClick={() => toggleWeek(week)}
                          className={`px-3 py-1.5 rounded border text-sm transition ${
                            rule.weeks?.includes(week)
                              ? 'bg-green-600 text-white border-green-600'
                              : 'bg-white text-gray-700 border-gray-300 hover:bg-gray-100'
                          }`}
                        >
                          {week}주차
                        </button>
                      ))}
                    </div>
                  </div>
                )}
              </div>

              <div className="mt-6 flex justify-between items-center">
                <button
                  onClick={handleClear}
                  className="px-4 py-2 rounded border border-gray-300 hover:bg-gray-100"
                >
                  지우기
                </button>

                <div className="flex gap-2">
                  <button
                    onClick={onCancel}
                    className="px-4 py-2 rounded border border-gray-300 hover:bg-gray-100"
                  >
                    취소
                  </button>
                  <button
                    onClick={() => onSave(rule)}
                    className="px-4 py-2 rounded bg-blue-600 text-white hover:bg-blue-700"
                  >
                    저장
                  </button>
                </div>
              </div>
            </DialogPanel>
          </TransitionChild>
        </div>
      </Dialog>
    </Transition>
  );
}

 

📦 1. 주요 타입 정의

export interface HolidayRule {
  frequency: '매주' | '격주' | '매월 1회' | '매월 2회';
  days: DayOfWeek[]; // 선택한 요일
  weeks?: number[]; // '매월 1회' 또는 '매월 2회'일 때 사용
}
export type DayOfWeek = '월' | '화' | '수' | '목' | '금' | '토' | '일';


🔄 2. 컴포넌트 동작 흐름


1. 초기 상태 세팅

const [rule, setRule] = useState<HolidayRule>(defaultValue);
  • 부모 컴포넌트에서 전달된 defaultValue로 상태 초기화

2. 휴무 주기 선택 (frequency)

<select onChange={handleFrequencyChange}>
  <option value="매주">매주</option>
  <option value="격주">격주</option>
  <option value="매월 1회">매월 1회</option>
  <option value="매월 2회">매월 2회</option>
</select>
const handleFrequencyChange = (e) => {
  setRule({
    frequency: value,
    days: [],
    weeks: value === '매월 1회' || value === '매월 2회' ? [] : undefined,
  });
};
  • 주기 변경 시 기존 days와 weeks 초기화
  • 주차 선택이 필요한 주기 (매월 1회, 매월 2회)일 때만 weeks 배열 유지

3. 요일 선택

const toggleDay = (day: DayOfWeek) => {
  // 토글 방식으로 day 추가/제거
};
{allDays.map((day) => (
  <button onClick={() => toggleDay(day)}>
    {day}
  </button>
))}
  • 선택한 요일은 파란색으로 강조 (bg-blue-600)

4. 주차 선택 (1~5주차)

{(rule.frequency === '매월 1회' || rule.frequency === '매월 2회') && (
  // 1~5주차 선택 UI 표시
)}
const toggleWeek = (week: number) => {
  if (rule.frequency === '매월 1회') {
    setRule({ ...rule, weeks: [week] }); // 하나만 선택 가능
  } else {
    // 매월 2회는 토글 방식으로 다중 선택
  }
};
  • 선택한 주차는 초록색으로 강조 (bg-green-600)

5. 지우기 버튼

<button onClick={handleClear}>지우기</button>
const emptyRule = { frequency: '매주', days: [], weeks: [] };
  • 휴무 규칙을 초기 상태로 되돌림

6. 취소 / 저장

<button onClick={onCancel}>취소</button>
<button onClick={() => onSave(rule)}>저장</button>
  • 취소: 모달 닫기만 수행
  • 저장: 현재 설정된 rule을 부모 컴포넌트로 전달

🎯 사용 예시
매월 2회, 월요일과 수요일, 2주차/4주차에 쉬는 경우

{
  frequency: '매월 2회',
  days: ['월', '수'],
  weeks: [2, 4],
}


🧠 UX 포인트

  • frequency에 따라 동적으로 weeks 표시 유무 제어
  • 요일, 주차는 토글 UI
  • 선택된 항목만 색상 강조 → 사용자 피드백 명확
  • onSave와 onCancel로 부모 컴포넌트와의 연결 깔끔하게 분리

✅ 요약

기능 설명
휴무 주기 선택 매주, 격주, 매월 1회, 매월 2회
요일 선택 복수 선택 가능
몇째 주 선택 주기가 매월일 때만 표시, 매월 1회는 단일 선택
저장/취소/초기화 모달 닫기 또는 설정 반영 가능

 

휴무일 판단

이 StoreLandingPage 컴포넌트에서는 오늘이 매장 휴무일인지 판단하고, 

그에 따라 영업 상태를 보여주는 기능이 구현되어 있습니다. 

🔍 휴무일 판단 및 화면 표출 전체 흐름


✅ 1. 오늘의 요일과 날짜 정보 계산

const today = new Date();
const dayIndex = today.getDay(); // 0(일) ~ 6(토)
const days: DayOfWeek[] = ['일', '월', '화', '수', '목', '금', '토'];
const todayLabel = days[dayIndex]; // 현재 요일 문자열 ('월', '화' 등)


✅ 2. 오늘이 휴무일인지 여부 판별

const rule: HolidayRule | undefined = store.holidayRule;

const isTodayHoliday =
  rule?.days?.includes(todayLabel) &&
  (
    rule.frequency === '매주' || // 매주 해당 요일
    rule.frequency === '격주' || // (격주도 같은 방식으로 적용됨)
    (
      (rule.frequency === '매월 1회' || rule.frequency === '매월 2회') &&
      rule.weeks?.includes(getWeekNumberOfMonth(today))
    )
  );
  • rule.days에 오늘 요일이 포함되어 있고
  • 주기가 매주, 격주, 또는 매월 N회 + 주차 조건 충족이면 휴무일로 판정합니다.

👉 getWeekNumberOfMonth() 함수 설명

const getWeekNumberOfMonth = (date: Date): number => {
  const first = new Date(date.getFullYear(), date.getMonth(), 1); // 그 달 1일
  const offset = first.getDay(); // 그 달 1일이 무슨 요일인지
  const adjustedDate = date.getDate() + offset;
  return Math.ceil(adjustedDate / 7); // 주차 계산
};

 

✅ 3. 오늘의 영업시간 정보와 비교

const todayHour = store.businessHours[todayLabel]; // 오늘의 영업시간 정보
  • todayHour.opening과 closing이 없거나
  • isTodayHoliday === true이면 → 휴무 처리

✅ 4. 화면 표출 (renderTodayBusinessHour())


🔹 휴무일인 경우:

<p className="text-sm"><strong>{todayLabel}요일</strong>: 오늘은 휴무입니다.</p>
<p className="text-sm font-semibold mt-1 text-red-500">영업 중이 아닙니다.</p>


🔹 영업일인 경우:
opening, closing과 현재 시간을 비교하여 영업 중인지 계산:

const isOpenNow = now >= openTime && now < closeTime;
  • 결과에 따라 메시지 표시:
<p>{todayLabel}요일: {todayHour.opening} ~ {todayHour.closing}</p>
<p className={isOpenNow ? 'text-green-600' : 'text-red-500'}>
  {isOpenNow ? '영업 중입니다' : '영업 중이 아닙니다.'}
</p>


✅ 5. 휴무일 요약 표출 (renderHolidayRule())

<p className="text-sm">
  {rule.frequency} 휴무: {rule.days.join(', ')}
  {rule.weeks && rule.weeks.length > 0 && <> (매월 {rule.weeks.join(', ')}주차)</>}
</p>


예시 출력:

매월 2회 휴무: 월, 수 (매월 1, 3주차)


✅ 요약

항목 설명
요일 판별 Date.getDay()를 활용해 오늘의 요일 (월, 화 등)을 얻음
주차 판별 getWeekNumberOfMonth()로 현재 달의 몇째 주인지 계산
휴무 판단 holidayRule.frequency + days + weeks를 조합하여 휴무 여부 판단
영업 시간 판별 오늘의 opening/closing과 현재 시간 비교
결과 출력 영업 중 / 휴무 여부를 화면에 문구와 색상으로 구분하여 출력

 

 

https://github.com/inetsos/next-tailwind-firebase-order-app

 

GitHub - inetsos/next-tailwind-firebase-order-app

Contribute to inetsos/next-tailwind-firebase-order-app development by creating an account on GitHub.

github.com