diff --git a/README.md b/README.md index 950676f..72a29f7 100644 --- a/README.md +++ b/README.md @@ -58,3 +58,20 @@ session에 있는 role 키로 구분한다 session.role === 'Partner' - 이외의 경우 -> 굳이 체크할 필요 없어보임\ session.role === 'User' + + +# 지붕재 적합성 TODO + +``` +const suitableCheck = (value: string) => { + if (value === '×') { + return + } else if (value === 'ー') { + return + } else { + return + } + } +``` + +- 추후 지붕재 적합성 데이터 CUD 구현 시 ×, ー 데이터 관리 필요 diff --git a/src/app/api/comm-code/route.ts b/src/app/api/comm-code/route.ts new file mode 100644 index 0000000..6b4852c --- /dev/null +++ b/src/app/api/comm-code/route.ts @@ -0,0 +1,44 @@ +import { NextRequest, NextResponse } from 'next/server' +import { prisma } from '@/libs/prisma' +import type { CommCode } from '@/types/CommCode' + +export async function GET(request: NextRequest) { + try { + const searchParams = request.nextUrl.searchParams + const headCode = searchParams.get('headCode') + + // @ts-ignore + const headCd = await prisma.BC_COMM_H.findFirst({ + where: { + HEAD_ID: headCode, + }, + select: { + HEAD_CD: true, + }, + }) + + if (!headCd) { + return NextResponse.json({ error: `${headCode}를 찾을 수 없습니다` }, { status: 404 }) + } + + // @ts-ignore + const roofMaterials: CommCode[] = await prisma.BC_COMM_L.findMany({ + where: { + HEAD_CD: headCd.HEAD_CD, + }, + select: { + HEAD_CD: true, + CODE: true, + CODE_JP: true, + }, + orderBy: { + CODE: 'asc', + }, + }) + + return NextResponse.json(roofMaterials) + } catch (error) { + console.error('❌ 데이터 조회 중 오류가 발생했습니다:', error) + return NextResponse.json({ error: '데이터 조회 중 오류가 발생했습니다' }, { status: 500 }) + } +} diff --git a/src/app/api/suitable/category/route.ts b/src/app/api/suitable/category/route.ts deleted file mode 100644 index 288a74a..0000000 --- a/src/app/api/suitable/category/route.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { NextResponse } from 'next/server' -import { prisma } from '@/libs/prisma' - -export async function GET() { - // @ts-ignore - const roofMaterialCategory = await prisma.MS_SUITABLE.findMany({ - select: { - roof_material: true, - }, - distinct: ['roof_material'], - orderBy: { - roof_material: 'asc', - }, - }) - return NextResponse.json(roofMaterialCategory) -} diff --git a/src/app/api/suitable/detail/route.ts b/src/app/api/suitable/detail/route.ts deleted file mode 100644 index d29f666..0000000 --- a/src/app/api/suitable/detail/route.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { NextResponse } from 'next/server' -import { prisma } from '@/libs/prisma' - -export async function GET(request: Request) { - const { searchParams } = new URL(request.url) - const roofMaterial = searchParams.get('roof-material') - console.log('🚀 ~ GET ~ roof-material:', roofMaterial) - - // @ts-ignore - const suitables = await prisma.MS_SUITABLE.findMany({ - where: { - roof_material: roofMaterial, - }, - }) - - return NextResponse.json(suitables) -} diff --git a/src/app/api/suitable/list/route.ts b/src/app/api/suitable/list/route.ts index d789275..32eac33 100644 --- a/src/app/api/suitable/list/route.ts +++ b/src/app/api/suitable/list/route.ts @@ -1,5 +1,6 @@ import { NextRequest, NextResponse } from 'next/server' import { prisma } from '@/libs/prisma' +import { SUITABLE_HEAD_CODE, type SuitableMain } from '@/types/Suitable' export async function GET(request: NextRequest) { try { @@ -7,26 +8,75 @@ export async function GET(request: NextRequest) { const category = searchParams.get('category') const keyword = searchParams.get('keyword') - let whereCondition: any = {} + let MainWhereCondition: any = {} + const whereCondition: string[] = [] + const params: string[] = [] if (category) { - whereCondition['roof_material'] = category + whereCondition.push(`${SUITABLE_HEAD_CODE.ROOF_MT_CD} = @P1`) + params.push(category) + MainWhereCondition[SUITABLE_HEAD_CODE.ROOF_MT_CD] = category } if (keyword) { - whereCondition['product_name'] = { + whereCondition.push('PRODUCT_NAME LIKE @P2') + params.push(`%${keyword}%`) + MainWhereCondition['PRODUCT_NAME'] = { contains: keyword, } } - console.log('🚀 ~ /api/suitable/list: ~ prisma where condition:', whereCondition) + const startTime = performance.now() + console.log(`쿼리 (main table) 시작 시간: ${startTime}ms`) // @ts-ignore - const suitables = await prisma.MS_SUITABLE.findMany({ - where: whereCondition, + const suitable = await prisma.MS_SUITABLE_MAIN.findMany({ + select: { + ID: true, + PRODUCT_NAME: true, + ROOF_MT_CD: true, + }, + where: MainWhereCondition, orderBy: { - product_name: 'asc', + PRODUCT_NAME: 'asc', }, }) - return NextResponse.json(suitables) + const endTime = performance.now() + console.log(`쿼리 (main table) 종료 시간: ${endTime - startTime}ms`) + + const mainIds: number[] = suitable.map((item: SuitableMain) => item.id) + + + const startTime2 = performance.now() + console.log(`쿼리 (detail table) 시작 시간: ${startTime2}ms`) + let detailQuery = ` + SELECT + msd.main_id + , ( + SELECT + msd_json.id + , msd_json.trestle_mfpc_cd + , msd_json.trestle_manufacturer_product_name + , msd_json.memo + FROM ms_suitable_detail msd_json + WHERE msd.main_id = msd_json.main_id + FOR JSON PATH + ) AS detail + FROM ms_suitable_detail msd + -- WHERE 1=1 + GROUP BY msd.main_id + ` + if (whereCondition.length > 0) { + detailQuery = detailQuery.replace('-- WHERE 1=1', `WHERE msd.main_id IN @P1`) + } + // @ts-ignore + const detail = await prisma.$queryRawUnsafe(detailQuery, ...mainIds) + + const endTime2 = performance.now() + console.log(`쿼리 (detail table) 종료 시간: ${endTime2 - startTime2}ms`) + + const endTime3 = performance.now() + console.log(`쿼리 총 실행 시간: ${endTime3 - startTime}ms`) + + return NextResponse.json({ suitable, detail }) } catch (error) { console.error('❌ 데이터 조회 중 오류가 발생했습니다:', error) return NextResponse.json({ error: '데이터 조회 중 오류가 발생했습니다' }, { status: 500 }) diff --git a/src/app/api/suitable/list/test/route.ts b/src/app/api/suitable/list/test/route.ts new file mode 100644 index 0000000..e4688bd --- /dev/null +++ b/src/app/api/suitable/list/test/route.ts @@ -0,0 +1,71 @@ +import { NextRequest, NextResponse } from 'next/server' +import { prisma } from '@/libs/prisma' +import { SUITABLE_HEAD_CODE, type Suitable } from '@/types/Suitable' + +export async function GET(request: NextRequest) { + try { + const searchParams = request.nextUrl.searchParams + const category = searchParams.get('category') + const keyword = searchParams.get('keyword') + + const whereCondition: string[] = [] + const params: string[] = [] + if (category) { + whereCondition.push(`${SUITABLE_HEAD_CODE.ROOF_MT_CD} = @P1`) + params.push(category) + } + if (keyword) { + whereCondition.push('PRODUCT_NAME LIKE @P2') + params.push(`%${keyword}%`) + } + + const startTime = performance.now() + console.log(`쿼리 시작 시간: ${startTime}ms`) + + let query = ` + SELECT + msm.id + , msm.product_name + , msm.manu_ft_cd + , msm.roof_mt_cd + , msm.roof_sh_cd + , details.detail + FROM ms_suitable_main msm + LEFT JOIN ( + SELECT + msd.main_id + , ( + SELECT + msd_json.id + , msd_json.trestle_mfpc_cd + , msd_json.trestle_manufacturer_product_name + , msd_json.memo + FROM ms_suitable_detail msd_json + WHERE msd.main_id = msd_json.main_id + FOR JSON PATH + ) AS detail + FROM ms_suitable_detail msd + GROUP BY msd.main_id + ) AS details + ON msm.id = details.main_id + -- AND details.main_id IN (#mainIds) + -- WHERE 1=1 + ORDER BY msm.product_name` + + // 검색 조건 추가 + if (whereCondition.length > 0) { + query = query.replace('-- WHERE 1=1', `WHERE ${whereCondition.join(' AND ')}`) + } + + // @ts-ignore + const suitable: Suitable[] = await prisma.$queryRawUnsafe(query, ...params) + + const endTime = performance.now() + console.log(`쿼리 실행 시간: ${endTime - startTime}ms`) + + return NextResponse.json(suitable) + } catch (error) { + console.error('❌ 데이터 조회 중 오류가 발생했습니다:', error) + return NextResponse.json({ error: '데이터 조회 중 오류가 발생했습니다' }, { status: 500 }) + } +} diff --git a/src/app/suitable-test/layout.tsx b/src/app/suitable-test/layout.tsx new file mode 100644 index 0000000..e5e7c3f --- /dev/null +++ b/src/app/suitable-test/layout.tsx @@ -0,0 +1,24 @@ +import type { ReactNode } from 'react' + +interface SuitableLayoutProps { + children: ReactNode +} + +export default function layout({ children }: SuitableLayoutProps) { + return ( + <> +
+
+
+
+
この適合表は参考資料として使用してください.
+
詳細やお問い合わせは1:1お問い合わせをご利用ください.
+
屋根材の選択or屋根材名を直接入力してください.
+
+
+ {children} +
+
+ + ) +} diff --git a/src/app/suitable-test/page.tsx b/src/app/suitable-test/page.tsx new file mode 100644 index 0000000..a5299fe --- /dev/null +++ b/src/app/suitable-test/page.tsx @@ -0,0 +1,9 @@ +import SuitableRaw from '@/components/suitable/SuitableRaw' + +export default function page() { + return ( + <> + + + ) +} diff --git a/src/components/suitable/Suitable.tsx b/src/components/suitable/Suitable.tsx index f9f6615..36a397f 100644 --- a/src/components/suitable/Suitable.tsx +++ b/src/components/suitable/Suitable.tsx @@ -1,28 +1,70 @@ 'use client' -import { useState } from 'react' -import SuitableCheckData from './SuitableCheckData' -import SuitableNoData from './SuitableNoData' import Image from 'next/image' +import { useEffect, useState } from 'react' +import SuitableList from './SuitableList' +import { useSuitable } from '@/hooks/useSuitable' +import { useSuitableStore } from '@/store/useSuitableStore' +import type { CommCode } from '@/types/CommCode' +import { SUITABLE_HEAD_CODE } from '@/types/Suitable' export default function Suitable() { - const [reference, setReference] = useState(false) + const [reference, setReference] = useState(true) + + const { getSuitableCommCode, refetchBySearch } = useSuitable() + const { suitableCommCode, selectedCategory, setSelectedCategory, searchValue, setSearchValue, setIsSearch, clearSelectedItems } = useSuitableStore() + + const handleInputSearch = async () => { + if (!searchValue.trim()) { + alert('屋根材の製品名を入力してください。') + return + } + setIsSearch(true) + refetchBySearch() + } + + const handleInputClear = () => { + setSearchValue('') + setIsSearch(false) + refetchBySearch() + } + + useEffect(() => { + refetchBySearch() + }, [selectedCategory]) + + useEffect(() => { + getSuitableCommCode() + return () => { + setSelectedCategory('') + setSearchValue('') + clearSelectedItems() + } + }, []) return (
- setSelectedCategory(e.target.value)}> + {suitableCommCode.get(SUITABLE_HEAD_CODE.ROOF_MT_CD)?.map((category: CommCode, index: number) => ( + + ))}
- - + setSearchValue(e.target.value)} + /> + {searchValue &&
@@ -68,37 +110,8 @@ export default function Suitable() {
- {/* checkData */} - {/* 데이터 없을경우 버튼 영역 안보여야함 */} - - - - - - {/* 데이터 없을경우 버튼 영역 안보여야함 */} -
-
-
- -
-
- -
-
- -
-
-
+
- - {/* 검색기록 없을떄 위에 두 영역 안보이고 이 부분만 보여야 함*/} - {/* */} ) } diff --git a/src/components/suitable/SuitableButton.tsx b/src/components/suitable/SuitableButton.tsx new file mode 100644 index 0000000..f412c89 --- /dev/null +++ b/src/components/suitable/SuitableButton.tsx @@ -0,0 +1,25 @@ +'use client' + +export default function SuitableButton() { + return ( +
+
+
+ +
+
+ +
+
+ +
+
+
+ ) +} diff --git a/src/components/suitable/SuitableCheckData.tsx b/src/components/suitable/SuitableCheckData.tsx deleted file mode 100644 index 2a57c21..0000000 --- a/src/components/suitable/SuitableCheckData.tsx +++ /dev/null @@ -1,81 +0,0 @@ -'use client' - -import Image from 'next/image' - -export default function SuitableCheckData() { - return ( - <> -
-
-
- - -
-
- -
-
-
    -
  • -
    -
    - - -
    -
    -
    - -
    -
    -
    -
  • -
  • -
    -
    - - -
    -
    -
    - -
    -
    - -
    -
    -
    -
  • -
  • -
    -
    - - -
    -
    -
    - -
    -
    - -
    -
    -
    -
  • -
  • -
    -
    - - -
    -
    -
    - -
    -
    -
    -
  • -
-
- - ) -} diff --git a/src/components/suitable/SuitableList.tsx b/src/components/suitable/SuitableList.tsx new file mode 100644 index 0000000..18d94e5 --- /dev/null +++ b/src/components/suitable/SuitableList.tsx @@ -0,0 +1,174 @@ +'use client' + +import Image from 'next/image' +import { useState, useEffect, useRef, useCallback, useMemo } from 'react' +import SuitableButton from './SuitableButton' +import SuitableNoData from './SuitableNoData' +import { useSuitable } from '@/hooks/useSuitable' +import { useSuitableStore } from '@/store/useSuitableStore' +import { SUITABLE_HEAD_CODE, type SuitableMain, type SuitableDetail } from '@/types/Suitable' + +// 한 번에 로드할 아이템 수 +const ITEMS_PER_PAGE = 100 + +export default function SuitableList() { + const { toCodeName, suitableSearchResults, isSearchLoading, toSuitableDetail } = useSuitable() + const { selectedItems, addSelectedItem, removeSelectedItem } = useSuitableStore() + + const [openItems, setOpenItems] = useState>(new Set()) + const [visibleItems, setVisibleItems] = useState([]) + const [page, setPage] = useState(1) + const [isLoadingMore, setIsLoadingMore] = useState(false) + const observerTarget = useRef(null) + + // 선택된 아이템 확인 함수 메모이제이션 + const isItemSelected = useCallback( + (itemId: number) => { + return selectedItems.some((selected) => selected === itemId) + }, + [selectedItems], + ) + + // 초기 데이터 로드 + useEffect(() => { + if (suitableSearchResults) { + const initialItems = suitableSearchResults.suitable.slice(0, ITEMS_PER_PAGE) + setVisibleItems(initialItems) + setPage(1) + } + }, [suitableSearchResults]) + + // Intersection Observer 설정 + useEffect(() => { + const observer = new IntersectionObserver( + (entries) => { + if (entries[0].isIntersecting && suitableSearchResults && !isLoadingMore) { + const nextPage = page + 1 + const startIndex = (nextPage - 1) * ITEMS_PER_PAGE + const endIndex = startIndex + ITEMS_PER_PAGE + const nextItems = suitableSearchResults.suitable.slice(startIndex, endIndex) + + if (nextItems.length > 0) { + setIsLoadingMore(true) + setVisibleItems((prev) => [...prev, ...nextItems]) + setPage(nextPage) + setIsLoadingMore(false) + } + } + }, + { + threshold: 0.2, + }, + ) + + if (observerTarget.current) { + observer.observe(observerTarget.current) + } + + return () => observer.disconnect() + }, [page, suitableSearchResults, isLoadingMore]) + + const handleItemClick = useCallback( + (itemId: number) => { + isItemSelected(itemId) ? removeSelectedItem(itemId) : addSelectedItem(itemId) + }, + [isItemSelected, addSelectedItem, removeSelectedItem], + ) + + const toggleItemOpen = useCallback((itemId: number) => { + setOpenItems((prev) => { + const newOpenItems = new Set(prev) + newOpenItems.has(itemId) ? newOpenItems.delete(itemId) : newOpenItems.add(itemId) + return newOpenItems + }) + }, []) + + // TODO: 추후 지붕재 적합성 데이터 CUD 구현 시 ×, ー 데이터 관리 필요 + const suitableCheck = useCallback((value: string) => { + if (value === '×') { + return ( +
+ +
+ ) + } else if (value === 'ー') { + return ( +
+ +
+ ) + } else { + return ( +
+ +
+ ) + } + }, []) + + // 메모이제이션된 아이템 렌더링 + const renderItem = useCallback( + (item: SuitableMain) => { + const isSelected = isItemSelected(item.id) + const isOpen = openItems.has(item.id) + + return ( +
+
+
+ handleItemClick(item.id)} /> + +
+
+ +
+
+
    + {toSuitableDetail(item.id).map((subItem: SuitableDetail) => ( +
  • +
    +
    + + +
    +
    + {suitableCheck(subItem.trestleManufacturerProductName)} + {subItem.memo && ( +
    + +
    + )} +
    +
    +
  • + ))} +
