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 (
-
@@ -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 페이지
+
+ setSelectedCategory(e.target.value)}>
+
+ {suitableCommCode.get(SUITABLE_HEAD_CODE.ROOF_MT_CD)?.map((category: CommCode, index: number) => (
+
+ ))}
+
+
+
+
+ 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