반응형
반응형
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에 대한 "키"를 입력해주면 됩니다

 

반응형
반응형

백엔드는 프리즈마 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>
}
반응형