Compare commits

..

No commits in common. "5feef03187185d21f15f83f8e308f9f98c684878" and "b36844303355504af49af3ca7634969b989d79e2" have entirely different histories.

5 changed files with 67 additions and 76 deletions

View File

@ -65,11 +65,9 @@ export async function GET(request: NextRequest) {
const suitable: Suitable[] = await prisma.$queryRawUnsafe(query, pageNumber, itemPerPage)
return NextResponse.json(suitable, {
headers: {
'spinner-state': 'true',
},
})
// console.log(`검색 조건 :::: 카테고리: ${category}, 키워드: ${keyword}`)
return NextResponse.json(suitable)
} catch (error) {
console.error('❌ 데이터 조회 중 오류가 발생했습니다:', error)
return NextResponse.json({ error: '데이터 조회 중 오류가 발생했습니다' }, { status: 500 })

View File

@ -6,7 +6,6 @@ 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() {
@ -16,7 +15,7 @@ export default function SuitableDetailPopup() {
const [openItems, setOpenItems] = useState<Set<number>>(new Set())
/* 아이템 열기/닫기 */
// 아이템 열기/닫기
const toggleItemOpen = useCallback((itemId: number) => {
setOpenItems((prev) => {
const newOpenItems = new Set(prev)
@ -25,11 +24,6 @@ export default function SuitableDetailPopup() {
})
}, [])
/* 데이터 로딩 상태 처리 */
useEffect(() => {
useSpinnerStore.getState().setIsShow(isSelectedSuitablesLoading)
}, [isSelectedSuitablesLoading])
useEffect(() => {
setSelectedItemsSearching(true)
}, [])
@ -51,56 +45,60 @@ export default function SuitableDetailPopup() {
</div>
<div className="modal-body">
<div className="compliance-check-pop-wrap">
{selectedSuitables?.map((item: Suitable) => (
<div className={`compliance-check-bx ${openItems.has(item.id) ? 'act' : ''}`} key={item.id}>
<div className="check-name-wrap">
<div className="check-name">{item.productName}</div>
<div className="check-name-btn">
<button className="bx-btn" onClick={() => toggleItemOpen(item.id)}></button>
{isSelectedSuitablesLoading ? (
<div>Loading...</div>
) : (
selectedSuitables?.map((item: Suitable) => (
<div className={`compliance-check-bx ${openItems.has(item.id) ? 'act' : ''}`} key={item.id}>
<div className="check-name-wrap">
<div className="check-name">{item.productName}</div>
<div className="check-name-btn">
<button className="bx-btn" onClick={() => toggleItemOpen(item.id)}></button>
</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-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={'/assets/images/sub/compliance_tip_icon.svg'} width={22} height={22} alt=""></Image>
<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 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 />
</div>

View File

@ -6,7 +6,6 @@ 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() {
@ -25,7 +24,7 @@ export default function SuitableList() {
const [openItems, setOpenItems] = useState<Set<number>>(new Set())
const observerTarget = useRef<HTMLDivElement>(null)
/* 선택된 아이템 확인 - 메인 하위 아이템 indeterminate 확인 */
// 선택된 아이템 확인 - 메인 하위 아이템 indeterminate 확인
const isMainIndeterminate = useMemo(
() => (mainId: number, detailCnt: number) => {
const mainItem = selectedItems.get(mainId)
@ -35,7 +34,7 @@ export default function SuitableList() {
[selectedItems],
)
/* 선택된 아이템 확인 */
// 선택된 아이템 확인
const isItemSelected = useCallback(
(mainId: number, detailId?: number): boolean => {
const mainItem = selectedItems.get(mainId)
@ -46,7 +45,7 @@ export default function SuitableList() {
[selectedItems],
)
/* 아이템 클릭 */
// 아이템 클릭
const handleItemClick = useCallback(
(mainId: number, detailId?: number, detailIds?: Set<number>): void => {
setSelectedItemsSearching(false)
@ -55,7 +54,7 @@ export default function SuitableList() {
[isItemSelected, addSelectedItem, removeSelectedItem],
)
/* 아이템 열기/닫기 */
// 아이템 열기/닫기
const toggleItemOpen = useCallback((itemId: number) => {
setOpenItems((prev) => {
const newOpenItems = new Set(prev)
@ -64,7 +63,7 @@ export default function SuitableList() {
})
}, [])
/* 아이템 렌더링 */
// 아이템 렌더링
const renderItem = useCallback(
(item: Suitable) => {
return (
@ -116,10 +115,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) => {
@ -129,7 +128,7 @@ export default function SuitableList() {
},
{
threshold: 0,
rootMargin: `${window.innerHeight * 0.2}px`,
rootMargin: '100px',
},
)
@ -140,18 +139,15 @@ export default function SuitableList() {
return () => observer.disconnect()
}, [hasNextPage, isFetchingNextPage, fetchNextPage])
/* 데이터 로딩 상태 처리 */
useEffect(() => {
useSpinnerStore.getState().setIsShow(isLoading || isFetchingNextPage)
}, [isLoading, isFetchingNextPage])
/* 조회 데이터 없는 경우 */
if (isLoading) return <div>Loading...</div>
if (!suitableList.length) return <SuitableNoData />
return (
<>
{suitableList.map(renderItem)}
<div ref={observerTarget} />
<div ref={observerTarget} className="loading-indicator">
{isFetchingNextPage && <div className="loading-more"> ...</div>}
</div>
<SuitableButton />
</>
)

View File

@ -111,7 +111,7 @@ export default function Header() {
</div>
</SwiperSlide>
<SwiperSlide>
<div className="side-swiper-card" onClick={() => router.push('/suitable')}>
<div className="side-swiper-card">
<div className="side-swiper-icon icon01"></div>
<div className="side-swiper-infor"> </div>
</div>

View File

@ -9,10 +9,9 @@ export function useAxios() {
}
const responseHandler = (response: AxiosResponse) => {
/* spinner 조작 커스텀이 필요한 경우 api 응답 헤더에 spinner-state: true 추가 */
if (!response.headers['spinner-state']) {
useSpinnerStore.getState().setIsShow(false)
}
// if (response.headers['spinner-state'] === undefined) {
useSpinnerStore.getState().setIsShow(false)
// }
response.data = transferResponse(response)
return response
}