From cc8ef6a7d3f0f813819a20b8742dee9617b371df Mon Sep 17 00:00:00 2001 From: Daseul Kim Date: Mon, 28 Apr 2025 16:51:01 +0900 Subject: [PATCH 1/5] =?UTF-8?q?feat:=20=EC=A7=80=EB=B6=95=EC=9E=AC=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=A1=B0=ED=9A=8C=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/suitable.ts | 21 ++++++- src/app/api/suitable/category/route.ts | 16 +++++ src/app/api/suitable/list/route.ts | 37 ++++++++++-- src/components/Suitable.tsx | 35 ++++++++--- src/components/SuitableSearch.tsx | 82 +++++++++++++++++++++++--- src/hooks/useSuitable.ts | 30 ++++++++++ src/store/useSuitableStore.ts | 68 +++++++++++++++++++++ 7 files changed, 266 insertions(+), 23 deletions(-) create mode 100644 src/app/api/suitable/category/route.ts create mode 100644 src/hooks/useSuitable.ts create mode 100644 src/store/useSuitableStore.ts diff --git a/src/api/suitable.ts b/src/api/suitable.ts index e8458c4..277bc7e 100644 --- a/src/api/suitable.ts +++ b/src/api/suitable.ts @@ -38,14 +38,31 @@ export interface Suitable { } export const suitableApi = { - getList: async (): Promise => { - const response = await axiosInstance.get('/api/suitable/list') + getList: async (category?: string, keyword?: string): Promise => { + let condition: any = {} + if (category) { + condition['category'] = category + } + if (keyword) { + condition['keyword'] = { + contains: keyword, + } + } + console.log('๐Ÿš€ ~ getList: ~ condition:', condition) + const response = await axiosInstance.get('/api/suitable/list', { params: condition }) console.log('๐Ÿš€ ~ getList: ~ response:', response) return response.data }, + getCategory: async (): Promise => { + const response = await axiosInstance.get('/api/suitable/category') + console.log('๐Ÿš€ ~ getCategory: ~ response:', response) + return response.data + }, + getDetails: async (roofMaterial: string): Promise => { const response = await axiosInstance.get(`/api/suitable/details?roof-material=${roofMaterial}`) + console.log('๐Ÿš€ ~ getDetails: ~ response:', response) return response.data }, diff --git a/src/app/api/suitable/category/route.ts b/src/app/api/suitable/category/route.ts new file mode 100644 index 0000000..288a74a --- /dev/null +++ b/src/app/api/suitable/category/route.ts @@ -0,0 +1,16 @@ +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/list/route.ts b/src/app/api/suitable/list/route.ts index e1a44cb..22c3d8a 100644 --- a/src/app/api/suitable/list/route.ts +++ b/src/app/api/suitable/list/route.ts @@ -1,8 +1,35 @@ -import { NextResponse } from 'next/server' +import { NextRequest, NextResponse } from 'next/server' import { prisma } from '@/libs/prisma' -export async function GET() { - // @ts-ignore - const suitables = await prisma.MS_SUITABLE.findMany() - return NextResponse.json(suitables) + +export async function GET(request: NextRequest) { + try { + const searchParams = request.nextUrl.searchParams + const category = searchParams.get('category') + const keyword = searchParams.get('keyword') + + let whereCondition: any = {} + if (category) { + whereCondition['roof_material'] = category + } + if (keyword) { + whereCondition['product_name'] = { + contains: keyword, + } + } + console.log('๐Ÿš€ ~ /api/suitable/list: ~ prisma where condition:', whereCondition) + + // @ts-ignore + const suitables = await prisma.MS_SUITABLE.findMany({ + where: whereCondition, + orderBy: { + product_name: 'asc', + }, + }) + + return NextResponse.json(suitables) + } catch (error) { + console.error('โŒ ๋ฐ์ดํ„ฐ ์กฐํšŒ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค:', error) + return NextResponse.json({ error: '๋ฐ์ดํ„ฐ ์กฐํšŒ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค' }, { status: 500 }) + } } diff --git a/src/components/Suitable.tsx b/src/components/Suitable.tsx index f5766f1..1af6d86 100644 --- a/src/components/Suitable.tsx +++ b/src/components/Suitable.tsx @@ -1,16 +1,23 @@ 'use client' -import { suitableApi } from '@/api/suitable' +import { useSuitableStore } from '@/store/useSuitableStore' import { useQuery } from '@tanstack/react-query' import { useRouter } from 'next/navigation' +import type { Suitable as SuitableType } from '@/api/suitable' export default function Suitable() { const router = useRouter() - const { data, error, isPending } = useQuery({ - queryKey: ['suitable-list'], - queryFn: suitableApi.getList, + const { selectedItems, addSelectedItem } = useSuitableStore() + + const { data: suitableList, isLoading } = useQuery({ + queryKey: ['suitables', 'search'], + enabled: false, }) + if (isLoading || !suitableList) { + return
Loading...
+ } + return ( <>

Suitable

@@ -22,9 +29,23 @@ export default function Suitable() { > HOME - {error &&
Error: {error.message}
} - {isPending &&
Loading...
} - {data && data.map((item) =>
{item.product_name}
)} +
+

์„ ํƒ๋œ ์•„์ดํ…œ

+ {selectedItems.length > 0 ? selectedItems.map((item) =>
{item.product_name}
) :
์„ ํƒ๋œ ์•„์ดํ…œ์ด ์—†์Šต๋‹ˆ๋‹ค.
} +
+
+
+

๋ฐ์ดํ„ฐ ๋ชฉ๋ก

+ {suitableList ? ( + suitableList.map((item: SuitableType) => ( +
addSelectedItem(item)}> + {item.product_name} +
+ )) + ) : ( +
๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.
+ )} +
) } diff --git a/src/components/SuitableSearch.tsx b/src/components/SuitableSearch.tsx index 9583ede..e339f80 100644 --- a/src/components/SuitableSearch.tsx +++ b/src/components/SuitableSearch.tsx @@ -1,21 +1,85 @@ 'use client' -import { useState } from 'react' +import { useEffect, useState } from 'react' import { useRouter } from 'next/navigation' +import { useQuery } from '@tanstack/react-query' +import { useSuitable } from '@/hooks/useSuitable' export default function SuitableSearch() { const router = useRouter() - const [selectedValue, setSelectedValue] = useState('') + const [selectedCategory, setSelectedCategory] = useState('') + const [searchValue, setSearchValue] = useState('') + const { getCategories, getSuitables } = useSuitable() + + const { data: categories } = useQuery({ + queryKey: ['categories'], + queryFn: getCategories, + staleTime: 1000 * 60 * 60, // 60๋ถ„ + }) + + const { data: suitableList } = useQuery({ + queryKey: ['suitables', 'list'], + queryFn: async () => await getSuitables(), + staleTime: 1000 * 60 * 10, // 10๋ถ„ + gcTime: 1000 * 60 * 10, // 10๋ถ„ + }) + + const { data: suitableSearch, refetch: refetchBySearch } = useQuery({ + queryKey: ['suitables', 'search'], + queryFn: async () => { + // api๋ฅผ ํ˜ธ์ถœํ•˜๋Š” ๊ฒ€์ƒ‰ ๋ฐฉ๋ฒ• + // return await updateSearchResults(selectedCategory || undefined, searchValue || undefined) + // useQuery ์บ์‹œ ๋ฐ์ดํ„ฐ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒ€์ƒ‰ ๋ฐฉ๋ฒ• + return ( + suitableList?.filter((item) => { + const categoryMatch = !selectedCategory || item.roof_material === selectedCategory + const searchMatch = !searchValue || item.product_name.includes(searchValue) + return categoryMatch && searchMatch + }) ?? [] + ) + }, + staleTime: 1000 * 60 * 10, + gcTime: 1000 * 60 * 10, + enabled: false, + }) + + // ์ดˆ๊ธฐ ๋ฐ์ดํ„ฐ ๋กœ๋”ฉ๋œ ํ›„ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ์—†์œผ๋ฉด ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ๋กœ๋”ฉ + useEffect(() => { + if (suitableList && (!suitableSearch || suitableSearch.length === 0)) { + refetchBySearch() + } + }, [suitableList, suitableSearch]) + + // ์นดํ…Œ๊ณ ๋ฆฌ ์„ ํƒ ์‹œ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ๋กœ๋”ฉ + useEffect(() => { + refetchBySearch() + }, [selectedCategory]) + + const handleSearch = async () => { + if (!searchValue.trim()) { + alert('๊ฒ€์ƒ‰์–ด๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”.') + return + } + refetchBySearch() + } return ( <> - - +
+ + +
+
+ setSearchValue(e.target.value)} /> + +
) } diff --git a/src/hooks/useSuitable.ts b/src/hooks/useSuitable.ts new file mode 100644 index 0000000..62f00e2 --- /dev/null +++ b/src/hooks/useSuitable.ts @@ -0,0 +1,30 @@ +import { suitableApi } from '@/api/suitable' + +export function useSuitable() { + const getCategories = async () => { + try { + return await suitableApi.getCategory() + } catch (error) { + console.error('์นดํ…Œ๊ณ ๋ฆฌ ๋ฐ์ดํ„ฐ ๋กœ๋“œ ์‹คํŒจ:', error) + return [] + } + } + + const getSuitables = async () => { + try { + return await suitableApi.getList() + } catch (error) { + console.error('์ง€๋ถ•์žฌ ๋ฐ์ดํ„ฐ ๋กœ๋“œ ์‹คํŒจ:', error) + } + } + + const updateSearchResults = async (selectedCategory: string | undefined, searchValue: string | undefined) => { + try { + return await suitableApi.getList(selectedCategory, searchValue) + } catch (error) { + console.error('์ง€๋ถ•์žฌ ๋ฐ์ดํ„ฐ ๊ฒ€์ƒ‰ ์‹คํŒจ:', error) + } + } + + return { getCategories, getSuitables, updateSearchResults } +} diff --git a/src/store/useSuitableStore.ts b/src/store/useSuitableStore.ts new file mode 100644 index 0000000..17b88c5 --- /dev/null +++ b/src/store/useSuitableStore.ts @@ -0,0 +1,68 @@ +import { create } from 'zustand' +import { Suitable, suitableApi } from '@/api/suitable' + +interface SuitableState { + // // ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ๋ฆฌ์ŠคํŠธ + // searchResults: Suitable[] + // // ์ดˆ๊ธฐ ๋ฐ์ดํ„ฐ ๋กœ๋“œ + // fetchInitializeData: () => Promise + // // ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ์„ค์ • + // setSearchResults: (results: Suitable[]) => void + // // ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ์ดˆ๊ธฐํ™” + // resetSearchResults: () => void + + // ์„ ํƒ๋œ ์•„์ดํ…œ ๋ฆฌ์ŠคํŠธ + selectedItems: Suitable[] + // ์„ ํƒ๋œ ์•„์ดํ…œ ์ถ”๊ฐ€ + addSelectedItem: (item: Suitable) => void + // ์„ ํƒ๋œ ์•„์ดํ…œ ์ œ๊ฑฐ + removeSelectedItem: (itemId: number) => void + // ์„ ํƒ๋œ ์•„์ดํ…œ ๋ชจ๋‘ ์ œ๊ฑฐ + clearSelectedItems: () => void +} + +export const useSuitableStore = create((set) => ({ + // // ์ดˆ๊ธฐ ์ƒํƒœ + // searchResults: [], + + // // ์ดˆ๊ธฐ ๋ฐ์ดํ„ฐ ๋กœ๋“œ + // fetchInitializeData: async () => { + // const suitables = await fetchInitialSuitablee() + // set({ searchResults: suitables }) + // }, + + // // ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ์„ค์ • + // setSearchResults: (results) => set({ searchResults: results }), + + // // ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ์ดˆ๊ธฐํ™” + // resetSearchResults: () => set({ searchResults: [] }), + + // ์ดˆ๊ธฐ ์ƒํƒœ + selectedItems: [], + + // ์„ ํƒ๋œ ์•„์ดํ…œ ์ถ”๊ฐ€ (์ค‘๋ณต ๋ฐฉ์ง€) + addSelectedItem: (item) => + set((state) => ({ + selectedItems: state.selectedItems.some((i) => i.id === item.id) ? state.selectedItems : [...state.selectedItems, item], + })), + + // ์„ ํƒ๋œ ์•„์ดํ…œ ์ œ๊ฑฐ + removeSelectedItem: (itemId) => + set((state) => ({ + selectedItems: state.selectedItems.filter((item) => item.id !== itemId), + })), + + // ์„ ํƒ๋œ ์•„์ดํ…œ ๋ชจ๋‘ ์ œ๊ฑฐ + clearSelectedItems: () => set({ selectedItems: [] }), +})) + +// // ์ „์ฒด ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™” ํ•จ์ˆ˜ +// async function fetchInitialSuitablee() { +// try { +// const suitable = await suitableApi.getList() +// return suitable +// } catch (error) { +// console.error('์ดˆ๊ธฐ ๋ฐ์ดํ„ฐ ๋กœ๋“œ ์‹คํŒจ:', error) +// return [] +// } +// } From bfbbdc01b8dce44375f7516cf9ed6ff358e5b63f Mon Sep 17 00:00:00 2001 From: Daseul Kim Date: Tue, 29 Apr 2025 10:49:58 +0900 Subject: [PATCH 2/5] =?UTF-8?q?feat:=20=EC=A7=80=EB=B6=95=EC=9E=AC=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=84=A0=ED=83=9D=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Suitable.tsx | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/src/components/Suitable.tsx b/src/components/Suitable.tsx index 1af6d86..eb725a2 100644 --- a/src/components/Suitable.tsx +++ b/src/components/Suitable.tsx @@ -1,16 +1,25 @@ 'use client' import { useSuitableStore } from '@/store/useSuitableStore' -import { useQuery } from '@tanstack/react-query' +import { useQuery, useQueryClient } from '@tanstack/react-query' import { useRouter } from 'next/navigation' import type { Suitable as SuitableType } from '@/api/suitable' export default function Suitable() { const router = useRouter() - const { selectedItems, addSelectedItem } = useSuitableStore() + const queryClient = useQueryClient() + const { selectedItems, addSelectedItem, removeSelectedItem } = useSuitableStore() + const handleItemClick = (item: SuitableType) => { + if (!item.id) return // ์ดˆ๊ธฐ ๋ฐ์ดํ„ฐ import ๋•Œ๋ฌธ์— Suitable.id๊ฐ€ optional ํƒ€์ž…์ด๋ผ์„œ ๋ฐฉ์–ด ์ฒ˜๋ฆฌ ์ถ”๊ฐ€ + selectedItems.some((selected) => selected.id === item.id) ? removeSelectedItem(item.id) : addSelectedItem(item) + } const { data: suitableList, isLoading } = useQuery({ queryKey: ['suitables', 'search'], + queryFn: async () => { + const data = queryClient.getQueryData(['suitables', 'search']) as SuitableType[] + return data ?? [] + }, enabled: false, }) @@ -31,14 +40,22 @@ export default function Suitable() {

์„ ํƒ๋œ ์•„์ดํ…œ

- {selectedItems.length > 0 ? selectedItems.map((item) =>
{item.product_name}
) :
์„ ํƒ๋œ ์•„์ดํ…œ์ด ์—†์Šต๋‹ˆ๋‹ค.
} + {selectedItems.length > 0 ? ( + selectedItems.map((item) => ( +
handleItemClick(item)}> + {item.product_name} +
+ )) + ) : ( +
์„ ํƒ๋œ ์•„์ดํ…œ์ด ์—†์Šต๋‹ˆ๋‹ค.
+ )}

๋ฐ์ดํ„ฐ ๋ชฉ๋ก

{suitableList ? ( suitableList.map((item: SuitableType) => ( -
addSelectedItem(item)}> +
handleItemClick(item)}> {item.product_name}
)) From f801bacd12440da0493a0de943a8c8f09a600a06 Mon Sep 17 00:00:00 2001 From: yoosangwook Date: Tue, 29 Apr 2025 16:56:41 +0900 Subject: [PATCH 3/5] feat: Add update functionality to Survey Sales API and enhance UI for managing survey sales details --- src/api/surveySales.ts | 4 ++ src/app/api/survey-sales/route.ts | 22 ++++++++ src/app/suitable/[slug]/page.tsx | 1 + src/components/SuitableDetails.tsx | 17 ++++--- src/components/SurveySales.tsx | 80 ++++++++++++++++++++++++++++-- 5 files changed, 114 insertions(+), 10 deletions(-) diff --git a/src/api/surveySales.ts b/src/api/surveySales.ts index 3d9d727..e54f856 100644 --- a/src/api/surveySales.ts +++ b/src/api/surveySales.ts @@ -64,4 +64,8 @@ export const surveySalesApi = { const response = await axiosInstance.get('/api/survey-sales') return response.data }, + update: async (data: SurveySalesBasicInfo): Promise => { + const response = await axiosInstance.put(`/api/survey-sales`, data) + return response.data + }, } diff --git a/src/app/api/survey-sales/route.ts b/src/app/api/survey-sales/route.ts index decd3a3..7662111 100644 --- a/src/app/api/survey-sales/route.ts +++ b/src/app/api/survey-sales/route.ts @@ -11,3 +11,25 @@ export async function POST(request: Request) { return NextResponse.json({ message: 'Survey sales created successfully' }) } + +export async function GET(request: Request) { + // @ts-ignore + const res = await prisma.SD_SERVEY_SALES_BASIC_INFO.findMany({ + include: { + detail_info: true, + }, + }) + return NextResponse.json(res) +} + +export async function PUT(request: Request) { + const body = await request.json() + console.log('๐Ÿš€ ~ PUT ~ body:', body) + const detailInfo = { ...body.detail_info, basic_info_id: body.id } + console.log('๐Ÿš€ ~ PUT ~ detailInfo:', detailInfo) + // @ts-ignore + const res = await prisma.SD_SERVEY_SALES_DETAIL_INFO.create({ + data: detailInfo, + }) + return NextResponse.json({ message: 'Survey sales updated successfully' }) +} diff --git a/src/app/suitable/[slug]/page.tsx b/src/app/suitable/[slug]/page.tsx index 6a4f8a0..6c1959a 100644 --- a/src/app/suitable/[slug]/page.tsx +++ b/src/app/suitable/[slug]/page.tsx @@ -2,6 +2,7 @@ import SuitableDetails from '@/components/SuitableDetails' export default async function page({ params }: { params: Promise<{ slug: string }> }) { const { slug } = await params + console.log('๐Ÿš€ ~ page ~ slug:', slug) return ( <>

Suitable Details

diff --git a/src/components/SuitableDetails.tsx b/src/components/SuitableDetails.tsx index 3d698d7..795fcdc 100644 --- a/src/components/SuitableDetails.tsx +++ b/src/components/SuitableDetails.tsx @@ -1,15 +1,18 @@ 'use client' -import { suitableApi } from '@/api/suitable' -import { useQuery } from '@tanstack/react-query' +import { Suitable, suitableApi } from '@/api/suitable' +import { useQuery, useQueryClient } from '@tanstack/react-query' export default function SuitableDetails({ roofMaterial }: { roofMaterial: string }) { console.log('๐Ÿš€ ~ SuitableDetails ~ roofMaterial:', roofMaterial) - const { data, error, isPending } = useQuery({ - queryKey: ['suitable-details'], - queryFn: () => suitableApi.getDetails(roofMaterial), - staleTime: 0, - }) + // const { data, error, isPending } = useQuery({ + // queryKey: ['suitable-details'], + // queryFn: () => suitableApi.getDetails(roofMaterial), + // staleTime: 0, + // }) + const cache = useQueryClient() + const listData = cache.getQueryData(['suitable-list']) as Suitable[] + const data = listData.filter((item) => item.roof_material === roofMaterial) return ( <> diff --git a/src/components/SurveySales.tsx b/src/components/SurveySales.tsx index 387b7cd..f0dba94 100644 --- a/src/components/SurveySales.tsx +++ b/src/components/SurveySales.tsx @@ -1,9 +1,12 @@ 'use client' -import { surveySalesApi, SurveySalesBasicInfo } from '@/api/surveySales' -import { useMutation, useQueryClient } from '@tanstack/react-query' +import { surveySalesApi, SurveySalesBasicInfo, SurveySalesDetailInfo } from '@/api/surveySales' +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import { useState } from 'react' export default function SurveySales() { + const [isSearch, setIsSearch] = useState(false) + const queryClient = useQueryClient() const { @@ -34,6 +37,68 @@ export default function SurveySales() { createSurveySales(data) } + const { data, error: errorList } = useQuery({ + queryKey: ['survey-sales', 'list'], + queryFn: surveySalesApi.getList, + enabled: isSearch, + }) + + const { mutate: updateSurveySales } = useMutation({ + mutationFn: surveySalesApi.update, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['survey-sales', 'list'] }) + }, + }) + + const handleUpdateSurveySales = () => { + const detailData: SurveySalesDetailInfo = { + contract_capacity: '1100', + retail_company: 'test company', + supplementary_facilities: 1, + supplementary_facilities_etc: '', + installation_system: 3, + installation_system_etc: '', + construction_year: 4, + construction_year_etc: '', + roof_material: 1, + roof_material_etc: '', + roof_shape: 2, + roof_shape_etc: '', + roof_slope: '4.5', + house_structure: 1, + house_structure_etc: '', + rafter_material: 5, + rafter_material_etc: 'test message', + rafter_size: 3, + rafter_size_etc: '', + rafter_pitch: 2, + rafter_pitch_etc: '', + rafter_direction: 3, + open_field_plate_kind: 3, + open_field_plate_kind_etc: '', + open_field_plate_thickness: '', + leak_trace: false, + waterproof_material: 2, + waterproof_material_etc: '', + insulation_presence: 3, + insulation_presence_etc: '', + structure_order: 2, + structure_order_etc: '', + installation_availability: 1, + installation_availability_etc: '', + memo: 'test memo', + } + + if (!data) return + + const surveySalesData: SurveySalesBasicInfo = { + ...data[0], + detail_info: { ...detailData }, + } + + updateSurveySales(surveySalesData) + } + return ( <>
@@ -41,13 +106,22 @@ export default function SurveySales() { - + +
{/*

Be Warned

๊ธฐ๋ณธ ๋ฐ์ดํ„ฐ ์„ธํŒ… ๋˜์–ด์žˆ์Šต๋‹ˆ๋‹ค.

*/} +
+ {errorList &&
Error: {errorList.message}
} + {data && data.map((item) =>
{JSON.stringify(item)}
)} +
) } From 45b244d30954fd583edad2c3de561f6862ca8020 Mon Sep 17 00:00:00 2001 From: yoosangwook Date: Wed, 30 Apr 2025 15:01:23 +0900 Subject: [PATCH 4/5] chore: Refactor next.config.ts to streamline API rewrites and remove unnecessary CORS headers --- next.config.ts | 23 +---------------------- 1 file changed, 1 insertion(+), 22 deletions(-) diff --git a/next.config.ts b/next.config.ts index 8304bcd..1ff5ba6 100644 --- a/next.config.ts +++ b/next.config.ts @@ -7,31 +7,10 @@ const nextConfig: NextConfig = { includePaths: [path.join(__dirname, './src/styles')], }, async rewrites() { - return [ - { - source: '/:path*', - destination: `${process.env.NEXT_PUBLIC_API_URL}/:path*`, - }, - ] - }, - async headers() { return [ { source: '/api/:path*', - headers: [ - { - key: 'Access-Control-Allow-Origin', - value: '*', - }, - { - key: 'Access-Control-Allow-Methods', - value: 'GET, POST, PUT, DELETE, OPTIONS', - }, - { - key: 'Access-Control-Allow-Headers', - value: 'Content-Type, Authorization', - }, - ], + destination: `${process.env.NEXT_PUBLIC_API_URL}/api/:path*`, }, ] }, From adfe6d7a3281f476adf7f3fcf37b9e94cccacd87 Mon Sep 17 00:00:00 2001 From: Daseul Kim Date: Wed, 30 Apr 2025 16:20:36 +0900 Subject: [PATCH 5/5] =?UTF-8?q?feat:=20=EC=A7=80=EB=B6=95=EC=9E=AC=20?= =?UTF-8?q?=EC=A0=81=ED=95=A9=EC=84=B1=20pdf=20=EA=B8=B0=EB=B3=B8=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/api/suitable/details/route.ts | 1 + src/components/Suitable.tsx | 3 +- src/components/SuitableDetails.tsx | 74 +++++++++++++--- src/components/SuitablePdf.tsx | 116 ++++++++++++++++++++++++++ src/components/SuitableSearch.tsx | 5 +- 5 files changed, 184 insertions(+), 15 deletions(-) create mode 100644 src/components/SuitablePdf.tsx diff --git a/src/app/api/suitable/details/route.ts b/src/app/api/suitable/details/route.ts index 2645411..d29f666 100644 --- a/src/app/api/suitable/details/route.ts +++ b/src/app/api/suitable/details/route.ts @@ -1,4 +1,5 @@ import { NextResponse } from 'next/server' +import { prisma } from '@/libs/prisma' export async function GET(request: Request) { const { searchParams } = new URL(request.url) diff --git a/src/components/Suitable.tsx b/src/components/Suitable.tsx index eb725a2..e21b4d4 100644 --- a/src/components/Suitable.tsx +++ b/src/components/Suitable.tsx @@ -9,6 +9,7 @@ export default function Suitable() { const router = useRouter() const queryClient = useQueryClient() const { selectedItems, addSelectedItem, removeSelectedItem } = useSuitableStore() + const handleItemClick = (item: SuitableType) => { if (!item.id) return // ์ดˆ๊ธฐ ๋ฐ์ดํ„ฐ import ๋•Œ๋ฌธ์— Suitable.id๊ฐ€ optional ํƒ€์ž…์ด๋ผ์„œ ๋ฐฉ์–ด ์ฒ˜๋ฆฌ ์ถ”๊ฐ€ selectedItems.some((selected) => selected.id === item.id) ? removeSelectedItem(item.id) : addSelectedItem(item) @@ -23,7 +24,7 @@ export default function Suitable() { enabled: false, }) - if (isLoading || !suitableList) { + if (isLoading || !suitableList || suitableList.length === 0) { return
Loading...
} diff --git a/src/components/SuitableDetails.tsx b/src/components/SuitableDetails.tsx index 795fcdc..748c28f 100644 --- a/src/components/SuitableDetails.tsx +++ b/src/components/SuitableDetails.tsx @@ -1,23 +1,71 @@ 'use client' -import { Suitable, suitableApi } from '@/api/suitable' -import { useQuery, useQueryClient } from '@tanstack/react-query' +import { suitableApi } from '@/api/suitable' +import { useQuery } from '@tanstack/react-query' +import generatePDF, { usePDF, Options } from 'react-to-pdf' +import PDFContent from './SuitablePdf' + +const pdfOptions: Options = { + filename: 'page.pdf', + method: 'save', + page: { format: 'a4', margin: 10 }, + overrides: { + pdf: { + compress: true, + }, + }, +} export default function SuitableDetails({ roofMaterial }: { roofMaterial: string }) { - console.log('๐Ÿš€ ~ SuitableDetails ~ roofMaterial:', roofMaterial) - // const { data, error, isPending } = useQuery({ - // queryKey: ['suitable-details'], - // queryFn: () => suitableApi.getDetails(roofMaterial), - // staleTime: 0, - // }) - const cache = useQueryClient() - const listData = cache.getQueryData(['suitable-list']) as Suitable[] - const data = listData.filter((item) => item.roof_material === roofMaterial) + const { data, isLoading } = useQuery({ + queryKey: ['suitable-details'], + queryFn: () => suitableApi.getDetails(roofMaterial), + staleTime: 0, + enabled: !!roofMaterial, + }) + + const { toPDF, targetRef } = usePDF({ ...pdfOptions, method: 'open' }) + + if (isLoading) { + return
Loading...
+ } return ( <> -

Searched Roof Material: {roofMaterial}

- {data && data.map((item) =>
{item.product_name}
)} + + + {/* */} + {/*
*/} +
+

Searched Roof Material: {decodeURIComponent(roofMaterial as string)}

+ {data && + data.map((item) => ( +
+
+
product_name: {item.product_name}
+
manufacturer: {item.manufacturer}
+
roof_material: {item.roof_material}
+
shape: {item.shape}
+
support_roof_tile: {item.support_roof_tile}
+
support_roof_bracket: {item.support_roof_bracket}
+
yg_anchor: {item.yg_anchor}
+
+ ))} +
+ {/*
*/} +
+
+

PDF ๋‚ด์šฉ ๋‚˜์™€๋ผ

+ +
+
) } diff --git a/src/components/SuitablePdf.tsx b/src/components/SuitablePdf.tsx new file mode 100644 index 0000000..f059744 --- /dev/null +++ b/src/components/SuitablePdf.tsx @@ -0,0 +1,116 @@ +import { Suitable } from '@/api/suitable' + +interface StatusInfo { + statusIcon: string + statusText: string +} + +interface FittingItem { + name: string + value: string + memo: string +} + +function getStatusInfo(value: string): StatusInfo | null { + const val = value?.trim()?.replace(/ใƒผ|๏ผ/g, '-') || '' + + if (['โ—‹', 'ใ€‡'].includes(val)) { + return { + statusIcon: 'โœ…', + statusText: '่จญ็ฝฎๅฏ', + } + } + + if (['ร—', 'โœ•'].includes(val)) { + return { + statusIcon: 'โŒ', + statusText: '่จญ็ฝฎไธๅฏ', + } + } + + if (['-', '๏ผ', 'ใƒผ'].includes(val)) { + return { + statusIcon: 'โ“', + statusText: 'ใŠๅ•ใ„ๅˆใ‚ใ›ใใ ใ•ใ„', + } + } + + if (val === '') return null + + return { + statusIcon: 'โœ…', + statusText: `${val} ใง่จญ็ฝฎๅฏ`, + } +} + +function getFittingItems(item: Suitable): FittingItem[] { + return [ + { name: 'ๅฑ‹ๆ น็“ฆ็”จๆ”ฏๆŒ้‡‘ๅ…ท', value: item.support_roof_tile, memo: item.support_roof_tile_memo }, + { name: 'ๅฑ‹ๆ นใƒ–ใƒฉใ‚ฑใƒƒใƒˆ็”จๆ”ฏๆŒ้‡‘ๅ…ท', value: item.support_roof_bracket, memo: item.support_roof_bracket_memo }, + { name: 'YGใ‚ขใƒณใ‚ซใƒผ', value: item.yg_anchor, memo: item.yg_anchor_memo }, + { name: 'RGๅฑ‹ๆ น็“ฆใƒ‘ใƒผใƒ„', value: item.rg_roof_tile_part, memo: item.rg_roof_tile_part_memo }, + { name: 'DIDOใƒใƒณใƒˆๆ”ฏๆŒ็“ฆ2', value: item.dido_hunt_support_tile_2, memo: item.dido_hunt_support_tile_2_memo }, + { name: '้ซ˜ๅณถใƒ‘ใƒฏใƒผใƒ™ใƒผใ‚น', value: item.takashima_power_base, memo: item.takashima_power_base_memo }, + { name: '้ซ˜ๅณถ็“ฆใƒ–ใƒฉใ‚ฑใƒƒใƒˆ', value: item.takashima_tile_bracket, memo: item.takashima_tile_bracket_memo }, + { name: 'ใ‚นใƒฌใƒผใƒˆใƒ–ใƒฉใ‚ฑใƒƒใƒˆ4', value: item.slate_bracket_4, memo: item.slate_bracket_4_memo }, + { name: 'ใ‚นใƒฌใƒผใƒˆใ‚ทใƒณใ‚ฐใƒซใƒกใ‚ฟใƒซใƒ–ใƒฉใ‚ฑใƒƒใƒˆ', value: item.slate_single_metal_bracket, memo: item.slate_single_metal_bracket_memo }, + { name: 'DIDOใƒใƒณใƒˆใ‚ทใƒงใƒผใƒˆใƒฉใƒƒใ‚ฏ4', value: item.dido_hunt_short_rack_4, memo: item.dido_hunt_short_rack_4_memo }, + { + name: '้ซ˜ๅณถใ‚นใƒฌใƒผใƒˆใƒ–ใƒฉใ‚ฑใƒƒใƒˆใ‚นใƒฌใƒผใƒˆใ‚ทใƒณใ‚ฐใƒซ', + value: item.takashima_slate_bracket_slate_single, + memo: item.takashima_slate_bracket_slate_single_memo, + }, + { name: 'DFใƒกใ‚ฟใƒซใƒ–ใƒฉใ‚ฑใƒƒใƒˆ', value: item.df_metal_bracket, memo: item.df_metal_bracket_memo }, + { name: 'ใ‚นใƒฌใƒผใƒˆใƒกใ‚ฟใƒซใƒ–ใƒฉใ‚ฑใƒƒใƒˆ', value: item.slate_metal_bracket, memo: item.slate_metal_bracket_memo }, + { name: '้ซ˜ๅณถใ‚นใƒฌใƒผใƒˆใƒ–ใƒฉใ‚ฑใƒƒใƒˆใƒกใ‚ฟใƒซๅฑ‹ๆ น', value: item.takashima_slate_bracket_metal_roof, memo: item.takashima_slate_bracket_metal_roof_memo }, + ] +} + +function FittingItem({ fitting }: { fitting: FittingItem }) { + const statusInfo = getStatusInfo(fitting.value) + if (!statusInfo) return null + + return ( +
  • + {statusInfo.statusIcon} + + {fitting.name}๏ผš{statusInfo.statusText} + + {fitting.memo && ( + + ๅ‚™่€ƒ๏ผš + {fitting.memo} + + )} +
  • + ) +} + +export default function PDFContent({ data }: { data: Suitable[] }) { + return ( +
    + +

    ้ฉๅˆ็ตๆžœ (์ ํ•ฉ๊ฒฐ๊ณผ)

    + {data.map((item) => ( +
    +
    +

    + {item.product_name}๏ผˆ{item.manufacturer} / {item.roof_material} / {item.shape}๏ผ‰ +

    +
      + {getFittingItems(item).map((fitting, index) => ( + + ))} +
    +
    + ))} +
    + ) +} diff --git a/src/components/SuitableSearch.tsx b/src/components/SuitableSearch.tsx index e339f80..4e3aa13 100644 --- a/src/components/SuitableSearch.tsx +++ b/src/components/SuitableSearch.tsx @@ -4,6 +4,7 @@ import { useEffect, useState } from 'react' import { useRouter } from 'next/navigation' import { useQuery } from '@tanstack/react-query' import { useSuitable } from '@/hooks/useSuitable' +import Link from 'next/link' export default function SuitableSearch() { const router = useRouter() @@ -74,7 +75,9 @@ export default function SuitableSearch() { ))} - + + ์ง€๋ถ•์žฌ ์ข…๋ฅ˜ ์ƒ์„ธ๋ณด๊ธฐ ํŽ˜์ด์ง€ ์ด๋™ +
    setSearchValue(e.target.value)} />