diff --git a/src/app/api/suitable/list/route.ts b/src/app/api/suitable/list/route.ts index e19e702..6f9260c 100644 --- a/src/app/api/suitable/list/route.ts +++ b/src/app/api/suitable/list/route.ts @@ -65,9 +65,11 @@ export async function GET(request: NextRequest) { const suitable: Suitable[] = await prisma.$queryRawUnsafe(query, pageNumber, itemPerPage) - // console.log(`검색 조건 :::: 카테고리: ${category}, 키워드: ${keyword}`) - - return NextResponse.json(suitable) + return NextResponse.json(suitable, { + headers: { + 'spinner-state': 'true', + }, + }) } catch (error) { console.error('❌ 데이터 조회 중 오류가 발생했습니다:', error) return NextResponse.json({ error: '데이터 조회 중 오류가 발생했습니다' }, { status: 500 }) diff --git a/src/components/popup/SuitableDetailPopup.tsx b/src/components/popup/SuitableDetailPopup.tsx index 31ad8ed..e4635c0 100644 --- a/src/components/popup/SuitableDetailPopup.tsx +++ b/src/components/popup/SuitableDetailPopup.tsx @@ -6,6 +6,7 @@ import SuitableDetailPopupButton from './SuitableDetailPopupButton' import { useSuitable } from '@/hooks/useSuitable' import { usePopupController } from '@/store/popupController' import { useSuitableStore } from '@/store/useSuitableStore' +import { useSpinnerStore } from '@/store/spinnerStore' import { SUITABLE_HEAD_CODE, type Suitable, type SuitableDetail } from '@/types/Suitable' export default function SuitableDetailPopup() { @@ -15,7 +16,7 @@ export default function SuitableDetailPopup() { const [openItems, setOpenItems] = useState>(new Set()) - // 아이템 열기/닫기 + /* 아이템 열기/닫기 */ const toggleItemOpen = useCallback((itemId: number) => { setOpenItems((prev) => { const newOpenItems = new Set(prev) @@ -24,6 +25,11 @@ export default function SuitableDetailPopup() { }) }, []) + /* 데이터 로딩 상태 처리 */ + useEffect(() => { + useSpinnerStore.getState().setIsShow(isSelectedSuitablesLoading) + }, [isSelectedSuitablesLoading]) + useEffect(() => { setSelectedItemsSearching(true) }, []) @@ -45,60 +51,56 @@ export default function SuitableDetailPopup() {
- {isSelectedSuitablesLoading ? ( -
Loading...
- ) : ( - selectedSuitables?.map((item: Suitable) => ( -
-
-
{item.productName}
-
- -
-
-
-
-
屋根技研 支持瓦
-
{toCodeName(SUITABLE_HEAD_CODE.MANU_FT_CD, item.manuFtCd)}
-
-
-
屋根材
-
{toCodeName(SUITABLE_HEAD_CODE.ROOF_MT_CD, item.roofMtCd)}
-
-
-
金具タイプ
-
{toCodeName(SUITABLE_HEAD_CODE.ROOF_SH_CD, item.roofShCd)}
-
-
- {toSuitableDetail(item.detail).map((subItem: SuitableDetail) => ( -
-
-
{toCodeName(SUITABLE_HEAD_CODE.TRESTLE_MFPC_CD, subItem.trestleMfpcCd)}
-
-
- -
- {subItem.memo && ( -
- -
- )} -
-
-
{suitableCheckMemo(subItem.trestleManufacturerProductName)}
- {subItem.memo && ( -
-
備考
-
{subItem.memo}
-
- )} -
- ))} -
+ {selectedSuitables?.map((item: Suitable) => ( +
+
+
{item.productName}
+
+
- )) - )} +
+
+
屋根技研 支持瓦
+
{toCodeName(SUITABLE_HEAD_CODE.MANU_FT_CD, item.manuFtCd)}
+
+
+
屋根材
+
{toCodeName(SUITABLE_HEAD_CODE.ROOF_MT_CD, item.roofMtCd)}
+
+
+
金具タイプ
+
{toCodeName(SUITABLE_HEAD_CODE.ROOF_SH_CD, item.roofShCd)}
+
+
+ {toSuitableDetail(item.detail).map((subItem: SuitableDetail) => ( +
+
+
{toCodeName(SUITABLE_HEAD_CODE.TRESTLE_MFPC_CD, subItem.trestleMfpcCd)}
+
+
+ +
+ {subItem.memo && ( +
+ +
+ )} +
+
+
{suitableCheckMemo(subItem.trestleManufacturerProductName)}
+ {subItem.memo && ( +
+
備考
+
{subItem.memo}
+
+ )} +
+ ))} +
+
+
+ ))}
diff --git a/src/components/suitable/SuitableList.tsx b/src/components/suitable/SuitableList.tsx index 9db030d..918776a 100644 --- a/src/components/suitable/SuitableList.tsx +++ b/src/components/suitable/SuitableList.tsx @@ -6,6 +6,7 @@ import SuitableButton from './SuitableButton' import SuitableNoData from './SuitableNoData' import { useSuitable } from '@/hooks/useSuitable' import { useSuitableStore } from '@/store/useSuitableStore' +import { useSpinnerStore } from '@/store/spinnerStore' import { SUITABLE_HEAD_CODE, type Suitable, type SuitableDetail } from '@/types/Suitable' export default function SuitableList() { @@ -24,7 +25,7 @@ export default function SuitableList() { const [openItems, setOpenItems] = useState>(new Set()) const observerTarget = useRef(null) - // 선택된 아이템 확인 - 메인 하위 아이템 indeterminate 확인 + /* 선택된 아이템 확인 - 메인 하위 아이템 indeterminate 확인 */ const isMainIndeterminate = useMemo( () => (mainId: number, detailCnt: number) => { const mainItem = selectedItems.get(mainId) @@ -34,7 +35,7 @@ export default function SuitableList() { [selectedItems], ) - // 선택된 아이템 확인 + /* 선택된 아이템 확인 */ const isItemSelected = useCallback( (mainId: number, detailId?: number): boolean => { const mainItem = selectedItems.get(mainId) @@ -45,7 +46,7 @@ export default function SuitableList() { [selectedItems], ) - // 아이템 클릭 + /* 아이템 클릭 */ const handleItemClick = useCallback( (mainId: number, detailId?: number, detailIds?: Set): void => { setSelectedItemsSearching(false) @@ -54,7 +55,7 @@ export default function SuitableList() { [isItemSelected, addSelectedItem, removeSelectedItem], ) - // 아이템 열기/닫기 + /* 아이템 열기/닫기 */ const toggleItemOpen = useCallback((itemId: number) => { setOpenItems((prev) => { const newOpenItems = new Set(prev) @@ -63,7 +64,7 @@ export default function SuitableList() { }) }, []) - // 아이템 렌더링 + /* 아이템 렌더링 */ const renderItem = useCallback( (item: Suitable) => { return ( @@ -115,10 +116,10 @@ export default function SuitableList() { [isItemSelected, openItems, handleItemClick, toggleItemOpen, toCodeName, toSuitableDetail], ) - // 아이템 리스트 + /* 조회 데이터 리스트 */ const suitableList = useMemo(() => suitables?.pages.flat() ?? [], [suitables?.pages]) - // Intersection Observer 설정 + /* Intersection Observer 설정 */ useEffect(() => { const observer = new IntersectionObserver( (entries) => { @@ -128,7 +129,7 @@ export default function SuitableList() { }, { threshold: 0, - rootMargin: '100px', + rootMargin: `${window.innerHeight * 0.2}px`, }, ) @@ -139,15 +140,18 @@ export default function SuitableList() { return () => observer.disconnect() }, [hasNextPage, isFetchingNextPage, fetchNextPage]) - if (isLoading) return
Loading...
+ /* 데이터 로딩 상태 처리 */ + useEffect(() => { + useSpinnerStore.getState().setIsShow(isLoading || isFetchingNextPage) + }, [isLoading, isFetchingNextPage]) + + /* 조회 데이터 없는 경우 */ if (!suitableList.length) return return ( <> {suitableList.map(renderItem)} -
- {isFetchingNextPage &&
데이터를 불러오는 중...
} -
+
) diff --git a/src/hooks/useAxios.ts b/src/hooks/useAxios.ts index a3402bc..27b04a8 100644 --- a/src/hooks/useAxios.ts +++ b/src/hooks/useAxios.ts @@ -9,9 +9,10 @@ export function useAxios() { } const responseHandler = (response: AxiosResponse) => { - // if (response.headers['spinner-state'] === undefined) { - useSpinnerStore.getState().setIsShow(false) - // } + /* spinner 조작 커스텀이 필요한 경우 api 응답 헤더에 spinner-state: true 추가 */ + if (!response.headers['spinner-state']) { + useSpinnerStore.getState().setIsShow(false) + } response.data = transferResponse(response) return response }