+
+ ) + }, + [isItemSelected, openItems, handleItemClick, toggleItemOpen, suitableCheck, toCodeName, toSuitableDetail], + ) + + // 메모이제이션된 아이템 리스트 + const renderedItems = useMemo(() => { + return visibleItems.map(renderItem) + }, [visibleItems, renderItem]) + + if (isSearchLoading) { + return
Loading...
+ } + + if (!suitableSearchResults?.suitable.length) { + return + } + + return ( + <> + {renderedItems} +
+ {isLoadingMore &&
데이터를 불러오는 중...
} +
+ + + ) +} diff --git a/src/components/suitable/SuitableListRaw.tsx b/src/components/suitable/SuitableListRaw.tsx new file mode 100644 index 0000000..6dc7f36 --- /dev/null +++ b/src/components/suitable/SuitableListRaw.tsx @@ -0,0 +1,173 @@ +'use client' + +import Image from 'next/image' +import { useState, useEffect, useRef, useCallback, useMemo } from 'react' +import SuitableButton from './SuitableButton' +import SuitableNoData from './SuitableNoData' +import { useSuitableRaw, type Suitable } from '@/hooks/useSuitableRaw' +import { useSuitableStore } from '@/store/useSuitableStore' +import { SUITABLE_HEAD_CODE, type SuitableDetail } from '@/types/Suitable' + +// 한 번에 로드할 아이템 수 +const ITEMS_PER_PAGE = 100 + +export default function SuitableListRaw() { + const { toCodeName, suitableSearchResults, isSearchLoading, toSuitableDetail } = useSuitableRaw() + const { selectedItems, addSelectedItem, removeSelectedItem } = useSuitableStore() + const [openItems, setOpenItems] = useState>(new Set()) + const [visibleItems, setVisibleItems] = useState([]) + const [page, setPage] = useState(1) + const [isLoadingMore, setIsLoadingMore] = useState(false) + const observerTarget = useRef(null) + + // 선택된 아이템 확인 함수 메모이제이션 + const isItemSelected = useCallback( + (itemId: number) => { + return selectedItems.some((selected) => selected === itemId) + }, + [selectedItems], + ) + + // 초기 데이터 로드 + useEffect(() => { + if (suitableSearchResults) { + const initialItems = suitableSearchResults.slice(0, ITEMS_PER_PAGE) + setVisibleItems(initialItems) + setPage(1) + } + }, [suitableSearchResults]) + + // Intersection Observer 설정 + useEffect(() => { + const observer = new IntersectionObserver( + (entries) => { + if (entries[0].isIntersecting && suitableSearchResults && !isLoadingMore) { + const nextPage = page + 1 + const startIndex = (nextPage - 1) * ITEMS_PER_PAGE + const endIndex = startIndex + ITEMS_PER_PAGE + const nextItems = suitableSearchResults.slice(startIndex, endIndex) + + if (nextItems.length > 0) { + setIsLoadingMore(true) + setVisibleItems((prev) => [...prev, ...nextItems]) + setPage(nextPage) + setIsLoadingMore(false) + } + } + }, + { + threshold: 0.2, + }, + ) + + if (observerTarget.current) { + observer.observe(observerTarget.current) + } + + return () => observer.disconnect() + }, [page, suitableSearchResults, isLoadingMore]) + + const handleItemClick = useCallback( + (itemId: number) => { + isItemSelected(itemId) ? removeSelectedItem(itemId) : addSelectedItem(itemId) + }, + [isItemSelected, addSelectedItem, removeSelectedItem], + ) + + const toggleItemOpen = useCallback((itemId: number) => { + setOpenItems((prev) => { + const newOpenItems = new Set(prev) + newOpenItems.has(itemId) ? newOpenItems.delete(itemId) : newOpenItems.add(itemId) + return newOpenItems + }) + }, []) + + // TODO: 추후 지붕재 적합성 데이터 CUD 구현 시 ×, ー 데이터 관리 필요 + const suitableCheck = useCallback((value: string) => { + if (value === '×') { + return ( +
+ +
+ ) + } else if (value === 'ー') { + return ( +
+ +
+ ) + } else { + return ( +
+ +
+ ) + } + }, []) + + // 메모이제이션된 아이템 렌더링 + const renderItem = useCallback( + (item: Suitable) => { + const isSelected = isItemSelected(item.id) + const isOpen = openItems.has(item.id) + + return ( +
+
+
+ handleItemClick(item.id)} /> + +
+
+ +
+
+
    + {toSuitableDetail(item.detail).map((subItem: SuitableDetail) => ( +
  • +
    +
    + + +
    +
    + {suitableCheck(subItem.trestleManufacturerProductName)} + {subItem.memo && ( +
    + +
    + )} +
    +
    +
  • + ))} +
