반응형
반응형
const keywordEnrollOption: radio[] = [
  { value: Code.A, id: 'auto_enroll', label: '자동등록' },
  { value: Code.M, id: 'manual_enroll', label: '수동등록' },
];

export interface radio {
  value: string;
  id: string;
  label: string;
}
const RadioGroups = ({
  onValueChange,
  radios,
}: {
  onValueChange: RadioGroupContextValue['onValueChange'];
  radios: radio[];
}) => {
  return (
    <>
      <RadioGroup
        defaultValue={radios[0].value}
        className="flex"
        onValueChange={(value) => onValueChange(value)}
      >
        {radios.map((item) => (
          <div key={item.id} className="flex items-center space-x-2">
            <RadioGroupItem value={item.value} id={item.id} />
            <Label htmlFor={item.id}>{item.label}</Label>
          </div>
        ))}
      </RadioGroup>
    </>
  );
};

/** 컴포넌트 사용 **/
const dispatch = useAppDispatch();
const setKeywordStrategy = (value: string) => {
    dispatch(groupInfoSlice.actions.setKeywordStrategy(value));
};
  
<RadioGroups
    onValueChange={setKeywordStrategy}
    radios={keywordEnrollOption}
/>

RadioGroup 컴포넌트의 경우 https://ui.shadcn.com/에서 가져온 거라 그 부분은 본인이 만든 라디오 컴포넌트에 맞게 만드시면 됩니다

리덕스로 상태관리를 했으며 useState로 관리하는 것도 컴포넌트 사용에서 넘겨주는 Setter를 변경해서 넘겨주면 됩니다

반응형
반응형
export interface checkbox {
  text: string;
  code: string;
}

const weekOption: checkbox[] = [
  { text: '일', code: 'week1' },
  { text: '월', code: 'week2' },
  { text: '화', code: 'week3' },
  { text: '수', code: 'week4' },
  { text: '목', code: 'week5' },
  { text: '금', code: 'week6' },
  { text: '토', code: 'week7' },
];

