From 5be7237aa634ecd204d90f3ebe18f00e7180555c Mon Sep 17 00:00:00 2001 From: Daseul Kim Date: Thu, 29 May 2025 10:07:05 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=A7=80=EB=B6=95=EC=9E=AC=EC=A0=81?= =?UTF-8?q?=ED=95=A9=EC=84=B1=20=EC=83=81=EC=84=B8=ED=8C=9D=EC=97=85=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20=ED=91=9C=EC=B6=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EB=B0=8F=20TODO=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 24 +-- src/app/api/suitable/route.ts | 30 ++-- src/components/popup/SuitableDetailPopup.tsx | 141 ++++++------------ .../popup/SuitableDetailPopupButton.tsx | 16 +- src/components/suitable/SuitableList.tsx | 32 ++-- src/hooks/useSuitable.ts | 36 ++++- 6 files changed, 132 insertions(+), 147 deletions(-) diff --git a/README.md b/README.md index 6d332e0..c6869cd 100644 --- a/README.md +++ b/README.md @@ -64,15 +64,21 @@ session에 있는 role 키로 구분한다 # 지붕재 적합성 TODO ``` -const suitableCheck = (value: string) => { - if (value === '×') { - return - } else if (value === 'ー') { - return - } else { - return - } +const suitableCheckIcon = (value: string): string => { + const iconMap: Record = { + '×': '/assets/images/sub/compliance_x_icon.svg', + 'ー': '/assets/images/sub/compliance_quest_icon.svg', + default: '/assets/images/sub/compliance_check_icon.svg', } + return iconMap[value] || iconMap.default +} +const suitableCheckMemo = (value: string): string => { + if (value === '○') return '設置可' + if (value === '×') return '設置不可' + if (value === 'ー') return 'お問い合わせください' + return `${value}で設置可` +} ``` -- 추후 지붕재 적합성 데이터 CUD 구현 시 ×, ー 데이터 관리 필요 +- src/hooks/useSuitable.ts > suitableCheckIcon(), suitableCheckMemo() + - 추후 지붕재 적합성 데이터 CUD 구현 시 ×, ー 데이터 관리 필요 diff --git a/src/app/api/suitable/route.ts b/src/app/api/suitable/route.ts index df42e1e..173b6ea 100644 --- a/src/app/api/suitable/route.ts +++ b/src/app/api/suitable/route.ts @@ -2,12 +2,15 @@ import { NextRequest, NextResponse } from 'next/server' import { prisma } from '@/libs/prisma' import { Suitable } from '@/types/Suitable' -export async function GET(request: NextRequest) { +export async function POST(request: NextRequest) { try { - const searchParams = request.nextUrl.searchParams + const body: Record = await request.json() + const ids = body.ids + const detailIds = body.detailIds - const ids = searchParams.get('ids') - const detailIds = searchParams.get('subIds') + if (ids === '' || detailIds === '') { + return NextResponse.json({ error: '필수 파라미터가 누락되었습니다' }, { status: 400 }) + } let query = ` SELECT @@ -29,28 +32,22 @@ export async function GET(request: NextRequest) { , msd_json.memo FROM ms_suitable_detail msd_json WHERE msd.main_id = msd_json.main_id + AND msd_json.id IN (:detailIds) FOR JSON PATH ) AS detail FROM ms_suitable_detail msd GROUP BY msd.main_id ) AS details ON msm.id = details.main_id - --ids AND details.main_id IN (:mainIds) - --detailIds AND details.id IN (:detailIds) - WHERE 1=1 - --ids AND msm.id IN (:mainIds) + AND details.main_id IN (:mainIds) + WHERE + msm.id IN (:mainIds) ORDER BY msm.product_name; ` // 검색 조건 설정 - if (ids) { - query = query.replaceAll('--ids ', '') - query = query.replaceAll(':mainIds', ids) - if (detailIds) { - query = query.replaceAll('--detailIds ', '') - query = query.replaceAll(':detailIds', detailIds) - } - } + query = query.replaceAll(':mainIds', ids) + query = query.replaceAll(':detailIds', detailIds) const suitable: Suitable[] = await prisma.$queryRawUnsafe(query) @@ -60,4 +57,3 @@ export async function GET(request: NextRequest) { return NextResponse.json({ error: '데이터 조회 중 오류가 발생했습니다' }, { status: 500 }) } } - diff --git a/src/components/popup/SuitableDetailPopup.tsx b/src/components/popup/SuitableDetailPopup.tsx index 80524d0..4baf4ec 100644 --- a/src/components/popup/SuitableDetailPopup.tsx +++ b/src/components/popup/SuitableDetailPopup.tsx @@ -3,15 +3,13 @@ import Image from 'next/image' import { useCallback, useEffect, useState } from 'react' import { usePopupController } from '@/store/popupController' -import { useSuitableStore } from '@/store/useSuitableStore' import SuitableDetailPopupButton from './SuitableDetailPopupButton' import { useSuitable } from '@/hooks/useSuitable' -import { Suitable } from '@/types/Suitable' +import { SUITABLE_HEAD_CODE, type Suitable, type SuitableDetail } from '@/types/Suitable' export default function SuitableDetailPopup() { const popupController = usePopupController() - const { getSuitableDetails, serializeSelectedItems } = useSuitable() - const { selectedItems } = useSuitableStore() + const { getSelectedItemsData, toCodeName, toSuitableDetail, suitableCheckIcon, suitableCheckMemo } = useSuitable() const [openItems, setOpenItems] = useState>(new Set()) const [suitableDetails, setSuitableDetails] = useState([]) @@ -25,14 +23,9 @@ export default function SuitableDetailPopup() { }) }, []) - // 선택된 아이템 상세 데이터 가져오기 - const getSelectedItemsData = async () => { - const serialized: Map = serializeSelectedItems() - setSuitableDetails(await getSuitableDetails(serialized.get('ids') ?? '', serialized.get('detailIds') ?? '')) - } - useEffect(() => { - getSelectedItemsData() + // TODO: 로딩 처리 필요 + getSelectedItemsData().then((data) => setSuitableDetails(data)) }, []) return ( @@ -52,98 +45,56 @@ export default function SuitableDetailPopup() {
-
-
-
アースティ40
-
- -
-
-
-
-
屋根技研 支持瓦
-
㈱ダイトー
-
-
-
屋根材
-
-
-
-
金具タイプ
-
木ねじ打ち込み式
-
-
-
-
-
屋根技研 支持瓦
-
-
- -
-
- -
-
-
-
Dで設置可
-
-
備考
-
- 桟木なしの場合は支持金具平ー1で設置可能。その場合水返しが高い為、レベルプレート使用。桟木ありの場合は支持金具平ー2で設置可能 -
-
+ {suitableDetails.map((item: Suitable) => ( +
+
+
{item.productName}
+
+
-
-
-
屋根技研支持金具
-
-
- -
-
- -
-
-
-
設置不可
-
-
備考
-
入手困難
-
+
+
+
+
屋根技研 支持瓦
+
{toCodeName(SUITABLE_HEAD_CODE.MANU_FT_CD, item.manuFtCd)}
-
-
-
屋根技研YGアンカー
-
-
- -
-
-
-
お問い合わせください
-
-
備考
-
入手困難
-
+
+
屋根材
+
{toCodeName(SUITABLE_HEAD_CODE.ROOF_MT_CD, item.roofMtCd)}
-
-
-
ダイドーハント支持瓦Ⅱ
-
-
- +
+
金具タイプ
+
{toCodeName(SUITABLE_HEAD_CODE.ROOF_SH_CD, item.roofShCd)}
+
+
+ {toSuitableDetail(item.detail).map((subItem: SuitableDetail) => ( +
+
+
{toCodeName(SUITABLE_HEAD_CODE.TRESTLE_MFPC_CD, subItem.trestleMfpcCd)}
+
+
+ +
+ {subItem.memo && ( +
+ +
+ )} +
+
{suitableCheckMemo(subItem.trestleManufacturerProductName)}
+ {subItem.memo && ( +
+
備考
+
{subItem.memo}
+
+ )}
-
-
Ⅳ (D) で設置可
-
-
備考
-
入手困難
-
+ ))}
-
+ ))}
diff --git a/src/components/popup/SuitableDetailPopupButton.tsx b/src/components/popup/SuitableDetailPopupButton.tsx index 52e1bae..01f88ce 100644 --- a/src/components/popup/SuitableDetailPopupButton.tsx +++ b/src/components/popup/SuitableDetailPopupButton.tsx @@ -1,10 +1,16 @@ 'use client' +import { useRouter } from 'next/navigation' +import { usePopupController } from '@/store/popupController' + export default function SuitableDetailPopupButton() { + const popupController = usePopupController() + const router = useRouter() + return (
-
@@ -14,7 +20,13 @@ export default function SuitableDetailPopupButton() {
-
diff --git a/src/components/suitable/SuitableList.tsx b/src/components/suitable/SuitableList.tsx index 9936494..7aaeb5e 100644 --- a/src/components/suitable/SuitableList.tsx +++ b/src/components/suitable/SuitableList.tsx @@ -9,7 +9,17 @@ import { useSuitableStore } from '@/store/useSuitableStore' import { SUITABLE_HEAD_CODE, type Suitable, type SuitableDetail } from '@/types/Suitable' export default function SuitableList() { - const { toCodeName, toSuitableDetail, toSuitableDetailIds, suitables, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading } = useSuitable() + const { + toCodeName, + toSuitableDetail, + toSuitableDetailIds, + suitables, + fetchNextPage, + hasNextPage, + isFetchingNextPage, + isLoading, + suitableCheckIcon, + } = useSuitable() const { selectedItems, addSelectedItem, removeSelectedItem } = useSuitableStore() const [openItems, setOpenItems] = useState>(new Set()) const observerTarget = useRef(null) @@ -52,20 +62,6 @@ export default function SuitableList() { }) }, []) - // TODO: 추후 지붕재 적합성 데이터 CUD 구현 시 ×, ー 데이터 관리 필요 - const suitableCheck = useCallback((value: string) => { - const iconMap: Record = { - '×': '/assets/images/sub/compliance_x_icon.svg', - ー: '/assets/images/sub/compliance_quest_icon.svg', - default: '/assets/images/sub/compliance_check_icon.svg', - } - return ( -
- -
- ) - }, []) - // 아이템 렌더링 const renderItem = useCallback( (item: Suitable) => { @@ -99,7 +95,9 @@ export default function SuitableList() {
- {suitableCheck(subItem.trestleManufacturerProductName)} +
+ +
{subItem.memo && (
@@ -113,7 +111,7 @@ export default function SuitableList() {
) }, - [isItemSelected, openItems, handleItemClick, toggleItemOpen, suitableCheck, toCodeName, toSuitableDetail], + [isItemSelected, openItems, handleItemClick, toggleItemOpen, toCodeName, toSuitableDetail], ) // 아이템 리스트 diff --git a/src/hooks/useSuitable.ts b/src/hooks/useSuitable.ts index 5acad45..23b89b5 100644 --- a/src/hooks/useSuitable.ts +++ b/src/hooks/useSuitable.ts @@ -62,7 +62,7 @@ export function useSuitable() { try { const params: Record = { ids: ids } if (detailIds) params.detailIds = detailIds - const response = await axiosInstance(null).get('/api/suitable', { params }) + const response = await axiosInstance(null).post('/api/suitable', params) return response.data } catch (error) { console.error('지붕재 상세 데이터 로드 실패:', error) @@ -134,17 +134,19 @@ export function useSuitable() { enabled: selectedCategory !== '' || searchKeyword !== '', }) - const serializeSelectedItems = (): Map => { + const serializeSelectedItems = (): { ids: string; detailIds: string } => { const ids: string[] = [] const detailIds: string[] = [] for (const [key, value] of selectedItems) { ids.push(String(key)) for (const id of value) detailIds.push(String(id)) } - return new Map([ - ['ids', ids.join(',')], - ['detailIds', detailIds.join(',')], - ]) + return { ids: ids.join(','), detailIds: detailIds.length > 0 ? detailIds.join(',') : '' } + } + + const getSelectedItemsData = async (): Promise => { + const { ids, detailIds } = serializeSelectedItems() + return await getSuitableDetails(ids, detailIds) } const clearSuitableSearch = ({ items = false, category = false, keyword = false }: { items?: boolean; category?: boolean; keyword?: boolean }) => { @@ -153,6 +155,24 @@ export function useSuitable() { if (keyword) clearSearchKeyword() } + // TODO: 추후 지붕재 적합성 데이터 CUD 구현 시 ×, ー 데이터 관리 필요 + const suitableCheckIcon = (value: string): string => { + const iconMap: Record = { + '×': '/assets/images/sub/compliance_x_icon.svg', + 'ー': '/assets/images/sub/compliance_quest_icon.svg', + default: '/assets/images/sub/compliance_check_icon.svg', + } + return iconMap[value] || iconMap.default + } + + // TODO: 추후 지붕재 적합성 데이터 CUD 구현 시 ○, ×, ー 데이터 관리 필요 + const suitableCheckMemo = (value: string): string => { + if (value === '○') return '設置可' + if (value === '×') return '設置不可' + if (value === 'ー') return 'お問い合わせください' + return `${value}で設置可` + } + return { getSuitables, getSuitableIds, @@ -166,7 +186,9 @@ export function useSuitable() { hasNextPage, isFetchingNextPage, isLoading, - serializeSelectedItems, + getSelectedItemsData, clearSuitableSearch, + suitableCheckIcon, + suitableCheckMemo, } } -- 2.47.2