← back to jeonghoon0126__covering-spot

Function bodies 369 total

All specs Real LLM only Function bodies
handleStatusChange function · typescript · L162-L245 (84 LOC)
src/app/admin/bookings/[id]/page.tsx
  async function handleStatusChange(newStatus: string) {
    if (!booking || !token) return;
    const needsPrice =
      newStatus === "quote_confirmed" && !finalPriceInput.trim();
    if (needsPrice) {
      alert("최종 견적을 입력해주세요");
      return;
    }
    const needsTime =
      newStatus === "quote_confirmed" && !confirmedTimeInput;
    if (needsTime) {
      alert("수거 시간을 확정해주세요");
      return;
    }

    // 슬롯 충돌 경고: 견적 확정 시 시간대 가용 여부 재확인
    if (newStatus === "quote_confirmed" && confirmedTimeInput) {
      try {
        const slotRes = await fetch(`/api/slots?date=${booking.date}&excludeId=${booking.id}`);
        const slotData = await slotRes.json();
        const slotInfo = (slotData.slots || []).find(
          (s: { time: string; available: boolean }) => s.time === confirmedTimeInput
        );
        if (slotInfo && !slotInfo.available) {
          const proceed = confirm(
            "선택한 시간대가 이미 마감되었습니다. 그래도 확정하시겠습니까?"
          );
          if (!proceed) return;
     
refetchBooking function · typescript · L247-L264 (18 LOC)
src/app/admin/bookings/[id]/page.tsx
  async function refetchBooking() {
    if (!token || !id) return;
    try {
      const r = await fetch(`/api/admin/bookings/${id}`, {
        headers: { Authorization: `Bearer ${token}` },
      });
      const data = await r.json();
      if (data?.booking) {
        setBooking(data.booking);
        if (data.booking.finalPrice != null) setFinalPriceInput(String(data.booking.finalPrice));
        setAdminMemoInput(data.booking.adminMemo || "");
        if (data.booking.confirmedTime) setConfirmedTimeInput(data.booking.confirmedTime);
        if (data.booking.confirmedDuration != null) setConfirmedDurationInput(data.booking.confirmedDuration);
        if (data.booking.completionPhotos?.length) setCompletionPhotos(data.booking.completionPhotos);
        setCrewSizeInput(data.booking.crewSize ?? 1);
      }
    } catch { /* ignore */ }
  }
handleSaveCrewSize function · typescript · L266-L289 (24 LOC)
src/app/admin/bookings/[id]/page.tsx
  async function handleSaveCrewSize() {
    if (!booking || !token || crewSizeInput == null) return;
    setSaving(true);
    try {
      const res = await fetch(`/api/admin/bookings/${booking.id}`, {
        method: "PUT",
        headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
        body: JSON.stringify({ crewSize: crewSizeInput, expectedUpdatedAt: booking.updatedAt }),
      });
      const data = await res.json();
      if (res.ok) {
        setBooking(data.booking);
      } else if (res.status === 409) {
        alert("다른 탭에서 이미 수정되었습니다.");
        await refetchBooking();
      } else {
        alert(data.error || "저장 실패");
      }
    } catch {
      alert("네트워크 오류");
    } finally {
      setSaving(false);
    }
  }
handleSaveMemo function · typescript · L291-L323 (33 LOC)
src/app/admin/bookings/[id]/page.tsx
  async function handleSaveMemo() {
    if (!booking || !token) return;
    setSaving(true);
    try {
      // adminMemo만 저장 — finalPrice/confirmedTime은 견적 확정 플로우에서만 변경
      const body: Record<string, unknown> = {
        adminMemo: adminMemoInput,
        expectedUpdatedAt: booking.updatedAt,
      };
      const res = await fetch(`/api/admin/bookings/${booking.id}`, {
        method: "PUT",
        headers: {
          "Content-Type": "application/json",
          Authorization: `Bearer ${token}`,
        },
        body: JSON.stringify(body),
      });
      const data = await res.json();
      if (res.ok) {
        setBooking(data.booking);
        alert("저장되었습니다");
      } else if (res.status === 409) {
        alert("다른 탭에서 이미 수정되었습니다. 최신 데이터를 불러옵니다.");
        await refetchBooking();
      } else {
        alert(data.error || "저장 실패");
      }
    } catch {
      alert("네트워크 오류");
    } finally {
      setSaving(false);
    }
  }
loadAuditLogs function · typescript · L341-L351 (11 LOC)
src/app/admin/bookings/[id]/page.tsx
  function loadAuditLogs() {
    if (!token || !id) return;
    fetch(`/api/admin/bookings/${id}/audit`, {
      headers: { Authorization: `Bearer ${token}` },
    })
      .then((r) => r.json())
      .then((data) => {
        if (data.logs) setAuditLogs(data.logs);
      })
      .catch(() => {});
  }
formatPhone function · typescript · L40-L45 (6 LOC)
src/app/admin/bookings/new/page.tsx
function formatPhone(value: string): string {
  const digits = value.replace(/\D/g, "").slice(0, 11);
  if (digits.length <= 3) return digits;
  if (digits.length <= 7) return `${digits.slice(0, 3)}-${digits.slice(3)}`;
  return `${digits.slice(0, 3)}-${digits.slice(3, 7)}-${digits.slice(7)}`;
}
formatPrice function · typescript · L47-L49 (3 LOC)
src/app/admin/bookings/new/page.tsx
function formatPrice(n: number): string {
  return n.toLocaleString("ko-KR");
}
All rows above produced by Repobility · https://repobility.com
updateField function · typescript · L118-L123 (6 LOC)
src/app/admin/bookings/new/page.tsx
  function updateField(field: keyof FormData, value: string) {
    setForm((prev) => ({ ...prev, [field]: value }));
    if (errors[field as keyof FormErrors]) {
      setErrors((prev) => ({ ...prev, [field]: undefined }));
    }
  }
getItemQty function · typescript · L125-L127 (3 LOC)
src/app/admin/bookings/new/page.tsx
  function getItemQty(cat: string, name: string): number {
    return selectedItems.find((i) => i.category === cat && i.name === name)?.quantity ?? 0;
  }
updateItemQty function · typescript · L129-L150 (22 LOC)
src/app/admin/bookings/new/page.tsx
  function updateItemQty(
    cat: string,
    name: string,
    displayName: string,
    price: number,
    loadingCube: number,
    delta: number,
  ) {
    setSelectedItems((prev) => {
      const idx = prev.findIndex((i) => i.category === cat && i.name === name);
      if (idx >= 0) {
        const next = [...prev];
        next[idx] = { ...next[idx], quantity: next[idx].quantity + delta };
        if (next[idx].quantity <= 0) next.splice(idx, 1);
        return next;
      }
      if (delta > 0) {
        return [...prev, { category: cat, name, displayName, price, quantity: 1, loadingCube }];
      }
      return prev;
    });
  }
addCustomItem function · typescript · L152-L157 (6 LOC)
src/app/admin/bookings/new/page.tsx
  function addCustomItem() {
    const trimmed = customItemName.trim();
    if (!trimmed) return;
    updateItemQty("직접입력", trimmed, trimmed, 0, 0, 1);
    setCustomItemName("");
  }
validate function · typescript · L174-L198 (25 LOC)
src/app/admin/bookings/new/page.tsx
  function validate(): boolean {
    const next: FormErrors = {};

    if (!form.customerName.trim()) {
      next.customerName = "고객 이름을 입력해주세요";
    }

    const phoneDigits = form.phone.replace(/\D/g, "");
    if (!phoneDigits) {
      next.phone = "전화번호를 입력해주세요";
    } else if (phoneDigits.length < 10 || phoneDigits.length > 11) {
      next.phone = "올바른 전화번호를 입력해주세요";
    }

    if (!form.address.trim()) {
      next.address = "주소를 입력해주세요";
    } else if (!form.area) {
      if (!confirm("서비스 지역을 감지하지 못했습니다.\n지역 없이 예약을 생성하시겠습니까?")) {
        return false;
      }
    }

    setErrors(next);
    return Object.keys(next).length === 0;
  }
handleSubmit function · typescript · L200-L250 (51 LOC)
src/app/admin/bookings/new/page.tsx
  async function handleSubmit(e: React.FormEvent) {
    e.preventDefault();
    if (!validate()) return;

    setSubmitting(true);
    try {
      const priceNum = priceOverride
        ? Number(priceOverride.replace(/\D/g, ""))
        : itemsTotal;

      const res = await fetch("/api/admin/bookings", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          Authorization: `Bearer ${token}`,
        },
        body: JSON.stringify({
          customerName: form.customerName.trim(),
          phone: formatPhone(form.phone),
          address: form.address.trim(),
          addressDetail: form.addressDetail.trim(),
          area: form.area,
          items: selectedItems,
          estimatedPrice: priceNum,
          date: form.date,
          timeSlot: form.timeSlot,
          memo: form.memo.trim(),
          source: form.source,
          hasGroundAccess: form.hasGroundAccess,
        }),
      });

      if (res.status === 401) {
        
formatDate function · typescript · L93-L97 (5 LOC)
src/app/admin/calendar/page.tsx
function formatDate(dateStr: string): string {
  const d = new Date(dateStr + "T00:00:00");
  const weekdays = ["일", "월", "화", "수", "목", "금", "토"];
  return `${d.getFullYear()}. ${d.getMonth() + 1}. ${d.getDate()}. (${weekdays[d.getDay()]})`;
}
getToday function · typescript · L99-L101 (3 LOC)
src/app/admin/calendar/page.tsx
function getToday(): string {
  return new Date().toLocaleDateString("sv-SE", { timeZone: "Asia/Seoul" });
}
Repobility · severity-and-effort ranking · https://repobility.com
addDays function · typescript · L103-L110 (8 LOC)
src/app/admin/calendar/page.tsx
function addDays(dateStr: string, days: number): string {
  const d = new Date(dateStr + "T00:00:00");
  d.setDate(d.getDate() + days);
  const y = d.getFullYear();
  const m = String(d.getMonth() + 1).padStart(2, "0");
  const day = String(d.getDate()).padStart(2, "0");
  return `${y}-${m}-${day}`;
}
getWeekStart function · typescript · L112-L121 (10 LOC)
src/app/admin/calendar/page.tsx
function getWeekStart(dateStr: string): string {
  const d = new Date(dateStr + "T00:00:00");
  const dayOfWeek = d.getDay(); // 0=일, 1=월, ...
  const diff = dayOfWeek === 0 ? -6 : 1 - dayOfWeek; // 월요일로 이동
  d.setDate(d.getDate() + diff);
  const y = d.getFullYear();
  const m = String(d.getMonth() + 1).padStart(2, "0");
  const dd = String(d.getDate()).padStart(2, "0");
  return `${y}-${m}-${dd}`;
}
getWeekDays function · typescript · L123-L125 (3 LOC)
src/app/admin/calendar/page.tsx
function getWeekDays(mondayStr: string): string[] {
  return Array.from({ length: 7 }, (_, i) => addDays(mondayStr, i));
}
formatShortDate function · typescript · L127-L130 (4 LOC)
src/app/admin/calendar/page.tsx
function formatShortDate(dateStr: string): string {
  const d = new Date(dateStr + "T00:00:00");
  return `${d.getMonth() + 1}/${d.getDate()}`;
}
timeToHours function · typescript · L135-L138 (4 LOC)
src/app/admin/calendar/page.tsx
function timeToHours(timeStr: string): number {
  const [h, m] = timeStr.split(":").map(Number);
  return h + m / 60;
}
timeToPercent function · typescript · L141-L144 (4 LOC)
src/app/admin/calendar/page.tsx
function timeToPercent(timeStr: string): number {
  const hours = timeToHours(timeStr);
  return ((hours - GANTT_START_HOUR) / GANTT_HOURS) * 100;
}
pixelOffsetToTime function · typescript · L147-L154 (8 LOC)
src/app/admin/calendar/page.tsx
function pixelOffsetToTime(offsetPx: number, totalWidth: number): string {
  const ratio = Math.max(0, Math.min(1, offsetPx / totalWidth));
  const hours = GANTT_START_HOUR + ratio * GANTT_HOURS;
  const h = Math.floor(hours);
  // 30분 단위로 반올림
  const m = hours - h >= 0.5 ? 30 : 0;
  return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}`;
}
GanttBlock function · typescript · L165-L248 (84 LOC)
src/app/admin/calendar/page.tsx
function GanttBlock({ booking, isUnloading = false, onDragStart, onClick }: GanttBlockProps) {
  const time = booking.confirmedTime || booking.timeSlot;
  if (!time) return null;

  const leftPercent = timeToPercent(time);
  const duration = booking.confirmedDuration ?? 1; // 기본 1시간
  const widthPercent = (duration / GANTT_HOURS) * 100;

  // 범위 벗어나는 블록 클리핑
  if (leftPercent >= 100 || leftPercent < 0) return null;

  const clampedWidth = Math.min(widthPercent, 100 - leftPercent);
  const address = booking.address?.slice(0, 20) ?? "";
  const cube = booking.totalLoadingCube ?? 0;

  if (isUnloading) {
    return (
      <div
        style={{
          position: "absolute",
          left: `${leftPercent}%`,
          width: `${Math.max(clampedWidth, 3)}%`,
          top: "4px",
          bottom: "4px",
          backgroundColor: "#ECEFF1",
          borderLeft: "3px solid #90A4AE",
          borderRadius: "4px",
          zIndex: 1,
          display: "flex",
          alignItems: "cent
Repobility · code-quality intelligence · https://repobility.com
GanttView function · typescript · L260-L579 (320 LOC)
src/app/admin/calendar/page.tsx
function GanttView({ drivers, bookings, token, onBookingUpdated, onBookingClick }: GanttViewProps) {
  const gridRef = useRef<HTMLDivElement>(null);
  const [dragState, setDragState] = useState<DragState | null>(null);
  const [dropTarget, setDropTarget] = useState<{ driverId: string | null; time: string } | null>(null);
  const [updating, setUpdating] = useState<string | null>(null); // 업데이트 중인 bookingId

  // 기사별 예약 그룹핑
  const driverBookingsMap = useMemo(() => {
    const map: Record<string, Booking[]> = {};
    // 기사 행
    for (const d of drivers) {
      map[d.id] = bookings.filter((b) => b.driverId === d.id);
    }
    // 미배차 행
    map["__unassigned__"] = bookings.filter((b) => !b.driverId);
    return map;
  }, [drivers, bookings]);

  // unloadingStopAfter를 가진 예약 다음에 하차지 블록 삽입용 맵
  const unloadingTargetIds = useMemo(() => {
    const set = new Set<string>();
    for (const b of bookings) {
      if (b.unloadingStopAfter) set.add(b.id);
    }
    return set;
  }, [bookings]);

 
handleTabChange function · typescript · L199-L203 (5 LOC)
src/app/admin/dashboard/page.tsx
  function handleTabChange(tabKey: string) {
    setActiveTab(tabKey);
    setCurrentPage(1);
    setSelectedBookings(new Set());
  }
handleDateChange function · typescript · L206-L210 (5 LOC)
src/app/admin/dashboard/page.tsx
  function handleDateChange(field: "from" | "to", val: string) {
    if (field === "from") setDateFrom(val);
    else setDateTo(val);
    setCurrentPage(1);
  }
requestQuickAction function · typescript · L212-L214 (3 LOC)
src/app/admin/dashboard/page.tsx
  function requestQuickAction(booking: Booking, newStatus: string, label: string) {
    setConfirmPending({ bookingId: booking.id, newStatus, label });
  }
executeQuickAction function · typescript · L216-L241 (26 LOC)
src/app/admin/dashboard/page.tsx
  async function executeQuickAction(booking: Booking) {
    if (!confirmPending) return;
    const { newStatus } = confirmPending;
    setConfirmPending(null);
    setQuickLoading(booking.id);
    try {
      const res = await fetch(`/api/admin/bookings/${booking.id}`, {
        method: "PUT",
        headers: {
          "Content-Type": "application/json",
          Authorization: `Bearer ${token}`,
        },
        body: JSON.stringify({ status: newStatus, expectedUpdatedAt: booking.updatedAt }),
      });
      if (res.ok) {
        fetchBookings();
      } else {
        const data = await res.json();
        showToast(data.error || "변경 실패", true);
      }
    } catch {
      showToast("네트워크 오류", true);
    } finally {
      setQuickLoading(null);
    }
  }
toggleBooking function · typescript · L243-L250 (8 LOC)
src/app/admin/dashboard/page.tsx
  function toggleBooking(id: string) {
    setSelectedBookings((prev) => {
      const next = new Set(prev);
      if (next.has(id)) next.delete(id);
      else next.add(id);
      return next;
    });
  }
toggleAll function · typescript · L252-L258 (7 LOC)
src/app/admin/dashboard/page.tsx
  function toggleAll() {
    if (selectedBookings.size === bookings.length && bookings.length > 0) {
      setSelectedBookings(new Set());
    } else {
      setSelectedBookings(new Set(bookings.map((b) => b.id)));
    }
  }
executeBulkStatusChange function · typescript · L260-L293 (34 LOC)
src/app/admin/dashboard/page.tsx
  async function executeBulkStatusChange() {
    if (!bulkStatus || selectedBookings.size === 0) return;
    setBulkConfirmPending(false);
    setBulkLoading(true);
    let successCount = 0;
    let failCount = 0;

    for (const bookingId of selectedBookings) {
      try {
        const bk = bookings.find((b) => b.id === bookingId);
        const res = await fetch(`/api/admin/bookings/${bookingId}`, {
          method: "PUT",
          headers: {
            "Content-Type": "application/json",
            Authorization: `Bearer ${token}`,
          },
          body: JSON.stringify({ status: bulkStatus, expectedUpdatedAt: bk?.updatedAt }),
        });
        if (res.ok) successCount++;
        else failCount++;
      } catch {
        failCount++;
      }
    }

    setBulkLoading(false);
    setSelectedBookings(new Set());
    setBulkStatus("");
    fetchBookings();

    if (failCount > 0) {
      showToast(`${successCount}건 성공, ${failCount}건 실패`, true);
    }
  }
Provenance: Repobility (https://repobility.com) — every score reproducible from /scan/
exportCSV function · typescript · L296-L341 (46 LOC)
src/app/admin/dashboard/page.tsx
  async function exportCSV() {
    try {
      const params = new URLSearchParams();
      if (activeTab !== "all") params.set("status", activeTab);
      if (debouncedSearch) params.set("search", debouncedSearch);
      if (dateFrom) params.set("dateFrom", dateFrom);
      if (dateTo) params.set("dateTo", dateTo);
      params.set("limit", "1000");

      const res = await fetch(`/api/admin/bookings?${params.toString()}`, {
        headers: { Authorization: `Bearer ${token}` },
      });
      const data = await res.json();
      const rows: Booking[] = data.bookings || [];

      const headers = ["날짜", "시간", "고객명", "전화번호", "지역", "주소", "인원", "사다리", "사다리금액", "품목수", "예상금액", "확정금액", "기사", "상태"];
      const csvRows = rows.map((b) => [
        b.date,
        b.confirmedTime || b.timeSlot,
        b.customerName,
        b.phone,
        b.area,
        `${b.address} ${b.addressDetail || ""}`.trim(),
        String(b.crewSize),
        b.needLadder ? "필요" : "",
        b.needLadder && b.l
handleSheetPreview function · typescript · L343-L361 (19 LOC)
src/app/admin/dashboard/page.tsx
  async function handleSheetPreview() {
    if (!sheetURL.trim()) return;
    setSheetLoading(true);
    try {
      const res = await fetch("/api/admin/bookings/sheet-import", {
        method: "POST",
        headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
        body: JSON.stringify({ url: sheetURL, dryRun: true }),
      });
      const data = await res.json();
      if (!res.ok) { showToast(data.error || "미리보기 실패", true); return; }
      setSheetRows(data.rows);
      setSheetStep("preview");
    } catch {
      showToast("네트워크 오류", true);
    } finally {
      setSheetLoading(false);
    }
  }
handleSheetImport function · typescript · L363-L381 (19 LOC)
src/app/admin/dashboard/page.tsx
  async function handleSheetImport() {
    setSheetLoading(true);
    try {
      const res = await fetch("/api/admin/bookings/sheet-import", {
        method: "POST",
        headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
        body: JSON.stringify({ url: sheetURL, dryRun: false }),
      });
      const data = await res.json();
      if (!res.ok) { showToast(data.error || "임포트 실패", true); return; }
      setSheetResult({ succeeded: data.succeeded, failed: data.failed, skipped: data.skipped });
      setSheetStep("done");
      fetchBookings();
    } catch {
      showToast("네트워크 오류", true);
    } finally {
      setSheetLoading(false);
    }
  }
closeSheetImport function · typescript · L383-L389 (7 LOC)
src/app/admin/dashboard/page.tsx
  function closeSheetImport() {
    setShowSheetImport(false);
    setSheetURL("");
    setSheetStep("input");
    setSheetRows([]);
    setSheetResult(null);
  }
calcServiceMins function · typescript · L93-L95 (3 LOC)
src/app/admin/dispatch/page.tsx
function calcServiceMins(totalLoadingCube: number | undefined): number {
  return Math.max(1, BASE_SERVICE_MINS + Math.round((totalLoadingCube || 0) * CUBE_MINS_PER_M3));
}
calcTravelMins function · typescript · L98-L101 (4 LOC)
src/app/admin/dispatch/page.tsx
function calcTravelMins(lat1: number, lng1: number, lat2: number, lng2: number): number {
  const km = haversine(lat1, lng1, lat2, lng2) * ROUTE_ROAD_FACTOR;
  return Math.max(1, Math.round(km / ROUTE_AVG_SPEED_KMH * 60));
}
getDriverColor function · typescript · L107-L110 (4 LOC)
src/app/admin/dispatch/page.tsx
function getDriverColor(idx: number): string {
  const hue = Math.round((idx * 137.508) % 360);
  return `hsl(${hue}, 65%, 50%)`;
}
getToday function · typescript · L114-L116 (3 LOC)
src/app/admin/dispatch/page.tsx
function getToday(): string {
  return new Date().toLocaleDateString("sv-SE", { timeZone: "Asia/Seoul" });
}
All rows above produced by Repobility · https://repobility.com
formatDateShort function · typescript · L118-L124 (7 LOC)
src/app/admin/dispatch/page.tsx
function formatDateShort(dateStr: string): string {
  // KST 기준 명시적 파싱 (서버/클라이언트 시간대 차이 방어)
  const [y, m, d] = dateStr.split("-").map(Number);
  const weekdays = ["일", "월", "화", "수", "목", "금", "토"];
  const date = new Date(y, m - 1, d);
  return `${m}/${d} (${weekdays[date.getDay()]})`;
}
getLoadingPercent function · typescript · L126-L129 (4 LOC)
src/app/admin/dispatch/page.tsx
function getLoadingPercent(used: number, capacity: number): number {
  if (capacity <= 0) return 0;
  return Math.min(100, Math.round((used / capacity) * 100));
}
itemsSummary function · typescript · L131-L137 (7 LOC)
src/app/admin/dispatch/page.tsx
function itemsSummary(items: BookingItem[] | undefined | null): string {
  if (!Array.isArray(items) || items.length === 0) return "-";
  const first = items[0];
  if (!first) return "-";
  const label = `${first.category || ""} ${first.name || ""}`.trim() || "품목";
  return items.length > 1 ? `${label} 외 ${items.length - 1}종` : label;
}
showToast function · typescript · L184-L188 (5 LOC)
src/app/admin/dispatch/page.tsx
  function showToast(msg: string, type: "success" | "error" | "warning" = "error") {
    if (toastTimerRef.current) clearTimeout(toastTimerRef.current);
    setToast({ msg, type });
    toastTimerRef.current = setTimeout(() => setToast(null), 3500);
  }
toggleDriverSlot function · typescript · L494-L503 (10 LOC)
src/app/admin/dispatch/page.tsx
  function toggleDriverSlot(driverId: string, slot: string) {
    setDriverSlotFilters((prev) => {
      const current = prev[driverId] ?? [...SLOT_ORDER];
      const next = current.includes(slot)
        ? current.filter((s) => s !== slot)
        : [...current, slot];
      // 전체 허용 = 필터 없음으로 정규화
      return { ...prev, [driverId]: next.length === SLOT_ORDER.length ? [] : next };
    });
  }
toggleCheck function · typescript · L692-L699 (8 LOC)
src/app/admin/dispatch/page.tsx
  function toggleCheck(id: string) {
    setCheckedIds((prev) => {
      const next = new Set(prev);
      if (next.has(id)) next.delete(id);
      else next.add(id);
      return next;
    });
  }
toggleAllUnassigned function · typescript · L702-L710 (9 LOC)
src/app/admin/dispatch/page.tsx
  function toggleAllUnassigned() {
    const unassigned = filteredBookings.filter((b) => !b.driverId);
    const allChecked = unassigned.length > 0 && unassigned.every((b) => checkedIds.has(b.id));
    if (allChecked) {
      setCheckedIds(new Set());
    } else {
      setCheckedIds(new Set(unassigned.map((b) => b.id)));
    }
  }
scrollToNextUnassigned function · typescript · L714-L722 (9 LOC)
src/app/admin/dispatch/page.tsx
  function scrollToNextUnassigned(excludeIds: string[]) {
    const next = filteredBookingsRef.current.find((b) => !b.driverId && !excludeIds.includes(b.id ?? ""));
    if (next?.id) {
      const targetId = next.id;
      setTimeout(() => {
        cardRefs.current.get(targetId)?.scrollIntoView({ behavior: "smooth", block: "nearest" });
      }, 50);
    }
  }
Repobility · severity-and-effort ranking · https://repobility.com
handleDispatch function · typescript · L724-L781 (58 LOC)
src/app/admin/dispatch/page.tsx
  async function handleDispatch(bookingId: string, driverId: string) {
    const driver = drivers.find((d) => d.id === driverId);
    if (!driver || !token || dispatching) return;

    // 배차 직전에 다음 미배차 계산 (옵티미스틱 업데이트 전)
    scrollToNextUnassigned([bookingId]);

    // 옵티미스틱: 로컬 상태 즉시 반영
    const prevBooking = bookings.find((b) => b.id === bookingId);
    setBookings((prev) =>
      prev.map((b) =>
        b.id === bookingId ? { ...b, driverId: driver.id, driverName: driver.name } : b,
      ),
    );
    setDriverStats((prev) =>
      prev.map((stat) => {
        const cube = prevBooking?.totalLoadingCube || 0;
        if (stat.driverId === driver.id) {
          return { ...stat, assignedCount: stat.assignedCount + 1, totalLoadingCube: stat.totalLoadingCube + cube };
        }
        if (prevBooking?.driverId && stat.driverId === prevBooking.driverId) {
          return { ...stat, assignedCount: Math.max(0, stat.assignedCount - 1), totalLoadingCube: Math.max(0, stat.totalLoadingCube
handleBatchDispatch function · typescript · L784-L864 (81 LOC)
src/app/admin/dispatch/page.tsx
  async function handleBatchDispatch() {
    const driver = drivers.find((d) => d.id === batchDriverId);
    if (!driver || !token || checkedIds.size === 0 || dispatching) return;

    const targetIds = Array.from(checkedIds).filter((id) =>
      filteredBookings.some((b) => b.id === id),
    );

    // 배차 직전에 다음 미배차 계산
    scrollToNextUnassigned(targetIds);

    // 옵티미스틱: 로컬 상태 즉시 반영 (재배차 시 기존 기사 차감 포함)
    const targetBookings = targetIds.map((id) => bookings.find((bk) => bk.id === id)).filter(Boolean) as Booking[];
    const prevDriverDeltas = new Map<string, { count: number; cube: number }>();
    // 신규 기사로 실제 이동하는 건만 카운트 (이미 해당 기사 담당인 건 제외 → 중복 카운트 방지)
    const newToDriverBookings = targetBookings.filter((b) => b.driverId !== driver.id);
    const newToDriverCount = newToDriverBookings.length;
    const newToDriverCube = newToDriverBookings.reduce((sum, b) => sum + (b.totalLoadingCube || 0), 0);
    targetBookings.forEach((b) => {
      const cube = b.totalLoadingCube || 0;
     
handleUnassign function · typescript · L867-L911 (45 LOC)
src/app/admin/dispatch/page.tsx
  async function handleUnassign(bookingId: string) {
    if (!token || dispatching) return;

    // 옵티미스틱: 로컬 상태 즉시 반영
    const target = bookings.find((b) => b.id === bookingId);
    setBookings((prev) =>
      prev.map((b) =>
        b.id === bookingId ? { ...b, driverId: null, driverName: null } : b,
      ),
    );
    if (target?.driverId) {
      setDriverStats((prev) =>
        prev.map((stat) =>
          stat.driverId === target.driverId
            ? { ...stat, assignedCount: Math.max(0, stat.assignedCount - 1), totalLoadingCube: Math.max(0, stat.totalLoadingCube - (target.totalLoadingCube || 0)) }
            : stat,
        ),
      );
    }

    setDispatching(true);
    try {
      const res = await fetch(`/api/admin/bookings/${bookingId}`, {
        method: "PUT",
        headers: {
          "Content-Type": "application/json",
          Authorization: `Bearer ${token}`,
        },
        body: JSON.stringify({ driverId: null, driverName: null }),
      });
      if (re
page 1 / 8next ›