| 
 | 1 | +'use client';  | 
 | 2 | + | 
 | 3 | +import { useEffect, useMemo, useRef } from 'react';  | 
 | 4 | + | 
 | 5 | +import type { FunnelProps, StepProps } from '@/v1/base/Funnel/Funnel';  | 
 | 6 | +import { assert } from '@/utils/assert';  | 
 | 7 | + | 
 | 8 | +import { Funnel, Step } from '@/v1/base/Funnel/Funnel';  | 
 | 9 | +import { usePathname, useRouter, useSearchParams } from 'next/navigation';  | 
 | 10 | + | 
 | 11 | +export type NonEmptyArray<T> = readonly [T, ...T[]];  | 
 | 12 | + | 
 | 13 | +type RouteFunnelProps<Steps extends NonEmptyArray<string>> = Omit<  | 
 | 14 | +  FunnelProps<Steps>,  | 
 | 15 | +  'steps' | 'step'  | 
 | 16 | +>;  | 
 | 17 | + | 
 | 18 | +type FunnelComponent<Steps extends NonEmptyArray<string>> = ((  | 
 | 19 | +  props: RouteFunnelProps<Steps>  | 
 | 20 | +) => JSX.Element) & {  | 
 | 21 | +  Step: (props: StepProps<Steps>) => JSX.Element;  | 
 | 22 | +};  | 
 | 23 | + | 
 | 24 | +const DEFAULT_STEP_QUERY_KEY = 'funnel-step';  | 
 | 25 | + | 
 | 26 | +/**  | 
 | 27 | + * 사용자에게 초기 step을 강제하고 싶을 땐  | 
 | 28 | + * option의 initialStep을 작성해 주세요.  | 
 | 29 | + */  | 
 | 30 | +export const useFunnel = <Steps extends NonEmptyArray<string>>(  | 
 | 31 | +  steps: Steps,  | 
 | 32 | +  options?: {  | 
 | 33 | +    stepQueryKey?: string;  | 
 | 34 | +    initialStep?: Steps[number];  | 
 | 35 | +    onStepChange?: (name: Steps[number]) => void;  | 
 | 36 | +  }  | 
 | 37 | +): readonly [FunnelComponent<Steps>, (step: Steps[number]) => void] => {  | 
 | 38 | +  const router = useRouter();  | 
 | 39 | +  const searchParams = useSearchParams();  | 
 | 40 | +  const pathname = usePathname();  | 
 | 41 | + | 
 | 42 | +  const hasRunOnce = useRef(false);  | 
 | 43 | + | 
 | 44 | +  const step = searchParams.get('funnel-step') as string;  | 
 | 45 | +  const stepQueryKey = options?.stepQueryKey ?? DEFAULT_STEP_QUERY_KEY;  | 
 | 46 | + | 
 | 47 | +  useEffect(() => {  | 
 | 48 | +    if (options?.initialStep && !hasRunOnce.current) {  | 
 | 49 | +      hasRunOnce.current = true;  | 
 | 50 | +      router.replace(pathname);  | 
 | 51 | +    }  | 
 | 52 | +  }, [options?.initialStep, router, pathname]);  | 
 | 53 | + | 
 | 54 | +  assert(steps.length > 0, 'steps가 비어있습니다.');  | 
 | 55 | + | 
 | 56 | +  const FunnelComponent = useMemo(  | 
 | 57 | +    () =>  | 
 | 58 | +      Object.assign(  | 
 | 59 | +        function RouteFunnel(props: RouteFunnelProps<Steps>) {  | 
 | 60 | +          const currentStep = step ?? options?.initialStep;  | 
 | 61 | + | 
 | 62 | +          assert(  | 
 | 63 | +            currentStep != null,  | 
 | 64 | +            `표시할 스텝을 ${stepQueryKey} 쿼리 파라미터에 지정해주세요. 쿼리 파라미터가 없을 때 초기 스텝을 렌더하려면 useFunnel의 두 번째 파라미터 options에 initialStep을 지정해주세요.`  | 
 | 65 | +          );  | 
 | 66 | + | 
 | 67 | +          return <Funnel<Steps> steps={steps} step={currentStep} {...props} />;  | 
 | 68 | +        },  | 
 | 69 | +        {  | 
 | 70 | +          Step,  | 
 | 71 | +        }  | 
 | 72 | +      ),  | 
 | 73 | +    // eslint-disable-next-line react-hooks/exhaustive-deps  | 
 | 74 | +    [step]  | 
 | 75 | +  );  | 
 | 76 | + | 
 | 77 | +  const setStep = (step: Steps[number]) => {  | 
 | 78 | +    const params = new URLSearchParams(searchParams.toString());  | 
 | 79 | +    params.set('funnel-step', `${step}`);  | 
 | 80 | + | 
 | 81 | +    return router.replace(`?${params.toString()}`);  | 
 | 82 | +  };  | 
 | 83 | + | 
 | 84 | +  return [FunnelComponent, setStep] as unknown as readonly [  | 
 | 85 | +    FunnelComponent<Steps>,  | 
 | 86 | +    (step: Steps[number]) => Promise<void>  | 
 | 87 | +  ];  | 
 | 88 | +};  | 
0 commit comments