← back to jeonghoon0126__covering-spot

Function bodies 369 total

All specs Real LLM only Function bodies
OptionButton function · typescript · L14-L66 (53 LOC)
src/components/ui/OptionButton.tsx
  function OptionButton(
    {
      selected = false,
      disabled = false,
      description,
      children,
      className = "",
      ...rest
    },
    ref,
  ) {
    const stateClass = disabled
      ? "bg-bg-warm2 border-disable-alt text-disable-strong cursor-not-allowed"
      : selected
        ? "bg-brand-50 border-brand-400 text-brand-700"
        : "bg-white border-border text-text-primary hover:border-brand-300";

    const classes = [
      "flex flex-col items-start gap-1 w-full",
      "p-4 border rounded-md",
      "transition-all duration-200",
      "focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-400",
      "cursor-pointer",
      stateClass,
      className,
    ]
      .filter(Boolean)
      .join(" ");

    return (
      <button
        ref={ref}
        type="button"
        disabled={disabled}
        role="option"
        aria-selected={selected}
        className={classes}
        {...rest}
      >
        <span classN
Radio function · typescript · L14-L79 (66 LOC)
src/components/ui/Radio.tsx
  function Radio(
    { checked = false, disabled = false, error = false, label, className = "", ...rest },
    ref,
  ) {
    /* Outer circle color */
    const circleColor = (() => {
      if (disabled) return "border-disable-normal bg-disable-assistive";
      if (error) return "border-semantic-red";
      if (checked) return "border-brand-400";
      return "border-border-strong";
    })();

    return (
      <label
        className={[
          "inline-flex items-center gap-2",
          disabled ? "cursor-not-allowed" : "cursor-pointer",
          className,
        ]
          .filter(Boolean)
          .join(" ")}
      >
        {/* Hidden native radio */}
        <input
          ref={ref}
          type="radio"
          checked={checked}
          disabled={disabled}
          className="sr-only peer"
          {...rest}
        />

        {/* Custom radio circle */}
        <span
          className={[
            "relative flex shrink-0 items-center justify-center",
  
ScrollReveal function · typescript · L12-L26 (15 LOC)
src/components/ui/ScrollReveal.tsx
export function ScrollReveal({ children, delay = 0, className = "" }: Props) {
  const { ref, visible } = useScrollReveal();

  return (
    <div
      ref={ref}
      className={`transition-all duration-700 ease-[cubic-bezier(0.16,1,0.3,1)] ${
        visible ? "opacity-100 translate-y-0" : "opacity-0 translate-y-8"
      } ${className}`}
      style={{ transitionDelay: `${delay}s` }}
    >
      {children}
    </div>
  );
}
SectionHeader function · typescript · L8-L24 (17 LOC)
src/components/ui/SectionHeader.tsx
export function SectionHeader({ tag, title, desc, center }: Props) {
  return (
    <div className={`mb-16 ${center ? "text-center" : ""}`}>
      {tag && (
        <span className="inline-flex items-center h-7 px-3 rounded-full bg-primary-tint text-primary text-[13px] font-semibold mb-4">
          {tag}
        </span>
      )}
      <h2 className="text-[40px] font-extrabold tracking-[-1.5px] mb-3.5 leading-[1.15] max-lg:text-[32px] max-md:text-[28px]">
        {title}
      </h2>
      {desc && (
        <p className="text-[17px] text-text-sub leading-relaxed">{desc}</p>
      )}
    </div>
  );
}
SegmentedField function · typescript · L21-L69 (49 LOC)
src/components/ui/SegmentedField.tsx
  function SegmentedField(
    { options, value, onChange, disabled = false, className = "", ...rest },
    ref,
  ) {
    return (
      <div
        ref={ref}
        className={[
          "inline-flex gap-1 rounded-md bg-fill-tint p-1",
          disabled ? "opacity-60 pointer-events-none" : "",
          className,
        ]
          .filter(Boolean)
          .join(" ")}
        role="radiogroup"
        {...rest}
      >
        {options.map((option) => {
          const isActive = option.value === value;

          return (
            <button
              key={option.value}
              type="button"
              role="radio"
              aria-checked={isActive}
              disabled={disabled}
              onClick={() => onChange(option.value)}
              className={[
                "flex-1 flex items-center justify-center",
                "h-9 rounded-sm px-3",
                "text-sm leading-[22px]",
                "transition-all duration-200",
              
TextArea function · typescript · L16-L107 (92 LOC)
src/components/ui/TextArea.tsx
  function TextArea(
    {
      label,
      error = false,
      helperText,
      disabled = false,
      rows = 4,
      maxLength,
      value,
      className = "",
      ...rest
    },
    ref,
  ) {
    const [focused, setFocused] = useState(false);

    /* Border color by state */
    const borderColor = (() => {
      if (disabled) return "border-disable-alt";
      if (error) return "border-semantic-red";
      if (focused) return "border-brand-400 ring-1 ring-brand-400";
      return "border-border";
    })();

    /* Textarea classes */
    const textareaClasses = [
      "w-full rounded-md px-4 py-3 text-base leading-6",
      "outline-none transition-all duration-200 resize-y",
      "placeholder:text-text-muted",
      "border",
      borderColor,
      disabled
        ? "bg-disable-assistive text-disable-strong cursor-not-allowed"
        : "bg-white text-text-primary",
      className,
    ]
      .filter(Boolean)
      .join(" ");

    /* Character count */
    cons
TextField function · typescript · L25-L120 (96 LOC)
src/components/ui/TextField.tsx
  function TextField(
    {
      label,
      error = false,
      helperText,
      disabled = false,
      size = "md",
      maxLength,
      value,
      className = "",
      required,
      ...rest
    },
    ref,
  ) {
    const [focused, setFocused] = useState(false);

    /* Border color by state */
    const borderColor = (() => {
      if (disabled) return "border-disable-alt";
      if (error) return "border-semantic-red";
      if (focused) return "border-brand-400 ring-1 ring-brand-400";
      return "border-border";
    })();

    /* Input classes */
    const inputClasses = [
      "w-full rounded-md px-4 text-base leading-6",
      "outline-none transition-all duration-200",
      "placeholder:text-text-muted",
      sizeStyles[size],
      borderColor,
      "border",
      disabled
        ? "bg-disable-assistive text-disable-strong cursor-not-allowed"
        : "bg-white text-text-primary",
      className,
    ]
      .filter(Boolean)
      .join(" ");

    /* Cha
Repobility analyzer · published findings · https://repobility.com
getActiveExperiment function · typescript · L31-L33 (3 LOC)
src/config/experiments.ts
export function getActiveExperiment(): Experiment | null {
  return EXPERIMENTS.find((e) => e.enabled) || null;
}
getActiveExperiments function · typescript · L36-L38 (3 LOC)
src/config/experiments.ts
export function getActiveExperiments(): Experiment[] {
  return EXPERIMENTS.filter((e) => e.enabled);
}
assignVariant function · typescript · L41-L49 (9 LOC)
src/config/experiments.ts
export function assignVariant(exp: Experiment): string {
  const rand = Math.random() * 100;
  let cumulative = 0;
  for (let i = 0; i < exp.variants.length; i++) {
    cumulative += exp.weights[i];
    if (rand < cumulative) return exp.variants[i];
  }
  return exp.variants[0];
}
ExperimentProvider function · typescript · L28-L59 (32 LOC)
src/contexts/ExperimentContext.tsx
export function ExperimentProvider({ children }: { children: React.ReactNode }) {
  const [experiments, setExperiments] = useState<ExperimentMap>(new Map());

  useEffect(() => {
    const activeExps = getActiveExperiments();
    if (activeExps.length === 0) return;

    const map = new Map<string, string>();
    for (const exp of activeExps) {
      const variant = Cookies.get(`ab_${exp.name}`);
      if (variant) {
        map.set(exp.name, variant);
      }
    }
    setExperiments(map);
  }, []);

  // 하위 호환: 첫 번째 실험 정보
  const firstEntry = experiments.entries().next().value;
  const experimentName = firstEntry ? firstEntry[0] : null;
  const variant = firstEntry ? firstEntry[1] : null;

  const getVariant = (name: string): string | null => {
    return experiments.get(name) || null;
  };

  return (
    <ExperimentContext.Provider value={{ experimentName, variant, experiments, getVariant }}>
      {children}
    </ExperimentContext.Provider>
  );
}
useExperiment function · typescript · L61-L63 (3 LOC)
src/contexts/ExperimentContext.tsx
export function useExperiment() {
  return useContext(ExperimentContext);
}
detectAreaFromAddress function · typescript · L16-L44 (29 LOC)
src/data/spot-areas.ts
export function detectAreaFromAddress(
  sigungu: string,
  sido: string,
): SpotArea | null {
  // 1. 서울 25개 구: sigungu 직접 매칭
  const directMatch = SPOT_AREAS.find((a) => a.name === sigungu);
  if (directMatch) return directMatch;

  // 2. 경기도 시 매칭: "김포시" → "김포", "고양시 덕양구" → "고양"
  if (sido.startsWith("경기")) {
    const cityName = sigungu.split("시")[0]; // "고양시 덕양구" → "고양"
    const cityMatch = SPOT_AREAS.find((a) => a.name === cityName);
    if (cityMatch) return cityMatch;
  }

  // 3. 인천광역시 → "인천"
  if (sido.startsWith("인천")) {
    return SPOT_AREAS.find((a) => a.name === "인천") || null;
  }

  // 4. 충청남도 시 매칭: "천안시 동남구" → "천안", "아산시" → "아산"
  if (sido.startsWith("충청남")) {
    const cityName = sigungu.split("시")[0];
    const cityMatch = SPOT_AREAS.find((a) => a.name === cityName);
    if (cityMatch) return cityMatch;
  }

  return null;
}
useCarousel function · typescript · L11-L193 (183 LOC)
src/hooks/useCarousel.ts
export function useCarousel({
  totalItems,
  cardWidth = 320,
  autoplayInterval = 3500,
}: UseCarouselOptions) {
  const trackRef = useRef<HTMLDivElement>(null);
  const wrapperRef = useRef<HTMLDivElement>(null);
  const autoplayRef = useRef<ReturnType<typeof setInterval> | null>(null);
  const [currentPage, setCurrentPage] = useState(0);
  const [totalPages, setTotalPages] = useState(1);

  // Calculate pages
  useEffect(() => {
    const wrapper = wrapperRef.current;
    if (!wrapper) return;
    const visibleCards = Math.max(Math.floor(wrapper.offsetWidth / cardWidth), 1);
    setTotalPages(Math.ceil(totalItems / visibleCards));
  }, [totalItems, cardWidth]);

  const goTo = useCallback(
    (page: number) => {
      const track = trackRef.current;
      const wrapper = wrapperRef.current;
      if (!track || !wrapper) return;

      const clamped = Math.max(0, Math.min(page, totalPages - 1));
      setCurrentPage(clamped);

      const visibleCards = Math.max(
        Math.floor(
getCurrentOffset function · typescript · L106-L109 (4 LOC)
src/hooks/useCarousel.ts
    function getCurrentOffset(): number {
      const m = track!.style.transform.match(/translateX\((-?[\d.]+)px\)/);
      return m ? parseFloat(m[1]) : 0;
    }
Repobility (the analyzer behind this table) · https://repobility.com
onMouseDown function · typescript · L111-L118 (8 LOC)
src/hooks/useCarousel.ts
    function onMouseDown(e: MouseEvent) {
      isDragging = true;
      startX = e.pageX;
      startOffset = getCurrentOffset();
      track!.style.transition = "none";
      stopAutoplay();
      e.preventDefault();
    }
onMouseMove function · typescript · L120-L126 (7 LOC)
src/hooks/useCarousel.ts
    function onMouseMove(e: MouseEvent) {
      if (!isDragging) return;
      const dx = e.pageX - startX;
      const maxOff = track!.scrollWidth - wrapper!.offsetWidth;
      const newOff = Math.max(-maxOff - 40, Math.min(40, startOffset + dx));
      track!.style.transform = `translateX(${newOff}px)`;
    }
onMouseUp function · typescript · L128-L139 (12 LOC)
src/hooks/useCarousel.ts
    function onMouseUp(e: MouseEvent) {
      if (!isDragging) return;
      isDragging = false;
      const dx = e.pageX - startX;
      if (Math.abs(dx) > 50) {
        if (dx < 0) goTo(currentPage + 1);
        else goTo(currentPage - 1);
      } else {
        goTo(currentPage);
      }
      startAutoplay();
    }
onTouchStart function · typescript · L159-L162 (4 LOC)
src/hooks/useCarousel.ts
    function onTouchStart(e: TouchEvent) {
      touchStartX = e.touches[0].pageX;
      stopAutoplay();
    }
onTouchEnd function · typescript · L164-L171 (8 LOC)
src/hooks/useCarousel.ts
    function onTouchEnd(e: TouchEvent) {
      const diff = e.changedTouches[0].pageX - touchStartX;
      if (Math.abs(diff) > 60) {
        if (diff < 0) goTo(currentPage + 1);
        else goTo(currentPage - 1);
      }
      startAutoplay();
    }
usePWAInstall function · typescript · L10-L53 (44 LOC)
src/hooks/usePWAInstall.ts
export function usePWAInstall() {
  const [deferredPrompt, setDeferredPrompt] =
    useState<BeforeInstallPromptEvent | null>(null);
  const [isMobile, setIsMobile] = useState(false);

  useEffect(() => {
    // SW 등록
    if ("serviceWorker" in navigator) {
      navigator.serviceWorker.register("/sw.js").catch(() => {});
    }

    // 모바일 판별 (UA + 터치 + 화면폭)
    const mobile =
      /Android|iPhone|iPad|iPod/i.test(navigator.userAgent) ||
      ("ontouchstart" in window && window.innerWidth < 768);
    setIsMobile(mobile);

    // 설치 프롬프트 캐치
    const handler = (e: Event) => {
      e.preventDefault();
      setDeferredPrompt(e as BeforeInstallPromptEvent);
    };
    window.addEventListener("beforeinstallprompt", handler);

    return () => window.removeEventListener("beforeinstallprompt", handler);
  }, []);

  const install = useCallback(async () => {
    if (!deferredPrompt) return false;
    deferredPrompt.prompt();
    const { outcome } = await deferredPrompt.userChoice;
    if (
useScrollPosition function · typescript · L5-L25 (21 LOC)
src/hooks/useScrollPosition.ts
export function useScrollPosition() {
  const [scrollY, setScrollY] = useState(0);

  useEffect(() => {
    let ticking = false;

    const handle = () => {
      if (ticking) return;
      ticking = true;
      requestAnimationFrame(() => {
        setScrollY(window.scrollY);
        ticking = false;
      });
    };

    window.addEventListener("scroll", handle, { passive: true });
    return () => window.removeEventListener("scroll", handle);
  }, []);

  return scrollY;
}
useScrollReveal function · typescript · L5-L35 (31 LOC)
src/hooks/useScrollReveal.ts
export function useScrollReveal(threshold = 0.1, initialVisible = false) {
  const ref = useRef<HTMLDivElement>(null);
  const [visible, setVisible] = useState(initialVisible);

  useEffect(() => {
    if (initialVisible) return;

    const el = ref.current;
    if (!el) return;

    if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
      setVisible(true);
      return;
    }

    const observer = new IntersectionObserver(
      ([entry]) => {
        if (entry.isIntersecting) {
          setVisible(true);
          observer.unobserve(el);
        }
      },
      { threshold, rootMargin: "0px 0px -40px 0px" }
    );

    observer.observe(el);
    return () => observer.disconnect();
  }, [threshold, initialVisible]);

  return { ref, visible };
}
All rows scored by the Repobility analyzer (https://repobility.com)
hasPermission function · typescript · L15-L17 (3 LOC)
src/lib/admin-roles.ts
export function hasPermission(role: AdminRole, action: string): boolean {
  return ROLE_PERMISSIONS[role]?.includes(action) || role === "admin";
}
getExperimentVariant function · typescript · L53-L61 (9 LOC)
src/lib/analytics.ts
function getExperimentVariant(): Record<string, string> {
  if (typeof document === "undefined") return {};
  const result: Record<string, string> = {};
  const matches = document.cookie.matchAll(/ab_([^=]+)=([^;]+)/g);
  for (const match of matches) {
    result[`experiment_${match[1]}`] = match[2];
  }
  return result;
}
track function · typescript · L63-L90 (28 LOC)
src/lib/analytics.ts
export function track<T extends EventName>(
  event: T,
  properties?: T extends keyof EventProps ? EventProps[T] : never
) {
  if (typeof window === "undefined") return;

  const props = {
    ...properties,
    ...getExperimentVariant(),
    timestamp: Date.now(),
    url: window.location.href,
  };

  // Mixpanel
  if (window.mixpanel) {
    window.mixpanel.track(`[Spot] ${event}`, props);
  }

  // Airbridge
  if (window.airbridge) {
    window.airbridge.events.send(event, { customAttributes: props });
  }

  // GA4
  if (window.gtag) {
    window.gtag("event", event, props);
  }
}
getSecret function · typescript · L3-L9 (7 LOC)
src/lib/booking-token.ts
function getSecret(): string {
  const secret = process.env.ADMIN_PASSWORD;
  if (!secret) {
    throw new Error("ADMIN_PASSWORD 환경변수가 설정되지 않았습니다");
  }
  return secret;
}
getWindowIndex function · typescript · L15-L17 (3 LOC)
src/lib/booking-token.ts
function getWindowIndex(offset = 0): number {
  return Math.floor(Date.now() / (1000 * 60 * 60 * 24 * 30)) + offset;
}
computeToken function · typescript · L19-L25 (7 LOC)
src/lib/booking-token.ts
function computeToken(payload: string): string {
  return crypto
    .createHmac("sha256", getSecret())
    .update(payload)
    .digest("hex")
    .slice(0, 32);
}
generateBookingToken function · typescript · L28-L31 (4 LOC)
src/lib/booking-token.ts
export function generateBookingToken(phone: string): string {
  const digits = phone.replace(/[^\d]/g, "");
  return computeToken(`${digits}:${getWindowIndex()}`);
}
getKSTNow function · typescript · L8-L12 (5 LOC)
src/lib/booking-utils.ts
function getKSTNow(): Date {
  return new Date(
    new Date().toLocaleString("en-US", { timeZone: "Asia/Seoul" }),
  );
}
Want fix-PRs on findings? Install Repobility's GitHub App · github.com/apps/repobility-bot
getEarliestBookableDate function · typescript · L15-L22 (8 LOC)
src/lib/booking-utils.ts
export function getEarliestBookableDate(): string {
  const kst = getKSTNow();
  // 12시 이전 → 내일부터, 12시 이후 → 모레부터
  const daysToAdd = kst.getHours() < 12 ? 1 : 2;
  const earliest = new Date(kst);
  earliest.setDate(earliest.getDate() + daysToAdd);
  return `${earliest.getFullYear()}-${String(earliest.getMonth() + 1).padStart(2, "0")}-${String(earliest.getDate()).padStart(2, "0")}`;
}
isDateBookable function · typescript · L25-L27 (3 LOC)
src/lib/booking-utils.ts
export function isDateBookable(dateStr: string): boolean {
  return dateStr >= getEarliestBookableDate();
}
getCustomerDeadline function · typescript · L33-L36 (4 LOC)
src/lib/booking-utils.ts
export function getCustomerDeadline(bookingDate: string): Date {
  const pickupDate = new Date(bookingDate + "T00:00:00+09:00");
  return new Date(pickupDate.getTime() - 2 * 60 * 60 * 1000);
}
isBeforeDeadline function · typescript · L39-L41 (3 LOC)
src/lib/booking-utils.ts
export function isBeforeDeadline(bookingDate: string): boolean {
  return new Date() < getCustomerDeadline(bookingDate);
}
calcCrewSize function · typescript · L11-L16 (6 LOC)
src/lib/crew-utils.ts
export function calcCrewSize(totalLoadingCube: number): number {
  for (const t of CREW_THRESHOLDS) {
    if (totalLoadingCube >= t.minCube) return t.crew;
  }
  return 1;
}
rowToBooking function · typescript · L76-L120 (45 LOC)
src/lib/db.ts
function rowToBooking(row: Record<string, unknown>): Booking {
  return {
    id: row.id as string,
    date: row.date as string,
    timeSlot: row.time_slot as string,
    area: row.area as string,
    items: (row.items as Booking["items"]) || [],
    totalPrice: (row.total_price as number) || 0,
    crewSize: (row.crew_size as number) || 1,
    needLadder: (row.need_ladder as boolean) || false,
    ladderType: (row.ladder_type as string) || undefined,
    ladderHours:
      row.ladder_hours != null ? (row.ladder_hours as number) : undefined,
    ladderPrice: (row.ladder_price as number) || 0,
    customerName: row.customer_name as string,
    phone: row.phone as string,
    address: row.address as string,
    addressDetail: (row.address_detail as string) || "",
    memo: (row.memo as string) || "",
    status: (row.status as Booking["status"]) || "pending",
    createdAt: row.created_at as string,
    updatedAt: row.updated_at as string,
    hasElevator: (row.has_elevator as boolean)
bookingToRow function · typescript · L122-L163 (42 LOC)
src/lib/db.ts
function bookingToRow(b: Booking) {
  return {
    id: b.id,
    date: b.date,
    time_slot: b.timeSlot,
    area: b.area,
    items: b.items,
    total_price: b.totalPrice,
    crew_size: b.crewSize,
    need_ladder: b.needLadder,
    ladder_type: b.ladderType || null,
    ladder_hours: b.ladderHours ?? null,
    ladder_price: b.ladderPrice,
    customer_name: b.customerName,
    phone: b.phone,
    address: b.address,
    address_detail: b.addressDetail,
    memo: b.memo,
    status: b.status,
    created_at: b.createdAt,
    updated_at: b.updatedAt,
    has_elevator: b.hasElevator,
    has_parking: b.hasParking,
    has_ground_access: b.hasGroundAccess,
    estimate_min: b.estimateMin,
    estimate_max: b.estimateMax,
    final_price: b.finalPrice,
    photos: b.photos,
    admin_memo: b.adminMemo,
    confirmed_time: b.confirmedTime ?? null,
    confirmed_duration: b.confirmedDuration ?? null,
    completion_photos: b.completionPhotos || [],
    slack_thread_ts: b.slackThreadTs ??
partialToRow function · typescript · L165-L172 (8 LOC)
src/lib/db.ts
function partialToRow(updates: Partial<Booking>): Record<string, unknown> {
  const row: Record<string, unknown> = {};
  for (const [key, val] of Object.entries(updates)) {
    const dbKey = FIELD_MAP[key];
    if (dbKey) row[dbKey] = val;
  }
  return row;
}
Repobility analyzer · published findings · https://repobility.com
getBookings function · typescript · L176-L188 (13 LOC)
src/lib/db.ts
export async function getBookings(date?: string): Promise<Booking[]> {
  let query = supabase
    .from("bookings")
    .select("*")
    .neq("status", "cancelled")
    .order("created_at", { ascending: false });

  if (date) query = query.eq("date", date);

  const { data, error } = await query;
  if (error) throw error;
  return (data || []).map(rowToBooking);
}
getBookingById function · typescript · L190-L205 (16 LOC)
src/lib/db.ts
export async function getBookingById(
  id: string,
): Promise<Booking | null> {
  const { data, error } = await supabase
    .from("bookings")
    .select("*")
    .eq("id", id)
    .neq("status", "cancelled")
    .single();

  if (error) {
    if (error.code === "PGRST116") return null;
    throw error;
  }
  return data ? rowToBooking(data) : null;
}
getBookingByIdAdmin function · typescript · L208-L222 (15 LOC)
src/lib/db.ts
export async function getBookingByIdAdmin(
  id: string,
): Promise<Booking | null> {
  const { data, error } = await supabase
    .from("bookings")
    .select("*")
    .eq("id", id)
    .single();

  if (error) {
    if (error.code === "PGRST116") return null;
    throw error;
  }
  return data ? rowToBooking(data) : null;
}
getBookingsByPhone function · typescript · L224-L244 (21 LOC)
src/lib/db.ts
export async function getBookingsByPhone(
  phone: string,
): Promise<Booking[]> {
  // 하이픈 포함/미포함 모두 검색 (포맷 불일치 방어)
  const digits = phone.replace(/[^\d]/g, "");
  const formatted =
    digits.length >= 10
      ? `${digits.slice(0, 3)}-${digits.slice(3, 7)}-${digits.slice(7)}`
      : phone;

  // .or() 문자열 직접 삽입 대신 .in() 사용 (PostgREST 주입 방지)
  const phoneVariants = [...new Set([formatted, digits])];
  const { data, error } = await supabase
    .from("bookings")
    .select("*")
    .in("phone", phoneVariants)
    .order("created_at", { ascending: false });

  if (error) throw error;
  return (data || []).map(rowToBooking);
}
getAllBookings function · typescript · L246-L254 (9 LOC)
src/lib/db.ts
export async function getAllBookings(): Promise<Booking[]> {
  const { data, error } = await supabase
    .from("bookings")
    .select("*")
    .order("created_at", { ascending: false });

  if (error) throw error;
  return (data || []).map(rowToBooking);
}
getBookingStatusCounts function · typescript · L260-L275 (16 LOC)
src/lib/db.ts
export async function getBookingStatusCounts(): Promise<Record<string, number>> {
  // limit 10000: Supabase 기본 1000행 제한 우회 (대시보드 탭 배지 정확도 보장)
  const { data, error } = await supabase
    .from("bookings")
    .select("status")
    .limit(10000);

  if (error) throw error;

  const counts: Record<string, number> = {};
  for (const row of data || []) {
    const s = row.status as string;
    counts[s] = (counts[s] || 0) + 1;
  }
  return counts;
}
getBookingsByStatus function · typescript · L277-L288 (12 LOC)
src/lib/db.ts
export async function getBookingsByStatus(
  status: string,
): Promise<Booking[]> {
  const { data, error } = await supabase
    .from("bookings")
    .select("*")
    .eq("status", status)
    .order("created_at", { ascending: false });

  if (error) throw error;
  return (data || []).map(rowToBooking);
}
createBooking function · typescript · L290-L299 (10 LOC)
src/lib/db.ts
export async function createBooking(booking: Booking): Promise<Booking> {
  const { data, error } = await supabase
    .from("bookings")
    .insert(bookingToRow(booking))
    .select()
    .single();

  if (error) throw error;
  return rowToBooking(data);
}
Repobility (the analyzer behind this table) · https://repobility.com
updateBooking function · typescript · L301-L328 (28 LOC)
src/lib/db.ts
export async function updateBooking(
  id: string,
  updates: Partial<Booking>,
  expectedUpdatedAt?: string,
): Promise<Booking | null> {
  const row = {
    ...partialToRow(updates),
    updated_at: new Date().toISOString(),
  };

  let query = supabase
    .from("bookings")
    .update(row)
    .eq("id", id);

  // Optimistic locking: only update if updated_at matches
  if (expectedUpdatedAt) {
    query = query.eq("updated_at", expectedUpdatedAt);
  }

  const { data, error } = await query.select().single();

  if (error) {
    if (error.code === "PGRST116") return null; // No rows matched
    throw error;
  }
  return data ? rowToBooking(data) : null;
}
getBookingsPaginated function · typescript · L330-L337 (8 LOC)
src/lib/db.ts
export async function getBookingsPaginated(params: {
  status?: string;
  dateFrom?: string;
  dateTo?: string;
  search?: string;
  page?: number;
  limit?: number;
}): Promise<{ bookings: Booking[]; total: number }> {
deleteBooking function · typescript · L377-L380 (4 LOC)
src/lib/db.ts
export async function deleteBooking(id: string): Promise<boolean> {
  const result = await updateBooking(id, { status: "cancelled" });
  return result !== null;
}
‹ prevpage 6 / 8next ›