+
+ ) + }, + [isItemSelected, openItems, handleItemClick, toggleItemOpen, suitableCheck, toCodeName, toSuitableDetail], + ) + + // 메모이제이션된 아이템 리스트 + const renderedItems = useMemo(() => { + return visibleItems.map(renderItem) + }, [visibleItems, renderItem]) + + if (isSearchLoading) { + return
Loading...
+ } + + if (!suitableSearchResults?.length) { + return + } + + return ( + <> + {renderedItems} +
+ {isLoadingMore &&
데이터를 불러오는 중...
} +
+ + + ) +} diff --git a/src/components/suitable/SuitableRaw.tsx b/src/components/suitable/SuitableRaw.tsx new file mode 100644 index 0000000..d48dfea --- /dev/null +++ b/src/components/suitable/SuitableRaw.tsx @@ -0,0 +1,118 @@ +'use client' + +import Image from 'next/image' +import { useEffect, useState } from 'react' +import SuitableListRaw from './SuitableListRaw' +import { useSuitableRaw } from '@/hooks/useSuitableRaw' +import { useSuitableStore } from '@/store/useSuitableStore' +import type { CommCode } from '@/types/CommCode' +import { SUITABLE_HEAD_CODE } from '@/types/Suitable' + +export default function SuitableRaw() { + const [reference, setReference] = useState(true) + + const { getSuitableCommCode, refetchBySearch } = useSuitableRaw() + const { suitableCommCode, selectedCategory, setSelectedCategory, searchValue, setSearchValue, setIsSearch, clearSelectedItems } = useSuitableStore() + + const handleInputSearch = async () => { + if (!searchValue.trim()) { + alert('屋根材の製品名を入力してください。') + return + } + setIsSearch(true) + refetchBySearch() + } + + const handleInputClear = () => { + setSearchValue('') + setIsSearch(false) + refetchBySearch() + } + + useEffect(() => { + refetchBySearch() + }, [selectedCategory]) + + useEffect(() => { + getSuitableCommCode() + return () => { + setSelectedCategory('') + setSearchValue('') + clearSelectedItems() + } + }, []) + + return ( +
+ 테스트1 페이지 +
+ +
+
+
+ setSearchValue(e.target.value)} + /> + {searchValue &&
+
+
+
+
+
凡例
+
+ +
+
+
    +
  • +
    +
    + +
    + 設置可能 +
    +
  • +
  • +
    +
    + +
    + 設置不可 +
    +
  • +
  • +
    +
    + +
    + お問い合わせ +
    +
  • +
  • +
    +
    + +
    + 備考 +
    +
  • +
+
+ +
+
+ ) +} diff --git a/src/hooks/useCommCode.ts b/src/hooks/useCommCode.ts new file mode 100644 index 0000000..bb50240 --- /dev/null +++ b/src/hooks/useCommCode.ts @@ -0,0 +1,18 @@ +import { axiosInstance } from '@/libs/axios' +import type { CommCode } from '@/types/CommCode' + +export function useCommCode() { + const getCommCode = async (headCode: string): Promise => { + try { + const response = await axiosInstance(null).get('/api/comm-code', { params: { headCode: headCode } }) + return response.data + } catch (error) { + console.error(`common code (${headCode}) load failed:`, error) + return [] + } + } + + return { + getCommCode, + } +} diff --git a/src/hooks/useSuitable.ts b/src/hooks/useSuitable.ts index c3f0dde..4bdd9b2 100644 --- a/src/hooks/useSuitable.ts +++ b/src/hooks/useSuitable.ts @@ -1,30 +1,107 @@ -import { suitableApi } from '@/api/suitable' +import { useQuery } from '@tanstack/react-query' +import { axiosInstance, transformObjectKeys } from '@/libs/axios' +import { useSuitableStore } from '@/store/useSuitableStore' +import { useCommCode } from './useCommCode' +import { SUITABLE_HEAD_CODE, type SuitableDetailGroup, type SuitableMain, type Suitable, type SuitableDetail } from '@/types/Suitable' export function useSuitable() { - const getCategories = async () => { + const { getCommCode } = useCommCode() + const { selectedCategory, searchValue, suitableCommCode, setSuitableCommCode, isSearch } = useSuitableStore() + + const getSuitables = async (): Promise => { try { - // return await suitableApi.getCategory() + const response = await axiosInstance(null).get('/api/suitable/list') + return response.data } catch (error) { - console.error('카테고리 데이터 로드 실패:', error) + console.error('지붕재 데이터 로드 실패:', error) + return { suitable: [], detail: [] } + } + } + + // const updateSearchResults = async (selectedCategory: string | undefined, searchValue: string | undefined): Promise => { + // try { + // const response = await axiosInstance(null).get('/api/suitable/list', { params: { selectedCategory, searchValue } }) + // return response.data + // } catch (error) { + // console.error('지붕재 데이터 검색 실패:', error) + // return [] + // } + // } + + const getSuitableCommCode = () => { + const headCodes = Object.values(SUITABLE_HEAD_CODE) as SUITABLE_HEAD_CODE[] + for (const code of headCodes) { + getCommCode(code).then((res) => { + setSuitableCommCode(code, res) + }) + } + } + + const toCodeName = (headCode: string, code: string): string => { + const commCode = suitableCommCode.get(headCode) + return commCode?.find((item) => item.code === code)?.codeJp || '' + } + + const toSuitableDetail = (mainId: number): SuitableDetail[] => { + try { + const suitableDetailString = suitableList?.detail.find((item) => item.mainId === mainId)?.detail + if (!suitableDetailString) { + return [] + } + const suitableDetailArray = transformObjectKeys(JSON.parse(suitableDetailString)) as SuitableDetail[] + if (!Array.isArray(suitableDetailArray)) { + throw new Error('suitableDetailArray is not an array') + } + return suitableDetailArray + } catch (error) { + console.error('지붕재 데이터 파싱 실패:', error) return [] } } - const getSuitables = async () => { - try { - // return await suitableApi.getList() - } catch (error) { - console.error('지붕재 데이터 로드 실패:', error) - } - } + const { data: suitableList, isLoading: isInitialLoading } = useQuery({ + queryKey: ['suitables', 'list'], + queryFn: async () => await getSuitables(), + staleTime: 1000 * 60 * 10, // 10분 + gcTime: 1000 * 60 * 10, // 10분 + }) - const updateSearchResults = async (selectedCategory: string | undefined, searchValue: string | undefined) => { - try { - // return await suitableApi.getList(selectedCategory, searchValue) - } catch (error) { - console.error('지붕재 데이터 검색 실패:', error) - } - } + const { + data: suitableSearchResults, + refetch: refetchBySearch, + isLoading: isSearchLoading, + } = useQuery({ + queryKey: ['suitables', 'search', selectedCategory, isSearch], + queryFn: async () => { + if (!isSearch && !selectedCategory) { + // 검색 상태가 아니면 초기 데이터 반환 임시처리 + return isInitialLoading ? await getSuitables() : suitableList ?? { suitable: [], detail: [] } + } else { + const filteredSuitable = suitableList?.suitable.filter((item: SuitableMain) => { + const categoryMatch = !selectedCategory || item.roofMtCd === selectedCategory + const searchMatch = !searchValue || item.productName.includes(searchValue) + return categoryMatch && searchMatch + }) ?? [] + const mainIds = filteredSuitable.map((item: SuitableMain) => item.id) + const filteredDetail = suitableList?.detail.filter((item: SuitableDetailGroup) => { + return mainIds.includes(item.mainId) + }) ?? [] + return { suitable: filteredSuitable, detail: filteredDetail } + } + }, + staleTime: 1000 * 60 * 10, + gcTime: 1000 * 60 * 10, + enabled: true, + }) - return { getCategories, getSuitables, updateSearchResults } + return { + getSuitables, + getSuitableCommCode, + toCodeName, + toSuitableDetail, + suitableList, + suitableSearchResults, + refetchBySearch, + isSearchLoading, + } } diff --git a/src/hooks/useSuitableRaw.ts b/src/hooks/useSuitableRaw.ts new file mode 100644 index 0000000..a962244 --- /dev/null +++ b/src/hooks/useSuitableRaw.ts @@ -0,0 +1,109 @@ +import { useQuery } from '@tanstack/react-query' +import { axiosInstance, transformObjectKeys } from '@/libs/axios' +import { useSuitableStore } from '@/store/useSuitableStore' +import { useCommCode } from './useCommCode' +import { SUITABLE_HEAD_CODE, type SuitableDetail } from '@/types/Suitable' + +export type Suitable = { + id: number + productName: string + manuFtCd: string + roofMtCd: string + roofShCd: string + detail: string +} + +export function useSuitableRaw() { + const { getCommCode } = useCommCode() + const { selectedCategory, searchValue, suitableCommCode, setSuitableCommCode, isSearch } = useSuitableStore() + + const getSuitables = async (): Promise => { + try { + const response = await axiosInstance(null).get('/api/suitable/list/test') + return response.data + } catch (error) { + console.error('지붕재 데이터 로드 실패:', error) + return [] + } + } + + // const updateSearchResults = async (selectedCategory: string | undefined, searchValue: string | undefined): Promise => { + // try { + // const response = await axiosInstance(null).get('/api/suitable/list', { params: { selectedCategory, searchValue } }) + // return response.data + // } catch (error) { + // console.error('지붕재 데이터 검색 실패:', error) + // return [] + // } + // } + + const getSuitableCommCode = () => { + const headCodes = Object.values(SUITABLE_HEAD_CODE) as SUITABLE_HEAD_CODE[] + for (const code of headCodes) { + getCommCode(code).then((res) => { + setSuitableCommCode(code, res) + }) + } + } + + const toCodeName = (headCode: string, code: string): string => { + const commCode = suitableCommCode.get(headCode) + return commCode?.find((item) => item.code === code)?.codeJp || '' + } + + const toSuitableDetail = (suitableDetailString: string): SuitableDetail[] => { + try { + const suitableDetailArray = transformObjectKeys(JSON.parse(suitableDetailString)) as SuitableDetail[] + if (!Array.isArray(suitableDetailArray)) { + throw new Error('suitableDetailArray is not an array') + } + return suitableDetailArray + } catch (error) { + console.error('지붕재 데이터 파싱 실패:', error) + return [] + } + } + + const { data: suitableList, isLoading: isInitialLoading } = useQuery({ + queryKey: ['suitables', 'list'], + queryFn: async () => await getSuitables(), + staleTime: 1000 * 60 * 10, // 10분 + gcTime: 1000 * 60 * 10, // 10분 + }) + + const { + data: suitableSearchResults, + refetch: refetchBySearch, + isLoading: isSearchLoading, + // } = useQuery({ + } = useQuery({ + queryKey: ['suitables', 'search', selectedCategory, isSearch], + queryFn: async () => { + if (!isSearch && !selectedCategory) { + return isInitialLoading ? await getSuitables() : suitableList ?? [] // 검색 상태가 아니면 초기 데이터 반환 임시처리 + } else { + return ( + suitableList?.filter((item: Suitable) => { + const categoryMatch = !selectedCategory || item.roofMtCd === selectedCategory + const searchMatch = !searchValue || item.productName.includes(searchValue) + return categoryMatch && searchMatch + }) ?? [] + ) + } + }, + staleTime: 1000 * 60 * 10, + gcTime: 1000 * 60 * 10, + enabled: true, + }) + + return { + getSuitables, + getSuitableCommCode, + toCodeName, + toSuitableDetail, + suitableList, + suitableSearchResults, + refetchBySearch, + isSearchLoading, + } +} diff --git a/src/libs/axios.ts b/src/libs/axios.ts index 6b5fbbc..a5d355c 100644 --- a/src/libs/axios.ts +++ b/src/libs/axios.ts @@ -68,7 +68,7 @@ export const transferResponse = (response: any) => { } // camel case object 반환 -const transformObjectKeys = (obj: any): any => { +export const transformObjectKeys = (obj: any): any => { if (Array.isArray(obj)) { return obj.map(transformObjectKeys) } diff --git a/src/store/useSuitableStore.ts b/src/store/useSuitableStore.ts index 17b88c5..5fa4cd0 100644 --- a/src/store/useSuitableStore.ts +++ b/src/store/useSuitableStore.ts @@ -1,68 +1,71 @@ import { create } from 'zustand' -import { Suitable, suitableApi } from '@/api/suitable' +import type { CommCode } from '@/types/CommCode' interface SuitableState { - // // 검색 결과 리스트 - // searchResults: Suitable[] - // // 초기 데이터 로드 - // fetchInitializeData: () => Promise - // // 검색 결과 설정 - // setSearchResults: (results: Suitable[]) => void - // // 검색 결과 초기화 - // resetSearchResults: () => void + /* 공통코드 */ + suitableCommCode: Map + /* 공통코드 설정 */ + setSuitableCommCode: (headCode: string, commCode: CommCode[]) => void - // 선택된 아이템 리스트 - selectedItems: Suitable[] - // 선택된 아이템 추가 - addSelectedItem: (item: Suitable) => void - // 선택된 아이템 제거 + /* 검색 상태 */ + isSearch: boolean + /* 검색 상태 설정 */ + setIsSearch: (isSearch: boolean) => void + + /* 선택된 카테고리 */ + selectedCategory: string + /* 선택된 카테고리 설정 */ + setSelectedCategory: (category: string) => void + + /* 검색 값 */ + searchValue: string + /* 검색 값 설정 */ + setSearchValue: (value: string) => void + + /* 선택된 아이템 리스트 */ + selectedItems: number[] + /* 선택된 아이템 추가 */ + addSelectedItem: (itemId: number) => void + /* 선택된 아이템 제거 */ removeSelectedItem: (itemId: number) => void - // 선택된 아이템 모두 제거 + /* 선택된 아이템 모두 제거 */ clearSelectedItems: () => void } export const useSuitableStore = create((set) => ({ - // // 초기 상태 - // searchResults: [], + suitableCommCode: new Map() as Map, + isSearch: false as boolean, + selectedCategory: '' as string, + searchValue: '' as string, + selectedItems: [] as number[], - // // 초기 데이터 로드 - // fetchInitializeData: async () => { - // const suitables = await fetchInitialSuitablee() - // set({ searchResults: suitables }) - // }, - - // // 검색 결과 설정 - // setSearchResults: (results) => set({ searchResults: results }), - - // // 검색 결과 초기화 - // resetSearchResults: () => set({ searchResults: [] }), - - // 초기 상태 - selectedItems: [], - - // 선택된 아이템 추가 (중복 방지) - addSelectedItem: (item) => + /* 공통코드 설정 */ + setSuitableCommCode: (headCode: string, commCode: CommCode[]) => set((state) => ({ - selectedItems: state.selectedItems.some((i) => i.id === item.id) ? state.selectedItems : [...state.selectedItems, item], + suitableCommCode: new Map(state.suitableCommCode).set(headCode, commCode), })), - // 선택된 아이템 제거 - removeSelectedItem: (itemId) => + /* 검색 상태 설정 */ + setIsSearch: (isSearch: boolean) => set({ isSearch }), + + /* 선택된 카테고리 설정 */ + setSelectedCategory: (category: string) => set({ selectedCategory: category }), + + /* 검색 값 설정 */ + setSearchValue: (value: string) => set({ searchValue: value }), + + /* 선택된 아이템 추가 */ + addSelectedItem: (itemId: number) => set((state) => ({ - selectedItems: state.selectedItems.filter((item) => item.id !== itemId), + selectedItems: state.selectedItems.some((i) => i === itemId) ? state.selectedItems : [...state.selectedItems, itemId], })), - // 선택된 아이템 모두 제거 + /* 선택된 아이템 제거 */ + removeSelectedItem: (itemId: number) => + set((state) => ({ + selectedItems: state.selectedItems.filter((i) => i !== itemId), + })), + + /* 선택된 아이템 모두 제거 */ clearSelectedItems: () => set({ selectedItems: [] }), })) - -// // 전체 데이터 초기화 함수 -// async function fetchInitialSuitablee() { -// try { -// const suitable = await suitableApi.getList() -// return suitable -// } catch (error) { -// console.error('초기 데이터 로드 실패:', error) -// return [] -// } -// } diff --git a/src/types/CommCode.ts b/src/types/CommCode.ts new file mode 100644 index 0000000..5847047 --- /dev/null +++ b/src/types/CommCode.ts @@ -0,0 +1,5 @@ +export type CommCode = { + headCd: string + code: string + codeJp: string +} diff --git a/src/types/Suitable.ts b/src/types/Suitable.ts new file mode 100644 index 0000000..2e3563b --- /dev/null +++ b/src/types/Suitable.ts @@ -0,0 +1,44 @@ +export enum SUITABLE_HEAD_CODE { + /* 지붕재 제조사명 */ + MANU_FT_CD = 'MANU_FT_CD', + /* 지붕재 종류 */ + ROOF_MT_CD = 'ROOF_MT_CD', + /* 마운팅 브래킷 종류 */ + ROOF_SH_CD = 'ROOF_SH_CD', + /* 마운팅 브래킷 제조사명 및 제품코드드 */ + TRESTLE_MFPC_CD = 'TRESTLE_MFPC_CD', +} + +export type SuitableMain = { + id: number + productName: string + manuFtCd: string + roofMtCd: string + roofShCd: string +} + +export type SuitableDetail = { + id: number + mainId: number + trestleMfpcCd: string + trestleManufacturerProductName: string + memo: string +} + +// export type Suitable = { +// id: number +// productName: string +// manuFtCd: string +// roofMtCd: string +// roofShCd: string +// detail: string +// } + +export type SuitableDetailGroup = { + mainId: number + detail: string +} +export type Suitable = { + suitable: SuitableMain[] + detail: SuitableDetailGroup[] +} \ No newline at end of file