feat: 지붕재 데이터 조회 시 스피너 로딩처리 추가
This commit is contained in:
parent
3ff43475ac
commit
9eea0bcc58
@ -65,9 +65,11 @@ export async function GET(request: NextRequest) {
|
|||||||
|
|
||||||
const suitable: Suitable[] = await prisma.$queryRawUnsafe(query, pageNumber, itemPerPage)
|
const suitable: Suitable[] = await prisma.$queryRawUnsafe(query, pageNumber, itemPerPage)
|
||||||
|
|
||||||
// console.log(`검색 조건 :::: 카테고리: ${category}, 키워드: ${keyword}`)
|
return NextResponse.json(suitable, {
|
||||||
|
headers: {
|
||||||
return NextResponse.json(suitable)
|
'spinner-state': 'true',
|
||||||
|
},
|
||||||
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ 데이터 조회 중 오류가 발생했습니다:', error)
|
console.error('❌ 데이터 조회 중 오류가 발생했습니다:', error)
|
||||||
return NextResponse.json({ error: '데이터 조회 중 오류가 발생했습니다' }, { status: 500 })
|
return NextResponse.json({ error: '데이터 조회 중 오류가 발생했습니다' }, { status: 500 })
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import SuitableDetailPopupButton from './SuitableDetailPopupButton'
|
|||||||
import { useSuitable } from '@/hooks/useSuitable'
|
import { useSuitable } from '@/hooks/useSuitable'
|
||||||
import { usePopupController } from '@/store/popupController'
|
import { usePopupController } from '@/store/popupController'
|
||||||
import { useSuitableStore } from '@/store/useSuitableStore'
|
import { useSuitableStore } from '@/store/useSuitableStore'
|
||||||
|
import { useSpinnerStore } from '@/store/spinnerStore'
|
||||||
import { SUITABLE_HEAD_CODE, type Suitable, type SuitableDetail } from '@/types/Suitable'
|
import { SUITABLE_HEAD_CODE, type Suitable, type SuitableDetail } from '@/types/Suitable'
|
||||||
|
|
||||||
export default function SuitableDetailPopup() {
|
export default function SuitableDetailPopup() {
|
||||||
@ -15,7 +16,7 @@ export default function SuitableDetailPopup() {
|
|||||||
|
|
||||||
const [openItems, setOpenItems] = useState<Set<number>>(new Set())
|
const [openItems, setOpenItems] = useState<Set<number>>(new Set())
|
||||||
|
|
||||||
// 아이템 열기/닫기
|
/* 아이템 열기/닫기 */
|
||||||
const toggleItemOpen = useCallback((itemId: number) => {
|
const toggleItemOpen = useCallback((itemId: number) => {
|
||||||
setOpenItems((prev) => {
|
setOpenItems((prev) => {
|
||||||
const newOpenItems = new Set(prev)
|
const newOpenItems = new Set(prev)
|
||||||
@ -24,6 +25,11 @@ export default function SuitableDetailPopup() {
|
|||||||
})
|
})
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
/* 데이터 로딩 상태 처리 */
|
||||||
|
useEffect(() => {
|
||||||
|
useSpinnerStore.getState().setIsShow(isSelectedSuitablesLoading)
|
||||||
|
}, [isSelectedSuitablesLoading])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSelectedItemsSearching(true)
|
setSelectedItemsSearching(true)
|
||||||
}, [])
|
}, [])
|
||||||
@ -45,60 +51,56 @@ export default function SuitableDetailPopup() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="modal-body">
|
<div className="modal-body">
|
||||||
<div className="compliance-check-pop-wrap">
|
<div className="compliance-check-pop-wrap">
|
||||||
{isSelectedSuitablesLoading ? (
|
{selectedSuitables?.map((item: Suitable) => (
|
||||||
<div>Loading...</div>
|
<div className={`compliance-check-bx ${openItems.has(item.id) ? 'act' : ''}`} key={item.id}>
|
||||||
) : (
|
<div className="check-name-wrap">
|
||||||
selectedSuitables?.map((item: Suitable) => (
|
<div className="check-name">{item.productName}</div>
|
||||||
<div className={`compliance-check-bx ${openItems.has(item.id) ? 'act' : ''}`} key={item.id}>
|
<div className="check-name-btn">
|
||||||
<div className="check-name-wrap">
|
<button className="bx-btn" onClick={() => toggleItemOpen(item.id)}></button>
|
||||||
<div className="check-name">{item.productName}</div>
|
|
||||||
<div className="check-name-btn">
|
|
||||||
<button className="bx-btn" onClick={() => toggleItemOpen(item.id)}></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="compliance-check-pop-contents">
|
|
||||||
<div className="check-pop-data-wrap">
|
|
||||||
<div className="check-pop-data-tit">屋根技研 支持瓦</div>
|
|
||||||
<div className="check-pop-data-txt">{toCodeName(SUITABLE_HEAD_CODE.MANU_FT_CD, item.manuFtCd)}</div>
|
|
||||||
</div>
|
|
||||||
<div className="check-pop-data-wrap">
|
|
||||||
<div className="check-pop-data-tit">屋根材</div>
|
|
||||||
<div className="check-pop-data-txt">{toCodeName(SUITABLE_HEAD_CODE.ROOF_MT_CD, item.roofMtCd)}</div>
|
|
||||||
</div>
|
|
||||||
<div className="check-pop-data-wrap">
|
|
||||||
<div className="check-pop-data-tit">金具タイプ</div>
|
|
||||||
<div className="check-pop-data-txt">{toCodeName(SUITABLE_HEAD_CODE.ROOF_SH_CD, item.roofShCd)}</div>
|
|
||||||
</div>
|
|
||||||
<div className="check-pop-data-table-wrap">
|
|
||||||
{toSuitableDetail(item.detail).map((subItem: SuitableDetail) => (
|
|
||||||
<div className="check-pop-data-table" key={subItem.id}>
|
|
||||||
<div className="pop-data-table-head">
|
|
||||||
<div className="pop-data-table-head-name">{toCodeName(SUITABLE_HEAD_CODE.TRESTLE_MFPC_CD, subItem.trestleMfpcCd)}</div>
|
|
||||||
<div className="pop-data-table-head-icon">
|
|
||||||
<div className="compliance-icon">
|
|
||||||
<Image src={suitableCheckIcon(subItem.trestleManufacturerProductName)} width={22} height={22} alt="" />
|
|
||||||
</div>
|
|
||||||
{subItem.memo && (
|
|
||||||
<div className="compliance-icon">
|
|
||||||
<Image src={'/assets/images/sub/compliance_tip_icon.svg'} width={22} height={22} alt=""></Image>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="pop-data-table-body">{suitableCheckMemo(subItem.trestleManufacturerProductName)}</div>
|
|
||||||
{subItem.memo && (
|
|
||||||
<div className="pop-data-table-footer">
|
|
||||||
<div className="pop-data-table-footer-unit">備考</div>
|
|
||||||
<div className="pop-data-table-footer-data">{subItem.memo}</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))
|
<div className="compliance-check-pop-contents">
|
||||||
)}
|
<div className="check-pop-data-wrap">
|
||||||
|
<div className="check-pop-data-tit">屋根技研 支持瓦</div>
|
||||||
|
<div className="check-pop-data-txt">{toCodeName(SUITABLE_HEAD_CODE.MANU_FT_CD, item.manuFtCd)}</div>
|
||||||
|
</div>
|
||||||
|
<div className="check-pop-data-wrap">
|
||||||
|
<div className="check-pop-data-tit">屋根材</div>
|
||||||
|
<div className="check-pop-data-txt">{toCodeName(SUITABLE_HEAD_CODE.ROOF_MT_CD, item.roofMtCd)}</div>
|
||||||
|
</div>
|
||||||
|
<div className="check-pop-data-wrap">
|
||||||
|
<div className="check-pop-data-tit">金具タイプ</div>
|
||||||
|
<div className="check-pop-data-txt">{toCodeName(SUITABLE_HEAD_CODE.ROOF_SH_CD, item.roofShCd)}</div>
|
||||||
|
</div>
|
||||||
|
<div className="check-pop-data-table-wrap">
|
||||||
|
{toSuitableDetail(item.detail).map((subItem: SuitableDetail) => (
|
||||||
|
<div className="check-pop-data-table" key={subItem.id}>
|
||||||
|
<div className="pop-data-table-head">
|
||||||
|
<div className="pop-data-table-head-name">{toCodeName(SUITABLE_HEAD_CODE.TRESTLE_MFPC_CD, subItem.trestleMfpcCd)}</div>
|
||||||
|
<div className="pop-data-table-head-icon">
|
||||||
|
<div className="compliance-icon">
|
||||||
|
<Image src={suitableCheckIcon(subItem.trestleManufacturerProductName)} width={22} height={22} alt="" />
|
||||||
|
</div>
|
||||||
|
{subItem.memo && (
|
||||||
|
<div className="compliance-icon">
|
||||||
|
<Image src={'/assets/images/sub/compliance_tip_icon.svg'} width={22} height={22} alt=""></Image>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="pop-data-table-body">{suitableCheckMemo(subItem.trestleManufacturerProductName)}</div>
|
||||||
|
{subItem.memo && (
|
||||||
|
<div className="pop-data-table-footer">
|
||||||
|
<div className="pop-data-table-footer-unit">備考</div>
|
||||||
|
<div className="pop-data-table-footer-data">{subItem.memo}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
<SuitableDetailPopupButton />
|
<SuitableDetailPopupButton />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import SuitableButton from './SuitableButton'
|
|||||||
import SuitableNoData from './SuitableNoData'
|
import SuitableNoData from './SuitableNoData'
|
||||||
import { useSuitable } from '@/hooks/useSuitable'
|
import { useSuitable } from '@/hooks/useSuitable'
|
||||||
import { useSuitableStore } from '@/store/useSuitableStore'
|
import { useSuitableStore } from '@/store/useSuitableStore'
|
||||||
|
import { useSpinnerStore } from '@/store/spinnerStore'
|
||||||
import { SUITABLE_HEAD_CODE, type Suitable, type SuitableDetail } from '@/types/Suitable'
|
import { SUITABLE_HEAD_CODE, type Suitable, type SuitableDetail } from '@/types/Suitable'
|
||||||
|
|
||||||
export default function SuitableList() {
|
export default function SuitableList() {
|
||||||
@ -24,7 +25,7 @@ export default function SuitableList() {
|
|||||||
const [openItems, setOpenItems] = useState<Set<number>>(new Set())
|
const [openItems, setOpenItems] = useState<Set<number>>(new Set())
|
||||||
const observerTarget = useRef<HTMLDivElement>(null)
|
const observerTarget = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
// 선택된 아이템 확인 - 메인 하위 아이템 indeterminate 확인
|
/* 선택된 아이템 확인 - 메인 하위 아이템 indeterminate 확인 */
|
||||||
const isMainIndeterminate = useMemo(
|
const isMainIndeterminate = useMemo(
|
||||||
() => (mainId: number, detailCnt: number) => {
|
() => (mainId: number, detailCnt: number) => {
|
||||||
const mainItem = selectedItems.get(mainId)
|
const mainItem = selectedItems.get(mainId)
|
||||||
@ -34,7 +35,7 @@ export default function SuitableList() {
|
|||||||
[selectedItems],
|
[selectedItems],
|
||||||
)
|
)
|
||||||
|
|
||||||
// 선택된 아이템 확인
|
/* 선택된 아이템 확인 */
|
||||||
const isItemSelected = useCallback(
|
const isItemSelected = useCallback(
|
||||||
(mainId: number, detailId?: number): boolean => {
|
(mainId: number, detailId?: number): boolean => {
|
||||||
const mainItem = selectedItems.get(mainId)
|
const mainItem = selectedItems.get(mainId)
|
||||||
@ -45,7 +46,7 @@ export default function SuitableList() {
|
|||||||
[selectedItems],
|
[selectedItems],
|
||||||
)
|
)
|
||||||
|
|
||||||
// 아이템 클릭
|
/* 아이템 클릭 */
|
||||||
const handleItemClick = useCallback(
|
const handleItemClick = useCallback(
|
||||||
(mainId: number, detailId?: number, detailIds?: Set<number>): void => {
|
(mainId: number, detailId?: number, detailIds?: Set<number>): void => {
|
||||||
setSelectedItemsSearching(false)
|
setSelectedItemsSearching(false)
|
||||||
@ -54,7 +55,7 @@ export default function SuitableList() {
|
|||||||
[isItemSelected, addSelectedItem, removeSelectedItem],
|
[isItemSelected, addSelectedItem, removeSelectedItem],
|
||||||
)
|
)
|
||||||
|
|
||||||
// 아이템 열기/닫기
|
/* 아이템 열기/닫기 */
|
||||||
const toggleItemOpen = useCallback((itemId: number) => {
|
const toggleItemOpen = useCallback((itemId: number) => {
|
||||||
setOpenItems((prev) => {
|
setOpenItems((prev) => {
|
||||||
const newOpenItems = new Set(prev)
|
const newOpenItems = new Set(prev)
|
||||||
@ -63,7 +64,7 @@ export default function SuitableList() {
|
|||||||
})
|
})
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// 아이템 렌더링
|
/* 아이템 렌더링 */
|
||||||
const renderItem = useCallback(
|
const renderItem = useCallback(
|
||||||
(item: Suitable) => {
|
(item: Suitable) => {
|
||||||
return (
|
return (
|
||||||
@ -115,10 +116,10 @@ export default function SuitableList() {
|
|||||||
[isItemSelected, openItems, handleItemClick, toggleItemOpen, toCodeName, toSuitableDetail],
|
[isItemSelected, openItems, handleItemClick, toggleItemOpen, toCodeName, toSuitableDetail],
|
||||||
)
|
)
|
||||||
|
|
||||||
// 아이템 리스트
|
/* 조회 데이터 리스트 */
|
||||||
const suitableList = useMemo(() => suitables?.pages.flat() ?? [], [suitables?.pages])
|
const suitableList = useMemo(() => suitables?.pages.flat() ?? [], [suitables?.pages])
|
||||||
|
|
||||||
// Intersection Observer 설정
|
/* Intersection Observer 설정 */
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const observer = new IntersectionObserver(
|
const observer = new IntersectionObserver(
|
||||||
(entries) => {
|
(entries) => {
|
||||||
@ -128,7 +129,7 @@ export default function SuitableList() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
threshold: 0,
|
threshold: 0,
|
||||||
rootMargin: '100px',
|
rootMargin: `${window.innerHeight * 0.2}px`,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -139,15 +140,18 @@ export default function SuitableList() {
|
|||||||
return () => observer.disconnect()
|
return () => observer.disconnect()
|
||||||
}, [hasNextPage, isFetchingNextPage, fetchNextPage])
|
}, [hasNextPage, isFetchingNextPage, fetchNextPage])
|
||||||
|
|
||||||
if (isLoading) return <div>Loading...</div>
|
/* 데이터 로딩 상태 처리 */
|
||||||
|
useEffect(() => {
|
||||||
|
useSpinnerStore.getState().setIsShow(isLoading || isFetchingNextPage)
|
||||||
|
}, [isLoading, isFetchingNextPage])
|
||||||
|
|
||||||
|
/* 조회 데이터 없는 경우 */
|
||||||
if (!suitableList.length) return <SuitableNoData />
|
if (!suitableList.length) return <SuitableNoData />
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{suitableList.map(renderItem)}
|
{suitableList.map(renderItem)}
|
||||||
<div ref={observerTarget} className="loading-indicator">
|
<div ref={observerTarget} />
|
||||||
{isFetchingNextPage && <div className="loading-more">데이터를 불러오는 중...</div>}
|
|
||||||
</div>
|
|
||||||
<SuitableButton />
|
<SuitableButton />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -9,9 +9,10 @@ export function useAxios() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const responseHandler = (response: AxiosResponse) => {
|
const responseHandler = (response: AxiosResponse) => {
|
||||||
// if (response.headers['spinner-state'] === undefined) {
|
/* spinner 조작 커스텀이 필요한 경우 api 응답 헤더에 spinner-state: true 추가 */
|
||||||
useSpinnerStore.getState().setIsShow(false)
|
if (!response.headers['spinner-state']) {
|
||||||
// }
|
useSpinnerStore.getState().setIsShow(false)
|
||||||
|
}
|
||||||
response.data = transferResponse(response)
|
response.data = transferResponse(response)
|
||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user