const Checkboxes = ({
  checkboxOption,
  disabled,
}: {
  checkboxOption: checkbox[];
  disabled: boolean;
}) => {
  const [checkbox, setCheckbox] = useState<string[]>([]);

  const addCheckboxOption = (checked: CheckedState, value: string) => {
    let updatedOption: string[];

    if (checked) {
      updatedOption = [...checkbox, value];
    } else {
      updatedOption = checkbox.filter((el) => el !== value);
    }

    // checkboxOption에 적힌 순서대로 정렬 해서 추가
    const sortedCheckbox = checkboxOption.filter((checkbox) =>
      updatedOption.includes(checkbox.code),
    );

    setCheckbox(sortedCheckbox.map((checkbox) => checkbox.code));
  };

  return (
    <>
      {checkboxOption.map((checkbox) => (
        <React.Fragment key={checkbox.code}>
          <Checkbox
            id={checkbox.code}
            checked={disabled ? disabled : undefined}
            disabled={disabled}
            onCheckedChange={(e) => addCheckboxOption(e, checkbox.code)}
          />
          <label
            htmlFor={checkbox.code}
            className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
          >
            {checkbox.text}
          </label>
        </React.Fragment>
      ))}
    </>
  );

Checkbox박스 컴포넌트의 경우 https://ui.shadcn.com/에서 가져온 거라 그 부분은 본인이 만든 체크박스 컴포넌트에 맞게 만드시면 됩니다

 


 

interface CheckBoxActions {
  [key: string]: ActionCreatorWithPayload<string[], string>;
}

const checkboxActions: CheckBoxActions = {
  groupWeek: groupInfoSlice.actions.setExposureWeek,
  groupExposureArea: groupInfoSlice.actions.setExposureArea,
};

type SelectorFunction = (state: RootState) => string[];

const stateSelectors: Record<string, SelectorFunction> = {
  groupWeek: (state) => state.groupInfo.exposureWeek,
  groupExposureArea: (state) => state.groupInfo.exposureArea,
};

const Checkboxes = ({
  checkboxOption,
  actionName,
  disabled,
}: {
  checkboxOption: checkbox[];
  actionName: string;
  disabled: boolean;
}) => {
  const dispatch = useAppDispatch();
  const action = checkboxActions[actionName];

  const checkbox = useSelector((state: RootState) => {
    const selector = stateSelectors[actionName];
    return selector ? selector(state) : [];
  });

  const addCheckboxOption = (checked: CheckedState, value: string) => {
    let updatedOption: string[];

    if (checked) {
      updatedOption = [...checkbox, value];
    } else {
      updatedOption = checkbox.filter((el) => el !== value);
    }

    // 정렬 추가
    const sortedCheckbox = checkboxOption.filter((checkbox) =>
      updatedOption.includes(checkbox.code),
    );

    const result = sortedCheckbox.map((checkbox) => checkbox.code);
    dispatch(action(result));
  };

  return (
    <>
      {checkboxOption.map((checkbox) => (
        <React.Fragment key={checkbox.code}>
          <Checkbox
            id={checkbox.code}
            checked={disabled ? disabled : undefined}
            disabled={disabled}
            onCheckedChange={(e) => addCheckboxOption(e, checkbox.code)}
          />
          <label
            htmlFor={checkbox.code}
            className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
          >
            {checkbox.text}
          </label>
        </React.Fragment>
      ))}
    </>
  );

여기는 전역 상태관리 Redux를 사용한 경우 컴포넌트를 전역 상태관리할 수 있게 나름대로 코드를 만들어봤습니다

 

  • actionName 상태관리 변수명에 따라 useSelector로 값을 가져오며 상태관리 변수명에 따라 값을 Set할 수 있게 action을 설정해줍니다
  • checkboxActions, stateSelectors에서 actionName에 대한 "키"를 입력해주면 됩니다

 

반응형
반응형

📝Custom Hook

커스텀 훅(Custom Hook)은 React의 기능을 활용하여 사용자가 직접 만든 재사용 가능한 함수입니다

 

  • 커스텀 훅의 이름은 use로 시작해야한다
  • 반복문, 조건문, 중첩된 함수 내부에서는 훅을 호출하지 말아야 합니다 → 기본적으로 훅은 이런 곳에 쓰이면 안 됨
  • useEffect, useMemo, useCallback 등에서 의존성 배열을 적절히 관리 필요
  • 한 가지의 역할만 있어야한다
export function useSingleUpload() {
  const [preview, setPreview] = useState<string>();
  const [formData, setFormData] = useState(new FormData());

  const changeFile = (event: React.ChangeEvent<HTMLInputElement>) => {
    if (event.target.files && event.target.files.length > 0) {
      const fileFormData = new FormData();
      const file = event.target.files?.[0];

      /** GraphQL 쿼리와 변수를 formData에 추가 **/
      fileFormData.append(
        'operations',
        JSON.stringify({
          query: `
            mutation fileUpload($fileInput: FileInput!) {
              fileUpload(fileInput: $fileInput)
            }
          `,
          variables: {
            fileInput: {
              file_name: file.name,
              file_content_type: file.type,
            },
          },
        }),
      );

      /** 이진 데이터 매핑 **/
      fileFormData.append(
        'map',
        JSON.stringify({ '0': ['variables.fileInput.file_data'] }),
      );

      /** 이진 데이터 매핑 **/
      fileFormData.append('0', file);
      setFormData(fileFormData);

      setPreview(
        file?.type.startsWith('image/') ? URL.createObjectURL(file) : undefined,
      );
    }
  };

  const upload = useCallback(() => {
    fetch('http://localhost:4000/graphql', {
      method: 'POST',
      headers: {
        'x-apollo-operation-name': 'UploadFile', // application/json외 CSRF 방지 허용 Header
      },
      body: formData,
    })
      .then((response) => response.json())
      .then((data) => console.log(data))
      .catch((error) => console.error(error));
  }, [formData]);

  return { preview, formData, changeFile, upload };
}

파일업로드 훅에 대한 예제인데 preview, formData, changeFile, upload는 파일 업로드와 관련된 걸 반환해준다 즉, 한 가지 역할에 충실하다

반응형
반응형

백엔드는 프리즈마 ORM을 사용했고 커서기반으로 구현했습니다

 

Page

const ItemPage = () => {
  const TAKE = 10;
  const MEMBER_ID = 1; // 1 ~ 5
  const FIRST_CURSOR = 1;

  const { data, fetchNextPage, hasNextPage, isFetching } =
    useInfiniteFindItemsQuery(
      {
        memberId: MEMBER_ID,
        lastCursorId: FIRST_CURSOR,
      },
      {
        getNextPageParam: (lastPage, allPages) => {
          if (lastPage.findItems.length === 0) {
            return undefined;
          } else {
            return {
              memberId: MEMBER_ID,
              lastCursorId:
                lastPage.findItems[lastPage.findItems.length - 1]?.id + 1,
            };
          }
        },
        enabled: false,
        initialPageParam: {
          memberId: MEMBER_ID,
          lastCursorId: FIRST_CURSOR,
        },
      },
    );

  useEffect(() => {
    fetchNextPage();
  }, []);

  const observerRef = useIntersectionObserver({
    isFetching,
    hasNextPage,
    fetchNextPage,
  });

  return (
    <main className="mx-24 my-16">
      <SearchBar />
      {/*<Suspense fallback={ItemPage.Skeleton()}>*/}
      <div className="grid grid-cols-5 gap-x-14">
        {data?.pages.map((page, pageNum) =>
          page.findItems.map((item, index) => (
            <Item index={index + pageNum * TAKE} key={item.id} item={item} />
          )),
        )}
      </div>
      {/*</Suspense>*/}
      <div ref={observerRef} className="h-1" />
    </main>
  );
};

export default ItemPage;

 

 

최하단 엘리먼트 요소 감지 훅 및 다음 페이지 데이터 Fetch

import React, { useCallback, useEffect, useRef, useState } from 'react';
import {
  FetchNextPageOptions,
  InfiniteData,
  InfiniteQueryObserverResult,
} from '@tanstack/react-query';
import { IFindItemsQuery } from '@/gql/generated/graphql';

//hook props interface
interface IuseIntersectionObserverProps<TData = unknown> {
  isFetching: boolean | undefined;
  hasNextPage: boolean | undefined;
  fetchNextPage: (
    options?: FetchNextPageOptions,
  ) => Promise<
    InfiniteQueryObserverResult<InfiniteData<TData, unknown>, unknown>
  >;
}

export const useIntersectionObserver = <TData>({
  isFetching,
  hasNextPage,
  fetchNextPage,
}: IuseIntersectionObserverProps<TData>) => {
  const options = {
    root: null, // 감지할 대상 지정 null일 경우 뷰포트가 대상
    rootMargin: '0px', // root요소가 가장자리에 도달하는 즉시 교차 이벤트 감지
    threshold: 1.0, // 얼마나 많이 root요소와 교차하는지 설정 → 1.0 = 100% 완전히 겹친다
  };

  const handleIntersection = useCallback(
    (entries: IntersectionObserverEntry[]) => {
      const target = entries[0];
      if (target.isIntersecting && hasNextPage && !isFetching) {
        fetchNextPage();
      }
    },
    [hasNextPage, isFetching, fetchNextPage],
  );

  const observerRef = useRef(null);

  useEffect(() => {
    const observer = new IntersectionObserver(handleIntersection, options);
    if (observerRef.current) {
      observer.observe(observerRef.current);
    }
    return () => {
      observer.disconnect();
    };
  }, [handleIntersection, options]);

  return observerRef;
};

 

반응형
반응형
const cities = [
    {id: 1, name: "#전체"},
    {id: 2, name: "#서울"},
    {id: 3, name: "#경기"},
    {id: 4, name: "#인천"}
]

function Main() {

    const [selectedCity, setSelectedCity] = useState(cities[0].id);

    const selectCity = (cityId:number) => {
        setSelectedCity(cityId)
    }
    
    return <div>
        {cities.map((city) =>
        <button className={city.id === selectedCity ? "bg-blue-500 hover:bg-blue-700 text-white p-1 rounded-full mx-2" : "mx-2"}
                key={city.id}
                onClick={() => selectCity(city.id)}>
            {city.name}
        </button>)}
    </div>
}
반응형