From 36bcdd00a1c14c72cd151e33c84f87f5970f6d97 Mon Sep 17 00:00:00 2001 From: Daseul Kim Date: Fri, 23 May 2025 17:14:07 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=A7=80=EB=B6=95=EC=9E=AC=20=EC=A0=81?= =?UTF-8?q?=ED=95=A9=EC=84=B1=20=EC=A0=84=EC=B2=B4=EC=84=A0=ED=83=9D/?= =?UTF-8?q?=EC=A0=84=EC=B2=B4=ED=95=B4=EC=A0=9C=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EB=B0=8F=20=EA=B4=80=EB=A0=A8=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 지붕재 적합성 전체선택/전체해제 기능 추가 - 검색조건에 따른 데이터 조회 api 추가 - 선택된 데이터 조회 api 추가 --- src/app/api/suitable/pick/route.ts | 47 ++++++ src/app/api/suitable/route.ts | 67 ++++++++- src/components/popup/SuitableDetailPopup.tsx | 141 ++++-------------- .../popup/SuitableDetailPopupButton.tsx | 23 +++ src/components/suitable/SuitableButton.tsx | 30 +++- src/hooks/useSuitable.ts | 48 +++++- src/store/useSuitableStore.ts | 16 +- src/types/Suitable.ts | 5 + 8 files changed, 250 insertions(+), 127 deletions(-) create mode 100644 src/app/api/suitable/pick/route.ts create mode 100644 src/components/popup/SuitableDetailPopupButton.tsx diff --git a/src/app/api/suitable/pick/route.ts b/src/app/api/suitable/pick/route.ts new file mode 100644 index 0000000..54db249 --- /dev/null +++ b/src/app/api/suitable/pick/route.ts @@ -0,0 +1,47 @@ +import { NextRequest, NextResponse } from 'next/server' +import { prisma } from '@/libs/prisma' +import { 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') + + let query = ` + SELECT + msm.id + , details.detail_id + FROM ms_suitable_main msm + LEFT JOIN ( + SELECT + msd.main_id + , STRING_AGG(msd.id, ',') AS detail_id + FROM ms_suitable_detail msd + GROUP BY msd.main_id + ) AS details + ON msm.id = details.main_id + WHERE 1=1 + --roofMtCd AND msm.roof_mt_cd = ':roofMtCd' + --productName AND msm.product_name LIKE '%:productName%' + ; + ` + + // 검색 조건 설정 + if (category) { + query = query.replace('--roofMtCd ', '') + query = query.replace(':roofMtCd', category) + } + if (keyword) { + query = query.replace('--productName ', '') + query = query.replace(':productName', keyword) + } + + const suitableIdSet = await prisma.$queryRawUnsafe(query) + + return NextResponse.json(suitableIdSet) + } catch (error) { + console.error('❌ 데이터 조회 중 오류가 발생했습니다:', error) + return NextResponse.json({ error: '데이터 조회 중 오류가 발생했습니다' }, { status: 500 }) + } +} diff --git a/src/app/api/suitable/route.ts b/src/app/api/suitable/route.ts index 7f7be9c..df42e1e 100644 --- a/src/app/api/suitable/route.ts +++ b/src/app/api/suitable/route.ts @@ -1,12 +1,63 @@ -import { NextResponse } from 'next/server' +import { NextRequest, NextResponse } from 'next/server' import { prisma } from '@/libs/prisma' +import { Suitable } from '@/types/Suitable' -export async function POST(request: Request) { - const body = await request.json() - // @ts-ignore - const suitables = await prisma.MS_SUITABLE.createMany({ - data: body, - }) +export async function GET(request: NextRequest) { + try { + const searchParams = request.nextUrl.searchParams - return NextResponse.json({ message: 'Suitable created successfully' }) + const ids = searchParams.get('ids') + const detailIds = searchParams.get('subIds') + + 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 + --ids AND details.main_id IN (:mainIds) + --detailIds AND details.id IN (:detailIds) + WHERE 1=1 + --ids AND 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) + } + } + + const suitable: Suitable[] = await prisma.$queryRawUnsafe(query) + + return NextResponse.json(suitable) + } catch (error) { + console.error('❌ 데이터 조회 중 오류가 발생했습니다:', error) + return NextResponse.json({ error: '데이터 조회 중 오류가 발생했습니다' }, { status: 500 }) + } } + diff --git a/src/components/popup/SuitableDetailPopup.tsx b/src/components/popup/SuitableDetailPopup.tsx index 2d4095d..80524d0 100644 --- a/src/components/popup/SuitableDetailPopup.tsx +++ b/src/components/popup/SuitableDetailPopup.tsx @@ -1,10 +1,39 @@ 'use client' 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' export default function SuitableDetailPopup() { const popupController = usePopupController() + const { getSuitableDetails, serializeSelectedItems } = useSuitable() + const { selectedItems } = useSuitableStore() + + const [openItems, setOpenItems] = useState>(new Set()) + const [suitableDetails, setSuitableDetails] = useState([]) + + // 아이템 열기/닫기 + const toggleItemOpen = useCallback((itemId: number) => { + setOpenItems((prev) => { + const newOpenItems = new Set(prev) + newOpenItems.has(itemId) ? newOpenItems.delete(itemId) : newOpenItems.add(itemId) + return newOpenItems + }) + }, []) + + // 선택된 아이템 상세 데이터 가져오기 + const getSelectedItemsData = async () => { + const serialized: Map = serializeSelectedItems() + setSuitableDetails(await getSuitableDetails(serialized.get('ids') ?? '', serialized.get('detailIds') ?? '')) + } + + useEffect(() => { + getSelectedItemsData() + }, []) return (
@@ -23,11 +52,11 @@ export default function SuitableDetailPopup() {
-
+
アースティ40
- +
@@ -115,114 +144,8 @@ export default function SuitableDetailPopup() {
-
-
-
アースティ40
-
- -
-
-
-
-
屋根技研 支持瓦
-
㈱ダイトー
-
-
-
屋根材
-
-
-
-
金具タイプ
-
木ねじ打ち込み式
-
-
-
-
-
屋根技研 支持瓦
-
-
- -
-
- -
-
-
-
Dで設置可
-
-
備考
-
入手困難
-
-
-
-
-
屋根技研支持金具
-
-
- -
-
- -
-
-
-
設置不可
-
-
備考
-
入手困難
-
-
-
-
-
屋根技研YGアンカー
-
-
- -
-
-
-
お問い合わせください
-
-
備考
-
入手困難
-
-
-
-
-
ダイドーハント支持瓦Ⅱ
-
-
- -
-
-
-
Ⅳ (D) で設置可
-
-
備考
-
入手困難
-
-
-
-
-
-
-
-
- -
-
- -
-
- -
+
diff --git a/src/components/popup/SuitableDetailPopupButton.tsx b/src/components/popup/SuitableDetailPopupButton.tsx new file mode 100644 index 0000000..52e1bae --- /dev/null +++ b/src/components/popup/SuitableDetailPopupButton.tsx @@ -0,0 +1,23 @@ +'use client' + +export default function SuitableDetailPopupButton() { + return ( +
+
+ +
+
+ +
+
+ +
+
+ ) +} diff --git a/src/components/suitable/SuitableButton.tsx b/src/components/suitable/SuitableButton.tsx index 1aa6be5..20191d2 100644 --- a/src/components/suitable/SuitableButton.tsx +++ b/src/components/suitable/SuitableButton.tsx @@ -1,20 +1,42 @@ 'use client' import { usePopupController } from '@/store/popupController' +import { useSuitable } from '@/hooks/useSuitable' +import { useSuitableStore } from '@/store/useSuitableStore' export default function SuitableButton() { const popupController = usePopupController() + const { getSuitableIds } = useSuitable() + const { selectedItems, addAllSelectedItem, clearSelectedItems } = useSuitableStore() + + const handleSelectAll = async () => { + addAllSelectedItem(await getSuitableIds()) + } + + const handleOpenPopup = () => { + if (selectedItems.size === 0) { + alert('屋根材を選択してください。') + return + } + popupController.setSuitableDetailPopup(true) + } return (
- + {selectedItems.size === 0 ? ( + + ) : ( + + )}
-
diff --git a/src/hooks/useSuitable.ts b/src/hooks/useSuitable.ts index f651b4d..94e61a0 100644 --- a/src/hooks/useSuitable.ts +++ b/src/hooks/useSuitable.ts @@ -3,21 +3,19 @@ import { transformObjectKeys } from '@/libs/axios' import { useSuitableStore } from '@/store/useSuitableStore' import { useAxios } from './useAxios' import { useCommCode } from './useCommCode' -import { SUITABLE_HEAD_CODE, type Suitable, type SuitableDetail } from '@/types/Suitable' +import { SUITABLE_HEAD_CODE, type Suitable, type SuitableDetail, type SuitableIds } from '@/types/Suitable' export function useSuitable() { const { axiosInstance } = useAxios() const { getCommCode } = useCommCode() - const { itemPerPage, selectedCategory, searchValue, suitableCommCode, setSuitableCommCode, isSearch } = useSuitableStore() + const { itemPerPage, selectedCategory, searchValue, suitableCommCode, setSuitableCommCode, isSearch, selectedItems } = useSuitableStore() const getSuitables = async ({ pageNumber, - ids, category, keyword, }: { pageNumber?: number - ids?: string category?: string keyword?: string }): Promise => { @@ -26,7 +24,6 @@ export function useSuitable() { pageNumber: pageNumber || 1, itemPerPage: itemPerPage, } - if (ids) params.ids = ids if (category) params.category = category if (keyword) params.keyword = keyword @@ -38,6 +35,31 @@ export function useSuitable() { } } + const getSuitableIds = async (): Promise => { + try { + const params: Record = {} + if (selectedCategory) params.category = selectedCategory + if (searchValue) params.keyword = searchValue + const response = await axiosInstance(null).get('/api/suitable/pick', { params }) + return response.data + } catch (error) { + console.error('지붕재 아이디 로드 실패:', error) + return [] + } + } + + const getSuitableDetails = async (ids: string, detailIds?: string): Promise => { + try { + const params: Record = { ids: ids } + if (detailIds) params.detailIds = detailIds + const response = await axiosInstance(null).get('/api/suitable', { params }) + 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) { @@ -100,8 +122,23 @@ export function useSuitable() { gcTime: 1000 * 60 * 10, }) + const serializeSelectedItems = (): Map => { + 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 { getSuitables, + getSuitableIds, + getSuitableDetails, getSuitableCommCode, toCodeName, toSuitableDetail, @@ -111,5 +148,6 @@ export function useSuitable() { hasNextPage, isFetchingNextPage, isLoading, + serializeSelectedItems, } } diff --git a/src/store/useSuitableStore.ts b/src/store/useSuitableStore.ts index 74f4d0b..77ddfaa 100644 --- a/src/store/useSuitableStore.ts +++ b/src/store/useSuitableStore.ts @@ -1,5 +1,6 @@ import { create } from 'zustand' import type { CommCode } from '@/types/CommCode' +import type { SuitableIds } from '@/types/Suitable' interface SuitableState { /* 초기 데이터 로드 개수*/ @@ -27,8 +28,10 @@ interface SuitableState { /* 선택된 아이템 리스트 */ selectedItems: Map> - /* 선택된 아이템 추가 */ + /* 선택 아이템 추가 */ addSelectedItem: (mainId: number, detailId?: number, detailIds?: Set) => void + /* 아이템 전체 추가 */ + addAllSelectedItem: (suitableIds: SuitableIds[]) => void /* 선택된 아이템 제거 */ removeSelectedItem: (mainId: number, detailId?: number) => void /* 선택된 아이템 모두 제거 */ @@ -77,6 +80,17 @@ export const useSuitableStore = create((set) => ({ } }, + /* 아이템 전체 추가 */ + addAllSelectedItem: (suitableIds: SuitableIds[]) => { + set(() => { + const newSelectedItems = new Map() + suitableIds.forEach((suitableId) => { + newSelectedItems.set(suitableId.id, new Set(suitableId.detailId.split(',').map(Number))) + }) + return { selectedItems: newSelectedItems } + }) + }, + /* 선택된 아이템 제거 */ removeSelectedItem: (mainId: number, detailId?: number) => { set((state) => { diff --git a/src/types/Suitable.ts b/src/types/Suitable.ts index 270dd46..b8fbf72 100644 --- a/src/types/Suitable.ts +++ b/src/types/Suitable.ts @@ -34,3 +34,8 @@ export type Suitable = { detailCnt: number detail: string } + +export type SuitableIds = { + id: number + detailId: string +}