From ff644395ec3c774c7e909c657e1e83652abb6517 Mon Sep 17 00:00:00 2001 From: Daseul Kim Date: Wed, 14 May 2025 18:12:10 +0900 Subject: [PATCH 1/9] =?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=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EA=B8=B0=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/comm-code/route.ts | 44 ++++++++ src/app/api/suitable/category/route.ts | 16 --- src/app/api/suitable/detail/route.ts | 17 --- src/app/api/suitable/list/route.ts | 48 +++++++- src/components/suitable/Suitable.tsx | 89 +++++++++------ src/components/suitable/SuitableCheckData.tsx | 68 ------------ src/components/suitable/SuitableList.tsx | 76 +++++++++++++ src/hooks/useCommCode.ts | 18 +++ src/hooks/useSuitable.ts | 99 ++++++++++++++--- src/store/useSuitableStore.ts | 103 +++++++++--------- src/types/CommCode.ts | 5 + src/types/Suitable.ts | 40 +++++++ 12 files changed, 413 insertions(+), 210 deletions(-) create mode 100644 src/app/api/comm-code/route.ts delete mode 100644 src/app/api/suitable/category/route.ts delete mode 100644 src/app/api/suitable/detail/route.ts delete mode 100644 src/components/suitable/SuitableCheckData.tsx create mode 100644 src/components/suitable/SuitableList.tsx create mode 100644 src/hooks/useCommCode.ts create mode 100644 src/types/CommCode.ts create mode 100644 src/types/Suitable.ts 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..d6d8f37 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 } from '@/types/Suitable' export async function GET(request: NextRequest) { try { @@ -9,24 +10,59 @@ export async function GET(request: NextRequest) { let whereCondition: any = {} if (category) { - whereCondition['roof_material'] = category + whereCondition[SUITABLE_HEAD_CODE.ROOF_MT_CD] = category } if (keyword) { - whereCondition['product_name'] = { + whereCondition['PRODUCT_NAME'] = { contains: keyword, } } - console.log('🚀 ~ /api/suitable/list: ~ prisma where condition:', whereCondition) + // // 1 include 사용 + // // @ts-ignore + // const suitable = await prisma.MS_SUITABLE_MAIN.findMany({ + // where: whereCondition, + // include: { + // MS_SUITABLE_DETAIL: true, + // }, + // orderBy: { + // PRODUCT_NAME: 'asc', + // }, + // }) + + // 2 include 안쓰고 따로 쿼리, 쓸거만 골라서 조회 // @ts-ignore - const suitables = await prisma.MS_SUITABLE.findMany({ + const suitable = await prisma.MS_SUITABLE_MAIN.findMany({ + select: { + ID: true, + PRODUCT_NAME: true, + ROOF_MT_CD: true, + }, where: whereCondition, orderBy: { - product_name: 'asc', + PRODUCT_NAME: 'asc', }, }) + // @ts-ignore + const suitableDetail = await prisma.MS_SUITABLE_DETAIL.findMany({ + select: { + ID: true, + MAIN_ID: true, + TRESTLE_MFPC_CD: true, + TRESTLE_MANUFACTURER_PRODUCT_NAME: true, + MEMO: true, + }, + where: whereCondition + ? { + MAIN_ID: { + in: suitable.map((suitable: { ID: number }) => suitable.ID), + }, + } + : undefined, + orderBy: [{ MAIN_ID: 'asc' }, { TRESTLE_MANUFACTURER_PRODUCT_NAME: 'asc' }], + }) - return NextResponse.json(suitables) + return NextResponse.json({ suitable, suitableDetail }) } catch (error) { console.error('❌ 데이터 조회 중 오류가 발생했습니다:', error) return NextResponse.json({ error: '데이터 조회 중 오류가 발생했습니다' }, { status: 500 }) diff --git a/src/components/suitable/Suitable.tsx b/src/components/suitable/Suitable.tsx index 5120aa3..f8ebb3e 100644 --- a/src/components/suitable/Suitable.tsx +++ b/src/components/suitable/Suitable.tsx @@ -1,27 +1,69 @@ 'use client' -import { useState } from 'react' -import SuitableCheckData from './SuitableCheckData' -import SuitableNoData from './SuitableNoData' +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 { 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 &&
@@ -55,33 +97,8 @@ export default function Suitable() {
- {/* checkData */} - {/* 데이터 없을경우 버튼 영역 안보여야함 */} - - - - +
- {/* 데이터 없을경우 버튼 영역 안보여야함 */} -
-
- -
-
- -
-
- -
-
- {/* 검색기록 없을떄 위에 두 영역 안보이고 이 부분만 보여야 함*/} - {/* */} ) } diff --git a/src/components/suitable/SuitableCheckData.tsx b/src/components/suitable/SuitableCheckData.tsx deleted file mode 100644 index 2cef53b..0000000 --- a/src/components/suitable/SuitableCheckData.tsx +++ /dev/null @@ -1,68 +0,0 @@ -'use client' - -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..e5a21c6 --- /dev/null +++ b/src/components/suitable/SuitableList.tsx @@ -0,0 +1,76 @@ +'use client' + +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' + +export default function SuitableList() { + const { toCodeName, suitableSearchResults, isSearchLoading } = useSuitable() + const { selectedItems, addSelectedItem, removeSelectedItem } = useSuitableStore() + + const handleItemClick = (itemId: number) => { + selectedItems.some((selected) => selected === itemId) ? removeSelectedItem(itemId) : addSelectedItem(itemId) + } + + const suitableCheck = (value: string) => { + if (value === '×') { + return + } else if (value === 'ー') { + return + } else { + return + } + } + + const filterSuitableDetail = (mainId: number): SuitableDetail[] | undefined => { + const result: SuitableDetail[] = [] + for (const subItem of suitableSearchResults?.suitableDetail ?? []) { + if (subItem.MAIN_ID > mainId) break + if (subItem.MAIN_ID === mainId) { + result.push(subItem) + } + } + return result + } + + return ( + <> + {isSearchLoading ? ( +
Loading...
+ ) : suitableSearchResults && suitableSearchResults.suitable.length > 0 ? ( + suitableSearchResults.suitable.map((item: SuitableMain) => ( +
+
+
+ + +
+
+ +
+
+
    +
  • + {filterSuitableDetail(item.ID)?.map((subItem: SuitableDetail) => ( +
    +
    + + +
    +
    + {suitableCheck(subItem.TRESTLE_MANUFACTURER_PRODUCT_NAME)} + {subItem.MEMO && } +
    +
    + ))} +
  • +
+
+ )) + ) : ( + + )} + + ) +} 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 62f00e2..0be29a4 100644 --- a/src/hooks/useSuitable.ts +++ b/src/hooks/useSuitable.ts @@ -1,30 +1,95 @@ -import { suitableApi } from '@/api/suitable' +import { useQuery } from '@tanstack/react-query' +import { axiosInstance } from '@/libs/axios' +import { useSuitableStore } from '@/store/useSuitableStore' +import { useCommCode } from './useCommCode' +import { SUITABLE_HEAD_CODE, type SuitableData, type SuitableMain, type SuitableDetail } from '@/types/Suitable' export function useSuitable() { - const getCategories = async () => { - try { - return await suitableApi.getCategory() - } catch (error) { - console.error('카테고리 데이터 로드 실패:', error) - return [] - } - } + const { getCommCode } = useCommCode() + const { selectedCategory, searchValue, suitableCommCode, setSuitableCommCode, isSearch } = useSuitableStore() - const getSuitables = async () => { + const getSuitables = async (): Promise => { try { - return await suitableApi.getList() + const response = await axiosInstance(null).get('/api/suitable/list') + return response.data } catch (error) { console.error('지붕재 데이터 로드 실패:', error) + return { + suitable: [], + suitableDetail: [], + } } } - const updateSearchResults = async (selectedCategory: string | undefined, searchValue: string | undefined) => { - try { - return await suitableApi.getList(selectedCategory, searchValue) - } catch (error) { - console.error('지붕재 데이터 검색 실패:', error) + // 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) + }) } } - return { getCategories, getSuitables, updateSearchResults } + const toCodeName = (headCode: string, code: string): string => { + const commCode = suitableCommCode.get(headCode) + return commCode?.find((item) => item.CODE === code)?.CODE_JP || '' + } + + 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({ + queryKey: ['suitables', 'search', selectedCategory, searchValue], + queryFn: async () => { + if (!isSearch && !selectedCategory) { + return suitableList ?? (await getSuitables()) // 검색 상태가 아니면 초기 데이터 반환 임시처리 + } else { + const suitable = suitableList?.suitable.filter((item: SuitableMain) => { + const categoryMatch = !selectedCategory || item.ROOF_MT_CD === selectedCategory + const searchMatch = !searchValue || item.PRODUCT_NAME.includes(searchValue) + return categoryMatch && searchMatch + }) + const mainIds = suitable?.map((item: SuitableMain) => item.ID) + const suitableDetail = suitableList?.suitableDetail.filter((item: SuitableDetail) => { + return mainIds?.includes(item.MAIN_ID) + }) + return { + suitable: suitable ?? [], + suitableDetail: suitableDetail ?? [], + } + } + }, + staleTime: 1000 * 60 * 10, + gcTime: 1000 * 60 * 10, + enabled: true, + }) + + return { + getSuitables, + getSuitableCommCode, + toCodeName, + suitableList, + isInitialLoading, + suitableSearchResults, + refetchBySearch, + isSearchLoading, + } } 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..19daecd --- /dev/null +++ b/src/types/CommCode.ts @@ -0,0 +1,5 @@ +export type CommCode = { + HEAD_CD: string + CODE: string + CODE_JP: string +} diff --git a/src/types/Suitable.ts b/src/types/Suitable.ts new file mode 100644 index 0000000..5df0add --- /dev/null +++ b/src/types/Suitable.ts @@ -0,0 +1,40 @@ +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 SuitableIncludeDetail = { + ID: number + PRODUCT_NAME: string + MANU_FT_CD: string + ROOF_MT_CD: string + ROOF_SH_CD: string + MS_SUITABLE_DETAIL: SuitableDetail[] +} + +export type SuitableData = { + suitable: SuitableMain[] + suitableDetail: SuitableDetail[] +} + +export type SuitableMain = { + ID: number + PRODUCT_NAME: string + MANU_FT_CD: string + ROOF_MT_CD: string + ROOF_SH_CD: string +} + +export type SuitableDetail = { + ID: number + MAIN_ID: number + TRESTLE_MFPC_CD: string + TRESTLE_MANUFACTURER_PRODUCT_NAME: string + MEMO: string +} From 3e7a4140acb802e67ae689796d224c79d42135f3 Mon Sep 17 00:00:00 2001 From: Daseul Kim Date: Thu, 15 May 2025 13:27:03 +0900 Subject: [PATCH 2/9] =?UTF-8?q?docs:=20=EC=A7=80=EB=B6=95=EC=9E=AC=20?= =?UTF-8?q?=EC=A0=81=ED=95=A9=EC=84=B1=20TODO=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 17 +++++++++++++++++ src/components/suitable/SuitableList.tsx | 1 + 2 files changed, 18 insertions(+) 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/components/suitable/SuitableList.tsx b/src/components/suitable/SuitableList.tsx index e5a21c6..ce7485c 100644 --- a/src/components/suitable/SuitableList.tsx +++ b/src/components/suitable/SuitableList.tsx @@ -13,6 +13,7 @@ export default function SuitableList() { selectedItems.some((selected) => selected === itemId) ? removeSelectedItem(itemId) : addSelectedItem(itemId) } + // TODO: 추후 지붕재 적합성 데이터 CUD 구현 시 ×, ー 데이터 관리 필요 const suitableCheck = (value: string) => { if (value === '×') { return From 43928022982b85adf8825ddd9247405e1dc0c498 Mon Sep 17 00:00:00 2001 From: Daseul Kim Date: Thu, 15 May 2025 14:45:20 +0900 Subject: [PATCH 3/9] =?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=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20?= =?UTF-8?q?=EC=95=84=EC=BD=94=EB=94=94=EC=96=B8=20=EC=B2=98=EB=A6=AC=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80,=20=EC=A0=81=ED=95=A9=EC=84=B1=20=EC=95=84?= =?UTF-8?q?=EC=9D=B4=EC=BD=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/suitable/Suitable.tsx | 21 ++++++-- src/components/suitable/SuitableList.tsx | 68 +++++++++++++++--------- src/hooks/useSuitable.ts | 12 +++++ 3 files changed, 71 insertions(+), 30 deletions(-) diff --git a/src/components/suitable/Suitable.tsx b/src/components/suitable/Suitable.tsx index f8ebb3e..8a2989c 100644 --- a/src/components/suitable/Suitable.tsx +++ b/src/components/suitable/Suitable.tsx @@ -1,5 +1,6 @@ 'use client' +import Image from 'next/image' import { useEffect, useState } from 'react' import SuitableList from './SuitableList' import { useSuitable } from '@/hooks/useSuitable' @@ -77,22 +78,34 @@ export default function Suitable() {
  • - 設置可能 +
    + +
    + 設置可能
  • - 設置可能 +
    + +
    + 設置不可
  • - 設置可能 +
    + +
    + お問い合わせ
  • - 設置可能 +
    + +
    + 備考
diff --git a/src/components/suitable/SuitableList.tsx b/src/components/suitable/SuitableList.tsx index ce7485c..9a3ec85 100644 --- a/src/components/suitable/SuitableList.tsx +++ b/src/components/suitable/SuitableList.tsx @@ -1,71 +1,87 @@ 'use client' +import Image from 'next/image' +import { useState } from 'react' 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' export default function SuitableList() { - const { toCodeName, suitableSearchResults, isSearchLoading } = useSuitable() + const { toCodeName, suitableSearchResults, isSearchLoading, filterSuitableDetail } = useSuitable() const { selectedItems, addSelectedItem, removeSelectedItem } = useSuitableStore() + const [openItems, setOpenItems] = useState>(new Set()) const handleItemClick = (itemId: number) => { selectedItems.some((selected) => selected === itemId) ? removeSelectedItem(itemId) : addSelectedItem(itemId) } + const toggleItemOpen = (itemId: number) => { + setOpenItems((prev) => { + const newOpenItems = new Set(prev) + newOpenItems.has(itemId) ? newOpenItems.delete(itemId) : newOpenItems.add(itemId) + return newOpenItems + }) + } + // TODO: 추후 지붕재 적합성 데이터 CUD 구현 시 ×, ー 데이터 관리 필요 const suitableCheck = (value: string) => { if (value === '×') { - return + return ( +
+ +
+ ) } else if (value === 'ー') { - return + return ( +
+ +
+ ) } else { - return + return ( +
+ +
+ ) } } - const filterSuitableDetail = (mainId: number): SuitableDetail[] | undefined => { - const result: SuitableDetail[] = [] - for (const subItem of suitableSearchResults?.suitableDetail ?? []) { - if (subItem.MAIN_ID > mainId) break - if (subItem.MAIN_ID === mainId) { - result.push(subItem) - } - } - return result - } - return ( <> {isSearchLoading ? (
Loading...
) : suitableSearchResults && suitableSearchResults.suitable.length > 0 ? ( suitableSearchResults.suitable.map((item: SuitableMain) => ( -
+
-
- +
+ handleItemClick(item.ID)} />
- +
    -
  • - {filterSuitableDetail(item.ID)?.map((subItem: SuitableDetail) => ( -
    -
    + {filterSuitableDetail(item.ID)?.map((subItem: SuitableDetail) => ( +
  • +
    +
    {suitableCheck(subItem.TRESTLE_MANUFACTURER_PRODUCT_NAME)} - {subItem.MEMO && } + {subItem.MEMO && ( +
    + +
    + )}
    - ))} -
  • + + ))}
)) diff --git a/src/hooks/useSuitable.ts b/src/hooks/useSuitable.ts index 0be29a4..36c1b3f 100644 --- a/src/hooks/useSuitable.ts +++ b/src/hooks/useSuitable.ts @@ -82,6 +82,17 @@ export function useSuitable() { enabled: true, }) + const filterSuitableDetail = (mainId: number): SuitableDetail[] | undefined => { + const result: SuitableDetail[] = [] + for (const subItem of suitableSearchResults?.suitableDetail ?? []) { + if (subItem.MAIN_ID > mainId) break + if (subItem.MAIN_ID === mainId) { + result.push(subItem) + } + } + return result + } + return { getSuitables, getSuitableCommCode, @@ -91,5 +102,6 @@ export function useSuitable() { suitableSearchResults, refetchBySearch, isSearchLoading, + filterSuitableDetail, } } From 519e2ec1953415132edb79be7f190e5f4594a035 Mon Sep 17 00:00:00 2001 From: Daseul Kim Date: Thu, 15 May 2025 17:56:56 +0900 Subject: [PATCH 4/9] =?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=20=EB=B2=94=EB=A1=80=20default=20?= =?UTF-8?q?=EC=97=B4=EA=B8=B0,=20=EC=A7=80=EB=B6=95=EC=9E=AC=20=EC=A0=81?= =?UTF-8?q?=ED=95=A9=EC=84=B1=20=EB=B2=84=ED=8A=BC=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/Suitable.tsx | 2 +- src/components/suitable/SuitableButton.tsx | 25 ++++++++ src/components/suitable/SuitableList.tsx | 66 ++++++++++++---------- 3 files changed, 61 insertions(+), 32 deletions(-) create mode 100644 src/components/suitable/SuitableButton.tsx diff --git a/src/components/suitable/Suitable.tsx b/src/components/suitable/Suitable.tsx index 8a2989c..fe2793d 100644 --- a/src/components/suitable/Suitable.tsx +++ b/src/components/suitable/Suitable.tsx @@ -9,7 +9,7 @@ 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() 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/SuitableList.tsx b/src/components/suitable/SuitableList.tsx index 9a3ec85..d436bc9 100644 --- a/src/components/suitable/SuitableList.tsx +++ b/src/components/suitable/SuitableList.tsx @@ -2,6 +2,7 @@ import Image from 'next/image' import { useState } from 'react' +import SuitableButton from './SuitableButton' import SuitableNoData from './SuitableNoData' import { useSuitable } from '@/hooks/useSuitable' import { useSuitableStore } from '@/store/useSuitableStore' @@ -52,39 +53,42 @@ export default function SuitableList() { {isSearchLoading ? (
Loading...
) : suitableSearchResults && suitableSearchResults.suitable.length > 0 ? ( - suitableSearchResults.suitable.map((item: SuitableMain) => ( -
-
-
- handleItemClick(item.ID)} /> - -
-
- + <> + {suitableSearchResults.suitable.map((item: SuitableMain) => ( +
+
+
+ handleItemClick(item.ID)} /> + +
+
+ +
+
    + {filterSuitableDetail(item.ID)?.map((subItem: SuitableDetail) => ( +
  • +
    +
    + + +
    +
    + {suitableCheck(subItem.TRESTLE_MANUFACTURER_PRODUCT_NAME)} + {subItem.MEMO && ( +
    + +
    + )} +
    +
    +
  • + ))} +
-
    - {filterSuitableDetail(item.ID)?.map((subItem: SuitableDetail) => ( -
  • -
    -
    - - -
    -
    - {suitableCheck(subItem.TRESTLE_MANUFACTURER_PRODUCT_NAME)} - {subItem.MEMO && ( -
    - -
    - )} -
    -
    -
  • - ))} -
-
- )) + ))} + + ) : ( )} From 7a6b9cbf92de019c9e53f4fbcebc33fda9b76918 Mon Sep 17 00:00:00 2001 From: Daseul Kim Date: Tue, 20 May 2025 13:51:49 +0900 Subject: [PATCH 5/9] =?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=20=EC=A1=B0=ED=9A=8C=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=ED=8F=AC=EB=A9=A7=20=EB=B3=80=EA=B2=BD=20?= =?UTF-8?q?=EB=B0=8F=20=EC=A1=B0=ED=9A=8C=20=EB=A1=9C=EC=A7=81=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/api/suitable/list/route.ts | 88 ++++++---- src/app/api/suitable/list/test/route.ts | 71 ++++++++ src/app/suitable-test/layout.tsx | 24 +++ src/app/suitable-test/page.tsx | 9 + src/components/suitable/Suitable.tsx | 4 +- src/components/suitable/SuitableList.tsx | 177 ++++++++++++++------ src/components/suitable/SuitableListRaw.tsx | 173 +++++++++++++++++++ src/components/suitable/SuitableRaw.tsx | 118 +++++++++++++ src/hooks/useSuitable.ts | 76 ++++----- src/hooks/useSuitableRaw.ts | 109 ++++++++++++ src/libs/axios.ts | 2 +- src/types/CommCode.ts | 6 +- src/types/Suitable.ts | 52 +++--- 13 files changed, 754 insertions(+), 155 deletions(-) create mode 100644 src/app/api/suitable/list/test/route.ts create mode 100644 src/app/suitable-test/layout.tsx create mode 100644 src/app/suitable-test/page.tsx create mode 100644 src/components/suitable/SuitableListRaw.tsx create mode 100644 src/components/suitable/SuitableRaw.tsx create mode 100644 src/hooks/useSuitableRaw.ts diff --git a/src/app/api/suitable/list/route.ts b/src/app/api/suitable/list/route.ts index d6d8f37..32eac33 100644 --- a/src/app/api/suitable/list/route.ts +++ b/src/app/api/suitable/list/route.ts @@ -1,6 +1,6 @@ import { NextRequest, NextResponse } from 'next/server' import { prisma } from '@/libs/prisma' -import { SUITABLE_HEAD_CODE } from '@/types/Suitable' +import { SUITABLE_HEAD_CODE, type SuitableMain } from '@/types/Suitable' export async function GET(request: NextRequest) { try { @@ -8,29 +8,24 @@ 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[SUITABLE_HEAD_CODE.ROOF_MT_CD] = 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, } } + const startTime = performance.now() + console.log(`쿼리 (main table) 시작 시간: ${startTime}ms`) - // // 1 include 사용 - // // @ts-ignore - // const suitable = await prisma.MS_SUITABLE_MAIN.findMany({ - // where: whereCondition, - // include: { - // MS_SUITABLE_DETAIL: true, - // }, - // orderBy: { - // PRODUCT_NAME: 'asc', - // }, - // }) - - // 2 include 안쓰고 따로 쿼리, 쓸거만 골라서 조회 // @ts-ignore const suitable = await prisma.MS_SUITABLE_MAIN.findMany({ select: { @@ -38,31 +33,50 @@ export async function GET(request: NextRequest) { PRODUCT_NAME: true, ROOF_MT_CD: true, }, - where: whereCondition, + where: MainWhereCondition, orderBy: { PRODUCT_NAME: 'asc', }, }) - // @ts-ignore - const suitableDetail = await prisma.MS_SUITABLE_DETAIL.findMany({ - select: { - ID: true, - MAIN_ID: true, - TRESTLE_MFPC_CD: true, - TRESTLE_MANUFACTURER_PRODUCT_NAME: true, - MEMO: true, - }, - where: whereCondition - ? { - MAIN_ID: { - in: suitable.map((suitable: { ID: number }) => suitable.ID), - }, - } - : undefined, - orderBy: [{ MAIN_ID: 'asc' }, { TRESTLE_MANUFACTURER_PRODUCT_NAME: 'asc' }], - }) - return NextResponse.json({ suitable, suitableDetail }) + 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 fe2793d..36a397f 100644 --- a/src/components/suitable/Suitable.tsx +++ b/src/components/suitable/Suitable.tsx @@ -48,8 +48,8 @@ export default function Suitable() { diff --git a/src/components/suitable/SuitableList.tsx b/src/components/suitable/SuitableList.tsx index d436bc9..18d94e5 100644 --- a/src/components/suitable/SuitableList.tsx +++ b/src/components/suitable/SuitableList.tsx @@ -1,32 +1,90 @@ 'use client' import Image from 'next/image' -import { useState } from 'react' +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, filterSuitableDetail } = useSuitable() + 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 handleItemClick = (itemId: number) => { - selectedItems.some((selected) => selected === itemId) ? removeSelectedItem(itemId) : addSelectedItem(itemId) - } + // 선택된 아이템 확인 함수 메모이제이션 + const isItemSelected = useCallback( + (itemId: number) => { + return selectedItems.some((selected) => selected === itemId) + }, + [selectedItems], + ) - const toggleItemOpen = (itemId: number) => { + // 초기 데이터 로드 + 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 = (value: string) => { + const suitableCheck = useCallback((value: string) => { if (value === '×') { return (
@@ -46,52 +104,71 @@ export default function SuitableList() {
) } + }, []) + + // 메모이제이션된 아이템 렌더링 + 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 ( <> - {isSearchLoading ? ( -
Loading...
- ) : suitableSearchResults && suitableSearchResults.suitable.length > 0 ? ( - <> - {suitableSearchResults.suitable.map((item: SuitableMain) => ( -
-
-
- handleItemClick(item.ID)} /> - -
-
- -
-
-
    - {filterSuitableDetail(item.ID)?.map((subItem: SuitableDetail) => ( -
  • -
    -
    - - -
    -
    - {suitableCheck(subItem.TRESTLE_MANUFACTURER_PRODUCT_NAME)} - {subItem.MEMO && ( -
    - -
    - )} -
    -
    -
  • - ))} -
-
- ))} - - - ) : ( - - )} + {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/useSuitable.ts b/src/hooks/useSuitable.ts index 36c1b3f..4bdd9b2 100644 --- a/src/hooks/useSuitable.ts +++ b/src/hooks/useSuitable.ts @@ -1,23 +1,20 @@ import { useQuery } from '@tanstack/react-query' -import { axiosInstance } from '@/libs/axios' +import { axiosInstance, transformObjectKeys } from '@/libs/axios' import { useSuitableStore } from '@/store/useSuitableStore' import { useCommCode } from './useCommCode' -import { SUITABLE_HEAD_CODE, type SuitableData, type SuitableMain, type SuitableDetail } from '@/types/Suitable' +import { SUITABLE_HEAD_CODE, type SuitableDetailGroup, type SuitableMain, type Suitable, type SuitableDetail } from '@/types/Suitable' export function useSuitable() { const { getCommCode } = useCommCode() const { selectedCategory, searchValue, suitableCommCode, setSuitableCommCode, isSearch } = useSuitableStore() - const getSuitables = async (): Promise => { + const getSuitables = async (): Promise => { try { - const response = await axiosInstance(null).get('/api/suitable/list') + const response = await axiosInstance(null).get('/api/suitable/list') return response.data } catch (error) { console.error('지붕재 데이터 로드 실패:', error) - return { - suitable: [], - suitableDetail: [], - } + return { suitable: [], detail: [] } } } @@ -42,10 +39,27 @@ export function useSuitable() { const toCodeName = (headCode: string, code: string): string => { const commCode = suitableCommCode.get(headCode) - return commCode?.find((item) => item.CODE === code)?.CODE_JP || '' + return commCode?.find((item) => item.code === code)?.codeJp || '' } - const { data: suitableList, isLoading: isInitialLoading } = useQuery({ + 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 { data: suitableList, isLoading: isInitialLoading } = useQuery({ queryKey: ['suitables', 'list'], queryFn: async () => await getSuitables(), staleTime: 1000 * 60 * 10, // 10분 @@ -56,25 +70,23 @@ export function useSuitable() { data: suitableSearchResults, refetch: refetchBySearch, isLoading: isSearchLoading, - } = useQuery({ - queryKey: ['suitables', 'search', selectedCategory, searchValue], + } = useQuery({ + queryKey: ['suitables', 'search', selectedCategory, isSearch], queryFn: async () => { if (!isSearch && !selectedCategory) { - return suitableList ?? (await getSuitables()) // 검색 상태가 아니면 초기 데이터 반환 임시처리 + // 검색 상태가 아니면 초기 데이터 반환 임시처리 + return isInitialLoading ? await getSuitables() : suitableList ?? { suitable: [], detail: [] } } else { - const suitable = suitableList?.suitable.filter((item: SuitableMain) => { - const categoryMatch = !selectedCategory || item.ROOF_MT_CD === selectedCategory - const searchMatch = !searchValue || item.PRODUCT_NAME.includes(searchValue) + 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 = suitable?.map((item: SuitableMain) => item.ID) - const suitableDetail = suitableList?.suitableDetail.filter((item: SuitableDetail) => { - return mainIds?.includes(item.MAIN_ID) - }) - return { - suitable: suitable ?? [], - suitableDetail: suitableDetail ?? [], - } + }) ?? [] + 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, @@ -82,26 +94,14 @@ export function useSuitable() { enabled: true, }) - const filterSuitableDetail = (mainId: number): SuitableDetail[] | undefined => { - const result: SuitableDetail[] = [] - for (const subItem of suitableSearchResults?.suitableDetail ?? []) { - if (subItem.MAIN_ID > mainId) break - if (subItem.MAIN_ID === mainId) { - result.push(subItem) - } - } - return result - } - return { getSuitables, getSuitableCommCode, toCodeName, + toSuitableDetail, suitableList, - isInitialLoading, suitableSearchResults, refetchBySearch, isSearchLoading, - filterSuitableDetail, } } 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 d973f9d..39bd17b 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/types/CommCode.ts b/src/types/CommCode.ts index 19daecd..5847047 100644 --- a/src/types/CommCode.ts +++ b/src/types/CommCode.ts @@ -1,5 +1,5 @@ export type CommCode = { - HEAD_CD: string - CODE: string - CODE_JP: string + headCd: string + code: string + codeJp: string } diff --git a/src/types/Suitable.ts b/src/types/Suitable.ts index 5df0add..2e3563b 100644 --- a/src/types/Suitable.ts +++ b/src/types/Suitable.ts @@ -9,32 +9,36 @@ export enum SUITABLE_HEAD_CODE { TRESTLE_MFPC_CD = 'TRESTLE_MFPC_CD', } -export type SuitableIncludeDetail = { - ID: number - PRODUCT_NAME: string - MANU_FT_CD: string - ROOF_MT_CD: string - ROOF_SH_CD: string - MS_SUITABLE_DETAIL: SuitableDetail[] -} - -export type SuitableData = { - suitable: SuitableMain[] - suitableDetail: SuitableDetail[] -} - export type SuitableMain = { - ID: number - PRODUCT_NAME: string - MANU_FT_CD: string - ROOF_MT_CD: string - ROOF_SH_CD: string + id: number + productName: string + manuFtCd: string + roofMtCd: string + roofShCd: string } export type SuitableDetail = { - ID: number - MAIN_ID: number - TRESTLE_MFPC_CD: string - TRESTLE_MANUFACTURER_PRODUCT_NAME: string - MEMO: string + 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 From 2d1184e1c09c9d53878e5cb28952cc4dd179d916 Mon Sep 17 00:00:00 2001 From: yoosangwook Date: Tue, 20 May 2025 13:59:58 +0900 Subject: [PATCH 6/9] chore: Update environment variables for database connection and add MySQL2 dependency; implement partner API route and database query functionality --- .env.development | 8 +-- .env.production | 8 +-- package.json | 2 + pnpm-lock.yaml | 89 ++++++++++++++++++++++++++++++++++ src/app/api/partner/route.ts | 48 ++++++++++++++++++ src/components/ui/Main.tsx | 11 +++++ src/libs/partner.tsx | 38 +++++++++++++++ src/providers/EdgeProvider.tsx | 14 ++++++ 8 files changed, 210 insertions(+), 8 deletions(-) create mode 100644 src/app/api/partner/route.ts create mode 100644 src/libs/partner.tsx diff --git a/.env.development b/.env.development index 75df0fa..1a372fa 100644 --- a/.env.development +++ b/.env.development @@ -10,8 +10,8 @@ NEXT_PUBLIC_QSP_API_URL=http://1.248.227.176:8120 NEXT_PUBLIC_INQUIRY_API_URL=http://1.248.227.176:38080 #QPARTNER 로그인 api -DB_HOST=asdf -DB_USER=asdf -DB_PASSWORD=asdf -DB_DATABASE=asdf +DB_HOST=202.218.61.226 +DB_USER=readonly +DB_PASSWORD=aAjmFW12iHKW84l1 +DB_DATABASE=qpartners DB_PORT=3306 \ No newline at end of file diff --git a/.env.production b/.env.production index e1a6d43..4c39e83 100644 --- a/.env.production +++ b/.env.production @@ -8,8 +8,8 @@ NEXT_PUBLIC_QSP_API_URL=http://1.248.227.176:8120 NEXT_PUBLIC_INQUIRY_API_URL=http://1.248.227.176:38080 #QPARTNER 로그인 api -DB_HOST=asdf -DB_USER=asdf -DB_PASSWORD=asdf -DB_DATABASE=asdf +DB_HOST=202.218.61.226 +DB_USER=readonly +DB_PASSWORD=aAjmFW12iHKW84l1 +DB_DATABASE=qpartners DB_PORT=3306 \ No newline at end of file diff --git a/package.json b/package.json index 519fd8a..a57feca 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "iron-session": "^8.0.4", "lucide": "^0.503.0", "mssql": "^11.0.1", + "mysql2": "^3.14.1", "next": "15.2.4", "react": "^19.0.0", "react-dom": "^19.0.0", @@ -27,6 +28,7 @@ }, "devDependencies": { "@tailwindcss/postcss": "^4", + "@types/mysql": "^2.15.27", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b1875d7..9d972d8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,6 +29,9 @@ importers: mssql: specifier: ^11.0.1 version: 11.0.1 + mysql2: + specifier: ^3.14.1 + version: 3.14.1 next: specifier: 15.2.4 version: 15.2.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.87.0) @@ -57,6 +60,9 @@ importers: '@tailwindcss/postcss': specifier: ^4 version: 4.0.17 + '@types/mysql': + specifier: ^2.15.27 + version: 2.15.27 '@types/node': specifier: ^20 version: 20.17.28 @@ -676,6 +682,9 @@ packages: '@tediousjs/connection-string@0.5.0': resolution: {integrity: sha512-7qSgZbincDDDFyRweCIEvZULFAw5iz/DeunhvuxpL31nfntX3P4Yd4HkHBRg9H8CdqY1e5WFN1PZIz/REL9MVQ==} + '@types/mysql@2.15.27': + resolution: {integrity: sha512-YfWiV16IY0OeBfBCk8+hXKmdTKrKlwKN1MNKAPBu5JYxLwBEZl7QzeEpGnlZb3VMGJrrGmB84gXiH+ofs/TezA==} + '@types/node@20.17.28': resolution: {integrity: sha512-DHlH/fNL6Mho38jTy7/JT7sn2wnXI+wULR6PV4gy4VHLVvnrV/d3pHAMQHhc4gjdLmK2ZiPoMxzp6B3yRajLSQ==} @@ -712,6 +721,10 @@ packages: engines: {node: '>= 4.5.0'} hasBin: true + aws-ssl-profiles@1.1.2: + resolution: {integrity: sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==} + engines: {node: '>= 6.0.0'} + axios@1.8.4: resolution: {integrity: sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==} @@ -826,6 +839,10 @@ packages: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} + denque@2.1.0: + resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} + engines: {node: '>=0.10'} + detect-libc@1.0.3: resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==} engines: {node: '>=0.10'} @@ -911,6 +928,9 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + generate-function@2.3.1: + resolution: {integrity: sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==} + get-intrinsic@1.3.0: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} @@ -994,6 +1014,9 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + is-property@1.0.2: + resolution: {integrity: sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==} + is-wsl@3.1.0: resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==} engines: {node: '>=16'} @@ -1112,6 +1135,17 @@ packages: lodash.once@4.1.1: resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + long@5.3.2: + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + + lru-cache@7.18.3: + resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} + engines: {node: '>=12'} + + lru.min@1.1.2: + resolution: {integrity: sha512-Nv9KddBcQSlQopmBHXSsZVY5xsdlZkdH/Iey0BlcBYggMd4two7cZnKOK9vmy3nY0O5RGH99z1PCeTpPqszUYg==} + engines: {bun: '>=1.0.0', deno: '>=1.30.0', node: '>=8.0.0'} + lucide@0.503.0: resolution: {integrity: sha512-ZAVlxBU4dbSUAVidb2eT0fH3bTtKCj7M2aZNAVsFOrcnazvYJFu6I8OxFE+Fmx5XNf22Cw4Ln3NBHfBxNfoFOw==} @@ -1139,6 +1173,14 @@ packages: engines: {node: '>=18'} hasBin: true + mysql2@3.14.1: + resolution: {integrity: sha512-7ytuPQJjQB8TNAYX/H2yhL+iQOnIBjAMam361R7UAL0lOVXWjtdrmoL9HYKqKoLp/8UUTRcvo1QPvK9KL7wA8w==} + engines: {node: '>= 8.0'} + + named-placeholders@1.1.3: + resolution: {integrity: sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==} + engines: {node: '>=12.0.0'} + nanoid@3.3.11: resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -1275,6 +1317,9 @@ packages: engines: {node: '>=10'} hasBin: true + seq-queue@0.0.5: + resolution: {integrity: sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==} + sharp@0.33.5: resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -1289,6 +1334,10 @@ packages: sprintf-js@1.1.3: resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==} + sqlstring@2.3.3: + resolution: {integrity: sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==} + engines: {node: '>= 0.6'} + stackblur-canvas@2.7.0: resolution: {integrity: sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==} engines: {node: '>=0.1.14'} @@ -1890,6 +1939,10 @@ snapshots: '@tediousjs/connection-string@0.5.0': {} + '@types/mysql@2.15.27': + dependencies: + '@types/node': 20.17.28 + '@types/node@20.17.28': dependencies: undici-types: 6.19.8 @@ -1923,6 +1976,8 @@ snapshots: atob@2.1.2: {} + aws-ssl-profiles@1.1.2: {} + axios@1.8.4: dependencies: follow-redirects: 1.15.9 @@ -2041,6 +2096,8 @@ snapshots: delayed-stream@1.0.0: {} + denque@2.1.0: {} + detect-libc@1.0.3: optional: true @@ -2141,6 +2198,10 @@ snapshots: function-bind@1.1.2: {} + generate-function@2.3.1: + dependencies: + is-property: 1.0.2 + get-intrinsic@1.3.0: dependencies: call-bind-apply-helpers: 1.0.2 @@ -2230,6 +2291,8 @@ snapshots: is-number@7.0.0: optional: true + is-property@1.0.2: {} + is-wsl@3.1.0: dependencies: is-inside-container: 1.0.0 @@ -2346,6 +2409,12 @@ snapshots: lodash.once@4.1.1: {} + long@5.3.2: {} + + lru-cache@7.18.3: {} + + lru.min@1.1.2: {} + lucide@0.503.0: {} math-intrinsics@1.1.0: {} @@ -2375,6 +2444,22 @@ snapshots: transitivePeerDependencies: - supports-color + mysql2@3.14.1: + dependencies: + aws-ssl-profiles: 1.1.2 + denque: 2.1.0 + generate-function: 2.3.1 + iconv-lite: 0.6.3 + long: 5.3.2 + lru.min: 1.1.2 + named-placeholders: 1.1.3 + seq-queue: 0.0.5 + sqlstring: 2.3.3 + + named-placeholders@1.1.3: + dependencies: + lru-cache: 7.18.3 + nanoid@3.3.11: {} native-duplexpair@1.0.0: {} @@ -2508,6 +2593,8 @@ snapshots: semver@7.7.1: {} + seq-queue@0.0.5: {} + sharp@0.33.5: dependencies: color: 4.2.3 @@ -2544,6 +2631,8 @@ snapshots: sprintf-js@1.1.3: {} + sqlstring@2.3.3: {} + stackblur-canvas@2.7.0: optional: true diff --git a/src/app/api/partner/route.ts b/src/app/api/partner/route.ts new file mode 100644 index 0000000..fe3d82b --- /dev/null +++ b/src/app/api/partner/route.ts @@ -0,0 +1,48 @@ +import { NextResponse } from 'next/server' +import executeQuery from '@/libs/partner' + +export async function GET(request: Request) { + const sql = `SELECT + r.data_id, + u.id AS user_id, + u.login_id AS user_login_id, + u.password AS user_password, + u.user_name AS user_name, + u.user_name_kana AS user_name_kana, + u.sei AS user_sei, + u.mei AS user_mei, + u.sei_kana AS user_sei_kana, + u.mei_kana AS user_mei_kana, + u.user_tel AS user_tel, + u.user_fax AS user_fax, + u.status AS user_status, + u.seko_id AS user_seko_id, + u.seko_limit AS user_seko_limit, + s.id AS supplier_id, + s.code AS supplier_code, + s.name AS supplier_name, + s.name_kana AS supplier_name_kana, + s.kind AS supplier_kind +FROM + R_DATA r +JOIN + M_USER u ON r.data_id = u.id +JOIN + M_SUPPLIER s ON r.relation_id = s.id +WHERE + u.status = '1' + AND + u.seko_id is not null + AND + u.seko_limit > now() + AND + s.kind = '4' + AND + u.login_id = ? + AND + u.password = ? + ` + const data = await executeQuery(sql, []) + console.log('🚀 ~ GET ~ data:', data) + return NextResponse.json(data) +} diff --git a/src/components/ui/Main.tsx b/src/components/ui/Main.tsx index 01cdbf0..53df44f 100644 --- a/src/components/ui/Main.tsx +++ b/src/components/ui/Main.tsx @@ -1,9 +1,20 @@ 'use client' import { useRouter } from 'next/navigation' +import { useEffect } from 'react' export default function Main() { const router = useRouter() + useEffect(() => { + const fetchData = async () => { + const res = await fetch('/api/partner') + console.log('🚀 ~ fetchData ~ res:', res) + const data = await res.json() + console.log('🚀 ~ fetchData ~ data:', data) + } + fetchData() + }, []) + return ( <>
diff --git a/src/libs/partner.tsx b/src/libs/partner.tsx new file mode 100644 index 0000000..2b17677 --- /dev/null +++ b/src/libs/partner.tsx @@ -0,0 +1,38 @@ +import { createPool } from 'mysql2' + +const pool = createPool({ + host: process.env.DB_HOST as string, + user: process.env.DB_USER as string, + password: process.env.DB_PASSWORD as string, + database: process.env.DB_DATABASE as string, + port: Number(process.env.DB_PORT), + waitForConnections: true, + connectionLimit: 10, + queueLimit: 0, +}) + +pool.getConnection((err, conn) => { + if (err) console.log('Error connecting to db...') + else console.log('Connected to db...!') + conn.release() +}) + +const executeQuery = (query: string, arrParams: any[]) => { + return new Promise((resolve, reject) => { + try { + pool.query(query, arrParams, (err, data) => { + if (err) { + console.log('🚀 ~ pool.query ~ err:', err) + reject(err) + } + console.log('🚀 ~ pool.query ~ data:', data) + resolve(data) + }) + } catch (err) { + console.log('🚀 ~ returnnewPromise ~ err:', err) + reject(err) + } + }) +} + +export default executeQuery diff --git a/src/providers/EdgeProvider.tsx b/src/providers/EdgeProvider.tsx index 4cfd67e..ed2d329 100644 --- a/src/providers/EdgeProvider.tsx +++ b/src/providers/EdgeProvider.tsx @@ -7,6 +7,7 @@ import { useHeaderStore } from '@/store/header' import { usePopupController } from '@/store/popupController' import { useSideNavState } from '@/store/sideNavState' import { useSessionStore } from '@/store/session' +import { tracking } from '@/libs/tracking' declare global { interface Window { @@ -27,6 +28,17 @@ export default function EdgeProvider({ children, sessionData }: EdgeProviderProp const { setAlertMsg, setAlertBtn, setAlert, setAlert2, setAlert2BtnYes, setAlert2BtnNo } = usePopupController() const { session, setSession } = useSessionStore() + /** + * 사용자 이벤트 트래킹 처리 + * + */ + const handlePageEvent = (path: string) => { + tracking({ + url: path, + data: '', + }) + } + /** * alert 함수 - window.alert 함수 대체 * @param msg @@ -88,6 +100,8 @@ export default function EdgeProvider({ children, sessionData }: EdgeProviderProp } //사이드바 초기화 reset() + // 페이지 이벤트 트래킹 + // handlePageEvent(pathname) }, [pathname]) return <>{children} From fcd80cbe3b0e763fd03887d5150afbf2c6371163 Mon Sep 17 00:00:00 2001 From: nalpari Date: Tue, 20 May 2025 14:16:19 +0900 Subject: [PATCH 7/9] ... --- .env.development | 10 ++--- .env.production | 10 ++--- src/app/api/partner/route.ts | 81 ++++++++++++++++++------------------ src/app/layout.tsx | 2 +- 4 files changed, 52 insertions(+), 51 deletions(-) diff --git a/.env.development b/.env.development index 1a372fa..506406f 100644 --- a/.env.development +++ b/.env.development @@ -10,8 +10,8 @@ NEXT_PUBLIC_QSP_API_URL=http://1.248.227.176:8120 NEXT_PUBLIC_INQUIRY_API_URL=http://1.248.227.176:38080 #QPARTNER 로그인 api -DB_HOST=202.218.61.226 -DB_USER=readonly -DB_PASSWORD=aAjmFW12iHKW84l1 -DB_DATABASE=qpartners -DB_PORT=3306 \ No newline at end of file +#DB_HOST=202.218.61.226 +#DB_USER=readonly +#DB_PASSWORD=aAjmFW12iHKW84l1 +#DB_DATABASE=qpartners +#DB_PORT=3306 \ No newline at end of file diff --git a/.env.production b/.env.production index 4c39e83..67f32f2 100644 --- a/.env.production +++ b/.env.production @@ -8,8 +8,8 @@ NEXT_PUBLIC_QSP_API_URL=http://1.248.227.176:8120 NEXT_PUBLIC_INQUIRY_API_URL=http://1.248.227.176:38080 #QPARTNER 로그인 api -DB_HOST=202.218.61.226 -DB_USER=readonly -DB_PASSWORD=aAjmFW12iHKW84l1 -DB_DATABASE=qpartners -DB_PORT=3306 \ No newline at end of file +#DB_HOST=202.218.61.226 +#DB_USER=readonly +#DB_PASSWORD=aAjmFW12iHKW84l1 +#DB_DATABASE=qpartners +#DB_PORT=3306 \ No newline at end of file diff --git a/src/app/api/partner/route.ts b/src/app/api/partner/route.ts index fe3d82b..60b1540 100644 --- a/src/app/api/partner/route.ts +++ b/src/app/api/partner/route.ts @@ -2,46 +2,47 @@ import { NextResponse } from 'next/server' import executeQuery from '@/libs/partner' export async function GET(request: Request) { - const sql = `SELECT - r.data_id, - u.id AS user_id, - u.login_id AS user_login_id, - u.password AS user_password, - u.user_name AS user_name, - u.user_name_kana AS user_name_kana, - u.sei AS user_sei, - u.mei AS user_mei, - u.sei_kana AS user_sei_kana, - u.mei_kana AS user_mei_kana, - u.user_tel AS user_tel, - u.user_fax AS user_fax, - u.status AS user_status, - u.seko_id AS user_seko_id, - u.seko_limit AS user_seko_limit, - s.id AS supplier_id, - s.code AS supplier_code, - s.name AS supplier_name, - s.name_kana AS supplier_name_kana, - s.kind AS supplier_kind -FROM - R_DATA r -JOIN - M_USER u ON r.data_id = u.id -JOIN - M_SUPPLIER s ON r.relation_id = s.id -WHERE - u.status = '1' - AND - u.seko_id is not null - AND - u.seko_limit > now() - AND - s.kind = '4' - AND - u.login_id = ? - AND - u.password = ? - ` + // const sqls = `SELECT + // r.data_id, + // u.id AS user_id, + // u.login_id AS user_login_id, + // u.password AS user_password, + // u.user_name AS user_name, + // u.user_name_kana AS user_name_kana, + // u.sei AS user_sei, + // u.mei AS user_mei, + // u.sei_kana AS user_sei_kana, + // u.mei_kana AS user_mei_kana, + // u.user_tel AS user_tel, + // u.user_fax AS user_fax, + // u.status AS user_status, + // u.seko_id AS user_seko_id, + // u.seko_limit AS user_seko_limit, + // s.id AS supplier_id, + // s.code AS supplier_code, + // s.name AS supplier_name, + // s.name_kana AS supplier_name_kana, + // s.kind AS supplier_kind + // FROM + // R_DATA r + // JOIN + // M_USER u ON r.data_id = u.id + // JOIN + // M_SUPPLIER s ON r.relation_id = s.id + // WHERE + // u.status = '1' + // AND + // u.seko_id is not null + // AND + // u.seko_limit > now() + // AND + // s.kind = '4' + // AND + // u.login_id = ? + // AND + // u.password = ? + // ` + const sql = 'SELECT * FROM M_USER' const data = await executeQuery(sql, []) console.log('🚀 ~ GET ~ data:', data) return NextResponse.json(data) diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 7ba05b0..508c340 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -22,7 +22,7 @@ interface RootLayoutProps { header: ReactNode footer: ReactNode floatBtn: ReactNode -}6 +} export default async function RootLayout({ children, header, footer, floatBtn }: RootLayoutProps): Promise { const cookieStore = await cookies() From c1641e167d9d9932eb6c8471d52af741721eafe6 Mon Sep 17 00:00:00 2001 From: nalpari Date: Tue, 20 May 2025 14:19:00 +0900 Subject: [PATCH 8/9] ...2 --- src/components/ui/Main.tsx | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/components/ui/Main.tsx b/src/components/ui/Main.tsx index 53df44f..01cdbf0 100644 --- a/src/components/ui/Main.tsx +++ b/src/components/ui/Main.tsx @@ -1,20 +1,9 @@ 'use client' import { useRouter } from 'next/navigation' -import { useEffect } from 'react' export default function Main() { const router = useRouter() - useEffect(() => { - const fetchData = async () => { - const res = await fetch('/api/partner') - console.log('🚀 ~ fetchData ~ res:', res) - const data = await res.json() - console.log('🚀 ~ fetchData ~ data:', data) - } - fetchData() - }, []) - return ( <>
From 6033054c6e260148bed0b2429b11abf78c5d56c3 Mon Sep 17 00:00:00 2001 From: yoosangwook Date: Tue, 20 May 2025 17:08:25 +0900 Subject: [PATCH 9/9] chore: Update environment variables for database connection and refactor authentication API routes to improve session handling and login logic --- .env.development | 10 +- .env.production | 10 +- src/app/api/auth/chg-pwd/route.ts | 1 - src/app/api/auth/logout/route.ts | 6 +- src/app/api/auth/route.ts | 4 +- src/app/api/partner/route.ts | 170 ++++++++++++++++++++++-------- src/components/Login.tsx | 10 +- src/providers/EdgeProvider.tsx | 9 +- 8 files changed, 156 insertions(+), 64 deletions(-) diff --git a/.env.development b/.env.development index 506406f..1a372fa 100644 --- a/.env.development +++ b/.env.development @@ -10,8 +10,8 @@ NEXT_PUBLIC_QSP_API_URL=http://1.248.227.176:8120 NEXT_PUBLIC_INQUIRY_API_URL=http://1.248.227.176:38080 #QPARTNER 로그인 api -#DB_HOST=202.218.61.226 -#DB_USER=readonly -#DB_PASSWORD=aAjmFW12iHKW84l1 -#DB_DATABASE=qpartners -#DB_PORT=3306 \ No newline at end of file +DB_HOST=202.218.61.226 +DB_USER=readonly +DB_PASSWORD=aAjmFW12iHKW84l1 +DB_DATABASE=qpartners +DB_PORT=3306 \ No newline at end of file diff --git a/.env.production b/.env.production index 67f32f2..4c39e83 100644 --- a/.env.production +++ b/.env.production @@ -8,8 +8,8 @@ NEXT_PUBLIC_QSP_API_URL=http://1.248.227.176:8120 NEXT_PUBLIC_INQUIRY_API_URL=http://1.248.227.176:38080 #QPARTNER 로그인 api -#DB_HOST=202.218.61.226 -#DB_USER=readonly -#DB_PASSWORD=aAjmFW12iHKW84l1 -#DB_DATABASE=qpartners -#DB_PORT=3306 \ No newline at end of file +DB_HOST=202.218.61.226 +DB_USER=readonly +DB_PASSWORD=aAjmFW12iHKW84l1 +DB_DATABASE=qpartners +DB_PORT=3306 \ No newline at end of file diff --git a/src/app/api/auth/chg-pwd/route.ts b/src/app/api/auth/chg-pwd/route.ts index 436f101..71e9f6b 100644 --- a/src/app/api/auth/chg-pwd/route.ts +++ b/src/app/api/auth/chg-pwd/route.ts @@ -1,5 +1,4 @@ import { NextResponse } from 'next/server' - import { axiosInstance } from '@/libs/axios' export async function POST(req: Request) { diff --git a/src/app/api/auth/logout/route.ts b/src/app/api/auth/logout/route.ts index 390f8d1..78f0a58 100644 --- a/src/app/api/auth/logout/route.ts +++ b/src/app/api/auth/logout/route.ts @@ -1,8 +1,8 @@ -import { sessionOptions } from '@/libs/session' -import { SessionData } from '@/types/Auth' -import { getIronSession } from 'iron-session' +import type { SessionData } from '@/types/Auth' import { cookies } from 'next/headers' import { NextResponse } from 'next/server' +import { getIronSession } from 'iron-session' +import { sessionOptions } from '@/libs/session' export async function GET(request: Request) { const cookieStore = await cookies() diff --git a/src/app/api/auth/route.ts b/src/app/api/auth/route.ts index 32ac8f9..8caef42 100644 --- a/src/app/api/auth/route.ts +++ b/src/app/api/auth/route.ts @@ -1,12 +1,10 @@ +import type { SessionData } from '@/types/Auth' import { cookies } from 'next/headers' import { NextResponse } from 'next/server' - import { getIronSession } from 'iron-session' import { axiosInstance } from '@/libs/axios' import { sessionOptions } from '@/libs/session' -import type { SessionData } from '@/types/Auth' - export async function POST(request: Request) { const { loginId, pwd } = await request.json() diff --git a/src/app/api/partner/route.ts b/src/app/api/partner/route.ts index 60b1540..abb63df 100644 --- a/src/app/api/partner/route.ts +++ b/src/app/api/partner/route.ts @@ -1,49 +1,129 @@ +import type { SessionData } from '@/types/Auth' import { NextResponse } from 'next/server' +import { cookies } from 'next/headers' +import { getIronSession } from 'iron-session' import executeQuery from '@/libs/partner' +import { sessionOptions } from '@/libs/session' -export async function GET(request: Request) { - // const sqls = `SELECT - // r.data_id, - // u.id AS user_id, - // u.login_id AS user_login_id, - // u.password AS user_password, - // u.user_name AS user_name, - // u.user_name_kana AS user_name_kana, - // u.sei AS user_sei, - // u.mei AS user_mei, - // u.sei_kana AS user_sei_kana, - // u.mei_kana AS user_mei_kana, - // u.user_tel AS user_tel, - // u.user_fax AS user_fax, - // u.status AS user_status, - // u.seko_id AS user_seko_id, - // u.seko_limit AS user_seko_limit, - // s.id AS supplier_id, - // s.code AS supplier_code, - // s.name AS supplier_name, - // s.name_kana AS supplier_name_kana, - // s.kind AS supplier_kind - // FROM - // R_DATA r - // JOIN - // M_USER u ON r.data_id = u.id - // JOIN - // M_SUPPLIER s ON r.relation_id = s.id - // WHERE - // u.status = '1' - // AND - // u.seko_id is not null - // AND - // u.seko_limit > now() - // AND - // s.kind = '4' - // AND - // u.login_id = ? - // AND - // u.password = ? - // ` - const sql = 'SELECT * FROM M_USER' - const data = await executeQuery(sql, []) - console.log('🚀 ~ GET ~ data:', data) - return NextResponse.json(data) +export async function POST(request: Request) { + const cookieStore = await cookies() + const session = await getIronSession(cookieStore, sessionOptions) + const { loginId, pwd } = await request.json() + + const sql = ` + SELECT + r.data_id, + u.id AS user_id, + u.login_id AS user_login_id, + u.password AS user_password, + u.user_name AS user_name, + u.user_name_kana AS user_name_kana, + u.sei AS user_sei, + u.mei AS user_mei, + u.sei_kana AS user_sei_kana, + u.mei_kana AS user_mei_kana, + u.user_tel AS user_tel, + u.user_fax AS user_fax, + u.status AS user_status, + u.seko_id AS user_seko_id, + u.seko_limit AS user_seko_limit, + s.id AS supplier_id, + s.code AS supplier_code, + s.name AS supplier_name, + s.name_kana AS supplier_name_kana, + s.kind AS supplier_kind + FROM + R_DATA r + JOIN + M_USER u ON r.data_id = u.id + JOIN + M_SUPPLIER s ON r.relation_id = s.id + WHERE + u.status = '1' + AND + u.seko_id is not null + AND + u.seko_limit > now() + AND + s.kind = '4' + AND + u.login_id = ? + AND + u.password = ? + ` + // const sql = 'SELECT * FROM M_USER' + const data = (await executeQuery(sql, [loginId, pwd])) as any[] + console.log('🚀 ~ POST ~ data:', data) + + if (data.length > 0) { + console.log('start session edit!') + session.langCd = null + session.currPage = null + session.rowCount = null + session.startRow = null + session.endRow = null + session.compCd = null + session.agencyStoreId = null + session.storeId = data[0].supplier_code + session.storeNm = data[0].supplier_name + session.userId = data[0].user_login_id + session.category = data[0].supplier_name + session.userNm = `${data[0].user_sei} ${data[0].user_mei}` + session.userNmKana = `${data[0].user_sei_kana} ${data[0].user_mei_kana}` + session.telNo = data[0].tel + session.fax = data[0].fax + session.email = data[0].user_login_id + session.lastEditUser = null + session.storeGubun = null + session.pwCurr = null + session.pwdInitYn = null + session.apprStatCd = null + session.loginFailCnt = null + session.loginFailMinYn = null + session.priceViewStatCd = null + session.groupId = null + session.storeLvl = null + session.custCd = null + session.builderNo = data[0].user_seko_id + session.isLoggedIn = true + session.role = 'Partner' + + console.log('end session edit!') + + await session.save() + } + + // qsp 유저 데이터 모양과 맞춰서 변환 + const result = { + LANG_CD: null, + CURR_PAGE: null, + ROW_COUNT: null, + START_ROW: null, + END_ROW: null, + COMP_CD: null, + AGENCY_STORE_ID: null, + STORE_ID: data[0].supplier_code, + STORE_NM: data[0].supplier_name, + USER_ID: data[0].user_login_id, + CATEGORY: data[0].supplier_name, + USER_NM: `${data[0].user_sei} ${data[0].user_mei}`, + USER_NM_KANA: `${data[0].user_sei_kana} ${data[0].user_mei_kana}`, + TEL_NO: data[0].tel, + FAX: data[0].fax, + EMAIL: data[0].user_login_id, + LAST_EDIT_USER: null, + STORE_GUBUN: null, + PW_CURR: null, + PWD_INIT_YN: null, + APPR_STAT_CD: null, + LOGIN_FAIL_CNT: null, + LOGIN_FAIL_MIN_YN: null, + PRICE_VIEW_STAT_CD: null, + GROUP_ID: null, + STORE_LVL: null, + CUST_CD: null, + BUILDER_NO: data[0].user_seko_id, + } + + return NextResponse.json({ code: 200, message: 'Partner Login is Succecss!!', result }) } diff --git a/src/components/Login.tsx b/src/components/Login.tsx index 5f086af..d5edc00 100644 --- a/src/components/Login.tsx +++ b/src/components/Login.tsx @@ -48,7 +48,14 @@ export default function Login() { } = useQuery({ queryKey: ['login', 'account'], queryFn: async () => { - const { data } = await axiosInstance('').post(`/api/auth`, { + let url = '' + if (!isPartners) { + url = '/api/auth' + } else { + url = '/api/partner' + } + + const { data } = await axiosInstance('').post(`${url}`, { loginId: account.loginId, pwd: account.pwd, }) @@ -68,6 +75,7 @@ export default function Login() { indivisualData: account.pwd, }) // 세션 정보 저장 + console.log('🚀 ~ Login ~ loginData:', loginData) setSession({ ...session, ...loginData?.result, diff --git a/src/providers/EdgeProvider.tsx b/src/providers/EdgeProvider.tsx index ed2d329..de3c00f 100644 --- a/src/providers/EdgeProvider.tsx +++ b/src/providers/EdgeProvider.tsx @@ -1,7 +1,7 @@ 'use client' import { useEffect } from 'react' -import { usePathname } from 'next/navigation' +import { usePathname, useRouter } from 'next/navigation' import { useHeaderStore } from '@/store/header' import { usePopupController } from '@/store/popupController' @@ -22,12 +22,19 @@ interface EdgeProviderProps { } export default function EdgeProvider({ children, sessionData }: EdgeProviderProps) { + const router = useRouter() const pathname = usePathname() const { setBackBtn } = useHeaderStore() const { reset } = useSideNavState() const { setAlertMsg, setAlertBtn, setAlert, setAlert2, setAlert2BtnYes, setAlert2BtnNo } = usePopupController() const { session, setSession } = useSessionStore() + if (pathname === '/login') { + if (session?.isLoggedIn) { + router.push('/') + } + } + /** * 사용자 이벤트 트래킹 처리 *