From ff644395ec3c774c7e909c657e1e83652abb6517 Mon Sep 17 00:00:00 2001 From: Daseul Kim Date: Wed, 14 May 2025 18:12:10 +0900 Subject: [PATCH 01/36] =?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 02/36] =?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 03/36] =?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 04/36] =?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 c653df0ce7776883680e0f3e30a8e2aa3fcf014b Mon Sep 17 00:00:00 2001 From: keyy1315 Date: Fri, 16 May 2025 11:06:41 +0900 Subject: [PATCH 05/36] fix: change address input to can change --- .../survey-sale/detail/form/etcProcess/SelectBoxEtc.tsx | 2 +- src/components/survey-sale/temp/basicRegist.tsx | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/components/survey-sale/detail/form/etcProcess/SelectBoxEtc.tsx b/src/components/survey-sale/detail/form/etcProcess/SelectBoxEtc.tsx index 9469a40..f509972 100644 --- a/src/components/survey-sale/detail/form/etcProcess/SelectBoxEtc.tsx +++ b/src/components/survey-sale/detail/form/etcProcess/SelectBoxEtc.tsx @@ -93,7 +93,7 @@ export const selectBoxOptions: Record void }) { - const { addressData } = useAddressStore() const { session } = useSessionStore() @@ -129,7 +127,6 @@ export default function BasicRegist({ placeholder="都道府県" value={basicInfoData.ADDRESS ?? ''} onChange={(e) => handleChange('ADDRESS', e.target.value)} - readOnly />
From 6bcc466a768066ecea02aedbc4ddd77529977071 Mon Sep 17 00:00:00 2001 From: keyy1315 Date: Fri, 16 May 2025 14:58:15 +0900 Subject: [PATCH 06/36] fix: change filtering survey --- src/app/api/survey-sales/route.ts | 31 +++++++++---------- src/components/survey-sale/list/ListTable.tsx | 16 ++++++---- .../survey-sale/list/SearchForm.tsx | 8 ++--- 3 files changed, 29 insertions(+), 26 deletions(-) diff --git a/src/app/api/survey-sales/route.ts b/src/app/api/survey-sales/route.ts index d4b676a..23f3d18 100644 --- a/src/app/api/survey-sales/route.ts +++ b/src/app/api/survey-sales/route.ts @@ -16,7 +16,7 @@ type SearchParams = { } type WhereCondition = { - AND?: any[] + AND: any[] OR?: any[] [key: string]: any } @@ -43,7 +43,7 @@ const ITEMS_PER_PAGE = 10 * @returns 검색 조건 객체 */ const createKeywordSearchCondition = (keyword: string, searchOption: string): WhereCondition => { - const where: WhereCondition = {} + const where: WhereCondition = { AND: [] } if (searchOption === 'all') { // 모든 필드 검색 시 OR 조건 사용 @@ -74,7 +74,6 @@ const createKeywordSearchCondition = (keyword: string, searchOption: string): Wh where.ID = { equals: null } } } - return where } @@ -105,21 +104,18 @@ const createMemberRoleCondition = (params: SearchParams): WhereCondition => { AND: [ { STORE: { equals: params.store } }, { - OR: [ - { CONSTRUCTION_POINT: { equals: null } }, - { CONSTRUCTION_POINT: { equals: '' } } - ] - } - ] + OR: [{ CONSTRUCTION_POINT: { equals: null } }, { CONSTRUCTION_POINT: { equals: '' } }], + }, + ], }, { AND: [ { STORE: { equals: params.store } }, { CONSTRUCTION_POINT: { not: null } }, { CONSTRUCTION_POINT: { not: '' } }, - { SUBMISSION_STATUS: { equals: true } } - ] - } + { SUBMISSION_STATUS: { equals: true } }, + ], + }, ] break @@ -159,20 +155,23 @@ export async function GET(request: Request) { } // 검색 조건 구성 - const where: WhereCondition = {} + const where: WhereCondition = { AND: [] } // 내가 작성한 매물 조건 적용 if (params.isMySurvey) { - where.REPRESENTATIVE = params.isMySurvey + where.AND.push({ REPRESENTATIVE: params.isMySurvey }) } // 키워드 검색 조건 적용 if (params.keyword && params.searchOption) { - Object.assign(where, createKeywordSearchCondition(params.keyword, params.searchOption)) + where.AND.push(createKeywordSearchCondition(params.keyword, params.searchOption)) } // 회원 유형 조건 적용 - Object.assign(where, createMemberRoleCondition(params)) + const roleCondition = createMemberRoleCondition(params) + if (Object.keys(roleCondition).length > 0) { + where.AND.push(roleCondition) + } // 데이터 조회 또는 카운트 if (params.offset) { diff --git a/src/components/survey-sale/list/ListTable.tsx b/src/components/survey-sale/list/ListTable.tsx index 16351b7..468f01d 100644 --- a/src/components/survey-sale/list/ListTable.tsx +++ b/src/components/survey-sale/list/ListTable.tsx @@ -19,22 +19,26 @@ export default function ListTable() { const { session } = useSessionStore() useEffect(() => { - if (surveyList && surveyList.length > 0) { + if (surveyList) { if (offset === 0) { setHeldSurveyList(surveyList) } else { - const remainingList = heldSurveyList.slice(offset, offset + 10) - if (JSON.stringify(remainingList) !== JSON.stringify(surveyList)) { - setHeldSurveyList((prev) => [...prev, ...surveyList]) - } + setHeldSurveyList(prev => [...prev, ...surveyList]) } setHasMore(surveyListCount > offset + 10) + } else { + setHeldSurveyList([]) + setHasMore(false) } - }, [surveyList, surveyListCount, offset, session?.role]) + }, [surveyList, surveyListCount, offset]) + + console.log('surveyList:: ', surveyList) + console.log('heldSurveyList:: ', heldSurveyList) const handleDetailClick = (id: number) => { router.push(`/survey-sale/${id}`) } + const handleItemsInit = () => { setHeldSurveyList([]) setOffset(0) diff --git a/src/components/survey-sale/list/SearchForm.tsx b/src/components/survey-sale/list/SearchForm.tsx index 3f3d234..16a259d 100644 --- a/src/components/survey-sale/list/SearchForm.tsx +++ b/src/components/survey-sale/list/SearchForm.tsx @@ -17,7 +17,7 @@ export default function SearchForm({ onItemsInit, memberRole, userId }: { onItem } setKeyword(searchKeyword) setSearchOption(option) - onItemsInit() + // onItemsInit() } const searchOptions = memberRole === 'Partner' ? SEARCH_OPTIONS_PARTNERS : SEARCH_OPTIONS @@ -38,7 +38,7 @@ export default function SearchForm({ onItemsInit, memberRole, userId }: { onItem if (e.target.value === 'all') { setKeyword('') setSearchKeyword('') - onItemsInit() + // onItemsInit() setSearchOption('all') setOption('all') } else { @@ -80,7 +80,7 @@ export default function SearchForm({ onItemsInit, memberRole, userId }: { onItem checked={isMySurvey === userId} onChange={() => { setIsMySurvey(isMySurvey === userId ? null : userId) - onItemsInit() + // onItemsInit() }} /> @@ -94,7 +94,7 @@ export default function SearchForm({ onItemsInit, memberRole, userId }: { onItem value={sort} onChange={(e) => { setSort(e.target.value as 'created' | 'updated') - onItemsInit() + // onItemsInit() }} > From cdae73b95e54bfb990eb07ecd10780ed7e96710c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=AF=BC=EC=8B=9D?= <43837214+Minsiki@users.noreply.github.com> Date: Fri, 16 May 2025 15:32:23 +0900 Subject: [PATCH 07/36] =?UTF-8?q?=F0=9F=9A=A8chore:=20Sync=20Sass=20[?= =?UTF-8?q?=EC=A1=B0=EC=82=AC=EB=A7=A4=EB=AC=BC]=20=EC=83=81=EC=84=B8,=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=ED=8D=BC=EB=B8=94=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/assets/images/sub/sale_toggle_btn.svg | 4 + .../images/sub/sale_toggle_btn_white.svg | 6 + src/app/survey-sale/[id]/page.tsx | 20 +- src/app/survey-sale/layout.tsx | 5 +- .../survey-sale/detail/BasicForm.tsx | 175 ++++++++++-------- .../survey-sale/detail/DataTable.tsx | 2 +- .../survey-sale/detail/DetailForm.tsx | 101 +++++----- src/styles/base/_check-radio.scss | 3 + src/styles/components/_sub.scss | 97 +++++++--- src/types/Survey.ts | 4 +- 10 files changed, 249 insertions(+), 168 deletions(-) create mode 100644 public/assets/images/sub/sale_toggle_btn.svg create mode 100644 public/assets/images/sub/sale_toggle_btn_white.svg diff --git a/public/assets/images/sub/sale_toggle_btn.svg b/public/assets/images/sub/sale_toggle_btn.svg new file mode 100644 index 0000000..ad2504a --- /dev/null +++ b/public/assets/images/sub/sale_toggle_btn.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/assets/images/sub/sale_toggle_btn_white.svg b/public/assets/images/sub/sale_toggle_btn_white.svg new file mode 100644 index 0000000..3575065 --- /dev/null +++ b/public/assets/images/sub/sale_toggle_btn_white.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/app/survey-sale/[id]/page.tsx b/src/app/survey-sale/[id]/page.tsx index 52202ca..bc48257 100644 --- a/src/app/survey-sale/[id]/page.tsx +++ b/src/app/survey-sale/[id]/page.tsx @@ -1,11 +1,29 @@ import DataTable from '@/components/survey-sale/detail/DataTable' import DetailForm from '@/components/survey-sale/detail/DetailForm' +import { SurveyBasicInfo } from '@/types/Survey' export default function page() { + const surveyInfo: SurveyBasicInfo = { + ID: 1, + REPRESENTATIVE: 'HG', + STORE: 'HWJ(T01)', + CONSTRUCTION_POINT: '施工点名表示', + INVESTIGATION_DATE: '2021-01-01', + BUILDING_NAME: 'ビル名表示', + CUSTOMER_NAME: '顧客名表示', + POST_CODE: '1234567890', + ADDRESS: '東京都千代田区永田町1-7-1', + ADDRESS_DETAIL: '永田町ビル101号室', + SUBMISSION_STATUS: true, + SUBMISSION_DATE: '2021-01-01', + DETAIL_INFO: null, + REG_DT: new Date(), + UPT_DT: new Date(), + } return ( <> - + ) } diff --git a/src/app/survey-sale/layout.tsx b/src/app/survey-sale/layout.tsx index f558ad6..83f37c0 100644 --- a/src/app/survey-sale/layout.tsx +++ b/src/app/survey-sale/layout.tsx @@ -9,10 +9,7 @@ export default function layout({ children, navTab }: SurveySaleLayoutProps) { return ( <>
-
- {navTab} - {children} -
+
{children}
) diff --git a/src/components/survey-sale/detail/BasicForm.tsx b/src/components/survey-sale/detail/BasicForm.tsx index f293495..5b7fb31 100644 --- a/src/components/survey-sale/detail/BasicForm.tsx +++ b/src/components/survey-sale/detail/BasicForm.tsx @@ -1,10 +1,18 @@ 'use client' -import { useEffect } from 'react' +import { useEffect, useState } from 'react' import { useSurveySaleTabState } from '@/store/surveySaleTabState' +import { SurveyBasicInfo } from '@/types/Survey' +import { Mode } from 'fs' -export default function BasicForm() { +export default function BasicForm(props: { + surveyInfo: Partial + setSurveyInfo: (surveyInfo: Partial) => void + mode: Mode +}) { + const { surveyInfo, setSurveyInfo, mode } = props const { setBasicInfoSelected } = useSurveySaleTabState() + const [isFlip, setIsFlip] = useState(true) useEffect(() => { setBasicInfoSelected() @@ -12,84 +20,101 @@ export default function BasicForm() { return ( <> -
-
-
-
担当者名
- -
-
-
販売店
- -
-
-
施工店
- +
+
setIsFlip(!isFlip)}> +
基本情報
+
+
-
+
+
+
+
+
担当者名
+ setSurveyInfo({ ...surveyInfo, REPRESENTATIVE: e.target.value })} + /> +
+
+
販売店
+ setSurveyInfo({ ...surveyInfo, BUILDING_NAME: e.target.value })} + /> +
+
+
施工店
+ setSurveyInfo({ ...surveyInfo, CONSTRUCTION_POINT: e.target.value })} + /> +
+
+
+
+
+
+
現地調査日
+ {['CREATE', 'EDIT'].includes(mode as 'CREATE' | 'EDIT') ? ( +
+ + +
+ ) : ( + + )} +
+
+ {/* 건물명 */} +
建物名
+ +
+
+ {/* 고객명 */} +
建物名
+ +
+
+
郵便番号/都道府県
+
+ {/* 우편번호 */} +
+ +
+ {/* 도도부현 */} +
+ +
+
+ {/* 주소 */} +
+ +
+
+ +
+
-
-
-
-
現地調査日
-
- - -
-
-
-
建物名
- -
-
-
建物名
- -
-
-
建物名
-
-
- -
-
- +
+
市区町村名, 以後の住所
+
-
- -
-
-
-
市区町村名, 以後の住所
- -
-
-
-
- -
-
- -
-
-
diff --git a/src/components/survey-sale/detail/DataTable.tsx b/src/components/survey-sale/detail/DataTable.tsx index 414164c..b47c092 100644 --- a/src/components/survey-sale/detail/DataTable.tsx +++ b/src/components/survey-sale/detail/DataTable.tsx @@ -3,7 +3,7 @@ export default function DataTable() { return ( <> -
+
diff --git a/src/components/survey-sale/detail/DetailForm.tsx b/src/components/survey-sale/detail/DetailForm.tsx index 9462e95..6e5fc24 100644 --- a/src/components/survey-sale/detail/DetailForm.tsx +++ b/src/components/survey-sale/detail/DetailForm.tsx @@ -1,62 +1,53 @@ 'use client' -export default function DetailForm() { +import { Mode, SurveyBasicInfo } from '@/types/Survey' +import { useEffect, useState } from 'react' +import ButtonForm from './ButtonForm' +import BasicForm from './BasicForm' +import RoofForm from './RoofForm' + +export default function DetailForm(props: { surveyInfo?: SurveyBasicInfo; mode?: Mode }) { + const [surveyInfo, setSurveyInfo] = useState>( + props.surveyInfo ?? { + REPRESENTATIVE: '', + CONSTRUCTION_POINT: '', + STORE: '', + INVESTIGATION_DATE: '', + BUILDING_NAME: '', + CUSTOMER_NAME: '', + POST_CODE: '', + ADDRESS: '', + ADDRESS_DETAIL: '', + SUBMISSION_STATUS: true, + SUBMISSION_DATE: '2021-01-01', + DETAIL_INFO: null, + REG_DT: new Date(), + UPT_DT: new Date(), + }, + ) + const [roofValue, setRoofValue] = useState(false) + const [mode, setMode] = useState(props.mode ?? 'CREATE') + const basicInfoProps = { surveyInfo, setSurveyInfo, mode } + const roofInfoProps = { surveyInfo, mode } + const buttonFormProps = { mode, setMode } + + useEffect(() => { + // setMode(props.surveyInfo ? 'EDIT' : 'CREATE') + }, [props.surveyInfo]) + + useEffect(() => { + console.log(surveyInfo) + }, [surveyInfo]) + return ( <> -
-
-
-
担当者名
- -
-
-
販売店
- -
-
-
施工店
- -
-
-
- -
-
-
-
現地調査日
- -
-
-
建物名
- -
-
-
顧客名
- -
-
-
-
- -
-
- -
-
- -
-
- -
-
+
+ {mode} + {/* 기본정보 */} + + {/* 전기/지붕정보 */} + +
) diff --git a/src/styles/base/_check-radio.scss b/src/styles/base/_check-radio.scss index d4d2904..47c50cb 100644 --- a/src/styles/base/_check-radio.scss +++ b/src/styles/base/_check-radio.scss @@ -85,6 +85,9 @@ input[type="checkbox"]:disabled + label::after{ cursor: default; } + input[type="checkbox"]:disabled:checked + label::before{ + background-color: #A8B6C7; + } &.ch-bld{ input[type="checkbox"]:checked + label{ font-weight: 500; diff --git a/src/styles/components/_sub.scss b/src/styles/components/_sub.scss index ebcb374..0723ab1 100644 --- a/src/styles/components/_sub.scss +++ b/src/styles/components/_sub.scss @@ -77,36 +77,11 @@ } } } -.sale-detail-tab-relative{ - height: 40px; - margin-bottom: 10px; -} -.sale-detail-tab-wrap{ - position: fixed; - top: 66px; - left: 0; - width: 100%; - height: 40px; - background-color: $white-fff; - z-index: 98000; - .sale-detail-tab-inner{ - position: relative; - @include flex(0px); - align-items: center; - height: 100%; - .sale-detail-tab{ - flex: 1; - height: 100%; - background-color: #fff; - border-top: 1px solid #DDDFE2; - border-bottom: 1px solid #DDDFE2; - @include defaultFont($font-s-13, $font-w-500, $font-c); - &.act{ - color: $white-fff; - background-color: #5F738E; - border-color: #5F738E; - } - } +.sale-form-btn-wrap{ + padding: 20px 20px 0 ; + background-color: #fff; + .btn-flex-wrap{ + margin-top: 0; } } @@ -202,6 +177,11 @@ } // 매물 상세 +.sale-data-table-wrap{ + padding: 24px; + background-color: #fff; + border-top: 1px solid #ECECEC; +} .sale-data-table{ width: 100%; table-layout: fixed; @@ -242,6 +222,63 @@ } } +.sale-detail-toggle-wrap{ + border-top: 1px solid #ECECEC; +} +.sale-detail-toggle-bx{ + border-bottom: 1px solid #ECECEC; +} +.sale-detail-toggle-head{ + @include flex(5px); + padding: 14px 18px; + background-color: $white-fff; + cursor: pointer; + .sale-detail-toggle-name{ + @include defaultFont($font-s-13, $font-w-500, $font-c); + } + .sale-detail-toggle-btn-wrap{ + margin-left: auto; + .sale-detail-toggle-btn{ + display: block; + width: 22px; + height: 22px; + background: url(/assets/images/sub/sale_toggle_btn.svg)no-repeat center; + background-size: cover + } + } +} +.sale-detail-toggle-cont{ + display: none; + .sale-frame{ + padding: 24px 20px; + &:first-child{ + padding-top: 24px; + } + &:last-child{ + padding-bottom: 24px; + } + } +} + +.sale-detail-toggle-bx{ + &.act{ + .sale-detail-toggle-head{ + background-color: #5F738E; + .sale-detail-toggle-name{ + color: #fff + } + .sale-detail-toggle-btn-wrap{ + .sale-detail-toggle-btn{ + background: url(/assets/images/sub/sale_toggle_btn_white.svg)no-repeat center; + } + } + } + .sale-detail-toggle-cont{ + display: block; + } + } +} + // 매물 기본정보 .form-flex{ @include flex(5px); diff --git a/src/types/Survey.ts b/src/types/Survey.ts index 1401c47..83d2ec1 100644 --- a/src/types/Survey.ts +++ b/src/types/Survey.ts @@ -1,5 +1,3 @@ -import { SEARCH_OPTIONS_ENUM, SEARCH_OPTIONS_PARTNERS_ENUM, SORT_OPTIONS_ENUM } from '@/store/surveyFilterStore' - export type SurveyBasicInfo = { ID: number REPRESENTATIVE: string @@ -130,3 +128,5 @@ export type SurveyRegistRequest = { SUBMISSION_DATE: string | null DETAIL_INFO: SurveyDetailRequest | null } + +export type Mode = 'CREATE' | 'EDIT' | 'READ' | 'TEMP' // 등록 | 수정 | 상세 | 임시저장 From e3b755896bf7e21f3c641c90ac8854b07d258648 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=AF=BC=EC=8B=9D?= <43837214+Minsiki@users.noreply.github.com> Date: Fri, 16 May 2025 15:33:00 +0900 Subject: [PATCH 08/36] =?UTF-8?q?[=EC=A1=B0=EC=82=AC=EB=A7=A4=EB=AC=BC]=20?= =?UTF-8?q?=EC=A0=84=EA=B8=B0.=EC=A7=80=EB=B6=95=EC=A0=95=EB=B3=B4,=20?= =?UTF-8?q?=EB=B2=84=ED=8A=BC=20=ED=8F=BC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../survey-sale/detail/ButtonForm.tsx | 81 ++++ .../survey-sale/detail/RoofForm.tsx | 396 ++++++++++++++++++ 2 files changed, 477 insertions(+) create mode 100644 src/components/survey-sale/detail/ButtonForm.tsx create mode 100644 src/components/survey-sale/detail/RoofForm.tsx diff --git a/src/components/survey-sale/detail/ButtonForm.tsx b/src/components/survey-sale/detail/ButtonForm.tsx new file mode 100644 index 0000000..84b4b4c --- /dev/null +++ b/src/components/survey-sale/detail/ButtonForm.tsx @@ -0,0 +1,81 @@ +import { Mode } from '@/types/Survey' + +export default function ButtonForm(props: { mode: Mode; setMode: (mode: Mode) => void }) { + const { mode, setMode } = props + return ( + <> + {mode === 'CREATE' && ( +
+
+
+ {/* 임시저장 */} + +
+
+ {/* 저장 */} + +
+
+ {/* 목록 */} + +
+
+
+ )} + {mode === 'TEMP' && ( +
+
+
+ {/* 수정 */} + +
+
+ {/* 삭제 */} + +
+
+
+ )} + {mode === 'EDIT' && ( +
+
+
+ {/* 목록 */} + +
+
+ {/* 제출 */} + +
+
+ {/* 수정 */} + +
+
+ {/* 삭제 */} + +
+
+
+ )} + + ) +} diff --git a/src/components/survey-sale/detail/RoofForm.tsx b/src/components/survey-sale/detail/RoofForm.tsx new file mode 100644 index 0000000..878fbf2 --- /dev/null +++ b/src/components/survey-sale/detail/RoofForm.tsx @@ -0,0 +1,396 @@ +import { useState } from 'react' +import { Mode, SurveyBasicInfo } from '@/types/Survey' + +export default function RoofForm(props: { surveyInfo: Partial; mode: Mode }) { + const { surveyInfo, mode } = props + const [isFlip, setIsFlip] = useState(true) + return ( +
+
setIsFlip(!isFlip)}> +
電気 / 屋根情報
+
+ +
+
+
+
+ {/* 전기 관계 */} +
電気関係
+
+
+ {/* 전기 계약 용량 */} +
電気契約容量
+
+ +
+ {mode === 'READ' && } + {mode !== 'READ' && ( +
+ +
+ )} +
+
+ {/* 전기 소매 회사사 */} +
電気小売会社
+ +
+
+ {/* 전기 부대 설비 */} +
+ 電気袋設備※複数選択可能 +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+
+
+ {/* 설치 희망 시스템 */} +
設置希望システム
+ {mode === 'READ' && ( +
+ +
+ )} + {mode !== 'READ' && ( +
+ +
+ )} +
+
+
+
+ {/* 지붕 관계 */} +
屋根関係
+
+
+ {/* 건축 연수 */} +
建築研修
+
+ {mode === 'READ' && } + {mode !== 'READ' && ( + + )} +
+
+ + +
+
+
+ {/* 지붕재 */} +
+ 屋根材※最大2個まで選択可能 +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+
+
+ {/* 지붕 모양 */} +
建築研修
+
+ {mode === 'READ' && } + {mode !== 'READ' && ( + + )} +
+
+ +
+
+
+ {/* 지붕 경사도도 */} +
屋根の斜面
+
+ + +
+
+
+ {/* 주택구조조 */} +
住宅構造
+
+ + +
+
+ + +
+
+ +
+
+
+ {/* 서까래 재질 */} +
垂木材質
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+
+
+ {/* 서까래 크기 */} +
垂木サイズ
+
+ {mode === 'READ' && } + {mode !== 'READ' && ( + + )} +
+
+ +
+
+
+ {/* 서까래 피치 */} +
垂木サイズ
+
+ {mode === 'READ' && } + {mode !== 'READ' && ( + + )} +
+
+ +
+
+
+ {/* 서까래 방향 */} +
垂木の方向
+
+
+ + +
+
+ + +
+
+
+
+ {/* 노지판 종류류 */} +
路地板の種類
+
+ {mode === 'READ' && } + {mode !== 'READ' && ( + + )} +
+
+ +
+
+
+
+ 路地板厚※小幅板を選択した場合, 厚さ. 小幅板間の間隔寸法を記載 +
+
+ + mm +
+
+
+ {/* 서까래 방향 */} +
垂木の方向
+
+
+ + +
+
+ + +
+
+
+
+ {/* 주택 구조 */} +
住宅構造
+
+ + +
+
+ + +
+
+ +
+
+
+ {/* 단열재 유무 */} +
断熱材の有無
+
+ + +
+
+ +
+
+ + +
+
+
+ {/* 지붕 구조의 순서 */} +
路地板の種類
+
+ {mode === 'READ' && } + {mode !== 'READ' && ( + + )} +
+
+ +
+
+
+ {/* 지붕 제품명 설치 가능 여부 확인 */} +
屋根製品名 設置可否確認
+
+ {mode === 'READ' && } + {mode !== 'READ' && ( + + )} +
+
+ +
+
+
+ {/* 메모 */} +
メモ
+
+ +
+
+
+
+
+
+ ) +} From 4e8fd462cf6e62fe4a6b008720c753d4efcf5ed5 Mon Sep 17 00:00:00 2001 From: yoosangwook Date: Fri, 16 May 2025 16:56:38 +0900 Subject: [PATCH 09/36] feat: add MS_USR_TRK model to Prisma schema, update axios instance with request interceptor, and enhance Footer component with PDF link --- package-lock.json | 804 +++++++++++++++++++++++++++- prisma/schema.prisma | 10 + src/app/api/tracking/route.ts | 33 ++ src/app/pdf/page.tsx | 9 + src/components/DownloadPDF.tsx | 748 ++++++++++++++++++++++++++ src/components/ui/common/Footer.tsx | 11 +- src/libs/axios.ts | 34 +- src/libs/tracking.ts | 10 + 8 files changed, 1646 insertions(+), 13 deletions(-) create mode 100644 src/app/api/tracking/route.ts create mode 100644 src/app/pdf/page.tsx create mode 100644 src/components/DownloadPDF.tsx create mode 100644 src/libs/tracking.ts diff --git a/package-lock.json b/package-lock.json index 07a4241..f753e7a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,9 @@ "@prisma/client": "^6.7.0", "@tanstack/react-query": "^5.71.0", "@tanstack/react-query-devtools": "^5.71.0", + "@types/html-pdf": "^3.0.3", "axios": "^1.8.4", + "html-pdf": "^3.0.1", "iron-session": "^8.0.4", "mssql": "^11.0.1", "next": "15.2.4", @@ -1940,6 +1942,15 @@ "integrity": "sha512-7qSgZbincDDDFyRweCIEvZULFAw5iz/DeunhvuxpL31nfntX3P4Yd4HkHBRg9H8CdqY1e5WFN1PZIz/REL9MVQ==", "license": "MIT" }, + "node_modules/@types/html-pdf": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/html-pdf/-/html-pdf-3.0.3.tgz", + "integrity": "sha512-Cw6EpCU5OdSG/yytol7hFNLHxwNoYqOeYL+1GqjhA3YBMJTC8mvT5tFmpLpjrj4WKqe7QoerX4pDwQcXsTotIA==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/node": { "version": "20.17.28", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.28.tgz", @@ -2014,6 +2025,43 @@ "node": ">= 14" } }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "license": "MIT", + "optional": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.8" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -2032,6 +2080,23 @@ "node": ">= 4.5.0" } }, + "node_modules/aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": "*" + } + }, + "node_modules/aws4": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz", + "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==", + "license": "MIT", + "optional": true + }, "node_modules/axios": { "version": "1.8.4", "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz", @@ -2072,6 +2137,16 @@ ], "license": "MIT" }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, "node_modules/bl": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-6.1.0.tgz", @@ -2133,12 +2208,29 @@ "ieee754": "^1.2.1" } }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": "*" + } + }, "node_modules/buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", "license": "BSD-3-Clause" }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT", + "optional": true + }, "node_modules/bundle-name": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", @@ -2225,6 +2317,13 @@ "license": "MIT", "optional": true }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", + "license": "Apache-2.0", + "optional": true + }, "node_modules/chokidar": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", @@ -2312,6 +2411,48 @@ "node": ">=16" } }, + "node_modules/concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "engines": [ + "node >= 0.8" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/concat-stream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "optional": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/concat-stream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "optional": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/cookie": { "version": "0.7.2", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", @@ -2333,6 +2474,13 @@ "url": "https://opencollective.com/core-js" } }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "license": "MIT", + "optional": true + }, "node_modules/css-line-break": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", @@ -2349,6 +2497,19 @@ "devOptional": true, "license": "MIT" }, + "node_modules/dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", + "license": "MIT", + "optional": true, + "dependencies": { + "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/debug": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", @@ -2449,6 +2610,17 @@ "node": ">= 0.4" } }, + "node_modules/ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", + "license": "MIT", + "optional": true, + "dependencies": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", @@ -2517,6 +2689,13 @@ "node": ">= 0.4" } }, + "node_modules/es6-promise": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==", + "license": "MIT", + "optional": true + }, "node_modules/esbuild": { "version": "0.25.4", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz", @@ -2589,6 +2768,80 @@ "node": ">=0.8.x" } }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT", + "optional": true + }, + "node_modules/extract-zip": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.7.0.tgz", + "integrity": "sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA==", + "license": "BSD-2-Clause", + "optional": true, + "dependencies": { + "concat-stream": "^1.6.2", + "debug": "^2.6.9", + "mkdirp": "^0.5.4", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + } + }, + "node_modules/extract-zip/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "optional": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/extract-zip/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT", + "optional": true + }, + "node_modules/extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", + "engines": [ + "node >=0.6.0" + ], + "license": "MIT", + "optional": true + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT", + "optional": true + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "license": "MIT", + "optional": true + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "license": "MIT", + "optional": true, + "dependencies": { + "pend": "~1.2.0" + } + }, "node_modules/fflate": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", @@ -2628,6 +2881,16 @@ } } }, + "node_modules/forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": "*" + } + }, "node_modules/form-data": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", @@ -2643,6 +2906,18 @@ "node": ">= 6" } }, + "node_modules/fs-extra": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-1.0.0.tgz", + "integrity": "sha512-VerQV6vEKuhDWD2HGOybV6v5I73syoc/cXAbKlgTC7M/oFVEtklWlp9QH2Ijw3IaWDOQcMkldSPa7zXy79Z/UQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^2.1.0", + "klaw": "^1.0.0" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -2704,6 +2979,16 @@ "node": ">= 0.4" } }, + "node_modules/getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", + "license": "MIT", + "optional": true, + "dependencies": { + "assert-plus": "^1.0.0" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -2720,9 +3005,34 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, + "devOptional": true, "license": "ISC" }, + "node_modules/har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==", + "license": "ISC", + "optional": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/har-validator": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", + "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", + "deprecated": "this library is no longer supported", + "license": "MIT", + "optional": true, + "dependencies": { + "ajv": "^6.12.3", + "har-schema": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -2750,6 +3060,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/hasha": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/hasha/-/hasha-2.2.0.tgz", + "integrity": "sha512-jZ38TU/EBiGKrmyTNNZgnvCZHNowiRI4+w/I9noMlekHTZH3KyGgvJLmhSgykeAQ9j2SYPDosM0Bg3wHfzibAQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "is-stream": "^1.0.1", + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -2762,6 +3086,22 @@ "node": ">= 0.4" } }, + "node_modules/html-pdf": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-pdf/-/html-pdf-3.0.1.tgz", + "integrity": "sha512-CKNSacmQn+CKJ2GNfT4UYKaPy/T3Ndj82yJ2aju/UPmnvWNjIpyumqRqkFU0mwT6BTHBFhFGTnXN8dBn4Bdj0Q==", + "deprecated": "Please migrate your projects to a newer library like puppeteer", + "license": "MIT", + "bin": { + "html-pdf": "bin/index.js" + }, + "engines": { + "node": ">=4.0.0" + }, + "optionalDependencies": { + "phantomjs-prebuilt": "^2.1.16" + } + }, "node_modules/html2canvas": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", @@ -2788,6 +3128,22 @@ "node": ">= 14" } }, + "node_modules/http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + }, + "engines": { + "node": ">=0.8", + "npm": ">=1.3.7" + } + }, "node_modules/https-proxy-agent": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", @@ -2942,6 +3298,23 @@ "node": ">=0.12.0" } }, + "node_modules/is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "license": "MIT", + "optional": true + }, "node_modules/is-wsl": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", @@ -2957,6 +3330,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT", + "optional": true + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC", + "optional": true + }, + "node_modules/isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", + "license": "MIT", + "optional": true + }, "node_modules/jiti": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", @@ -2973,6 +3367,44 @@ "integrity": "sha512-/GDnfQYsltsjRswQhN9fhv3EMw2sCpUdrdxyWDOUK7eyD++r3gRhzgiQgc/x4MAv2i1iuQ4lxO5mvqM3vj4bwA==", "license": "MIT" }, + "node_modules/jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", + "license": "MIT", + "optional": true + }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "license": "(AFL-2.1 OR BSD-3-Clause)", + "optional": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "license": "MIT", + "optional": true + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "license": "ISC", + "optional": true + }, + "node_modules/jsonfile": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz", + "integrity": "sha512-PKllAqbgLgxHaj8TElYymKCAgrASebJrWpTnEkOaTowt23VKXXN0sUeriJ+eh7y6ufb/CC5ap11pz71/cM0hUw==", + "license": "MIT", + "optional": true, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, "node_modules/jsonwebtoken": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", @@ -3034,6 +3466,22 @@ "html2canvas": "^1.0.0-rc.5" } }, + "node_modules/jsprim": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", + "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", + "license": "MIT", + "optional": true, + "dependencies": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, "node_modules/jwa": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", @@ -3055,6 +3503,23 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/kew": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/kew/-/kew-0.7.0.tgz", + "integrity": "sha512-IG6nm0+QtAMdXt9KvbgbGdvY50RSrw+U4sGZg+KlrSKPJEwVE5JVoI3d7RWfSMdBQneRheeAOj3lIjX5VL/9RQ==", + "license": "Apache-2.0", + "optional": true + }, + "node_modules/klaw": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/klaw/-/klaw-1.3.1.tgz", + "integrity": "sha512-TED5xi9gGQjGpNnvRWknrwAB1eL5GciPfVFOt3Vk1OJCVDQbzuSfrF3hkUQKlsgKrG1F+0t5W0m+Fje1jIt8rw==", + "license": "MIT", + "optional": true, + "optionalDependencies": { + "graceful-fs": "^4.1.9" + } + }, "node_modules/lightningcss": { "version": "1.29.2", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.29.2.tgz", @@ -3386,6 +3851,29 @@ "node": ">= 0.6" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "optional": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "optional": true, + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -3525,6 +4013,16 @@ "license": "MIT", "optional": true }, + "node_modules/oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": "*" + } + }, "node_modules/open": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/open/-/open-10.1.0.tgz", @@ -3543,6 +4041,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "license": "MIT", + "optional": true + }, "node_modules/performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", @@ -3550,6 +4055,29 @@ "license": "MIT", "optional": true }, + "node_modules/phantomjs-prebuilt": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/phantomjs-prebuilt/-/phantomjs-prebuilt-2.1.16.tgz", + "integrity": "sha512-PIiRzBhW85xco2fuj41FmsyuYHKjKuXWmhjy3A/Y+CMpN/63TV+s9uzfVhsUwFe0G77xWtHBG8xmXf5BqEUEuQ==", + "deprecated": "this package is now deprecated", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "es6-promise": "^4.0.3", + "extract-zip": "^1.6.5", + "fs-extra": "^1.0.0", + "hasha": "^2.2.0", + "kew": "^0.7.0", + "progress": "^1.1.8", + "request": "^2.81.0", + "request-progress": "^2.0.1", + "which": "^1.2.10" + }, + "bin": { + "phantomjs": "bin/phantomjs" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -3569,6 +4097,29 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==", + "license": "MIT", + "optional": true, + "dependencies": { + "pinkie": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/postcss": { "version": "8.5.3", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", @@ -3636,12 +4187,61 @@ "node": ">= 0.6.0" } }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT", + "optional": true + }, + "node_modules/progress": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/progress/-/progress-1.1.8.tgz", + "integrity": "sha512-UdA8mJ4weIkUBO224tIarHzuHs4HuYiJvsuGT7j/SPQiUJVjYvNDBIPa0hAorduOfjGohB/qHWRa/lrrWX/mXw==", + "optional": true, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "license": "MIT" }, + "node_modules/psl": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", + "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", + "license": "MIT", + "optional": true, + "dependencies": { + "punycode": "^2.3.1" + }, + "funding": { + "url": "https://github.com/sponsors/lupomontero" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", + "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.6" + } + }, "node_modules/raf": { "version": "3.4.1", "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", @@ -3722,6 +4322,75 @@ "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", "license": "MIT" }, + "node_modules/request": { + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/request-progress": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/request-progress/-/request-progress-2.0.1.tgz", + "integrity": "sha512-dxdraeZVUNEn9AvLrxkgB2k6buTlym71dJk1fk4v8j3Ou3RKNm07BcgbHdj2lLgYGfqX71F+awb1MR+tWPFJzA==", + "license": "MIT", + "optional": true, + "dependencies": { + "throttleit": "^1.0.0" + } + }, + "node_modules/request/node_modules/form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/request/node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "license": "MIT", + "optional": true, + "bin": { + "uuid": "bin/uuid" + } + }, "node_modules/rfdc": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", @@ -3865,6 +4534,32 @@ "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", "license": "BSD-3-Clause" }, + "node_modules/sshpk": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", + "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + }, + "bin": { + "sshpk-conv": "bin/sshpk-conv", + "sshpk-sign": "bin/sshpk-sign", + "sshpk-verify": "bin/sshpk-verify" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/stackblur-canvas": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz", @@ -4030,6 +4725,16 @@ "utrie": "^1.0.2" } }, + "node_modules/throttleit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.1.tgz", + "integrity": "sha512-vDZpf9Chs9mAdfY046mcPt8fg5QSZr37hEH4TXYBnDF+izxgrbRGUAAaBvIk/fJm9aOFCGFd1EsNg5AZCbnQCQ==", + "license": "MIT", + "optional": true, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -4043,12 +4748,53 @@ "node": ">=8.0" } }, + "node_modules/tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "license": "Unlicense", + "optional": true + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT", + "optional": true + }, "node_modules/typescript": { "version": "5.8.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", @@ -4075,6 +4821,16 @@ "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", "license": "MIT" }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "license": "BSD-2-Clause", + "optional": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, "node_modules/usehooks-ts": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/usehooks-ts/-/usehooks-ts-3.1.1.tgz", @@ -4090,6 +4846,13 @@ "react": "^16.8.0 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT", + "optional": true + }, "node_modules/utrie": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", @@ -4108,6 +4871,45 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", + "engines": [ + "node >=0.6.0" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "license": "MIT", + "optional": true, + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, "node_modules/zustand": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.3.tgz", diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 7996ce0..83c5c61 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -207,3 +207,13 @@ model MS_SUITABLE_MAIN { UPT_DT DateTime? MS_SUITABLE_DETAIL MS_SUITABLE_DETAIL[] } + +/// The underlying table does not contain a valid unique identifier and can therefore currently not be handled by Prisma Client. +model MS_USR_TRK { + ID Int @id @default(autoincrement()) + OWNER String @db.VarChar(100) + TYPE String @db.VarChar(50) + URL String? @db.VarChar(200) + data String? @db.VarChar(200) + REG_DT DateTime @default(now()) +} diff --git a/src/app/api/tracking/route.ts b/src/app/api/tracking/route.ts new file mode 100644 index 0000000..d5f5607 --- /dev/null +++ b/src/app/api/tracking/route.ts @@ -0,0 +1,33 @@ +import type { SessionData } from '@/types/Auth' +import { NextResponse } from 'next/server' +import { cookies } from 'next/headers' +import { Prisma } from '@prisma/client' +import { getIronSession } from 'iron-session' +import { sessionOptions } from '@/libs/session' + +export const POST = async (request: Request) => { + const { url, data } = await request.json() + + const cookieStore = await cookies() + const session = await getIronSession(cookieStore, sessionOptions) + + let owner = session.userId + let type = '' + if (url.includes('api')) { + type = 'api' + } else { + type = 'page' + } + + // @ts-ignore + const result = await Prisma.MS_USR_TRK.create({ + data: { + owner, + type, + url, + data: JSON.stringify(data), + }, + }) + + return NextResponse.json({ message: 'Tracking data received', result }) +} diff --git a/src/app/pdf/page.tsx b/src/app/pdf/page.tsx new file mode 100644 index 0000000..9a48790 --- /dev/null +++ b/src/app/pdf/page.tsx @@ -0,0 +1,9 @@ +import DownloadPdf from '@/components/DownloadPDF' + +export default function page() { + return ( + <> + + + ) +} diff --git a/src/components/DownloadPDF.tsx b/src/components/DownloadPDF.tsx new file mode 100644 index 0000000..8532483 --- /dev/null +++ b/src/components/DownloadPDF.tsx @@ -0,0 +1,748 @@ +'use client' + +import { useRef } from 'react' +import generatePDF, { Margin, Resolution } from 'react-to-pdf' + +export default function DownloadPdf() { + const targetRef = useRef(null) + const handleDownPdf = () => { + const options = { + method: 'open' as const, + resolution: Resolution.HIGH, + page: { + margin: Margin.SMALL, + format: 'letter', + orientation: 'landscape' as const, + }, + canvas: { + mimeType: 'image/png' as const, + qualityRatio: 1, + }, + overrides: { + pdf: { + compress: true, + }, + canvas: { + useCORS: true, + }, + }, + } + + // generatePDF(targetRef, options) + generatePDF(targetRef, { filename: 'page.pdf' }) + } + return ( + <> + +
+
+
+
+ HWJ 現地調査シート1/2 +
+
+
+

+ 現地明登施工店名 +

+

+ Sheet2 No.4Sheet2 +

+
+
+

現地阴買日

+

2025.05.09

+
+
+
+
+
+ + + + + + + + + + +
+ お客様名 + + Sheet2No.1 +
+ ご住所 + + Sheet2No.2 +
+
+
+
も気開係
+ + + + + + + + + + + + + + + + + +
+ 雨気契约容国 + + Sheet2No.1 + + 電気契約会社 + + Sheet2No.1 +
+ 電気付带設備 + + Sheet2No.7 選式回答表示/自由入力回答表示 +
+ 設置希望システム + + Sheet2No.8 選択式回表示/自由入力回表 +
+
+
+
屋根眀係
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ 築年数 + + No.9 + + 至根材 + + No.10 + + 座根形状 + + No.11 +
+ 座根勾配 + + No.12 + + 住宅樠造 + + No.13 +
+ 並木材質 + + No.14 + + 垂木サイズ + + No.15 +
+ 垂木ビッチ + + No.16 + + 垂木方向 + + No.17 +
+ 野地板種類 + + No.18 + + 野地板厚さ + + No.19 +
+ 兩漏の形跡 + + No.20 +
+ ルーフィング種類 + + No.21 +
+ 断熱材の有無 + + No.22 +
+ 屋根構造の順番 + + No.23 +
+
+
+ + + + + + + +
+ 区根製品名設置可否確認 + + No.23 +
+
+
+
メモ
+
+ No.25 +
+
+
+
+ + ) +} diff --git a/src/components/ui/common/Footer.tsx b/src/components/ui/common/Footer.tsx index dedeb50..08cce69 100644 --- a/src/components/ui/common/Footer.tsx +++ b/src/components/ui/common/Footer.tsx @@ -1,8 +1,17 @@ +'use client' + +import Link from 'next/link' + export default function Footer() { return ( <>
-
COPYRIGHT©2025 Hanwha Japan All Rights Reserved
+
+ COPYRIGHT©2025 Hanwha Japan All Rights Reserved{' '} + + PDF + +
) diff --git a/src/libs/axios.ts b/src/libs/axios.ts index 5f1b076..80823ea 100644 --- a/src/libs/axios.ts +++ b/src/libs/axios.ts @@ -1,26 +1,38 @@ import axios from 'axios' +import { tracking } from './tracking' export const axiosInstance = (url: string | null | undefined) => { const baseURL = url || process.env.NEXT_PUBLIC_API_URL - - return axios.create({ + const instance = axios.create({ baseURL, headers: { Accept: 'application/json', }, }) + + instance.interceptors.request.use( + (config) => { + console.log('🚀 ~ config:', config) + return config + }, + (error) => { + return Promise.reject(error) + }, + ) + + return instance } // Request interceptor -axios.interceptors.request.use( - (config) => { - // 여기에 토큰 추가 등의 공통 로직을 넣을 수 있습니다 - return config - }, - (error) => { - return Promise.reject(error) - }, -) +// axios.interceptors.request.use( +// (config) => { +// // 여기에 토큰 추가 등의 공통 로직을 넣을 수 있습니다 +// return config +// }, +// (error) => { +// return Promise.reject(error) +// }, +// ) // Response interceptor axios.interceptors.response.use( diff --git a/src/libs/tracking.ts b/src/libs/tracking.ts new file mode 100644 index 0000000..1fc2d06 --- /dev/null +++ b/src/libs/tracking.ts @@ -0,0 +1,10 @@ +import { axiosInstance } from './axios' + +export const tracking = async (params: { url: string; data: string }) => { + const { url, data } = params + const result = await axiosInstance(null).post('/api/tracking', { + url, + data, + }) + console.log('🚀 ~ result ~ result:', result) +} From 3d9cd1d9923b59cf0ac461de0c28b33e690281b0 Mon Sep 17 00:00:00 2001 From: nalpari Date: Fri, 16 May 2025 17:46:02 +0900 Subject: [PATCH 10/36] fix: Update PDF download options to A4 format and portrait orientation --- src/components/DownloadPDF.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/DownloadPDF.tsx b/src/components/DownloadPDF.tsx index 8532483..5df8d87 100644 --- a/src/components/DownloadPDF.tsx +++ b/src/components/DownloadPDF.tsx @@ -11,8 +11,8 @@ export default function DownloadPdf() { resolution: Resolution.HIGH, page: { margin: Margin.SMALL, - format: 'letter', - orientation: 'landscape' as const, + format: 'A4', + orientation: 'portrait' as const, }, canvas: { mimeType: 'image/png' as const, @@ -28,8 +28,8 @@ export default function DownloadPdf() { }, } - // generatePDF(targetRef, options) - generatePDF(targetRef, { filename: 'page.pdf' }) + generatePDF(targetRef, options) + // generatePDF(targetRef, { filename: 'page.pdf' }) } return ( <> From 69b08706aa48d4d9b82244be81a6d8a874dd6f29 Mon Sep 17 00:00:00 2001 From: yoosangwook Date: Fri, 16 May 2025 17:58:00 +0900 Subject: [PATCH 11/36] fix: Add response interceptor to axios instance for error handling --- src/libs/axios.ts | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/libs/axios.ts b/src/libs/axios.ts index 80823ea..888dccf 100644 --- a/src/libs/axios.ts +++ b/src/libs/axios.ts @@ -20,6 +20,14 @@ export const axiosInstance = (url: string | null | undefined) => { }, ) + axios.interceptors.response.use( + (response) => transferResponse(response), + (error) => { + // 에러 처리 로직 + return Promise.reject(error) + }, + ) + return instance } @@ -35,13 +43,13 @@ export const axiosInstance = (url: string | null | undefined) => { // ) // Response interceptor -axios.interceptors.response.use( - (response) => transferResponse(response), - (error) => { - // 에러 처리 로직 - return Promise.reject(error) - }, -) +// axios.interceptors.response.use( +// (response) => transferResponse(response), +// (error) => { +// // 에러 처리 로직 +// return Promise.reject(error) +// }, +// ) // response데이터가 array, object에 따라 분기하여 키 변환 const transferResponse = (response: any) => { From e207cee460bcc4f1ae87790955f91be69267bd78 Mon Sep 17 00:00:00 2001 From: yoosangwook Date: Fri, 16 May 2025 17:59:25 +0900 Subject: [PATCH 12/36] fix: Correct axios instance reference in response interceptor for error handling --- src/libs/axios.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/axios.ts b/src/libs/axios.ts index 888dccf..8718318 100644 --- a/src/libs/axios.ts +++ b/src/libs/axios.ts @@ -20,7 +20,7 @@ export const axiosInstance = (url: string | null | undefined) => { }, ) - axios.interceptors.response.use( + instance.interceptors.response.use( (response) => transferResponse(response), (error) => { // 에러 처리 로직 From 9c0440b2337a622a7300e1a53bf188129eb4c452 Mon Sep 17 00:00:00 2001 From: keyy1315 Date: Fri, 16 May 2025 18:28:36 +0900 Subject: [PATCH 13/36] feat: add Survey new Design Component --- src/app/survey-sale/[id]/page.tsx | 2 +- src/app/survey-sale/basic-info/page.tsx | 2 +- src/app/survey-sale/regist/page.tsx | 5 +- src/app/survey-sale/roof-info/page.tsx | 2 +- src/components/survey-sale/RegistForm.tsx | 29 ++ .../survey-sale/detail/BasicForm.tsx | 39 +-- .../survey-sale/detail/ButtonForm.tsx | 4 +- .../survey-sale/detail/DataTable.tsx | 8 +- .../survey-sale/detail/DetailForm.tsx | 107 ++++-- .../survey-sale/detail/RoofForm.tsx | 320 +++++++----------- .../{form/BasicForm.tsx => my/basicForm.tsx} | 0 .../{DetailButton.tsx => my/detailButton.tsx} | 0 .../roofDetailForm.tsx} | 13 +- .../RoofInfoForm.tsx => my/roofInfoForm.tsx} | 6 +- 14 files changed, 269 insertions(+), 268 deletions(-) create mode 100644 src/components/survey-sale/RegistForm.tsx rename src/components/survey-sale/detail/{form/BasicForm.tsx => my/basicForm.tsx} (100%) rename src/components/survey-sale/detail/{DetailButton.tsx => my/detailButton.tsx} (100%) rename src/components/survey-sale/detail/{RoofDetailForm.tsx => my/roofDetailForm.tsx} (96%) rename src/components/survey-sale/detail/{form/RoofInfoForm.tsx => my/roofInfoForm.tsx} (98%) diff --git a/src/app/survey-sale/[id]/page.tsx b/src/app/survey-sale/[id]/page.tsx index bc48257..482dab4 100644 --- a/src/app/survey-sale/[id]/page.tsx +++ b/src/app/survey-sale/[id]/page.tsx @@ -23,7 +23,7 @@ export default function page() { return ( <> - + {/* */} ) } diff --git a/src/app/survey-sale/basic-info/page.tsx b/src/app/survey-sale/basic-info/page.tsx index 2c75137..7359926 100644 --- a/src/app/survey-sale/basic-info/page.tsx +++ b/src/app/survey-sale/basic-info/page.tsx @@ -1,4 +1,4 @@ -import BasicForm from '@/components/survey-sale/detail/form/BasicForm' +import BasicForm from '@/components/survey-sale/detail/my/basicForm' export default function page() { return ( diff --git a/src/app/survey-sale/regist/page.tsx b/src/app/survey-sale/regist/page.tsx index 5090aaa..cf18ff2 100644 --- a/src/app/survey-sale/regist/page.tsx +++ b/src/app/survey-sale/regist/page.tsx @@ -1,9 +1,10 @@ -import RegistForm from '@/components/survey-sale/temp/registForm' +// import RegistForm from '@/components/survey-sale/temp/registForm' +import RegistForm from '@/components/survey-sale/RegistForm' export default function RegistPage() { return ( <> - + ) } diff --git a/src/app/survey-sale/roof-info/page.tsx b/src/app/survey-sale/roof-info/page.tsx index 1c5358b..51797e6 100644 --- a/src/app/survey-sale/roof-info/page.tsx +++ b/src/app/survey-sale/roof-info/page.tsx @@ -1,4 +1,4 @@ -import RoofInfoForm from '@/components/survey-sale/detail/form/RoofInfoForm' +import RoofInfoForm from '@/components/survey-sale/detail/my/roofInfoForm' export default function page() { return ( diff --git a/src/components/survey-sale/RegistForm.tsx b/src/components/survey-sale/RegistForm.tsx new file mode 100644 index 0000000..b1cc240 --- /dev/null +++ b/src/components/survey-sale/RegistForm.tsx @@ -0,0 +1,29 @@ +import { Mode } from '@/types/Survey' +import { useSearchParams } from 'next/navigation' +import DetailForm from './detail/DetailForm' +import { useServey } from '@/hooks/useSurvey' +import { useEffect, useState } from 'react' +import { SurveyBasicInfo } from '@/types/Survey' +import { useSessionStore } from '@/store/session' + +export default function RegistForm() { + const searchParams = useSearchParams() + const id = searchParams.get('id') + + const { surveyDetail } = useServey(Number(id)) + const { session } = useSessionStore() + + const [mode, setMode] = useState('CREATE') + + useEffect(() => { + if (id) { + setMode('EDIT') + } + }, [id]) + + return ( + <> + + + ) +} diff --git a/src/components/survey-sale/detail/BasicForm.tsx b/src/components/survey-sale/detail/BasicForm.tsx index 5b7fb31..bee4623 100644 --- a/src/components/survey-sale/detail/BasicForm.tsx +++ b/src/components/survey-sale/detail/BasicForm.tsx @@ -2,15 +2,11 @@ import { useEffect, useState } from 'react' import { useSurveySaleTabState } from '@/store/surveySaleTabState' -import { SurveyBasicInfo } from '@/types/Survey' +import { SurveyBasicRequest } from '@/types/Survey' import { Mode } from 'fs' -export default function BasicForm(props: { - surveyInfo: Partial - setSurveyInfo: (surveyInfo: Partial) => void - mode: Mode -}) { - const { surveyInfo, setSurveyInfo, mode } = props +export default function BasicForm(props: { basicInfo: SurveyBasicRequest; setBasicInfo: (basicInfo: SurveyBasicRequest) => void; mode: Mode }) { + const { basicInfo, setBasicInfo, mode } = props const { setBasicInfoSelected } = useSurveySaleTabState() const [isFlip, setIsFlip] = useState(true) @@ -36,8 +32,8 @@ export default function BasicForm(props: { type="text" className="input-frame" readOnly={mode === 'READ'} - value={surveyInfo?.REPRESENTATIVE} - onChange={(e) => setSurveyInfo({ ...surveyInfo, REPRESENTATIVE: e.target.value })} + value={basicInfo?.REPRESENTATIVE ?? ''} + onChange={(e) => setBasicInfo({ ...basicInfo, REPRESENTATIVE: e.target.value })} />
@@ -46,8 +42,8 @@ export default function BasicForm(props: { type="text" className="input-frame" readOnly={mode === 'READ'} - value={surveyInfo?.BUILDING_NAME ?? ''} - onChange={(e) => setSurveyInfo({ ...surveyInfo, BUILDING_NAME: e.target.value })} + value={basicInfo?.STORE ?? ''} + onChange={(e) => setBasicInfo({ ...basicInfo, STORE: e.target.value })} />
@@ -56,8 +52,8 @@ export default function BasicForm(props: { type="text" className="input-frame" readOnly={mode === 'READ'} - value={surveyInfo?.CONSTRUCTION_POINT ?? ''} - onChange={(e) => setSurveyInfo({ ...surveyInfo, CONSTRUCTION_POINT: e.target.value })} + value={basicInfo?.CONSTRUCTION_POINT ?? ''} + onChange={(e) => setBasicInfo({ ...basicInfo, CONSTRUCTION_POINT: e.target.value })} />
@@ -71,38 +67,35 @@ export default function BasicForm(props: { - +
) : ( - + )}
{/* 건물명 */}
建物名
- +
{/* 고객명 */}
建物名
- +
郵便番号/都道府県
{/* 우편번호 */}
- +
{/* 도도부현 */}
- +
{/* 주소 */} -
- -
diff --git a/src/components/survey-sale/detail/ButtonForm.tsx b/src/components/survey-sale/detail/ButtonForm.tsx index 84b4b4c..4b2b9fa 100644 --- a/src/components/survey-sale/detail/ButtonForm.tsx +++ b/src/components/survey-sale/detail/ButtonForm.tsx @@ -1,6 +1,6 @@ -import { Mode } from '@/types/Survey' +import { Mode, SurveyBasicRequest, SurveyDetailRequest } from '@/types/Survey' -export default function ButtonForm(props: { mode: Mode; setMode: (mode: Mode) => void }) { +export default function ButtonForm(props: { mode: Mode; setMode: (mode: Mode) => void; data: { basic: SurveyBasicRequest; roof: SurveyDetailRequest } }) { const { mode, setMode } = props return ( <> diff --git a/src/components/survey-sale/detail/DataTable.tsx b/src/components/survey-sale/detail/DataTable.tsx index d049c33..be8da34 100644 --- a/src/components/survey-sale/detail/DataTable.tsx +++ b/src/components/survey-sale/detail/DataTable.tsx @@ -4,7 +4,7 @@ import { useServey } from '@/hooks/useSurvey' import { useParams, useSearchParams } from 'next/navigation' import { useEffect, useState } from 'react' import DetailForm from './DetailForm' -import RoofDetailForm from './RoofDetailForm' +import { SurveyBasicInfo } from '@/types/Survey' export default function DataTable() { const params = useParams() @@ -84,11 +84,7 @@ export default function DataTable() {
- {/* {tab === 'roof-info' ? ( - - ) : ( - - )} */} + ) } diff --git a/src/components/survey-sale/detail/DetailForm.tsx b/src/components/survey-sale/detail/DetailForm.tsx index 6e5fc24..92dbce4 100644 --- a/src/components/survey-sale/detail/DetailForm.tsx +++ b/src/components/survey-sale/detail/DetailForm.tsx @@ -1,52 +1,101 @@ 'use client' -import { Mode, SurveyBasicInfo } from '@/types/Survey' +import { Mode, SurveyBasicInfo, SurveyBasicRequest, SurveyDetailRequest } from '@/types/Survey' import { useEffect, useState } from 'react' import ButtonForm from './ButtonForm' import BasicForm from './BasicForm' import RoofForm from './RoofForm' +const roofInfoForm: SurveyDetailRequest = { + CONTRACT_CAPACITY: null, + RETAIL_COMPANY: null, + SUPPLEMENTARY_FACILITIES: null, + SUPPLEMENTARY_FACILITIES_ETC: null, + INSTALLATION_SYSTEM: null, + INSTALLATION_SYSTEM_ETC: null, + CONSTRUCTION_YEAR: null, + CONSTRUCTION_YEAR_ETC: null, + ROOF_MATERIAL: null, + ROOF_MATERIAL_ETC: null, + ROOF_SHAPE: null, + ROOF_SHAPE_ETC: null, + ROOF_SLOPE: null, + HOUSE_STRUCTURE: '1', + HOUSE_STRUCTURE_ETC: null, + RAFTER_MATERIAL: '1', + RAFTER_MATERIAL_ETC: null, + RAFTER_SIZE: null, + RAFTER_SIZE_ETC: null, + RAFTER_PITCH: null, + RAFTER_PITCH_ETC: null, + RAFTER_DIRECTION: '1', + OPEN_FIELD_PLATE_KIND: null, + OPEN_FIELD_PLATE_KIND_ETC: null, + OPEN_FIELD_PLATE_THICKNESS: null, + LEAK_TRACE: false, + WATERPROOF_MATERIAL: null, + WATERPROOF_MATERIAL_ETC: null, + INSULATION_PRESENCE: '1', + INSULATION_PRESENCE_ETC: null, + STRUCTURE_ORDER: null, + STRUCTURE_ORDER_ETC: null, + INSTALLATION_AVAILABILITY: null, + INSTALLATION_AVAILABILITY_ETC: null, + MEMO: null, +} + +const basicInfoForm: SurveyBasicRequest = { + REPRESENTATIVE: '', + STORE: null, + CONSTRUCTION_POINT: null, + INVESTIGATION_DATE: new Date().toLocaleDateString('en-CA'), + BUILDING_NAME: null, + CUSTOMER_NAME: null, + POST_CODE: null, + ADDRESS: null, + ADDRESS_DETAIL: null, + SUBMISSION_STATUS: false, + SUBMISSION_DATE: null, +} export default function DetailForm(props: { surveyInfo?: SurveyBasicInfo; mode?: Mode }) { - const [surveyInfo, setSurveyInfo] = useState>( - props.surveyInfo ?? { - REPRESENTATIVE: '', - CONSTRUCTION_POINT: '', - STORE: '', - INVESTIGATION_DATE: '', - BUILDING_NAME: '', - CUSTOMER_NAME: '', - POST_CODE: '', - ADDRESS: '', - ADDRESS_DETAIL: '', - SUBMISSION_STATUS: true, - SUBMISSION_DATE: '2021-01-01', - DETAIL_INFO: null, - REG_DT: new Date(), - UPT_DT: new Date(), - }, - ) - const [roofValue, setRoofValue] = useState(false) const [mode, setMode] = useState(props.mode ?? 'CREATE') - const basicInfoProps = { surveyInfo, setSurveyInfo, mode } - const roofInfoProps = { surveyInfo, mode } - const buttonFormProps = { mode, setMode } + const [basicInfoData, setBasicInfoData] = useState(basicInfoForm) + const [roofInfoData, setRoofInfoData] = useState(roofInfoForm) + + // useEffect(() => { + // // setMode(props.surveyInfo ? 'EDIT' : 'CREATE') + // }, [props.surveyInfo]) useEffect(() => { - // setMode(props.surveyInfo ? 'EDIT' : 'CREATE') + console.log(props.surveyInfo) }, [props.surveyInfo]) useEffect(() => { - console.log(surveyInfo) - }, [surveyInfo]) + if (props.surveyInfo && mode === 'EDIT') { + const { ID, UPT_DT, REG_DT, DETAIL_INFO, ...rest } = props.surveyInfo + setBasicInfoData(rest) + if (DETAIL_INFO) { + const { ID, UPT_DT, REG_DT, BASIC_INFO_ID, ...rest } = DETAIL_INFO + setRoofInfoData(rest) + } + } + }, [props.surveyInfo, mode]) + + const data = { + basic: basicInfoData, + roof: roofInfoData, + } + + const buttonFormProps = { mode, setMode, data } return ( <>
- {mode} + {/* {mode} */} {/* 기본정보 */} - + {/* 전기/지붕정보 */} - +
diff --git a/src/components/survey-sale/detail/RoofForm.tsx b/src/components/survey-sale/detail/RoofForm.tsx index 878fbf2..1e92ae9 100644 --- a/src/components/survey-sale/detail/RoofForm.tsx +++ b/src/components/survey-sale/detail/RoofForm.tsx @@ -1,8 +1,21 @@ import { useState } from 'react' -import { Mode, SurveyBasicInfo } from '@/types/Survey' +import { Mode, SurveyBasicInfo, SurveyDetailInfo, SurveyDetailRequest } from '@/types/Survey' +import { roof_material, supplementary_facilities } from './form/etcProcess/MultiCheckEtc' +import { selectBoxOptions } from './form/etcProcess/SelectBoxEtc' +import { radioEtcData } from './form/etcProcess/RadioEtc' -export default function RoofForm(props: { surveyInfo: Partial; mode: Mode }) { - const { surveyInfo, mode } = props +export default function RoofForm(props: { + roofInfo: SurveyDetailRequest | SurveyDetailInfo + setRoofInfo: (roofInfo: SurveyDetailRequest) => void + mode: Mode +}) { + const makeNumArr = (value: string) => { + return value + .split(',') + .map((v) => v.trim()) + .filter((v) => v.length > 0) + } + const { roofInfo, setRoofInfo, mode } = props const [isFlip, setIsFlip] = useState(true) return (
@@ -21,7 +34,7 @@ export default function RoofForm(props: { surveyInfo: Partial; {/* 전기 계약 용량 */}
電気契約容量
- +
{mode === 'READ' && } {mode !== 'READ' && ( @@ -39,7 +52,7 @@ export default function RoofForm(props: { surveyInfo: Partial;
{/* 전기 소매 회사사 */}
電気小売会社
- +
{/* 전기 부대 설비 */} @@ -47,34 +60,33 @@ export default function RoofForm(props: { surveyInfo: Partial; 電気袋設備※複数選択可能
-
+ {/*
-
+
*/} + {supplementary_facilities.map((item) => ( +
+ + +
+ ))}
- - -
-
- - -
-
- - -
-
- - + +
- +
{/* 설치 희망 시스템 */} -
設置希望システム
+ {/*
設置希望システム
{mode === 'READ' && (
@@ -90,7 +102,9 @@ export default function RoofForm(props: { surveyInfo: Partial;
- )} + )} */} +
設置希望システム
+
@@ -102,7 +116,7 @@ export default function RoofForm(props: { surveyInfo: Partial; {/* 건축 연수 */}
建築研修
- {mode === 'READ' && } + {/* {mode === 'READ' && } {mode !== 'READ' && ( - )} + )} */} +
-
+ {/*
-
+
*/}
{/* 지붕재 */} @@ -124,36 +139,26 @@ export default function RoofForm(props: { surveyInfo: Partial; 屋根材※最大2個まで選択可能
+ {roof_material.map((item) => ( +
+ + +
+ ))}
- - -
-
- - -
-
- - -
-
- - -
-
- - + +
- +
{/* 지붕 모양 */}
建築研修
- {mode === 'READ' && } + {/* {mode === 'READ' && } {mode !== 'READ' && ( - )} + )} */} +
@@ -172,207 +178,84 @@ export default function RoofForm(props: { surveyInfo: Partial; {/* 지붕 경사도도 */}
屋根の斜面
- +
{/* 주택구조조 */}
住宅構造
-
- - -
-
- - -
-
- -
+
{/* 서까래 재질 */}
垂木材質
-
-
- - -
-
- - -
-
- - -
-
-
- -
+
{/* 서까래 크기 */}
垂木サイズ
- {mode === 'READ' && } - {mode !== 'READ' && ( - - )} -
-
- +
{/* 서까래 피치 */}
垂木サイズ
- {mode === 'READ' && } - {mode !== 'READ' && ( - - )} -
-
- +
{/* 서까래 방향 */}
垂木の方向
-
- - -
-
- - -
+
{/* 노지판 종류류 */}
路地板の種類
- {mode === 'READ' && } - {mode !== 'READ' && ( - - )} -
-
- +
+ {/* 노지판 두께 */}
路地板厚※小幅板を選択した場合, 厚さ. 小幅板間の間隔寸法を記載
- + mm
- {/* 서까래 방향 */} -
垂木の方向
+ {/* 누수 흔적 */} +
水漏れの痕跡
-
- - -
-
- - -
+
- {/* 주택 구조 */} -
住宅構造
-
- - -
-
- - -
-
- -
+ {/* 방수재 종류 */} +
防水材の種類
+
{/* 단열재 유무 */}
断熱材の有無
-
- - -
-
- -
-
- - -
+
{/* 지붕 구조의 순서 */} -
路地板の種類
-
- {mode === 'READ' && } - {mode !== 'READ' && ( - - )} -
-
- -
+
屋根構造の順序
+
{/* 지붕 제품명 설치 가능 여부 확인 */}
屋根製品名 設置可否確認
-
- {mode === 'READ' && } - {mode !== 'READ' && ( - - )} -
-
- -
+
{/* 메모 */} @@ -382,8 +265,8 @@ export default function RoofForm(props: { surveyInfo: Partial; className="textarea-form" name="" id="" - defaultValue={'漏れの兆候があるため、正確な点検が必要です.'} placeholder="TextArea Filed" + value={roofInfo?.MEMO ?? ''} readOnly={mode === 'READ'} >
@@ -394,3 +277,56 @@ export default function RoofForm(props: { surveyInfo: Partial;
) } + +const SelectedBox = ({ column, detailInfoData }: { column: string; detailInfoData: SurveyDetailInfo }) => { + const selectedId = detailInfoData?.[column as keyof SurveyDetailInfo] + const etcValue = detailInfoData?.[`${column}_ETC` as keyof SurveyDetailInfo] + + return ( + <> + + {etcValue && } + + ) +} + +const RadioSelected = ({ column, detailInfoData }: { column: string; detailInfoData: SurveyDetailInfo | null }) => { + let selectedId = detailInfoData?.[column as keyof SurveyDetailInfo] + if (column === 'LEAK_TRACE') { + selectedId = Number(selectedId) + if (!selectedId) selectedId = 2 + } + + let etcValue = null + if (column !== 'RAFTER_DIRECTION') { + etcValue = detailInfoData?.[`${column}_ETC` as keyof SurveyDetailInfo] + } + const etcChecked = etcValue !== null && etcValue !== undefined && etcValue !== '' + + console.log('column: selectedId', column, selectedId) + return ( + <> + {radioEtcData[column as keyof typeof radioEtcData].map((item) => ( +
+ + +
+ ))} + {column !== 'RAFTER_DIRECTION' && column !== 'LEAK_TRACE' && column !== 'INSULATION_PRESENCE' && ( +
+ + +
+ )} + {etcChecked && ( +
+ +
+ )} + + ) +} diff --git a/src/components/survey-sale/detail/form/BasicForm.tsx b/src/components/survey-sale/detail/my/basicForm.tsx similarity index 100% rename from src/components/survey-sale/detail/form/BasicForm.tsx rename to src/components/survey-sale/detail/my/basicForm.tsx diff --git a/src/components/survey-sale/detail/DetailButton.tsx b/src/components/survey-sale/detail/my/detailButton.tsx similarity index 100% rename from src/components/survey-sale/detail/DetailButton.tsx rename to src/components/survey-sale/detail/my/detailButton.tsx diff --git a/src/components/survey-sale/detail/RoofDetailForm.tsx b/src/components/survey-sale/detail/my/roofDetailForm.tsx similarity index 96% rename from src/components/survey-sale/detail/RoofDetailForm.tsx rename to src/components/survey-sale/detail/my/roofDetailForm.tsx index a9a4031..6b8bbb5 100644 --- a/src/components/survey-sale/detail/RoofDetailForm.tsx +++ b/src/components/survey-sale/detail/my/roofDetailForm.tsx @@ -1,8 +1,8 @@ import { SurveyBasicInfo, SurveyDetailInfo } from '@/types/Survey' -import DetailButton from './DetailButton' -import { roof_material, supplementary_facilities } from './form/etcProcess/MultiCheckEtc' -import { selectBoxOptions } from './form/etcProcess/SelectBoxEtc' -import { radioEtcData } from './form/etcProcess/RadioEtc' +import DetailButton from './detailButton' +import { roof_material, supplementary_facilities } from '../form/etcProcess/MultiCheckEtc' +import { selectBoxOptions } from '../form/etcProcess/SelectBoxEtc' +import { radioEtcData } from '../form/etcProcess/RadioEtc' export default function RoofDetailForm({ surveyDetail, @@ -196,10 +196,7 @@ export default function RoofDetailForm({ - + ) diff --git a/src/components/survey-sale/detail/form/RoofInfoForm.tsx b/src/components/survey-sale/detail/my/roofInfoForm.tsx similarity index 98% rename from src/components/survey-sale/detail/form/RoofInfoForm.tsx rename to src/components/survey-sale/detail/my/roofInfoForm.tsx index 2a465d4..bd0a1f7 100644 --- a/src/components/survey-sale/detail/form/RoofInfoForm.tsx +++ b/src/components/survey-sale/detail/my/roofInfoForm.tsx @@ -6,9 +6,9 @@ import { useServey } from '@/hooks/useSurvey' import { SurveyDetailRequest } from '@/types/Survey' import { useRouter, useSearchParams } from 'next/navigation' import { useEffect, useState } from 'react' -import MultiCheckEtc from './etcProcess/MultiCheckEtc' -import SelectBoxEtc from './etcProcess/SelectBoxEtc' -import RadioEtc from './etcProcess/RadioEtc' +import MultiCheckEtc from '../form/etcProcess/MultiCheckEtc' +import SelectBoxEtc from '../form/etcProcess/SelectBoxEtc' +import RadioEtc from '../form/etcProcess/RadioEtc' const defaultDetailInfoForm: SurveyDetailRequest = { CONTRACT_CAPACITY: null, From 8d5d0b6244e5c59e126b23086961416305419ebd Mon Sep 17 00:00:00 2001 From: yoosangwook Date: Fri, 16 May 2025 18:29:16 +0900 Subject: [PATCH 14/36] fix: Update MS_USR_TRK model in Prisma schema to use uppercase 'DATA' field --- prisma/schema.prisma | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 83c5c61..4993d5b 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -214,6 +214,6 @@ model MS_USR_TRK { OWNER String @db.VarChar(100) TYPE String @db.VarChar(50) URL String? @db.VarChar(200) - data String? @db.VarChar(200) + DATA String? @db.VarChar(200) REG_DT DateTime @default(now()) } From ab86e16bc91fa7df97181123ed25f0aaeb207636 Mon Sep 17 00:00:00 2001 From: yoosangwook Date: Mon, 19 May 2025 13:45:06 +0900 Subject: [PATCH 15/36] fix: Add indexes to MS_SUITABLE_DETAIL and MS_SUITABLE_MAIN models in Prisma schema for improved query performance --- prisma/schema.prisma | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 4993d5b..50cdf7d 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -195,6 +195,8 @@ model MS_SUITABLE_DETAIL { REG_DT DateTime @default(now(), map: "DF__MS_SUITAB__creat__571DF1D5") UPT_DT DateTime? MS_SUITABLE_MAIN MS_SUITABLE_MAIN @relation(fields: [MAIN_ID], references: [ID], onUpdate: NoAction, map: "MS_SUITABLE_DETAIL_MS_SUITABLE_MAIN_FK") + + @@index([MAIN_ID, TRESTLE_MANUFACTURER_PRODUCT_NAME], map: "MS_SUITABLE_DETAIL_MAIN_ID_IDX") } model MS_SUITABLE_MAIN { @@ -206,6 +208,9 @@ model MS_SUITABLE_MAIN { REG_DT DateTime @default(now(), map: "DF__MS_SUITAB__creat__5441852A") UPT_DT DateTime? MS_SUITABLE_DETAIL MS_SUITABLE_DETAIL[] + + @@index([PRODUCT_NAME], map: "MS_SUITABLE_MAIN_PRODUCT_NAME_IDX") + @@index([ROOF_MT_CD, PRODUCT_NAME], map: "MS_SUITABLE_MAIN_ROOF_MT_CD_IDX") } /// The underlying table does not contain a valid unique identifier and can therefore currently not be handled by Prisma Client. @@ -214,6 +219,6 @@ model MS_USR_TRK { OWNER String @db.VarChar(100) TYPE String @db.VarChar(50) URL String? @db.VarChar(200) - DATA String? @db.VarChar(200) REG_DT DateTime @default(now()) + DATA String? @db.VarChar(200) } From eb1f6b23e76052728eb1dea11085390257c270a3 Mon Sep 17 00:00:00 2001 From: keyy1315 Date: Mon, 19 May 2025 13:47:57 +0900 Subject: [PATCH 16/36] refactor: Update Survey components and API routes for improved data handling and structure --- prisma/schema.prisma | 2 +- src/app/api/survey-sales/route.ts | 29 +- src/app/survey-sale/[id]/page.tsx | 19 - src/app/survey-sale/basic-info/page.tsx | 9 - src/app/survey-sale/regist/page.tsx | 3 +- src/app/survey-sale/roof-info/page.tsx | 9 - .../survey-sale/detail/BasicForm.tsx | 24 +- .../survey-sale/detail/DataTable.tsx | 17 +- .../survey-sale/detail/DetailForm.tsx | 109 +++--- .../survey-sale/{ => detail}/RegistForm.tsx | 2 +- .../survey-sale/detail/RoofForm.tsx | 274 ++++++++++++-- .../detail/form/etcProcess/MultiCheckEtc.tsx | 141 ------- .../detail/form/etcProcess/RadioEtc.tsx | 175 --------- .../detail/form/etcProcess/SelectBoxEtc.tsx | 246 ------------ .../survey-sale/detail/my/basicForm.tsx | 244 ------------ .../survey-sale/detail/my/detailButton.tsx | 119 ------ .../survey-sale/detail/my/roofDetailForm.tsx | 256 ------------- .../survey-sale/detail/my/roofInfoForm.tsx | 353 ------------------ src/components/survey-sale/list/ListTable.tsx | 37 +- .../survey-sale/list/SearchForm.tsx | 2 +- .../survey-sale/temp/basicRegist.tsx | 153 -------- .../survey-sale/temp/formButton.tsx | 92 ----- .../survey-sale/temp/registForm.tsx | 105 ------ .../survey-sale/temp/roofRegist.tsx | 284 -------------- src/hooks/useSurvey.ts | 76 ++-- src/libs/axios.ts | 17 +- src/types/Survey.ts | 226 +++++------ 27 files changed, 497 insertions(+), 2526 deletions(-) delete mode 100644 src/app/survey-sale/basic-info/page.tsx delete mode 100644 src/app/survey-sale/roof-info/page.tsx rename src/components/survey-sale/{ => detail}/RegistForm.tsx (94%) delete mode 100644 src/components/survey-sale/detail/form/etcProcess/MultiCheckEtc.tsx delete mode 100644 src/components/survey-sale/detail/form/etcProcess/RadioEtc.tsx delete mode 100644 src/components/survey-sale/detail/form/etcProcess/SelectBoxEtc.tsx delete mode 100644 src/components/survey-sale/detail/my/basicForm.tsx delete mode 100644 src/components/survey-sale/detail/my/detailButton.tsx delete mode 100644 src/components/survey-sale/detail/my/roofDetailForm.tsx delete mode 100644 src/components/survey-sale/detail/my/roofInfoForm.tsx delete mode 100644 src/components/survey-sale/temp/basicRegist.tsx delete mode 100644 src/components/survey-sale/temp/formButton.tsx delete mode 100644 src/components/survey-sale/temp/registForm.tsx delete mode 100644 src/components/survey-sale/temp/roofRegist.tsx diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 4993d5b..5c4c1d8 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -214,6 +214,6 @@ model MS_USR_TRK { OWNER String @db.VarChar(100) TYPE String @db.VarChar(50) URL String? @db.VarChar(200) - DATA String? @db.VarChar(200) REG_DT DateTime @default(now()) + DATA String? @db.VarChar(200) } diff --git a/src/app/api/survey-sales/route.ts b/src/app/api/survey-sales/route.ts index 23f3d18..a2df29d 100644 --- a/src/app/api/survey-sales/route.ts +++ b/src/app/api/survey-sales/route.ts @@ -173,23 +173,18 @@ export async function GET(request: Request) { where.AND.push(roleCondition) } - // 데이터 조회 또는 카운트 - if (params.offset) { - // 페이지네이션 데이터 조회 - //@ts-ignore - const surveys = await prisma.SD_SURVEY_SALES_BASIC_INFO.findMany({ - where, - orderBy: params.sort === 'created' ? { REG_DT: 'desc' } : { UPT_DT: 'desc' }, - skip: Number(params.offset), - take: ITEMS_PER_PAGE, - }) - return NextResponse.json(surveys) - } else { - // 전체 개수만 조회 - //@ts-ignore - const count = await prisma.SD_SURVEY_SALES_BASIC_INFO.count({ where }) - return NextResponse.json(count) - } + // 페이지네이션 데이터 조회 + //@ts-ignore + const surveys = await prisma.SD_SURVEY_SALES_BASIC_INFO.findMany({ + where, + orderBy: params.sort === 'created' ? { REG_DT: 'desc' } : { UPT_DT: 'desc' }, + skip: Number(params.offset), + take: ITEMS_PER_PAGE, + }) + // 전체 개수만 조회 + //@ts-ignore + const count = await prisma.SD_SURVEY_SALES_BASIC_INFO.count({ where }) + return NextResponse.json({ data: { data: surveys, count: count } }) } catch (error) { console.error(error) return NextResponse.json({ error: 'Fail Read Survey' }, { status: 500 }) diff --git a/src/app/survey-sale/[id]/page.tsx b/src/app/survey-sale/[id]/page.tsx index 482dab4..ac22151 100644 --- a/src/app/survey-sale/[id]/page.tsx +++ b/src/app/survey-sale/[id]/page.tsx @@ -1,25 +1,6 @@ import DataTable from '@/components/survey-sale/detail/DataTable' -import DetailForm from '@/components/survey-sale/detail/DetailForm' -import { SurveyBasicInfo } from '@/types/Survey' export default function page() { - const surveyInfo: SurveyBasicInfo = { - ID: 1, - REPRESENTATIVE: 'HG', - STORE: 'HWJ(T01)', - CONSTRUCTION_POINT: '施工点名表示', - INVESTIGATION_DATE: '2021-01-01', - BUILDING_NAME: 'ビル名表示', - CUSTOMER_NAME: '顧客名表示', - POST_CODE: '1234567890', - ADDRESS: '東京都千代田区永田町1-7-1', - ADDRESS_DETAIL: '永田町ビル101号室', - SUBMISSION_STATUS: true, - SUBMISSION_DATE: '2021-01-01', - DETAIL_INFO: null, - REG_DT: new Date(), - UPT_DT: new Date(), - } return ( <> diff --git a/src/app/survey-sale/basic-info/page.tsx b/src/app/survey-sale/basic-info/page.tsx deleted file mode 100644 index 7359926..0000000 --- a/src/app/survey-sale/basic-info/page.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import BasicForm from '@/components/survey-sale/detail/my/basicForm' - -export default function page() { - return ( - <> - - - ) -} diff --git a/src/app/survey-sale/regist/page.tsx b/src/app/survey-sale/regist/page.tsx index cf18ff2..0dec827 100644 --- a/src/app/survey-sale/regist/page.tsx +++ b/src/app/survey-sale/regist/page.tsx @@ -1,5 +1,4 @@ -// import RegistForm from '@/components/survey-sale/temp/registForm' -import RegistForm from '@/components/survey-sale/RegistForm' +import RegistForm from '@/components/survey-sale/detail/RegistForm' export default function RegistPage() { return ( diff --git a/src/app/survey-sale/roof-info/page.tsx b/src/app/survey-sale/roof-info/page.tsx deleted file mode 100644 index 51797e6..0000000 --- a/src/app/survey-sale/roof-info/page.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import RoofInfoForm from '@/components/survey-sale/detail/my/roofInfoForm' - -export default function page() { - return ( - <> - - - ) -} \ No newline at end of file diff --git a/src/components/survey-sale/detail/BasicForm.tsx b/src/components/survey-sale/detail/BasicForm.tsx index bee4623..716930e 100644 --- a/src/components/survey-sale/detail/BasicForm.tsx +++ b/src/components/survey-sale/detail/BasicForm.tsx @@ -32,8 +32,8 @@ export default function BasicForm(props: { basicInfo: SurveyBasicRequest; setBas type="text" className="input-frame" readOnly={mode === 'READ'} - value={basicInfo?.REPRESENTATIVE ?? ''} - onChange={(e) => setBasicInfo({ ...basicInfo, REPRESENTATIVE: e.target.value })} + value={basicInfo?.representative ?? ''} + onChange={(e) => setBasicInfo({ ...basicInfo, representative: e.target.value })} />
@@ -42,8 +42,8 @@ export default function BasicForm(props: { basicInfo: SurveyBasicRequest; setBas type="text" className="input-frame" readOnly={mode === 'READ'} - value={basicInfo?.STORE ?? ''} - onChange={(e) => setBasicInfo({ ...basicInfo, STORE: e.target.value })} + value={basicInfo?.store ?? ''} + onChange={(e) => setBasicInfo({ ...basicInfo, store: e.target.value })} />
@@ -52,8 +52,8 @@ export default function BasicForm(props: { basicInfo: SurveyBasicRequest; setBas type="text" className="input-frame" readOnly={mode === 'READ'} - value={basicInfo?.CONSTRUCTION_POINT ?? ''} - onChange={(e) => setBasicInfo({ ...basicInfo, CONSTRUCTION_POINT: e.target.value })} + value={basicInfo?.constructionPoint ?? ''} + onChange={(e) => setBasicInfo({ ...basicInfo, constructionPoint: e.target.value })} />
@@ -67,32 +67,32 @@ export default function BasicForm(props: { basicInfo: SurveyBasicRequest; setBas - + ) : ( - + )}
{/* 건물명 */}
建物名
- +
{/* 고객명 */}
建物名
- +
郵便番号/都道府県
{/* 우편번호 */}
- +
{/* 도도부현 */}
- +
{/* 주소 */} diff --git a/src/components/survey-sale/detail/DataTable.tsx b/src/components/survey-sale/detail/DataTable.tsx index be8da34..1d06e2e 100644 --- a/src/components/survey-sale/detail/DataTable.tsx +++ b/src/components/survey-sale/detail/DataTable.tsx @@ -11,7 +11,6 @@ export default function DataTable() { const id = params.id const searchParams = useSearchParams() - const tab = searchParams.get('tab') const isTemp = searchParams.get('isTemporary') const { surveyDetail, isLoadingSurveyDetail } = useServey(Number(id)) @@ -20,8 +19,8 @@ export default function DataTable() { const { validateSurveyDetail } = useServey(Number(id)) useEffect(() => { - if (surveyDetail?.DETAIL_INFO) { - const validate = validateSurveyDetail(surveyDetail.DETAIL_INFO) + if (surveyDetail?.detailInfo) { + const validate = validateSurveyDetail(surveyDetail.detailInfo) if (validate.trim() !== '') { setIsTemporary(false) } @@ -48,25 +47,25 @@ export default function DataTable() { 仮保存 ) : ( - {surveyDetail?.ID} + {surveyDetail?.id} )} 登録日 - {surveyDetail?.REG_DT ? new Date(surveyDetail?.REG_DT).toLocaleString() : ''} + {surveyDetail?.regDt ? new Date(surveyDetail.regDt).toLocaleString() : ''} 更新日時 - {surveyDetail?.UPT_DT ? new Date(surveyDetail?.UPT_DT).toLocaleString() : ''} + {surveyDetail?.uptDt ? new Date(surveyDetail.uptDt).toLocaleString() : ''} 提出可否 - {surveyDetail?.SUBMISSION_STATUS && surveyDetail?.SUBMISSION_DATE ? ( + {surveyDetail?.submissionStatus && surveyDetail?.submissionDate ? ( <> {/* TODO: 제출한 판매점 ID 추가 필요 */} -
{new Date(surveyDetail.SUBMISSION_DATE).toLocaleString()}
-
{surveyDetail.STORE}
+
{new Date(surveyDetail.submissionDate).toLocaleString()}
+
{surveyDetail.store}
) : ( '-' diff --git a/src/components/survey-sale/detail/DetailForm.tsx b/src/components/survey-sale/detail/DetailForm.tsx index 92dbce4..387bb4f 100644 --- a/src/components/survey-sale/detail/DetailForm.tsx +++ b/src/components/survey-sale/detail/DetailForm.tsx @@ -6,55 +6,55 @@ import ButtonForm from './ButtonForm' import BasicForm from './BasicForm' import RoofForm from './RoofForm' const roofInfoForm: SurveyDetailRequest = { - CONTRACT_CAPACITY: null, - RETAIL_COMPANY: null, - SUPPLEMENTARY_FACILITIES: null, - SUPPLEMENTARY_FACILITIES_ETC: null, - INSTALLATION_SYSTEM: null, - INSTALLATION_SYSTEM_ETC: null, - CONSTRUCTION_YEAR: null, - CONSTRUCTION_YEAR_ETC: null, - ROOF_MATERIAL: null, - ROOF_MATERIAL_ETC: null, - ROOF_SHAPE: null, - ROOF_SHAPE_ETC: null, - ROOF_SLOPE: null, - HOUSE_STRUCTURE: '1', - HOUSE_STRUCTURE_ETC: null, - RAFTER_MATERIAL: '1', - RAFTER_MATERIAL_ETC: null, - RAFTER_SIZE: null, - RAFTER_SIZE_ETC: null, - RAFTER_PITCH: null, - RAFTER_PITCH_ETC: null, - RAFTER_DIRECTION: '1', - OPEN_FIELD_PLATE_KIND: null, - OPEN_FIELD_PLATE_KIND_ETC: null, - OPEN_FIELD_PLATE_THICKNESS: null, - LEAK_TRACE: false, - WATERPROOF_MATERIAL: null, - WATERPROOF_MATERIAL_ETC: null, - INSULATION_PRESENCE: '1', - INSULATION_PRESENCE_ETC: null, - STRUCTURE_ORDER: null, - STRUCTURE_ORDER_ETC: null, - INSTALLATION_AVAILABILITY: null, - INSTALLATION_AVAILABILITY_ETC: null, - MEMO: null, + contractCapacity: null, + retailCompany: null, + supplementaryFacilities: null, + supplementaryFacilitiesEtc: null, + installationSystem: null, + installationSystemEtc: null, + constructionYear: null, + constructionYearEtc: null, + roofMaterial: null, + roofMaterialEtc: null, + roofShape: null, + roofShapeEtc: null, + roofSlope: null, + houseStructure: '1', + houseStructureEtc: null, + rafterMaterial: '1', + rafterMaterialEtc: null, + rafterSize: null, + rafterSizeEtc: null, + rafterPitch: null, + rafterPitchEtc: null, + rafterDirection: '1', + openFieldPlateKind: null, + openFieldPlateKindEtc: null, + openFieldPlateThickness: null, + leakTrace: false, + waterproofMaterial: null, + waterproofMaterialEtc: null, + insulationPresence: '1', + insulationPresenceEtc: null, + structureOrder: null, + structureOrderEtc: null, + installationAvailability: null, + installationAvailabilityEtc: null, + memo: null, } const basicInfoForm: SurveyBasicRequest = { - REPRESENTATIVE: '', - STORE: null, - CONSTRUCTION_POINT: null, - INVESTIGATION_DATE: new Date().toLocaleDateString('en-CA'), - BUILDING_NAME: null, - CUSTOMER_NAME: null, - POST_CODE: null, - ADDRESS: null, - ADDRESS_DETAIL: null, - SUBMISSION_STATUS: false, - SUBMISSION_DATE: null, + representative: '', + store: null, + constructionPoint: null, + investigationDate: new Date().toLocaleDateString('en-CA'), + buildingName: null, + customerName: null, + postCode: null, + address: null, + addressDetail: null, + submissionStatus: false, + submissionDate: null, } export default function DetailForm(props: { surveyInfo?: SurveyBasicInfo; mode?: Mode }) { @@ -62,20 +62,12 @@ export default function DetailForm(props: { surveyInfo?: SurveyBasicInfo; mode?: const [basicInfoData, setBasicInfoData] = useState(basicInfoForm) const [roofInfoData, setRoofInfoData] = useState(roofInfoForm) - // useEffect(() => { - // // setMode(props.surveyInfo ? 'EDIT' : 'CREATE') - // }, [props.surveyInfo]) - useEffect(() => { - console.log(props.surveyInfo) - }, [props.surveyInfo]) - - useEffect(() => { - if (props.surveyInfo && mode === 'EDIT') { - const { ID, UPT_DT, REG_DT, DETAIL_INFO, ...rest } = props.surveyInfo + if (props.surveyInfo && (mode === 'EDIT' || mode === 'READ')) { + const { id, uptDt, regDt, detailInfo, ...rest } = props.surveyInfo setBasicInfoData(rest) - if (DETAIL_INFO) { - const { ID, UPT_DT, REG_DT, BASIC_INFO_ID, ...rest } = DETAIL_INFO + if (detailInfo) { + const { id, uptDt, regDt, basicInfoId, ...rest } = detailInfo setRoofInfoData(rest) } } @@ -91,7 +83,6 @@ export default function DetailForm(props: { surveyInfo?: SurveyBasicInfo; mode?: return ( <>
- {/* {mode} */} {/* 기본정보 */} {/* 전기/지붕정보 */} diff --git a/src/components/survey-sale/RegistForm.tsx b/src/components/survey-sale/detail/RegistForm.tsx similarity index 94% rename from src/components/survey-sale/RegistForm.tsx rename to src/components/survey-sale/detail/RegistForm.tsx index b1cc240..4377713 100644 --- a/src/components/survey-sale/RegistForm.tsx +++ b/src/components/survey-sale/detail/RegistForm.tsx @@ -1,6 +1,6 @@ import { Mode } from '@/types/Survey' import { useSearchParams } from 'next/navigation' -import DetailForm from './detail/DetailForm' +import DetailForm from './DetailForm' import { useServey } from '@/hooks/useSurvey' import { useEffect, useState } from 'react' import { SurveyBasicInfo } from '@/types/Survey' diff --git a/src/components/survey-sale/detail/RoofForm.tsx b/src/components/survey-sale/detail/RoofForm.tsx index 1e92ae9..794bf21 100644 --- a/src/components/survey-sale/detail/RoofForm.tsx +++ b/src/components/survey-sale/detail/RoofForm.tsx @@ -1,8 +1,204 @@ import { useState } from 'react' -import { Mode, SurveyBasicInfo, SurveyDetailInfo, SurveyDetailRequest } from '@/types/Survey' -import { roof_material, supplementary_facilities } from './form/etcProcess/MultiCheckEtc' -import { selectBoxOptions } from './form/etcProcess/SelectBoxEtc' -import { radioEtcData } from './form/etcProcess/RadioEtc' +import { Mode, SurveyDetailInfo, SurveyDetailRequest } from '@/types/Survey' + +type RadioEtcKeys = 'houseStructure' | 'rafterMaterial' | 'waterproofMaterial' | 'insulationPresence' | 'rafterDirection' | 'leakTrace' +type SelectBoxKeys = + | 'installationSystem' + | 'constructionYear' + | 'roofShape' + | 'rafterPitch' + | 'rafterSize' + | 'openFieldPlateKind' + | 'structureOrder' + | 'installationAvailability' + +export const supplementary_facilities = [ + { id: 1, name: 'エコキュート' }, //에코큐트 + { id: 2, name: 'エネパーム' }, //에네팜 + { id: 3, name: '蓄電池システム' }, //축전지시스템 + { id: 4, name: '太陽光発電' }, //태양광발전 +] + +export const roof_material = [ + { id: 1, name: 'スレート' }, //슬레이트 + { id: 2, name: 'アスファルトシングル' }, //아스팔트 싱글 + { id: 3, name: '瓦' }, //기와 + { id: 4, name: '金属屋根' }, //금속지붕 +] + +export const selectBoxOptions: Record = { + installationSystem: [ + { + id: 1, + name: '太陽光発電', //태양광발전 + }, + { + id: 2, + name: 'ハイブリッド蓄電システム', //하이브리드축전지시스템 + }, + { + id: 3, + name: '蓄電池システム', //축전지시스템 + }, + ], + constructionYear: [ + { + id: 1, + name: '新築', //신축 + }, + { + id: 2, + name: '既築', //기존 + }, + ], + roofShape: [ + { + id: 1, + name: '切妻', //박공지붕 + }, + { + id: 2, + name: '寄棟', //기동 + }, + { + id: 3, + name: '片流れ', //한쪽흐름 + }, + ], + rafterSize: [ + { + id: 1, + name: '幅35mm以上×高さ48mm以上', + }, + { + id: 2, + name: '幅36mm以上×高さ46mm以上', + }, + { + id: 3, + name: '幅37mm以上×高さ43mm以上', + }, + { + id: 4, + name: '幅38mm以上×高さ40mm以上', + }, + ], + rafterPitch: [ + { + id: 1, + name: '455mm以下', + }, + { + id: 2, + name: '500mm以下', + }, + { + id: 3, + name: '606mm以下', + }, + ], + openFieldPlateKind: [ + { + id: 1, + name: '構造用合板', //구조용합판 + }, + { + id: 2, + name: 'OSB', //OSB + }, + { + id: 3, + name: 'パーティクルボード', //파티클보드 + }, + { + id: 4, + name: '小幅板', //소판 + }, + ], + structureOrder: [ + { + id: 1, + name: '屋根材', //지붕재 + }, + { + id: 2, + name: '防水材', //방수재 + }, + { + id: 3, + name: '屋根の基礎', //지붕의기초 + }, + { + id: 4, + name: '垂木', //서까래 + }, + ], + installationAvailability: [ + { + id: 1, + name: '確認済み', //확인완료 + }, + { + id: 2, + name: '未確認', //미확인 + }, + ], +} + +export const radioEtcData: Record = { + houseStructure: [ + { + id: 1, + label: '木製', + }, + ], + rafterMaterial: [ + { + id: 1, + label: '木製', + }, + { + id: 2, + label: '強制', + }, + ], + waterproofMaterial: [ + { + id: 1, + label: 'アスファルト屋根940(22kg以上)', + }, + ], + insulationPresence: [ + { + id: 1, + label: 'なし', + }, + { + id: 2, + label: 'あり', + }, + ], + rafterDirection: [ + { + id: 1, + label: '垂直垂木', + }, + { + id: 2, + label: '水平垂木', + }, + ], + leakTrace: [ + { + id: 1, + label: 'あり', + }, + { + id: 2, + label: 'なし', + }, + ], +} export default function RoofForm(props: { roofInfo: SurveyDetailRequest | SurveyDetailInfo @@ -34,7 +230,7 @@ export default function RoofForm(props: { {/* 전기 계약 용량 */}
電気契約容量
- +
{mode === 'READ' && } {mode !== 'READ' && ( @@ -52,7 +248,7 @@ export default function RoofForm(props: {
{/* 전기 소매 회사사 */}
電気小売会社
- +
{/* 전기 부대 설비 */} @@ -69,19 +265,19 @@ export default function RoofForm(props: {
))}
- - + +
- +
@@ -104,7 +300,7 @@ export default function RoofForm(props: {
)} */}
設置希望システム
- + @@ -126,7 +322,7 @@ export default function RoofForm(props: { )} */} - + {/*
@@ -141,17 +337,17 @@ export default function RoofForm(props: {
{roof_material.map((item) => (
- +
))}
- - + +
- +
@@ -168,7 +364,7 @@ export default function RoofForm(props: { )} */} - +
@@ -178,46 +374,46 @@ export default function RoofForm(props: { {/* 지붕 경사도도 */}
屋根の斜面
- +
{/* 주택구조조 */}
住宅構造
- +
{/* 서까래 재질 */}
垂木材質
- +
{/* 서까래 크기 */}
垂木サイズ
- +
{/* 서까래 피치 */}
垂木サイズ
- +
{/* 서까래 방향 */}
垂木の方向
- +
{/* 노지판 종류류 */}
路地板の種類
- +
@@ -226,7 +422,7 @@ export default function RoofForm(props: { 路地板厚※小幅板を選択した場合, 厚さ. 小幅板間の間隔寸法を記載
- + mm
@@ -234,28 +430,28 @@ export default function RoofForm(props: { {/* 누수 흔적 */}
水漏れの痕跡
- +
{/* 방수재 종류 */}
防水材の種類
- +
{/* 단열재 유무 */}
断熱材の有無
- +
{/* 지붕 구조의 순서 */}
屋根構造の順序
- +
{/* 지붕 제품명 설치 가능 여부 확인 */}
屋根製品名 設置可否確認
- +
{/* 메모 */} @@ -266,7 +462,7 @@ export default function RoofForm(props: { name="" id="" placeholder="TextArea Filed" - value={roofInfo?.MEMO ?? ''} + value={roofInfo?.memo ?? ''} readOnly={mode === 'READ'} >
@@ -280,7 +476,7 @@ export default function RoofForm(props: { const SelectedBox = ({ column, detailInfoData }: { column: string; detailInfoData: SurveyDetailInfo }) => { const selectedId = detailInfoData?.[column as keyof SurveyDetailInfo] - const etcValue = detailInfoData?.[`${column}_ETC` as keyof SurveyDetailInfo] + const etcValue = detailInfoData?.[`${column}Etc` as keyof SurveyDetailInfo] return ( <> @@ -296,18 +492,18 @@ const SelectedBox = ({ column, detailInfoData }: { column: string; detailInfoDat const RadioSelected = ({ column, detailInfoData }: { column: string; detailInfoData: SurveyDetailInfo | null }) => { let selectedId = detailInfoData?.[column as keyof SurveyDetailInfo] - if (column === 'LEAK_TRACE') { + if (column === 'leakTrace') { selectedId = Number(selectedId) if (!selectedId) selectedId = 2 } let etcValue = null - if (column !== 'RAFTER_DIRECTION') { - etcValue = detailInfoData?.[`${column}_ETC` as keyof SurveyDetailInfo] + if (column !== 'rafterDirection') { + etcValue = detailInfoData?.[`${column}Etc` as keyof SurveyDetailInfo] } const etcChecked = etcValue !== null && etcValue !== undefined && etcValue !== '' - console.log('column: selectedId', column, selectedId) + // console.log('column: selectedId', column, selectedId) return ( <> {radioEtcData[column as keyof typeof radioEtcData].map((item) => ( @@ -316,10 +512,10 @@ const RadioSelected = ({ column, detailInfoData }: { column: string; detailInfoD ))} - {column !== 'RAFTER_DIRECTION' && column !== 'LEAK_TRACE' && column !== 'INSULATION_PRESENCE' && ( + {column !== 'rafterDirection' && column !== 'leakTrace' && column !== 'insulationPresence' && (
- - + +
)} {etcChecked && ( diff --git a/src/components/survey-sale/detail/form/etcProcess/MultiCheckEtc.tsx b/src/components/survey-sale/detail/form/etcProcess/MultiCheckEtc.tsx deleted file mode 100644 index 30a1ca7..0000000 --- a/src/components/survey-sale/detail/form/etcProcess/MultiCheckEtc.tsx +++ /dev/null @@ -1,141 +0,0 @@ -import { SurveyDetailRequest } from '@/types/Survey' -import { useEffect, useState } from 'react' - -export const supplementary_facilities = [ - { id: 1, name: 'エコキュート' }, //에코큐트 - { id: 2, name: 'エネパーム' }, //에네팜 - { id: 3, name: '蓄電池システム' }, //축전지시스템 - { id: 4, name: '太陽光発電' }, //태양광발전 -] - -export const roof_material = [ - { id: 1, name: 'スレート' }, //슬레이트 - { id: 2, name: 'アスファルトシングル' }, //아스팔트 싱글 - { id: 3, name: '瓦' }, //기와 - { id: 4, name: '金属屋根' }, //금속지붕 -] - -export default function MultiCheckbox({ - column, - setDetailInfoData, - detailInfoData, -}: { - column: string - setDetailInfoData: (data: any) => void - detailInfoData: SurveyDetailRequest -}) { - const selectList = column === 'SUPPLEMENTARY_FACILITIES' ? supplementary_facilities : roof_material - - const [isOtherChecked, setIsOtherChecked] = useState(false) - const [otherValue, setOtherValue] = useState('') - - const makeNumArr = (value: string) => { - return value - .split(',') - .map((v) => v.trim()) - .filter((v) => v.length > 0) - } - - useEffect(() => { - if (detailInfoData[`${column}_ETC` as keyof SurveyDetailRequest]) { - setIsOtherChecked(true) - setOtherValue(detailInfoData[`${column}_ETC` as keyof SurveyDetailRequest] as string) - } - }, [detailInfoData]) - - const handleCheckbox = (dataIndex: number) => { - const value = makeNumArr(String(detailInfoData[column as keyof SurveyDetailRequest] ?? '')) - - let newValue: string[] - if (value.includes(String(dataIndex))) { - // 체크 해제 - newValue = value.filter((v) => v !== String(dataIndex)) - } else { - // 체크 - if (column === 'ROOF_MATERIAL') { - // 기타가 체크되어 있는지 확인 - const isOtherSelected = isOtherChecked - // 현재 선택된 항목 수 + 기타 선택 여부 - const totalSelected = value.length + (isOtherSelected ? 1 : 0) - - if (totalSelected >= 2) { - alert('屋根材は最大2個まで選択可能です。') - return - } - } - newValue = [...value, String(dataIndex)] - } - - setDetailInfoData({ - ...detailInfoData, - [column]: newValue.join(', '), - }) - } - - const handleOtherCheckbox = () => { - if (column === 'ROOF_MATERIAL') { - const value = makeNumArr(String(detailInfoData[column as keyof SurveyDetailRequest] ?? '')) - const currentSelected = value.length - if (!isOtherChecked && currentSelected >= 2) { - alert('Up to two roofing materials can be selected.') - return - } - } - - const newIsOtherChecked = !isOtherChecked - setIsOtherChecked(newIsOtherChecked) - setOtherValue('') - - setDetailInfoData({ - ...detailInfoData, - [`${column}_ETC`]: newIsOtherChecked ? '' : null, - }) - } - - const handleOtherInputChange = (e: React.ChangeEvent) => { - const value = e.target.value - setOtherValue(value) - setDetailInfoData({ - ...detailInfoData, - [`${column}_ETC`]: value, - }) - } - - return ( - <> - {column === 'SUPPLEMENTARY_FACILITIES' ? ( - <> -
- 電気袋設備※複数選択可能 -
- - ) : ( - <> -
- 屋根材※最大2個まで選択可能 -
- - )} -
- {selectList.map((item) => ( -
- handleCheckbox(item.id)} - /> - -
- ))} -
- - -
-
-
- -
- - ) -} diff --git a/src/components/survey-sale/detail/form/etcProcess/RadioEtc.tsx b/src/components/survey-sale/detail/form/etcProcess/RadioEtc.tsx deleted file mode 100644 index 29f3325..0000000 --- a/src/components/survey-sale/detail/form/etcProcess/RadioEtc.tsx +++ /dev/null @@ -1,175 +0,0 @@ -'use client' -import { useEffect, useState } from 'react' -import { SurveyDetailRequest } from '@/types/Survey' - -type RadioEtcKeys = 'HOUSE_STRUCTURE' | 'RAFTER_MATERIAL' | 'WATERPROOF_MATERIAL' | 'INSULATION_PRESENCE' | 'RAFTER_DIRECTION' | 'LEAK_TRACE' - -const translateJapanese: Record = { - HOUSE_STRUCTURE: '住宅構造', - RAFTER_MATERIAL: '垂木材質', - WATERPROOF_MATERIAL: '防水材の種類', - INSULATION_PRESENCE: '断熱材の有無', - RAFTER_DIRECTION: '垂木の方向', - LEAK_TRACE: '水漏れの痕跡', -} - -export const radioEtcData: Record = { - HOUSE_STRUCTURE: [ - { - id: 1, - label: '木製', - }, - ], - RAFTER_MATERIAL: [ - { - id: 1, - label: '木製', - }, - { - id: 2, - label: '強制', - }, - ], - WATERPROOF_MATERIAL: [ - { - id: 1, - label: 'アスファルト屋根940(22kg以上)', - }, - ], - INSULATION_PRESENCE: [ - { - id: 1, - label: 'なし', - }, - { - id: 2, - label: 'あり', - }, - ], - RAFTER_DIRECTION: [ - { - id: 1, - label: '垂直垂木', - }, - { - id: 2, - label: '水平垂木', - }, - ], - LEAK_TRACE: [ - { - id: 1, - label: 'あり', - }, - { - id: 2, - label: 'なし', - }, - ], -} - -export default function RadioEtc({ - column, - setDetailInfoData, - detailInfoData, -}: { - column: RadioEtcKeys - setDetailInfoData: (data: any) => void - detailInfoData: SurveyDetailRequest -}) { - const [isEtcSelected, setIsEtcSelected] = useState(false) - const [etcValue, setEtcValue] = useState('') - - useEffect(() => { - if (detailInfoData[`${column}_ETC` as keyof SurveyDetailRequest]) { - setIsEtcSelected(true) - setEtcValue(detailInfoData[`${column}_ETC` as keyof SurveyDetailRequest] as string) - } - }, [detailInfoData]) - - const handleRadioChange = (e: React.ChangeEvent) => { - // const value = e.target.value - // if (column === 'INSULATION_PRESENCE') { - // setIsEtcSelected(value === '2') - // setDetailInfoData({ - // ...detailInfoData, - // [column]: value, - // }) - // } else if (value === 'etc') { - // setIsEtcSelected(true) - // setDetailInfoData({ - // ...detailInfoData, - // [column]: null, - // }) - // } else { - // setIsEtcSelected(false) - // setEtcValue('') - // setDetailInfoData({ - // ...detailInfoData, - // [column]: value, - // [`${column}_ETC`]: null, - // }) - // } - const value = e.target.value - const isSpecialCase = column === 'INSULATION_PRESENCE' - const isEtc = value === 'etc' - const isSpecialEtc = isSpecialCase && value === '2' - - const updatedData: typeof detailInfoData = { - ...detailInfoData, - [column]: isEtc ? null : value, - [`${column}_ETC`]: isEtc ? '' : null, - } - - if (isSpecialEtc) { - updatedData[column] = value - } - - setIsEtcSelected(isEtc || isSpecialEtc) - if (!isEtc) setEtcValue('') - setDetailInfoData(updatedData) - } - - const handleEtcInputChange = (e: React.ChangeEvent) => { - const value = e.target.value - setEtcValue(value) - setDetailInfoData({ - ...detailInfoData, - [`${column}_ETC`]: value, - }) - } - - return ( -
-
{translateJapanese[column]}
- {radioEtcData[column].map((item) => ( -
- - -
- ))} - {column !== 'INSULATION_PRESENCE' && ( -
- - -
- )} -
- -
-
- ) -} diff --git a/src/components/survey-sale/detail/form/etcProcess/SelectBoxEtc.tsx b/src/components/survey-sale/detail/form/etcProcess/SelectBoxEtc.tsx deleted file mode 100644 index f509972..0000000 --- a/src/components/survey-sale/detail/form/etcProcess/SelectBoxEtc.tsx +++ /dev/null @@ -1,246 +0,0 @@ -import type { SurveyDetailRequest } from '@/types/Survey' -import { useEffect, useState } from 'react' - -export type SelectBoxKeys = - | 'INSTALLATION_SYSTEM' - | 'CONSTRUCTION_YEAR' - | 'ROOF_SHAPE' - | 'RAFTER_PITCH' - | 'RAFTER_SIZE' - | 'OPEN_FIELD_PLATE_KIND' - | 'STRUCTURE_ORDER' - | 'INSTALLATION_AVAILABILITY' - -const font: Record = { - INSTALLATION_SYSTEM: 'data-input-form-tit red-f', - CONSTRUCTION_YEAR: 'data-input-form-tit red-f', - ROOF_SHAPE: 'data-input-form-tit', - RAFTER_PITCH: 'data-input-form-tit red-f', - RAFTER_SIZE: 'data-input-form-tit red-f', - OPEN_FIELD_PLATE_KIND: 'data-input-form-tit', - STRUCTURE_ORDER: 'data-input-form-tit red-f', - INSTALLATION_AVAILABILITY: 'data-input-form-tit', -} - -const translateJapanese: Record = { - INSTALLATION_SYSTEM: '設置希望システム', - CONSTRUCTION_YEAR: '建築年数', - ROOF_SHAPE: '屋根の形状', - RAFTER_PITCH: '垂木傾斜', - RAFTER_SIZE: '垂木サイズ', - OPEN_FIELD_PLATE_KIND: '路地板の種類', - STRUCTURE_ORDER: '屋根構造の順序', - INSTALLATION_AVAILABILITY: '屋根製品名 設置可否確認', -} - -export const selectBoxOptions: Record = { - INSTALLATION_SYSTEM: [ - { - id: 1, - name: '太陽光発電', //태양광발전 - }, - { - id: 2, - name: 'ハイブリッド蓄電システム', //하이브리드축전지시스템 - }, - { - id: 3, - name: '蓄電池システム', //축전지시스템 - }, - ], - CONSTRUCTION_YEAR: [ - { - id: 1, - name: '新築', //신축 - }, - { - id: 2, - name: '既築', //기존 - }, - ], - ROOF_SHAPE: [ - { - id: 1, - name: '切妻', //박공지붕 - }, - { - id: 2, - name: '寄棟', //기동 - }, - { - id: 3, - name: '片流れ', //한쪽흐름 - }, - ], - RAFTER_SIZE: [ - { - id: 1, - name: '幅35mm以上×高さ48mm以上', - }, - { - id: 2, - name: '幅36mm以上×高さ46mm以上', - }, - { - id: 3, - name: '幅37mm以上×高さ43mm以上', - }, - { - id: 4, - name: '幅38mm以上×高さ40mm以上', - }, - ], - RAFTER_PITCH: [ - { - id: 1, - name: '455mm以下', - }, - { - id: 2, - name: '500mm以下', - }, - { - id: 3, - name: '606mm以下', - }, - ], - OPEN_FIELD_PLATE_KIND: [ - { - id: 1, - name: '構造用合板', //구조용합판 - }, - { - id: 2, - name: 'OSB', //OSB - }, - { - id: 3, - name: 'パーティクルボード', //파티클보드 - }, - { - id: 4, - name: '小幅板', //소판 - }, - ], - STRUCTURE_ORDER: [ - { - id: 1, - name: '屋根材', //지붕재 - }, - { - id: 2, - name: '防水材', //방수재 - }, - { - id: 3, - name: '屋根の基礎', //지붕의기초 - }, - { - id: 4, - name: '垂木', //서까래 - }, - ], - INSTALLATION_AVAILABILITY: [ - { - id: 1, - name: '確認済み', //확인완료 - }, - { - id: 2, - name: '未確認', //미확인 - }, - ], -} - -export default function SelectBoxForm({ - column, - setDetailInfoData, - detailInfoData, -}: { - column: SelectBoxKeys - setDetailInfoData: (data: any) => void - detailInfoData: SurveyDetailRequest -}) { - const [isEtcSelected, setIsEtcSelected] = useState(false) - const [etcValue, setEtcValue] = useState('') - - useEffect(() => { - if (detailInfoData[`${column}_ETC` as keyof SurveyDetailRequest]) { - setIsEtcSelected(true) - setEtcValue(detailInfoData[`${column}_ETC` as keyof SurveyDetailRequest] as string) - } - }, [detailInfoData]) - - const handleSelectChange = (e: React.ChangeEvent) => { - const value = e.target.value - const isSpecialCase = column === 'CONSTRUCTION_YEAR' || column === 'INSTALLATION_AVAILABILITY' - const isEtc = value === 'etc' - const isSpecialEtc = isSpecialCase && value === '2' - - const updatedData: typeof detailInfoData = { - ...detailInfoData, - [column]: isEtc ? null : value, - [`${column}_ETC`]: isEtc ? '' : null, - } - - // 건축연수 + 설치가능여부는 2번 선택 시 input 활성화 - if (isSpecialEtc) { - updatedData[column] = value - } - - setIsEtcSelected(isEtc || isSpecialEtc) - if (!isEtc) setEtcValue('') - setDetailInfoData(updatedData) - } - - const handleEtcInputChange = (e: React.ChangeEvent) => { - const value = e.target.value - setEtcValue(value) - setDetailInfoData({ - ...detailInfoData, - [`${column}_ETC`]: value, - }) - } - - return ( - <> -
-
{translateJapanese[column as keyof typeof translateJapanese]}
-
- -
-
- -
-
- - ) -} diff --git a/src/components/survey-sale/detail/my/basicForm.tsx b/src/components/survey-sale/detail/my/basicForm.tsx deleted file mode 100644 index 1f55838..0000000 --- a/src/components/survey-sale/detail/my/basicForm.tsx +++ /dev/null @@ -1,244 +0,0 @@ -'use client' - -import { useServey } from '@/hooks/useSurvey' -import { SurveyBasicRequest } from '@/types/Survey' -import { useRouter, useSearchParams } from 'next/navigation' -import { useState, useEffect } from 'react' -import { useSurveySaleTabState } from '@/store/surveySaleTabState' -import { usePopupController } from '@/store/popupController' -import { useAddressStore } from '@/store/addressStore' -import { useSessionStore } from '@/store/session' -// import { useUserType } from '@/hooks/useUserType' - -const defaultBasicInfoForm: SurveyBasicRequest = { - REPRESENTATIVE: '', - STORE: null, - CONSTRUCTION_POINT: null, - INVESTIGATION_DATE: new Date().toLocaleDateString('en-CA'), - BUILDING_NAME: null, - CUSTOMER_NAME: null, - POST_CODE: null, - ADDRESS: null, - ADDRESS_DETAIL: null, - SUBMISSION_STATUS: false, - SUBMISSION_DATE: null, -} - -const REQUIRED_FIELDS: (keyof SurveyBasicRequest)[] = ['REPRESENTATIVE', 'BUILDING_NAME', 'CUSTOMER_NAME'] - -export default function BasicForm() { - const searchParams = useSearchParams() - const id = searchParams.get('id') - const router = useRouter() - - const { setBasicInfoSelected } = useSurveySaleTabState() - const { surveyDetail, createSurvey, isCreatingSurvey, updateSurvey, isUpdatingSurvey } = useServey(Number(id)) - - const [basicInfoData, setBasicInfoData] = useState(defaultBasicInfoForm) - - const { addressData } = useAddressStore() - const { session } = useSessionStore() - - const popupController = usePopupController() - - useEffect(() => { - if (surveyDetail) { - const { ID, UPT_DT, REG_DT, DETAIL_INFO, ...rest } = surveyDetail - setBasicInfoData(rest) - } - if (addressData) { - setBasicInfoData({ - ...basicInfoData, - POST_CODE: addressData.post_code, - ADDRESS: addressData.address, - ADDRESS_DETAIL: addressData.address_detail, - }) - } - if (session?.isLoggedIn) { - setBasicInfoData((prev) => ({ - ...prev, - REPRESENTATIVE: session?.userId ?? '', - STORE: session?.storeNm ?? '', - CONSTRUCTION_POINT: session?.builderNo ?? '', - })) - } - setBasicInfoSelected() - }, [surveyDetail, addressData, session?.isLoggedIn, session?.userId, session?.storeNm, session?.builderNo]) - - const focusInput = (input: keyof SurveyBasicRequest) => { - const inputElement = document.getElementById(input) - if (inputElement) { - inputElement.focus() - } - } - - const validateSurvey = (basicInfoData: SurveyBasicRequest) => { - const emptyField = REQUIRED_FIELDS.find((field) => !basicInfoData[field]) - if (emptyField) { - focusInput(emptyField) - return false - } - return true - } - - const handleChange = (key: keyof SurveyBasicRequest, value: string) => { - setBasicInfoData({ ...basicInfoData, [key]: value }) - } - - const handleSave = async (isTemporary: boolean) => { - if (id) { - // updateSurvey(basicInfoData) - alert('保存しました。') - // router.push(`/survey-sale/${id}?tab=basic-info`) - } - if (isTemporary) { - // const saveId = await createSurvey(basicInfoData) - alert('一時保存されました。') - // router.push(`/survey-sale/${saveId}?tab=basic-info`) - } else { - if (validateSurvey(basicInfoData)) { - // const saveId = await createSurvey(basicInfoData) - alert('保存しました。') - // router.push(`/survey-sale/${saveId}?tab=basic-info`) - } - } - } - - if (isCreatingSurvey || isUpdatingSurvey) { - return
Loading...
- } - - return ( - <> -
-
-
-
担当者名
- handleChange('REPRESENTATIVE', e.target.value)} - /> -
- {(session?.role === 'Builder' || session?.role?.includes('Admin')) && ( - <> -
-
販売店
- handleChange('STORE', e.target.value)} - /> -
- - )} - {(session?.role === 'Partner' || session?.role === 'Builder') && ( -
-
施工店
- handleChange('CONSTRUCTION_POINT', e.target.value)} - /> -
- )} -
-
- -
-
-
-
現地調査日
-
- - handleChange('INVESTIGATION_DATE', e.target.value)} - /> -
-
-
-
建物名
- handleChange('BUILDING_NAME', e.target.value)} - /> -
-
-
顧客名
- handleChange('CUSTOMER_NAME', e.target.value)} - /> -
-
-
建物の住所
-
-
- -
-
- -
-
-
- -
-
-
-
市区町村名, 以後の住所
- handleChange('ADDRESS_DETAIL', e.target.value)} - /> -
-
-
-
- -
-
- -
-
- -
-
-
- - ) -} diff --git a/src/components/survey-sale/detail/my/detailButton.tsx b/src/components/survey-sale/detail/my/detailButton.tsx deleted file mode 100644 index 3a871bf..0000000 --- a/src/components/survey-sale/detail/my/detailButton.tsx +++ /dev/null @@ -1,119 +0,0 @@ -'use client' -import { useRouter, useSearchParams } from 'next/navigation' -import { useServey } from '@/hooks/useSurvey' -import { useSessionStore } from '@/store/session' -import { SurveyBasicInfo } from '@/types/Survey' -import { useState } from 'react' - -export default function DetailButton({ surveyDetail }: { surveyDetail: SurveyBasicInfo | null }) { - const router = useRouter() - const { session } = useSessionStore() - const { submitSurvey, deleteSurvey } = useServey(surveyDetail?.ID ?? 0) - - const searchParams = useSearchParams() - const isTemp = searchParams.get('isTemporary') - const [isTemporary, setIsTemporary] = useState(isTemp === 'true') - - const checkRole = () => { - switch (session?.role) { - case 'T01': - return session?.userNm === surveyDetail?.REPRESENTATIVE ? true : false - case 'Admin': - return session?.storeNm === surveyDetail?.STORE ? true : false - case 'Admin_Sub': - return session?.storeNm === surveyDetail?.STORE ? true : false - case 'Builder': - return session?.builderNo === surveyDetail?.CONSTRUCTION_POINT ? true : false - case 'Partner': - return session?.builderNo === surveyDetail?.CONSTRUCTION_POINT ? true : false - default: - return '' - } - } - - const handleSubmit = async () => { - const result = checkRole() - if (result) { - if (isTemporary) { - alert('一時保存されたデータは提出できません。') - return - } - window.neoConfirm( - '提出しますか??', - async () => { - if (surveyDetail?.ID) { - // TODO: 제출 페이지 추가 - alert('SUBMIT POPUP!!!!!!!!!!!') - await submitSurvey() - } - }, - () => null, - ) - } - } - const handleUpdate = () => { - const result = checkRole() - if (result) { - // router.push(`/survey-sale/basic-info?id=${surveyDetail?.ID}&isTemp=${isTemporary}`) - router.push(`/survey-sale/regist?id=${surveyDetail?.ID}`) - } else { - alert('担当者のみ修正可能です。') - } - } - const handleDelete = async () => { - window.neoConfirm( - '削除しますか?', - async () => { - if (surveyDetail?.ID) { - if (session.userNm === surveyDetail?.REPRESENTATIVE) { - await deleteSurvey() - alert('削除されました。') - router.push('/survey-sale') - } else { - alert('担当者のみ削除可能です。') - } - } - }, - () => null, - ) - } - - const isSubmitter = session?.storeNm === surveyDetail?.STORE && session?.builderNo === surveyDetail?.CONSTRUCTION_POINT - - return ( -
-
- -
- {isSubmitter && surveyDetail?.SUBMISSION_STATUS ? ( - <> - ) : ( - <> - {isTemporary || surveyDetail?.SUBMISSION_STATUS ? ( - <> - ) : ( - <> -
- -
- - )} -
- -
-
- -
- - )} -
- ) -} diff --git a/src/components/survey-sale/detail/my/roofDetailForm.tsx b/src/components/survey-sale/detail/my/roofDetailForm.tsx deleted file mode 100644 index 6b8bbb5..0000000 --- a/src/components/survey-sale/detail/my/roofDetailForm.tsx +++ /dev/null @@ -1,256 +0,0 @@ -import { SurveyBasicInfo, SurveyDetailInfo } from '@/types/Survey' -import DetailButton from './detailButton' -import { roof_material, supplementary_facilities } from '../form/etcProcess/MultiCheckEtc' -import { selectBoxOptions } from '../form/etcProcess/SelectBoxEtc' -import { radioEtcData } from '../form/etcProcess/RadioEtc' - -export default function RoofDetailForm({ - surveyDetail, - isLoadingSurveyDetail, -}: { - surveyDetail: SurveyBasicInfo | null - isLoadingSurveyDetail: boolean -}) { - console.log(surveyDetail) - - const makeNumArr = (value: string) => { - return value - .split(',') - .map((v) => v.trim()) - .filter((v) => v.length > 0) - } - - if (isLoadingSurveyDetail) { - return
Loading...
- } - return ( - <> -
-
-
- {/* 전기 계약 용량 */} -
電気契約容量
- -
- {/* 전기 소매 회사 */} -
-
電気小売会社
- -
- {/* 전기 부대 설비 */} -
-
電気附属設備
-
- {supplementary_facilities.map((item) => ( -
- - -
- ))} -
- - -
-
-
- -
-
- {/* 설치 희망 시스템 */} -
-
設置希望システム
- -
- {/* 건축 연수 */} -
-
建築年数
- -
- {/* 지붕재 */} -
-
屋根材
-
- {roof_material.map((item) => ( -
- - -
- ))} -
- - -
-
-
- -
-
- {/* 지붕 모양 */} -
-
屋根の形状
- -
- {/* 지붕 경사도 */} -
-
屋根の斜面
-
- - -
-
- {/* 주택 구조 */} -
-
住宅構造
- -
- {/* 서까래 재질 */} -
-
垂木の材質
- -
- {/* 서까래 크기 */} -
-
垂木の大きさ
- -
- {/* 서까래 피치 */} -
-
垂木のピッチ
- -
- {/* 서까래 방향 */} -
-
垂木の方向
- -
- {/* 노지판 종류 */} -
-
路地板の種類
- -
- {/* 노지판 두께 */} -
-
路地板厚
-
- - mm -
-
- {/* 누수 흔적 */} -
-
水漏れの痕跡
- -
- {/* 방수재 종류 */} -
-
防水材の種類
- -
- {/* 단열재 유무 */} -
-
断熱材の有無
- -
- {/* 구조 순서 */} -
-
屋根構造の順序
- -
- {/* 설치 가능 여부 */} -
-
設置可能な場合
- -
- {/* 메모 */} -
-
メモ
-
- -
-
-
-
-
- -
-
- -
-
- -
-
-
- - ) -} diff --git a/src/components/survey-sale/list/ListTable.tsx b/src/components/survey-sale/list/ListTable.tsx index 468f01d..b1ac703 100644 --- a/src/components/survey-sale/list/ListTable.tsx +++ b/src/components/survey-sale/list/ListTable.tsx @@ -7,33 +7,32 @@ import { useRouter } from 'next/navigation' import SearchForm from './SearchForm' import { useSurveyFilterStore } from '@/store/surveyFilterStore' import { useSessionStore } from '@/store/session' +import { SurveyBasicInfo } from '@/types/Survey' export default function ListTable() { const router = useRouter() - const { surveyList, isLoadingSurveyList, surveyListCount } = useServey() + const { surveyList, isLoadingSurveyList } = useServey() const { offset, setOffset } = useSurveyFilterStore() - const [heldSurveyList, setHeldSurveyList] = useState([]) + const [heldSurveyList, setHeldSurveyList] = useState([]) const [hasMore, setHasMore] = useState(false) const { session } = useSessionStore() useEffect(() => { - if (surveyList) { - if (offset === 0) { - setHeldSurveyList(surveyList) + if (!session.isLoggedIn || !('data' in surveyList)) return + if ('count' in surveyList && surveyList.count > 0) { + if (offset > 0) { + setHeldSurveyList((prev) => [...prev, ...surveyList.data]) } else { - setHeldSurveyList(prev => [...prev, ...surveyList]) + setHeldSurveyList(surveyList.data) } - setHasMore(surveyListCount > offset + 10) + setHasMore(surveyList.count > offset + 10) } else { setHeldSurveyList([]) setHasMore(false) } - }, [surveyList, surveyListCount, offset]) - - console.log('surveyList:: ', surveyList) - console.log('heldSurveyList:: ', heldSurveyList) + }, [surveyList, offset, session]) const handleDetailClick = (id: number) => { router.push(`/survey-sale/${id}`) @@ -48,22 +47,22 @@ export default function ListTable() { return ( <> - + {heldSurveyList.length > 0 ? (
    {heldSurveyList.map((survey) => ( -
  • handleDetailClick(survey.ID)}> +
  • handleDetailClick(survey.id)}>
    -
    {survey.ID}
    -
    {survey.INVESTIGATION_DATE}
    +
    {survey.id}
    +
    {survey.investigationDate}
    -
    {survey.BUILDING_NAME}
    -
    {survey.CUSTOMER_NAME}
    +
    {survey.buildingName}
    +
    {survey.customerName}
    -
    {survey.REPRESENTATIVE}
    -
    {new Date(survey.UPT_DT).toLocaleString()}
    +
    {survey.representative}
    +
    {new Date(survey.uptDt).toLocaleString()}
  • diff --git a/src/components/survey-sale/list/SearchForm.tsx b/src/components/survey-sale/list/SearchForm.tsx index 16a259d..327a85c 100644 --- a/src/components/survey-sale/list/SearchForm.tsx +++ b/src/components/survey-sale/list/SearchForm.tsx @@ -4,7 +4,7 @@ import { SEARCH_OPTIONS, SEARCH_OPTIONS_ENUM, SEARCH_OPTIONS_PARTNERS, useSurvey import { useRouter } from 'next/navigation' import { useState } from 'react' -export default function SearchForm({ onItemsInit, memberRole, userId }: { onItemsInit: () => void; memberRole: string; userId: string }) { +export default function SearchForm({ memberRole, userId }: { memberRole: string; userId: string }) { const router = useRouter() const { setSearchOption, setSort, setIsMySurvey, setKeyword, isMySurvey, keyword, searchOption, sort } = useSurveyFilterStore() const [searchKeyword, setSearchKeyword] = useState(keyword) diff --git a/src/components/survey-sale/temp/basicRegist.tsx b/src/components/survey-sale/temp/basicRegist.tsx deleted file mode 100644 index c5ddaf1..0000000 --- a/src/components/survey-sale/temp/basicRegist.tsx +++ /dev/null @@ -1,153 +0,0 @@ -'use client' - -import { SurveyBasicRequest, SurveyRegistRequest } from '@/types/Survey' -import { useEffect } from 'react' -import { usePopupController } from '@/store/popupController' -import { useAddressStore } from '@/store/addressStore' -import { useSessionStore } from '@/store/session' - -export default function BasicRegist({ - basicInfoData, - setBasicInfoData, -}: { - basicInfoData: SurveyBasicRequest - setBasicInfoData: (data: SurveyBasicRequest) => void -}) { - const { addressData } = useAddressStore() - const { session } = useSessionStore() - - const popupController = usePopupController() - - useEffect(() => { - if (addressData) { - setBasicInfoData({ - ...basicInfoData, - POST_CODE: addressData.post_code, - ADDRESS: addressData.address, - ADDRESS_DETAIL: addressData.address_detail, - }) - } - }, [addressData]) - - const handleChange = (key: keyof SurveyRegistRequest, value: string) => { - setBasicInfoData({ ...basicInfoData, [key]: value }) - } - - return ( - <> -
    -
    -
    -
    担当者名
    - -
    - {(session?.role === 'Builder' || session?.role?.includes('Admin')) && ( - <> -
    -
    販売店
    - -
    - - )} - {(session?.role === 'Partner' || session?.role === 'Builder') && ( -
    -
    施工店
    - -
    - )} -
    -
    - -
    -
    -
    -
    現地調査日
    -
    - - handleChange('INVESTIGATION_DATE', e.target.value)} - /> -
    -
    -
    -
    建物名
    - handleChange('BUILDING_NAME', e.target.value)} - /> -
    -
    -
    顧客名
    - handleChange('CUSTOMER_NAME', e.target.value)} - /> -
    -
    -
    建物の住所
    -
    -
    - -
    -
    - handleChange('ADDRESS', e.target.value)} - /> -
    -
    -
    - -
    -
    -
    -
    市区町村名, 以後の住所
    - handleChange('ADDRESS_DETAIL', e.target.value)} - /> -
    -
    -
    - - ) -} diff --git a/src/components/survey-sale/temp/formButton.tsx b/src/components/survey-sale/temp/formButton.tsx deleted file mode 100644 index 7b41003..0000000 --- a/src/components/survey-sale/temp/formButton.tsx +++ /dev/null @@ -1,92 +0,0 @@ -'use client' - -import { SurveyBasicRequest, SurveyRegistRequest } from '@/types/Survey' -import { SurveyDetailRequest } from '@/types/Survey' -import { useRouter } from 'next/navigation' -import { useServey } from '@/hooks/useSurvey' - -export default function FormButton({ - surveyData, - idParam, -}: { - surveyData: { basic: SurveyBasicRequest; roof: SurveyDetailRequest } - idParam: string | null -}) { - const router = useRouter() - const { validateSurveyDetail, createSurvey, updateSurvey } = useServey(Number(idParam)) - - const saveData = { - ...surveyData.basic, - DETAIL_INFO: surveyData.roof, - } - - const focusInput = (input: keyof SurveyRegistRequest) => { - const inputElement = document.getElementById(input) - if (inputElement) { - inputElement.focus() - } - } - - const handleSave = (isTemporary: boolean) => { - const emptyField = validateSurveyDetail(saveData.DETAIL_INFO) - if (!isTemporary) { - saveProcess(emptyField) - } else { - temporarySaveProcess() - } - } - const saveProcess = async (emptyField: string) => { - if (emptyField.trim() === '') { - if (idParam) { - // 매물 수정 (저장) - updateSurvey(saveData) - router.push(`/survey-sale/${idParam}`) - } else { - // 매물 생성 (저장) - const id = await createSurvey(saveData) - router.push(`/survey-sale/${id}`) - } - alert('保存されました。') - } else { - alert(emptyField + ' 項目が空です。') - focusInput(emptyField as keyof SurveyRegistRequest) - } - } - - const temporarySaveProcess = async () => { - if (idParam) { - // 매물 수정 (임시저장) - updateSurvey(saveData) - router.push(`/survey-sale/${idParam}?isTemporary=true`) - } else { - // 매물 생성 (임시저장) - const id = await createSurvey(saveData) - router.push(`/survey-sale/${id}?isTemporary=true`) - } - alert('一時保存されました。') - } - - return ( - <> -
    -
    -
    - -
    -
    - -
    -
    - -
    -
    -
    - - ) -} diff --git a/src/components/survey-sale/temp/registForm.tsx b/src/components/survey-sale/temp/registForm.tsx deleted file mode 100644 index 42e7267..0000000 --- a/src/components/survey-sale/temp/registForm.tsx +++ /dev/null @@ -1,105 +0,0 @@ -'use client' - -import { SurveyBasicRequest, SurveyDetailRequest, SurveyRegistRequest } from '@/types/Survey' -import FormButton from './formButton' -import { useEffect, useState } from 'react' -import BasicRegist from './basicRegist' -import RoofRegist from './roofRegist' -import { useSessionStore } from '@/store/session' -import { useSearchParams } from 'next/navigation' -import { useServey } from '@/hooks/useSurvey' - -const roofInfoForm: SurveyDetailRequest = { - CONTRACT_CAPACITY: null, - RETAIL_COMPANY: null, - SUPPLEMENTARY_FACILITIES: null, - SUPPLEMENTARY_FACILITIES_ETC: null, - INSTALLATION_SYSTEM: null, - INSTALLATION_SYSTEM_ETC: null, - CONSTRUCTION_YEAR: null, - CONSTRUCTION_YEAR_ETC: null, - ROOF_MATERIAL: null, - ROOF_MATERIAL_ETC: null, - ROOF_SHAPE: null, - ROOF_SHAPE_ETC: null, - ROOF_SLOPE: null, - HOUSE_STRUCTURE: '1', - HOUSE_STRUCTURE_ETC: null, - RAFTER_MATERIAL: '1', - RAFTER_MATERIAL_ETC: null, - RAFTER_SIZE: null, - RAFTER_SIZE_ETC: null, - RAFTER_PITCH: null, - RAFTER_PITCH_ETC: null, - RAFTER_DIRECTION: '1', - OPEN_FIELD_PLATE_KIND: null, - OPEN_FIELD_PLATE_KIND_ETC: null, - OPEN_FIELD_PLATE_THICKNESS: null, - LEAK_TRACE: false, - WATERPROOF_MATERIAL: null, - WATERPROOF_MATERIAL_ETC: null, - INSULATION_PRESENCE: '1', - INSULATION_PRESENCE_ETC: null, - STRUCTURE_ORDER: null, - STRUCTURE_ORDER_ETC: null, - INSTALLATION_AVAILABILITY: null, - INSTALLATION_AVAILABILITY_ETC: null, - MEMO: null, -} - -const basicInfoForm: SurveyBasicRequest = { - REPRESENTATIVE: '', - STORE: null, - CONSTRUCTION_POINT: null, - INVESTIGATION_DATE: new Date().toLocaleDateString('en-CA'), - BUILDING_NAME: null, - CUSTOMER_NAME: null, - POST_CODE: null, - ADDRESS: null, - ADDRESS_DETAIL: null, - SUBMISSION_STATUS: false, - SUBMISSION_DATE: null, -} - -export default function RegistForm() { - const searchParams = useSearchParams() - const id = searchParams.get('id') - - const { session } = useSessionStore() - const { surveyDetail } = useServey(Number(id)) - - const [basicInfoData, setBasicInfoData] = useState(basicInfoForm) - const [roofInfoData, setRoofInfoData] = useState(roofInfoForm) - - useEffect(() => { - if (session) { - setBasicInfoData({ - ...basicInfoForm, - REPRESENTATIVE: session.userNm ?? '', - STORE: session.role === 'T01' ? '' : session.storeNm ?? '', - CONSTRUCTION_POINT: session.builderNo ?? '', - }) - } - if (id && surveyDetail) { - const { ID, UPT_DT, REG_DT, DETAIL_INFO, ...rest } = surveyDetail - setBasicInfoData(rest) - if (surveyDetail?.DETAIL_INFO) { - const { ID, UPT_DT, REG_DT, BASIC_INFO_ID, ...rest } = surveyDetail.DETAIL_INFO - setRoofInfoData(rest) - } - } - }, [session, surveyDetail]) - - const surveyData = { - basic: basicInfoData, - roof: roofInfoData, - } - - return ( - <> - - - - - ) -} diff --git a/src/components/survey-sale/temp/roofRegist.tsx b/src/components/survey-sale/temp/roofRegist.tsx deleted file mode 100644 index 6c5492a..0000000 --- a/src/components/survey-sale/temp/roofRegist.tsx +++ /dev/null @@ -1,284 +0,0 @@ -'use client' - -import { useSurveySaleTabState } from '@/store/surveySaleTabState' - -import { SurveyBasicInfo, SurveyDetailRequest } from '@/types/Survey' -import { useEffect } from 'react' -import MultiCheckEtc from '../detail/form/etcProcess/MultiCheckEtc' -import SelectBoxEtc from '../detail/form/etcProcess/SelectBoxEtc' -import RadioEtc from '../detail/form/etcProcess/RadioEtc' - -export default function RoofRegist({ - roofInfoData, - setRoofInfoData, -}: { - roofInfoData: SurveyDetailRequest - setRoofInfoData: (data: SurveyDetailRequest) => void -}) { - const { setRoofInfoSelected } = useSurveySaleTabState() - - useEffect(() => { - setRoofInfoSelected() - }, []) - - const handleNumberInput = (key: keyof SurveyDetailRequest, value: number | string) => { - if (key === 'ROOF_SLOPE' || key === 'OPEN_FIELD_PLATE_THICKNESS') { - const stringValue = value.toString() - if (stringValue.length > 5) { - alert('保存できるサイズを超えました。') - return - } - if (stringValue.includes('.')) { - const decimalPlaces = stringValue.split('.')[1].length - if (decimalPlaces > 1) { - alert('小数点以下1桁までしか許されません。') - return - } - } - } - setRoofInfoData({ ...roofInfoData, [key]: value.toString() }) - } - - const handleTextInput = (key: keyof SurveyDetailRequest, value: string) => { - setRoofInfoData({ ...roofInfoData, [key]: value || null }) - } - - const handleBooleanInput = (key: keyof SurveyDetailRequest, value: boolean) => { - setRoofInfoData({ ...roofInfoData, [key]: value }) - } - - const handleUnitInput = (value: string) => { - const numericValue = roofInfoData.CONTRACT_CAPACITY?.replace(/[^0-9.]/g, '') || '' - setRoofInfoData({ - ...roofInfoData, - CONTRACT_CAPACITY: numericValue ? `${numericValue} ${value}` : value, - }) - } - - // const handleSave = async () => { - // if (id) { - // const emptyField = validateSurveyDetail(roofInfoData) - // if (emptyField.trim() === '') { - // const updatedBasicInfoData = { - // DETAIL_INFO: roofInfoData, - // } - // try { - // createSurveyDetail({ - // surveyId: Number(id), - // surveyDetail: updatedBasicInfoData, - // }) - // alert('調査物件を保存しました。') - // } catch (error) { - // alert(error) - // throw new Error('failed to create survey detail: ' + error) - // } - // router.push(`/survey-sale`) - // } else { - // alert(emptyField + ' は必須項目です。') - // focusOnInput(emptyField) - // } - // } else { - // alert('基本情報を作成した後、屋根情報を作成することができます。') - // } - // } - // const focusOnInput = (field: string) => { - // const input = document.getElementById(field) - // if (input) { - // input.focus() - // } - // } - return ( - <> -
    -
    電気関係
    -
    -
    - {/* 전기계약 용량 - contract_capacity */} -
    電気契約容量
    -
    - handleNumberInput('CONTRACT_CAPACITY', e.target.value)} - /> -
    -
    - -
    -
    - {/* 전기 소매 회사 - retail_company */} -
    -
    電気小売会社
    - handleTextInput('RETAIL_COMPANY', e.target.value)} - /> -
    - {/* 전기 부대 설비 - supplementary_facilities */} -
    - -
    - {/* 설치 희망 시스템 - installation_system */} - -
    -
    - -
    -
    屋根関係
    -
    - {/* 건축 연수 - construction_year */} - - {/* 지붕재 - roof_material */} -
    - -
    - {/* 지붕 모양 - roof_shape */} - - {/* 지붕 경사도 - roof_slope */} -
    -
    屋根の斜面
    -
    - handleNumberInput('ROOF_SLOPE', e.target.value)} - /> - -
    -
    - {/* 주택 구조 - house_structure */} - - {/* 서까래 재질 - rafter_material */} - - {/* 서까래 크기 - rafter_size */} - - {/* 서까래 피치 - rafter_pitch */} - - {/* 서까래 방향 - rafter_direction */} -
    -
    垂木の方向
    -
    -
    - handleNumberInput('RAFTER_DIRECTION', Number(e.target.value))} - checked={roofInfoData.RAFTER_DIRECTION === '1'} - /> - -
    -
    - handleNumberInput('RAFTER_DIRECTION', Number(e.target.value))} - checked={roofInfoData.RAFTER_DIRECTION === '2'} - /> - -
    -
    -
    - {/* 노지판 종류 - open_field_plate_kind */} - - {/* 노지판 두께 - open_field_plate_thickness */} -
    -
    - 路地板厚※小幅板を選択した場合, 厚さ. 小幅板間の間隔寸法を記載 -
    -
    - handleNumberInput('OPEN_FIELD_PLATE_THICKNESS', e.target.value)} - /> - mm -
    -
    - {/* 누수 흔적 - leak_trace */} -
    -
    水漏れの痕跡
    -
    -
    - handleBooleanInput('LEAK_TRACE', true)} - /> - -
    -
    - handleBooleanInput('LEAK_TRACE', false)} - /> - -
    -
    -
    - {/* 방수재 종류 - waterproof_material */} - - {/* 단열재 유무 - insulation_presence */} - - {/* 노지판 종류 - open_field_plate_kind */} - - {/* 설치 가능 여부 - installation_availability */} - - {/* 메모 - memo */} -
    -
    メモ
    -
    - -
    -
    - -
    -
    -
    -
    - - ) -} diff --git a/src/hooks/useSurvey.ts b/src/hooks/useSurvey.ts index a047706..d07cddf 100644 --- a/src/hooks/useSurvey.ts +++ b/src/hooks/useSurvey.ts @@ -1,43 +1,39 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' -import type { - SurveyBasicInfo, - SurveyDetailInfo, - SurveyDetailRequest, - SurveyDetailCoverRequest, - SurveyRegistRequest, -} from '@/types/Survey' +import type { SurveyBasicInfo, SurveyDetailInfo, SurveyDetailRequest, SurveyDetailCoverRequest, SurveyRegistRequest } from '@/types/Survey' import { axiosInstance } from '@/libs/axios' import { useSurveyFilterStore } from '@/store/surveyFilterStore' import { queryStringFormatter } from '@/utils/common-utils' import { useSessionStore } from '@/store/session' +import { useMemo } from 'react' +import { AxiosResponse } from 'axios' const requiredFields = [ { - field: 'INSTALLATION_SYSTEM', + field: 'installationSystem', name: '設置希望システム', }, { - field: 'CONSTRUCTION_YEAR', + field: 'constructionYear', name: '建築年数', }, { - field: 'RAFTER_SIZE', + field: 'rafterSize', name: '垂木サイズ', }, { - field: 'RAFTER_PITCH', + field: 'rafterPitch', name: '垂木傾斜', }, { - field: 'WATERPROOF_MATERIAL', + field: 'waterproofMaterial', name: '防水材', }, { - field: 'INSULATION_PRESENCE', + field: 'insulationPresence', name: '断熱材有無', }, { - field: 'STRUCTURE_ORDER', + field: 'structureOrder', name: '屋根構造の順序', }, ] @@ -60,9 +56,8 @@ type ZipCode = { } export function useServey(id?: number): { - surveyList: SurveyBasicInfo[] | [] + surveyList: { data: SurveyBasicInfo[]; count: number } | {} surveyDetail: SurveyBasicInfo | null - surveyListCount: number isLoadingSurveyList: boolean isLoadingSurveyDetail: boolean isCreatingSurvey: boolean @@ -75,15 +70,20 @@ export function useServey(id?: number): { submitSurvey: () => void validateSurveyDetail: (surveyDetail: SurveyDetailRequest) => string getZipCode: (zipCode: string) => Promise + refetchSurveyList: () => void } { const queryClient = useQueryClient() const { keyword, searchOption, isMySurvey, sort, offset } = useSurveyFilterStore() const { session } = useSessionStore() - const { data: surveyList, isLoading: isLoadingSurveyList } = useQuery({ + const { + data, + isLoading: isLoadingSurveyList, + refetch: refetchSurveyList, + } = useQuery({ queryKey: ['survey', 'list', keyword, searchOption, isMySurvey, sort, offset, session?.storeNm, session?.builderNo, session?.role], queryFn: async () => { - const resp = await axiosInstance(null).get('/api/survey-sales', { + const resp = await axiosInstance(null).get<{ data: SurveyBasicInfo[]; count: number }>('/api/survey-sales', { params: { keyword, searchOption, @@ -97,7 +97,15 @@ export function useServey(id?: number): { }) return resp.data }, + enabled: session?.isLoggedIn, }) + const surveyData = useMemo(() => { + if (!data) return {} + return { + data: data.data, + count: data.count, + } + }, [data]) const { data: surveyDetail, isLoading: isLoadingSurveyDetail } = useQuery({ queryKey: ['survey', id], @@ -110,28 +118,10 @@ export function useServey(id?: number): { enabled: id !== undefined, }) - const { data: surveyListCount } = useQuery({ - queryKey: ['survey', 'list', keyword, searchOption, isMySurvey, sort, session?.builderNo, session?.storeNm, session?.role], - queryFn: async () => { - const resp = await axiosInstance(null).get('/api/survey-sales', { - params: { - keyword, - searchOption, - isMySurvey, - sort, - builderNo: session?.builderNo, - store: session?.storeNm, - role: session?.role, - }, - }) - return resp.data - }, - }) - const { mutateAsync: createSurvey, isPending: isCreatingSurvey } = useMutation({ mutationFn: async (survey: SurveyRegistRequest) => { const resp = await axiosInstance(null).post('/api/survey-sales', survey) - return resp.data.ID ?? 0 + return resp.data.id ?? 0 }, onSuccess: (data) => { queryClient.invalidateQueries({ queryKey: ['survey', 'list'] }) @@ -190,7 +180,7 @@ export function useServey(id?: number): { }) const validateSurveyDetail = (surveyDetail: SurveyDetailRequest) => { - const etcFields = ['INSTALLATION_SYSTEM', 'CONSTRUCTION_YEAR', 'RAFTER_SIZE', 'RAFTER_PITCH', 'WATERPROOF_MATERIAL', 'STRUCTURE_ORDER'] as const + const etcFields = ['installationSystem', 'constructionYear', 'rafterSize', 'rafterPitch', 'waterproofMaterial', 'structureOrder'] as const const emptyField = requiredFields.find((field) => { if (etcFields.includes(field.field as (typeof etcFields)[number])) { @@ -202,9 +192,9 @@ export function useServey(id?: number): { } }) - const contractCapacity = surveyDetail.CONTRACT_CAPACITY + const contractCapacity = surveyDetail.contractCapacity if (contractCapacity && contractCapacity.trim() !== '' && contractCapacity.split(' ')?.length === 1) { - return 'CONTRACT_CAPACITY_UNIT' + return 'contractCapacityUnit' } return emptyField?.name || '' @@ -223,9 +213,8 @@ export function useServey(id?: number): { } return { - surveyList: surveyList || [], - surveyDetail: surveyDetail || null, - surveyListCount: surveyListCount || 0, + surveyList: surveyData, + surveyDetail: surveyDetail as SurveyBasicInfo | null, isLoadingSurveyList, isLoadingSurveyDetail, isCreatingSurvey, @@ -238,5 +227,6 @@ export function useServey(id?: number): { submitSurvey, validateSurveyDetail, getZipCode, + refetchSurveyList, } } diff --git a/src/libs/axios.ts b/src/libs/axios.ts index 8718318..d973f9d 100644 --- a/src/libs/axios.ts +++ b/src/libs/axios.ts @@ -21,7 +21,10 @@ export const axiosInstance = (url: string | null | undefined) => { ) instance.interceptors.response.use( - (response) => transferResponse(response), + (response) => { + response.data = transferResponse(response) + return response + }, (error) => { // 에러 처리 로직 return Promise.reject(error) @@ -52,7 +55,7 @@ export const axiosInstance = (url: string | null | undefined) => { // ) // response데이터가 array, object에 따라 분기하여 키 변환 -const transferResponse = (response: any) => { +export const transferResponse = (response: any) => { if (!response.data) return response.data // 배열인 경우 각 객체의 키를 변환 @@ -80,7 +83,11 @@ const transformObjectKeys = (obj: any): any => { return obj } -// snake case to camel case -const snakeToCamel = (str: string): string => { - return str.replace(/([-_][a-z])/g, (group) => group.toUpperCase().replace('-', '').replace('_', '')) + +export const camelToSnake = (str: string): string => { + return str.replace(/([A-Z])/g, (group) => `_${group.toLowerCase()}`) +} + +const snakeToCamel = (str: string): string => { + return str.toLowerCase().replace(/([-_][a-z])/g, (group) => group.toUpperCase().replace('-', '').replace('_', '')) } diff --git a/src/types/Survey.ts b/src/types/Survey.ts index 9030f21..36e7aa5 100644 --- a/src/types/Survey.ts +++ b/src/types/Survey.ts @@ -1,132 +1,132 @@ export type SurveyBasicInfo = { - ID: number - REPRESENTATIVE: string - STORE: string | null - CONSTRUCTION_POINT: string | null - INVESTIGATION_DATE: string | null - BUILDING_NAME: string | null - CUSTOMER_NAME: string | null - POST_CODE: string | null - ADDRESS: string | null - ADDRESS_DETAIL: string | null - SUBMISSION_STATUS: boolean - SUBMISSION_DATE: string | null - DETAIL_INFO: SurveyDetailInfo | null - REG_DT: Date - UPT_DT: Date + id: number + representative: string + store: string | null + constructionPoint: string | null + investigationDate: string | null + buildingName: string | null + customerName: string | null + postCode: string | null + address: string | null + addressDetail: string | null + submissionStatus: boolean + submissionDate: string | null + detailInfo: SurveyDetailInfo | null + regDt: Date + uptDt: Date } export type SurveyDetailInfo = { - ID: number - BASIC_INFO_ID: number - CONTRACT_CAPACITY: string | null - RETAIL_COMPANY: string | null - SUPPLEMENTARY_FACILITIES: string | null // number 배열 - SUPPLEMENTARY_FACILITIES_ETC: string | null - INSTALLATION_SYSTEM: string | null - INSTALLATION_SYSTEM_ETC: string | null - CONSTRUCTION_YEAR: string | null - CONSTRUCTION_YEAR_ETC: string | null - ROOF_MATERIAL: string | null // number 배열 - ROOF_MATERIAL_ETC: string | null - ROOF_SHAPE: string | null - ROOF_SHAPE_ETC: string | null - ROOF_SLOPE: string | null - HOUSE_STRUCTURE: string | null - HOUSE_STRUCTURE_ETC: string | null - RAFTER_MATERIAL: string | null - RAFTER_MATERIAL_ETC: string | null - RAFTER_SIZE: string | null - RAFTER_SIZE_ETC: string | null - RAFTER_PITCH: string | null - RAFTER_PITCH_ETC: string | null - RAFTER_DIRECTION: string | null - OPEN_FIELD_PLATE_KIND: string | null - OPEN_FIELD_PLATE_KIND_ETC: string | null - OPEN_FIELD_PLATE_THICKNESS: string | null - LEAK_TRACE: boolean | null - WATERPROOF_MATERIAL: string | null - WATERPROOF_MATERIAL_ETC: string | null - INSULATION_PRESENCE: string | null - INSULATION_PRESENCE_ETC: string | null - STRUCTURE_ORDER: string | null - STRUCTURE_ORDER_ETC: string | null - INSTALLATION_AVAILABILITY: string | null - INSTALLATION_AVAILABILITY_ETC: string | null - MEMO: string | null - REG_DT: Date - UPT_DT: Date + id: number + basicInfoId: number + contractCapacity: string | null + retailCompany: string | null + supplementaryFacilities: string | null // number 배열 + supplementaryFacilitiesEtc: string | null + installationSystem: string | null + installationSystemEtc: string | null + constructionYear: string | null + constructionYearEtc: string | null + roofMaterial: string | null // number 배열 + roofMaterialEtc: string | null + roofShape: string | null + roofShapeEtc: string | null + roofSlope: string | null + houseStructure: string | null + houseStructureEtc: string | null + rafterMaterial: string | null + rafterMaterialEtc: string | null + rafterSize: string | null + rafterSizeEtc: string | null + rafterPitch: string | null + rafterPitchEtc: string | null + rafterDirection: string | null + openFieldPlateKind: string | null + openFieldPlateKindEtc: string | null + openFieldPlateThickness: string | null + leakTrace: boolean | null + waterproofMaterial: string | null + waterproofMaterialEtc: string | null + insulationPresence: string | null + insulationPresenceEtc: string | null + structureOrder: string | null + structureOrderEtc: string | null + installationAvailability: string | null + installationAvailabilityEtc: string | null + memo: string | null + regDt: Date + uptDt: Date } export type SurveyBasicRequest = { - REPRESENTATIVE: string - STORE: string | null - CONSTRUCTION_POINT: string | null - INVESTIGATION_DATE: string | null - BUILDING_NAME: string | null - CUSTOMER_NAME: string | null - POST_CODE: string | null - ADDRESS: string | null - ADDRESS_DETAIL: string | null - SUBMISSION_STATUS: boolean - SUBMISSION_DATE: string | null + representative: string + store: string | null + constructionPoint: string | null + investigationDate: string | null + buildingName: string | null + customerName: string | null + postCode: string | null + address: string | null + addressDetail: string | null + submissionStatus: boolean + submissionDate: string | null } export type SurveyDetailRequest = { - CONTRACT_CAPACITY: string | null - RETAIL_COMPANY: string | null - SUPPLEMENTARY_FACILITIES: string | null // number 배열 - SUPPLEMENTARY_FACILITIES_ETC: string | null - INSTALLATION_SYSTEM: string | null - INSTALLATION_SYSTEM_ETC: string | null - CONSTRUCTION_YEAR: string | null - CONSTRUCTION_YEAR_ETC: string | null - ROOF_MATERIAL: string | null // number 배열 - ROOF_MATERIAL_ETC: string | null - ROOF_SHAPE: string | null - ROOF_SHAPE_ETC: string | null - ROOF_SLOPE: string | null - HOUSE_STRUCTURE: string | null - HOUSE_STRUCTURE_ETC: string | null - RAFTER_MATERIAL: string | null - RAFTER_MATERIAL_ETC: string | null - RAFTER_SIZE: string | null - RAFTER_SIZE_ETC: string | null - RAFTER_PITCH: string | null - RAFTER_PITCH_ETC: string | null - RAFTER_DIRECTION: string | null - OPEN_FIELD_PLATE_KIND: string | null - OPEN_FIELD_PLATE_KIND_ETC: string | null - OPEN_FIELD_PLATE_THICKNESS: string | null - LEAK_TRACE: boolean | null - WATERPROOF_MATERIAL: string | null - WATERPROOF_MATERIAL_ETC: string | null - INSULATION_PRESENCE: string | null - INSULATION_PRESENCE_ETC: string | null - STRUCTURE_ORDER: string | null - STRUCTURE_ORDER_ETC: string | null - INSTALLATION_AVAILABILITY: string | null - INSTALLATION_AVAILABILITY_ETC: string | null - MEMO: string | null + contractCapacity: string | null + retailCompany: string | null + supplementaryFacilities: string | null // number 배열 + supplementaryFacilitiesEtc: string | null + installationSystem: string | null + installationSystemEtc: string | null + constructionYear: string | null + constructionYearEtc: string | null + roofMaterial: string | null // number 배열 + roofMaterialEtc: string | null + roofShape: string | null + roofShapeEtc: string | null + roofSlope: string | null + houseStructure: string | null + houseStructureEtc: string | null + rafterMaterial: string | null + rafterMaterialEtc: string | null + rafterSize: string | null + rafterSizeEtc: string | null + rafterPitch: string | null + rafterPitchEtc: string | null + rafterDirection: string | null + openFieldPlateKind: string | null + openFieldPlateKindEtc: string | null + openFieldPlateThickness: string | null + leakTrace: boolean | null + waterproofMaterial: string | null + waterproofMaterialEtc: string | null + insulationPresence: string | null + insulationPresenceEtc: string | null + structureOrder: string | null + structureOrderEtc: string | null + installationAvailability: string | null + installationAvailabilityEtc: string | null + memo: string | null } export type SurveyDetailCoverRequest = { - DETAIL_INFO: SurveyDetailRequest + detailInfo: SurveyDetailRequest } export type SurveyRegistRequest = { - REPRESENTATIVE: string - STORE: string | null - CONSTRUCTION_POINT: string | null - INVESTIGATION_DATE: string | null - BUILDING_NAME: string | null - CUSTOMER_NAME: string | null - POST_CODE: string | null - ADDRESS: string | null - ADDRESS_DETAIL: string | null - SUBMISSION_STATUS: boolean - SUBMISSION_DATE: string | null - DETAIL_INFO: SurveyDetailRequest | null + representative: string + store: string | null + constructionPoint: string | null + investigationDate: string | null + buildingName: string | null + customerName: string | null + postCode: string | null + address: string | null + addressDetail: string | null + submissionStatus: boolean + submissionDate: string | null + detailInfo: SurveyDetailRequest | null } export type Mode = 'CREATE' | 'EDIT' | 'READ' | 'TEMP' // 등록 | 수정 | 상세 | 임시저장 From 7a6b9cbf92de019c9e53f4fbcebc33fda9b76918 Mon Sep 17 00:00:00 2001 From: Daseul Kim Date: Tue, 20 May 2025 13:51:49 +0900 Subject: [PATCH 17/36] =?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 f54260fc270c31f054c0386b88e393b032bf010e Mon Sep 17 00:00:00 2001 From: keyy1315 Date: Tue, 20 May 2025 13:53:44 +0900 Subject: [PATCH 18/36] feat: seporate save/submit/update process for reuse component --- src/app/api/survey-sales/[id]/route.ts | 107 ++-- src/app/api/survey-sales/route.ts | 39 +- src/app/survey-sale/[id]/page.tsx | 1 - src/app/survey-sale/regist/page.tsx | 4 +- .../survey-sale/detail/BasicForm.tsx | 109 ++-- .../survey-sale/detail/ButtonForm.tsx | 314 ++++++++--- .../survey-sale/detail/DataTable.tsx | 4 +- .../survey-sale/detail/DetailForm.tsx | 26 +- .../survey-sale/detail/RegistForm.tsx | 29 - .../survey-sale/detail/RoofForm.tsx | 497 +++++++++++++----- src/components/survey-sale/list/ListTable.tsx | 2 +- .../survey-sale/list/SearchForm.tsx | 4 - src/hooks/useSurvey.ts | 41 +- src/libs/axios.ts | 4 - 14 files changed, 824 insertions(+), 357 deletions(-) delete mode 100644 src/components/survey-sale/detail/RegistForm.tsx diff --git a/src/app/api/survey-sales/[id]/route.ts b/src/app/api/survey-sales/[id]/route.ts index 3da5981..3b88e86 100644 --- a/src/app/api/survey-sales/[id]/route.ts +++ b/src/app/api/survey-sales/[id]/route.ts @@ -1,9 +1,10 @@ -import { NextResponse } from 'next/server' +import { NextRequest, NextResponse } from 'next/server' import { prisma } from '@/libs/prisma' +import { convertToSnakeCase } from '../route' -export async function GET(request: Request, context: { params: { id: string } }) { +export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { try { - const { id } = await context.params + const { id } = await params // @ts-ignore const survey = await prisma.SD_SURVEY_SALES_BASIC_INFO.findUnique({ where: { ID: Number(id) }, @@ -18,34 +19,34 @@ export async function GET(request: Request, context: { params: { id: string } }) } } -export async function PUT(request: Request, context: { params: { id: string } }) { +export async function PUT(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { try { - const { id } = await context.params + const { id } = await params const body = await request.json() - console.log('body:: ', body) - - // DETAIL_INFO를 분리 const { DETAIL_INFO, ...basicInfo } = body + console.log('body:: ', body) // @ts-ignore const survey = await prisma.SD_SURVEY_SALES_BASIC_INFO.update({ where: { ID: Number(id) }, data: { - ...basicInfo, + ...convertToSnakeCase(basicInfo), UPT_DT: new Date(), - DETAIL_INFO: DETAIL_INFO - ? { - upsert: { - create: DETAIL_INFO, - update: DETAIL_INFO, - }, + DETAIL_INFO: DETAIL_INFO ? { + upsert: { + create: convertToSnakeCase(DETAIL_INFO), + update: convertToSnakeCase(DETAIL_INFO), + where: { + BASIC_INFO_ID: Number(id) } - : undefined, + } + } : undefined }, include: { - DETAIL_INFO: true, - }, + DETAIL_INFO: true + } }) + console.log('survey:: ', survey) return NextResponse.json(survey) } catch (error) { console.error('Error updating survey:', error) @@ -53,9 +54,9 @@ export async function PUT(request: Request, context: { params: { id: string } }) } } -export async function DELETE(request: Request, context: { params: { id: string } }) { +export async function DELETE(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { try { - const { id } = await context.params + const { id } = await params await prisma.$transaction(async (tx) => { // @ts-ignore @@ -86,9 +87,9 @@ export async function DELETE(request: Request, context: { params: { id: string } } } -export async function PATCH(request: Request, context: { params: { id: string } }) { +export async function PATCH(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { try { - const { id } = await context.params + const { id } = await params const body = await request.json() if (body.submit) { @@ -98,40 +99,42 @@ export async function PATCH(request: Request, context: { params: { id: string } data: { SUBMISSION_STATUS: true, SUBMISSION_DATE: new Date(), + UPT_DT: new Date(), }, }) return NextResponse.json({ message: 'Survey confirmed successfully' }) - } else { - // @ts-ignore - const hasDetails = await prisma.SD_SURVEY_SALES_DETAIL_INFO.findUnique({ - where: { BASIC_INFO_ID: Number(id) }, - }) - - if (hasDetails) { - //@ts-ignore - const result = await prisma.SD_SURVEY_SALES_BASIC_INFO.update({ - where: { ID: Number(id) }, - data: { - UPT_DT: new Date(), - DETAIL_INFO: { - update: body.DETAIL_INFO, - }, - }, - }) - return NextResponse.json(result) - } else { - // @ts-ignore - const survey = await prisma.SD_SURVEY_SALES_BASIC_INFO.update({ - where: { ID: Number(id) }, - data: { - DETAIL_INFO: { - create: body.DETAIL_INFO, - }, - }, - }) - return NextResponse.json({ message: 'Survey detail created successfully' }) - } } + // } else { + // // @ts-ignore + // const hasDetails = await prisma.SD_SURVEY_SALES_DETAIL_INFO.findUnique({ + // where: { BASIC_INFO_ID: Number(id) }, + // }) + + // if (hasDetails) { + // //@ts-ignore + // const result = await prisma.SD_SURVEY_SALES_BASIC_INFO.update({ + // where: { ID: Number(id) }, + // data: { + // UPT_DT: new Date(), + // DETAIL_INFO: { + // update: convertToSnakeCase(body.DETAIL_INFO), + // }, + // }, + // }) + // return NextResponse.json(result) + // } else { + // // @ts-ignore + // const survey = await prisma.SD_SURVEY_SALES_BASIC_INFO.update({ + // where: { ID: Number(id) }, + // data: { + // DETAIL_INFO: { + // create: convertToSnakeCase(body.DETAIL_INFO), + // }, + // }, + // }) + // return NextResponse.json({ message: 'Survey detail created successfully' }) + // } + // } } catch (error) { console.error('Error updating survey:', error) return NextResponse.json({ error: 'Failed to update survey' }, { status: 500 }) diff --git a/src/app/api/survey-sales/route.ts b/src/app/api/survey-sales/route.ts index a2df29d..aa9a549 100644 --- a/src/app/api/survey-sales/route.ts +++ b/src/app/api/survey-sales/route.ts @@ -219,19 +219,46 @@ export async function PUT(request: Request) { } } -export async function POST(request: Request) { +// 카멜케이스를 스네이크케이스로 변환하는 함수 +export const toSnakeCase = (str: string) => { + return str.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`); +} + +// 객체의 키를 스네이크케이스로 변환하는 함수 +export const convertToSnakeCase = (obj: any): Record => { + if (obj === null || obj === undefined) return obj; + + if (Array.isArray(obj)) { + return obj.map((item: any) => convertToSnakeCase(item)) + } + + if (typeof obj === 'object') { + return Object.keys(obj).reduce((acc, key) => { + const snakeKey = toSnakeCase(key).toUpperCase(); + acc[snakeKey] = convertToSnakeCase(obj[key]); + return acc; + }, {} as Record); + } + + return obj; +} + +export async function POST(request: Request) { try { const body = await request.json() - const { DETAIL_INFO, ...basicInfo } = body + console.log('body:: ', body) + + const { detailInfo, ...basicInfo } = body + // 기본 정보 생성 //@ts-ignore const result = await prisma.SD_SURVEY_SALES_BASIC_INFO.create({ data: { - ...basicInfo, + ...convertToSnakeCase(basicInfo), DETAIL_INFO: { - create: DETAIL_INFO, - }, - }, + create: convertToSnakeCase(detailInfo) + } + } }) return NextResponse.json(result) } catch (error) { diff --git a/src/app/survey-sale/[id]/page.tsx b/src/app/survey-sale/[id]/page.tsx index ac22151..acbb03e 100644 --- a/src/app/survey-sale/[id]/page.tsx +++ b/src/app/survey-sale/[id]/page.tsx @@ -4,7 +4,6 @@ export default function page() { return ( <> - {/* */} ) } diff --git a/src/app/survey-sale/regist/page.tsx b/src/app/survey-sale/regist/page.tsx index 0dec827..251d618 100644 --- a/src/app/survey-sale/regist/page.tsx +++ b/src/app/survey-sale/regist/page.tsx @@ -1,9 +1,9 @@ -import RegistForm from '@/components/survey-sale/detail/RegistForm' +import DetailForm from "@/components/survey-sale/detail/DetailForm"; export default function RegistPage() { return ( <> - + ) } diff --git a/src/components/survey-sale/detail/BasicForm.tsx b/src/components/survey-sale/detail/BasicForm.tsx index 716930e..0942abb 100644 --- a/src/components/survey-sale/detail/BasicForm.tsx +++ b/src/components/survey-sale/detail/BasicForm.tsx @@ -2,18 +2,45 @@ import { useEffect, useState } from 'react' import { useSurveySaleTabState } from '@/store/surveySaleTabState' -import { SurveyBasicRequest } from '@/types/Survey' -import { Mode } from 'fs' +import type { SurveyBasicRequest } from '@/types/Survey' +import type { Mode } from 'fs' +import { useSessionStore } from '@/store/session' +import { usePopupController } from '@/store/popupController' +import { useAddressStore } from '@/store/addressStore' export default function BasicForm(props: { basicInfo: SurveyBasicRequest; setBasicInfo: (basicInfo: SurveyBasicRequest) => void; mode: Mode }) { const { basicInfo, setBasicInfo, mode } = props const { setBasicInfoSelected } = useSurveySaleTabState() const [isFlip, setIsFlip] = useState(true) + const { session } = useSessionStore() + const { addressData } = useAddressStore() + useEffect(() => { setBasicInfoSelected() }, []) + useEffect(() => { + if (session?.isLoggedIn) { + setBasicInfo({ + ...basicInfo, + representative: session.userNm ?? '', + store: session.storeNm ?? null, + constructionPoint: session.builderNo ?? null, + }) + } + if (addressData) { + setBasicInfo({ + ...basicInfo, + postCode: addressData.post_code, + address: addressData.address, + addressDetail: addressData.address_detail, + }) + } + }, [session, addressData]) + + const popupController = usePopupController() + return ( <>
    @@ -31,31 +58,35 @@ export default function BasicForm(props: { basicInfo: SurveyBasicRequest; setBas setBasicInfo({ ...basicInfo, representative: e.target.value })} />
    -
    -
    販売店
    - setBasicInfo({ ...basicInfo, store: e.target.value })} - /> -
    -
    -
    施工店
    - setBasicInfo({ ...basicInfo, constructionPoint: e.target.value })} - /> -
    + {(session?.role === 'Builder' || session?.role?.includes('Admin')) && ( +
    +
    販売店
    + setBasicInfo({ ...basicInfo, store: e.target.value })} + /> +
    + )} + {(session?.role === 'Builder' || session?.role === 'Partner') && ( +
    +
    施工店
    + setBasicInfo({ ...basicInfo, constructionPoint: e.target.value })} + /> +
    + )}
@@ -67,7 +98,13 @@ export default function BasicForm(props: { basicInfo: SurveyBasicRequest; setBas - + setBasicInfo({ ...basicInfo, investigationDate: e.target.value })} + />
) : ( @@ -76,12 +113,24 @@ export default function BasicForm(props: { basicInfo: SurveyBasicRequest; setBas
{/* 건물명 */}
建物名
- + setBasicInfo({ ...basicInfo, buildingName: e.target.value })} + />
{/* 고객명 */} -
建物名
- +
お客様名
+ setBasicInfo({ ...basicInfo, customerName: e.target.value })} + />
郵便番号/都道府県
@@ -92,12 +141,12 @@ export default function BasicForm(props: { basicInfo: SurveyBasicRequest; setBas
{/* 도도부현 */}
- +
{/* 주소 */}
-
@@ -105,7 +154,7 @@ export default function BasicForm(props: { basicInfo: SurveyBasicRequest; setBas
市区町村名, 以後の住所
- +
diff --git a/src/components/survey-sale/detail/ButtonForm.tsx b/src/components/survey-sale/detail/ButtonForm.tsx index 4b2b9fa..a22ac5c 100644 --- a/src/components/survey-sale/detail/ButtonForm.tsx +++ b/src/components/survey-sale/detail/ButtonForm.tsx @@ -1,81 +1,267 @@ -import { Mode, SurveyBasicRequest, SurveyDetailRequest } from '@/types/Survey' +'use client' -export default function ButtonForm(props: { mode: Mode; setMode: (mode: Mode) => void; data: { basic: SurveyBasicRequest; roof: SurveyDetailRequest } }) { +import type { Mode, SurveyBasicRequest, SurveyDetailInfo, SurveyDetailRequest } from '@/types/Survey' +import { useSessionStore } from '@/store/session' +import { useEffect, useState } from 'react' +import { useParams, useRouter, useSearchParams } from 'next/navigation' +import { requiredFields, useServey } from '@/hooks/useSurvey' + +export default function ButtonForm(props: { + mode: Mode + setMode: (mode: Mode) => void + data: { basic: SurveyBasicRequest; roof: SurveyDetailRequest } +}) { + // 라우터 + const router = useRouter() const { mode, setMode } = props + const { session } = useSessionStore() + + const searchParams = useSearchParams() + const idParam = searchParams.get('id') + + const params = useParams() + const routeId = params.id + + const [isSubmitProcess, setIsSubmitProcess] = useState(false) + // ------------------------------------------------------------ + // 권한 + + // 제출권한 ㅇ + const [isSubmiter, setIsSubmiter] = useState(false) + // 작성자 + const [isWriter, setIsWriter] = useState(false) + const isSubmit = props.data.basic.submissionStatus + + useEffect(() => { + if (session?.isLoggedIn) { + setIsSubmiter(session.storeNm === props.data.basic.store && session.builderNo === props.data.basic.constructionPoint) + setIsWriter(session.userNm === props.data.basic.representative) + } + }, [session, props.data]) + + // ------------------------------------------------------------ + // 저장/임시저장/수정 + + const id = routeId ? Number(routeId) : Number(idParam) + const { deleteSurvey, submitSurvey, updateSurvey } = useServey(Number(id)) + const { validateSurveyDetail, createSurvey } = useServey() + let saveData = { + ...props.data.basic, + detailInfo: props.data.roof, + } + + const handleSave = (isTemporary: boolean) => { + const emptyField = validateSurveyDetail(props.data.roof) + console.log('handleSave, emptyField:: ', emptyField) + if (isTemporary) { + tempSaveProcess() + } else { + saveProcess(emptyField) + } + } + + const tempSaveProcess = async () => { + if (idParam) { + await updateSurvey(saveData) + router.push(`/survey-sale/detail?id=${idParam}&isTemporary=true`) + } else { + const id = await createSurvey(saveData) + router.push(`/survey-sale/detail?id=${id}&isTemporary=true`) + } + alert('一時保存されました。') + } + + const focusInput = (field: keyof SurveyDetailInfo) => { + const input = document.getElementById(field) + if (input) { + input.focus() + } + } + + const saveProcess = async (emptyField: string) => { + if (emptyField.trim() === '') { + if (idParam) { + // 수정 페이지에서 작성 후 제출 + if (isSubmitProcess) { + saveData = { + ...saveData, + submissionStatus: true, + submissionDate: new Date().toISOString(), + } + } + await updateSurvey(saveData) + router.push(`/survey-sale/${idParam}`) + } else { + const id = await createSurvey(saveData) + if (isSubmitProcess) { + submitProcess(id) + return + } + router.push(`/survey-sale/${id}`) + } + alert('保存されました。') + } else { + if (emptyField.includes('Unit')) { + alert('電気契約容量の単位を入力してください。') + focusInput(emptyField as keyof SurveyDetailInfo) + } else { + alert(requiredFields.find((field) => field.field === emptyField)?.name + ' 項目が空です。') + focusInput(emptyField as keyof SurveyDetailInfo) + } + } + } + // ------------------------------------------------------------ + // 삭제/제출 + + const handleDelete = async () => { + if (routeId) { + window.neoConfirm('削除しますか?', async () => { + await deleteSurvey() + router.push('/survey-sale') + }) + } + } + + const handleSubmit = async () => { + window.neoConfirm('提出しますか?', async () => { + setIsSubmitProcess(true) + if (routeId) { + submitProcess() + } else { + handleSave(false) + } + }) + } + const submitProcess = async (saveId?: number) => { + await submitSurvey(saveId) + alert('提出されました。') + router.push('/survey-sale') + } + // ------------------------------------------------------------ + + if (mode === 'READ' && isSubmit && isSubmiter) { + return ( + <> +
+
+ +
+
+ + ) + } + return ( <> - {mode === 'CREATE' && ( + {mode === 'READ' && (
-
- {/* 임시저장 */} - -
-
- {/* 저장 */} - -
-
- {/* 목록 */} - -
+ + + {(isWriter || !isSubmiter) && } + {!isSubmit && isSubmiter && }
)} - {mode === 'TEMP' && ( + + {(mode === 'CREATE' || mode === 'EDIT') && (
-
- {/* 수정 */} - -
-
- {/* 삭제 */} - -
-
-
- )} - {mode === 'EDIT' && ( -
-
-
- {/* 목록 */} - -
-
- {/* 제출 */} - -
-
- {/* 수정 */} - -
-
- {/* 삭제 */} - -
+ + + +
)} ) } + +// 목록 버튼 +function ListButton() { + const router = useRouter() + return ( +
+ {/* 목록 */} + +
+ ) +} + +function EditButton(props: { setMode: (mode: Mode) => void; id: string; mode: Mode }) { + const { setMode, id, mode } = props + const router = useRouter() + return ( +
+ {/* 수정 */} + +
+ ) +} + +function SubmitButton(props: { handleSubmit: () => void }) { + const { handleSubmit } = props + return ( +
+ {/* 제출 */} + +
+ ) +} + +function DeleteButton(props: { handleDelete: () => void }) { + const { handleDelete } = props + return ( +
+ {/* 삭제 */} + +
+ ) +} + +function SaveButton(props: { handleSave: (isTemporary: boolean) => void }) { + const { handleSave } = props + return ( +
+ {/* 저장 */} + +
+ ) +} + +function TempButton(props: { setMode: (mode: Mode) => void; handleSave: (isTemporary: boolean) => void }) { + const { setMode, handleSave } = props + const router = useRouter() + + return ( +
+ {/* 임시저장 */} + +
+ ) +} diff --git a/src/components/survey-sale/detail/DataTable.tsx b/src/components/survey-sale/detail/DataTable.tsx index 1d06e2e..210d80d 100644 --- a/src/components/survey-sale/detail/DataTable.tsx +++ b/src/components/survey-sale/detail/DataTable.tsx @@ -4,7 +4,7 @@ import { useServey } from '@/hooks/useSurvey' import { useParams, useSearchParams } from 'next/navigation' import { useEffect, useState } from 'react' import DetailForm from './DetailForm' -import { SurveyBasicInfo } from '@/types/Survey' +import type { SurveyBasicInfo } from '@/types/Survey' export default function DataTable() { const params = useParams() @@ -83,7 +83,7 @@ export default function DataTable() { - + ) } diff --git a/src/components/survey-sale/detail/DetailForm.tsx b/src/components/survey-sale/detail/DetailForm.tsx index 387bb4f..0467aec 100644 --- a/src/components/survey-sale/detail/DetailForm.tsx +++ b/src/components/survey-sale/detail/DetailForm.tsx @@ -1,10 +1,13 @@ 'use client' -import { Mode, SurveyBasicInfo, SurveyBasicRequest, SurveyDetailRequest } from '@/types/Survey' +import type { Mode, SurveyBasicInfo, SurveyBasicRequest, SurveyDetailRequest } from '@/types/Survey' import { useEffect, useState } from 'react' import ButtonForm from './ButtonForm' import BasicForm from './BasicForm' import RoofForm from './RoofForm' +import { useParams, useSearchParams } from 'next/navigation' +import { useServey } from '@/hooks/useSurvey' + const roofInfoForm: SurveyDetailRequest = { contractCapacity: null, retailCompany: null, @@ -57,21 +60,32 @@ const basicInfoForm: SurveyBasicRequest = { submissionDate: null, } -export default function DetailForm(props: { surveyInfo?: SurveyBasicInfo; mode?: Mode }) { - const [mode, setMode] = useState(props.mode ?? 'CREATE') +export default function DetailForm() { + const idParam = useSearchParams().get('id') + const routeId = useParams().id + + const id = idParam ?? routeId + + const { surveyDetail } = useServey(Number(id)) + + const [mode, setMode] = useState(idParam ? 'EDIT' : routeId ? 'READ' : 'CREATE') const [basicInfoData, setBasicInfoData] = useState(basicInfoForm) const [roofInfoData, setRoofInfoData] = useState(roofInfoForm) useEffect(() => { - if (props.surveyInfo && (mode === 'EDIT' || mode === 'READ')) { - const { id, uptDt, regDt, detailInfo, ...rest } = props.surveyInfo + if (surveyDetail && (mode === 'EDIT' || mode === 'READ')) { + const { id, uptDt, regDt, detailInfo, ...rest } = surveyDetail setBasicInfoData(rest) if (detailInfo) { const { id, uptDt, regDt, basicInfoId, ...rest } = detailInfo setRoofInfoData(rest) } } - }, [props.surveyInfo, mode]) + }, [surveyDetail, mode]) + + // console.log('mode:: ', mode) + // console.log('surveyDetail:: ', surveyDetail) + // console.log('roofInfoData:: ', roofInfoData) const data = { basic: basicInfoData, diff --git a/src/components/survey-sale/detail/RegistForm.tsx b/src/components/survey-sale/detail/RegistForm.tsx deleted file mode 100644 index 4377713..0000000 --- a/src/components/survey-sale/detail/RegistForm.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { Mode } from '@/types/Survey' -import { useSearchParams } from 'next/navigation' -import DetailForm from './DetailForm' -import { useServey } from '@/hooks/useSurvey' -import { useEffect, useState } from 'react' -import { SurveyBasicInfo } from '@/types/Survey' -import { useSessionStore } from '@/store/session' - -export default function RegistForm() { - const searchParams = useSearchParams() - const id = searchParams.get('id') - - const { surveyDetail } = useServey(Number(id)) - const { session } = useSessionStore() - - const [mode, setMode] = useState('CREATE') - - useEffect(() => { - if (id) { - setMode('EDIT') - } - }, [id]) - - return ( - <> - - - ) -} diff --git a/src/components/survey-sale/detail/RoofForm.tsx b/src/components/survey-sale/detail/RoofForm.tsx index 794bf21..60399fc 100644 --- a/src/components/survey-sale/detail/RoofForm.tsx +++ b/src/components/survey-sale/detail/RoofForm.tsx @@ -1,5 +1,5 @@ import { useState } from 'react' -import { Mode, SurveyDetailInfo, SurveyDetailRequest } from '@/types/Survey' +import type { Mode, SurveyDetailInfo, SurveyDetailRequest } from '@/types/Survey' type RadioEtcKeys = 'houseStructure' | 'rafterMaterial' | 'waterproofMaterial' | 'insulationPresence' | 'rafterDirection' | 'leakTrace' type SelectBoxKeys = @@ -12,14 +12,14 @@ type SelectBoxKeys = | 'structureOrder' | 'installationAvailability' -export const supplementary_facilities = [ +export const supplementaryFacilities = [ { id: 1, name: 'エコキュート' }, //에코큐트 { id: 2, name: 'エネパーム' }, //에네팜 { id: 3, name: '蓄電池システム' }, //축전지시스템 { id: 4, name: '太陽光発電' }, //태양광발전 ] -export const roof_material = [ +export const roofMaterial = [ { id: 1, name: 'スレート' }, //슬레이트 { id: 2, name: 'アスファルトシングル' }, //아스팔트 싱글 { id: 3, name: '瓦' }, //기와 @@ -200,19 +200,47 @@ export const radioEtcData: Record ], } +const makeNumArr = (value: string) => { + return value + .split(',') + .map((v) => v.trim()) + .filter((v) => v.length > 0) +} + export default function RoofForm(props: { roofInfo: SurveyDetailRequest | SurveyDetailInfo setRoofInfo: (roofInfo: SurveyDetailRequest) => void mode: Mode }) { - const makeNumArr = (value: string) => { - return value - .split(',') - .map((v) => v.trim()) - .filter((v) => v.length > 0) - } const { roofInfo, setRoofInfo, mode } = props const [isFlip, setIsFlip] = useState(true) + + const handleNumberInput = (key: keyof SurveyDetailRequest, value: number | string) => { + if (key === 'roofSlope' || key === 'openFieldPlateThickness') { + const stringValue = value.toString() + if (stringValue.length > 5) { + alert('保存できるサイズを超えました。') + return + } + if (stringValue.includes('.')) { + const decimalPlaces = stringValue.split('.')[1].length + if (decimalPlaces > 1) { + alert('小数点以下1桁までしか許されません。') + return + } + } + } + setRoofInfo({ ...roofInfo, [key]: value.toString() }) + } + + const handleUnitInput = (value: string) => { + const numericValue = roofInfo.contractCapacity?.replace(/[^0-9.]/g, '') || '' + setRoofInfo({ + ...roofInfo, + contractCapacity: numericValue ? `${numericValue} ${value}` : value, + }) + } + return (
setIsFlip(!isFlip)}> @@ -229,78 +257,55 @@ export default function RoofForm(props: {
{/* 전기 계약 용량 */}
電気契約容量
-
- -
- {mode === 'READ' && } + {mode === 'READ' && } {mode !== 'READ' && ( -
- +
+ handleNumberInput('contractCapacity', e.target.value)} + /> +
+ +
)}
{/* 전기 소매 회사사 */}
電気小売会社
- + setRoofInfo({ ...roofInfo, retailCompany: e.target.value })} + />
{/* 전기 부대 설비 */}
電気袋設備※複数選択可能
-
- {/*
- - -
*/} - {supplementary_facilities.map((item) => ( -
- - -
- ))} -
- - -
-
-
- -
+
- {/* 설치 희망 시스템 */} - {/*
設置希望システム
- {mode === 'READ' && ( -
- -
- )} - {mode !== 'READ' && ( -
- -
- )} */} -
設置希望システム
- +
設置希望システム
+
@@ -312,108 +317,73 @@ export default function RoofForm(props: { {/* 건축 연수 */}
建築研修
- {/* {mode === 'READ' && } - {mode !== 'READ' && ( - - )} */} - +
- {/*
- - -
*/}
{/* 지붕재 */}
屋根材※最大2個まで選択可能
-
- {roof_material.map((item) => ( -
- - -
- ))} -
- - -
-
-
- -
+
{/* 지붕 모양 */} -
建築研修
+
屋根の形状
- {/* {mode === 'READ' && } - {mode !== 'READ' && ( - - )} */} - -
-
- +
{/* 지붕 경사도도 */}
屋根の斜面
- + handleNumberInput('roofSlope', e.target.value)} + />
{/* 주택구조조 */}
住宅構造
- +
{/* 서까래 재질 */}
垂木材質
- +
{/* 서까래 크기 */}
垂木サイズ
- +
{/* 서까래 피치 */}
垂木サイズ
- +
{/* 서까래 방향 */}
垂木の方向
- +
{/* 노지판 종류류 */}
路地板の種類
- +
@@ -422,7 +392,13 @@ export default function RoofForm(props: { 路地板厚※小幅板を選択した場合, 厚さ. 小幅板間の間隔寸法を記載
- + handleNumberInput('openFieldPlateThickness', e.target.value)} + /> mm
@@ -430,28 +406,28 @@ export default function RoofForm(props: { {/* 누수 흔적 */}
水漏れの痕跡
- +
{/* 방수재 종류 */}
防水材の種類
- +
{/* 단열재 유무 */}
断熱材の有無
- +
{/* 지붕 구조의 순서 */}
屋根構造の順序
- +
{/* 지붕 제품명 설치 가능 여부 확인 */}
屋根製品名 設置可否確認
- +
{/* 메모 */} @@ -459,11 +435,12 @@ export default function RoofForm(props: {
@@ -474,23 +451,107 @@ export default function RoofForm(props: { ) } -const SelectedBox = ({ column, detailInfoData }: { column: string; detailInfoData: SurveyDetailInfo }) => { +const SelectedBox = ({ + mode, + column, + detailInfoData, + setRoofInfo, +}: { + mode: Mode + column: string + detailInfoData: SurveyDetailInfo + setRoofInfo: (roofInfo: SurveyDetailRequest) => void +}) => { const selectedId = detailInfoData?.[column as keyof SurveyDetailInfo] const etcValue = detailInfoData?.[`${column}Etc` as keyof SurveyDetailInfo] + const [isEtcSelected, setIsEtcSelected] = useState(etcValue !== null && etcValue !== undefined && etcValue !== '') + const [etcVal, setEtcVal] = useState(etcValue?.toString() ?? '') + + const handleSelectChange = (e: React.ChangeEvent) => { + const value = e.target.value + const isSpecialCase = column === 'constructionYear' || column === 'installationAvailability' + const isEtc = value === 'etc' + const isSpecialEtc = isSpecialCase && value === '2' + + const updatedData: typeof detailInfoData = { + ...detailInfoData, + [column]: isEtc ? null : value, + [`${column}Etc`]: isEtc ? '' : null, + } + + if (isSpecialEtc) { + updatedData[column] = value + } + + setIsEtcSelected(isEtc || isSpecialEtc) + if (!isEtc) setEtcVal('') + setRoofInfo(updatedData) + } + + const handleEtcInputChange = (e: React.ChangeEvent) => { + const value = e.target.value + setEtcVal(value) + setRoofInfo({ ...detailInfoData, [`${column}Etc`]: value }) + } + return ( <> - + {selectBoxOptions[column as keyof typeof selectBoxOptions].map((item) => ( + + ))} + {column !== 'installationAvailability' && column !== 'constructionYear' && ( + + )} + - {etcValue && } +
+ +
) } -const RadioSelected = ({ column, detailInfoData }: { column: string; detailInfoData: SurveyDetailInfo | null }) => { +const RadioSelected = ({ + mode, + column, + detailInfoData, + setRoofInfo, +}: { + mode: Mode + column: string + detailInfoData: SurveyDetailInfo + setRoofInfo: (roofInfo: SurveyDetailRequest) => void +}) => { let selectedId = detailInfoData?.[column as keyof SurveyDetailInfo] if (column === 'leakTrace') { selectedId = Number(selectedId) @@ -501,28 +562,182 @@ const RadioSelected = ({ column, detailInfoData }: { column: string; detailInfoD if (column !== 'rafterDirection') { etcValue = detailInfoData?.[`${column}Etc` as keyof SurveyDetailInfo] } - const etcChecked = etcValue !== null && etcValue !== undefined && etcValue !== '' + const [etcChecked, setEtcChecked] = useState(etcValue !== null && etcValue !== undefined && etcValue !== '') + const [etcVal, setEtcVal] = useState(etcValue?.toString() ?? '') + + const handleRadioChange = (e: React.ChangeEvent) => { + const value = e.target.value + if (column === 'leakTrace') { + handleBooleanRadioChange(value) + } + if (value === 'etc') { + setEtcChecked(true) + setRoofInfo({ ...detailInfoData, [column]: null, [`${column}Etc`]: '' }) + } else { + if (column === 'insulationPresence' && value === '2') { + setEtcChecked(true) + } else { + setEtcChecked(false) + } + setRoofInfo({ ...detailInfoData, [column]: value, [`${column}Etc`]: null }) + } + } + + const handleBooleanRadioChange = (value: string) => { + if (value === '1') { + setRoofInfo({ ...detailInfoData, leakTrace: true }) + } else { + setRoofInfo({ ...detailInfoData, leakTrace: false }) + } + } + + const handleEtcInputChange = (e: React.ChangeEvent) => { + const value = e.target.value + setEtcVal(value) + setRoofInfo({ ...detailInfoData, [`${column}Etc`]: value }) + } - // console.log('column: selectedId', column, selectedId) return ( <> {radioEtcData[column as keyof typeof radioEtcData].map((item) => (
- - + +
))} {column !== 'rafterDirection' && column !== 'leakTrace' && column !== 'insulationPresence' && (
- +
)} - {etcChecked && ( + {column !== 'leakTrace' && column !== 'rafterDirection' && (
- +
)} ) } + +const MultiCheck = ({ + mode, + column, + roofInfo, + setRoofInfo, +}: { + mode: Mode + column: string + roofInfo: SurveyDetailInfo + setRoofInfo: (roofInfo: SurveyDetailRequest) => void +}) => { + const multiCheckData = column === 'supplementaryFacilities' ? supplementaryFacilities : roofMaterial + + const [isOtherCheck, setIsOtherCheck] = useState(false) + const [otherValue, setOtherValue] = useState(roofInfo?.[`${column}Etc` as keyof SurveyDetailInfo]?.toString() ?? '') + + const handleCheckbox = (id: number) => { + const value = makeNumArr(String(roofInfo[column as keyof SurveyDetailInfo] ?? '')) + const isOtherSelected = roofInfo?.[`${column}Etc` as keyof SurveyDetailInfo] !== null + + let newValue: string[] + if (value.includes(String(id))) { + newValue = value.filter((v) => v !== String(id)) + } else { + if (column === 'roofMaterial') { + const totalSelected = value.length + (isOtherSelected ? 1 : 0) + + if (totalSelected >= 2) { + alert('屋根材は最大2個まで選択できます。') + return + } + } + newValue = [...value, String(id)] + } + setRoofInfo({ ...roofInfo, [column]: newValue.join(',') }) + } + + const handleOtherCheckbox = () => { + if (column === 'roofMaterial') { + const value = makeNumArr(String(roofInfo[column as keyof SurveyDetailInfo] ?? '')) + const currentSelected = value.length + if (!isOtherCheck && currentSelected >= 2) { + alert('屋根材は最大2個まで選択できます。') + return + } + } + const newIsOtherCheck = !isOtherCheck + setIsOtherCheck(newIsOtherCheck) + setOtherValue('') + + setRoofInfo({ ...roofInfo, [`${column}Etc`]: newIsOtherCheck ? '' : null }) + } + + const handleOtherInputChange = (e: React.ChangeEvent) => { + const value = e.target.value + setOtherValue(value) + setRoofInfo({ ...roofInfo, [`${column}Etc`]: value }) + } + + return ( + <> +
+ {multiCheckData.map((item) => ( +
+ handleCheckbox(item.id)} + /> + +
+ ))} +
+ + +
+
+
+ +
+ + ) +} diff --git a/src/components/survey-sale/list/ListTable.tsx b/src/components/survey-sale/list/ListTable.tsx index b1ac703..f1a3847 100644 --- a/src/components/survey-sale/list/ListTable.tsx +++ b/src/components/survey-sale/list/ListTable.tsx @@ -7,7 +7,7 @@ import { useRouter } from 'next/navigation' import SearchForm from './SearchForm' import { useSurveyFilterStore } from '@/store/surveyFilterStore' import { useSessionStore } from '@/store/session' -import { SurveyBasicInfo } from '@/types/Survey' +import type { SurveyBasicInfo } from '@/types/Survey' export default function ListTable() { const router = useRouter() diff --git a/src/components/survey-sale/list/SearchForm.tsx b/src/components/survey-sale/list/SearchForm.tsx index 327a85c..7f46e68 100644 --- a/src/components/survey-sale/list/SearchForm.tsx +++ b/src/components/survey-sale/list/SearchForm.tsx @@ -17,7 +17,6 @@ export default function SearchForm({ memberRole, userId }: { memberRole: string; } setKeyword(searchKeyword) setSearchOption(option) - // onItemsInit() } const searchOptions = memberRole === 'Partner' ? SEARCH_OPTIONS_PARTNERS : SEARCH_OPTIONS @@ -38,7 +37,6 @@ export default function SearchForm({ memberRole, userId }: { memberRole: string; if (e.target.value === 'all') { setKeyword('') setSearchKeyword('') - // onItemsInit() setSearchOption('all') setOption('all') } else { @@ -80,7 +78,6 @@ export default function SearchForm({ memberRole, userId }: { memberRole: string; checked={isMySurvey === userId} onChange={() => { setIsMySurvey(isMySurvey === userId ? null : userId) - // onItemsInit() }} /> @@ -94,7 +91,6 @@ export default function SearchForm({ memberRole, userId }: { memberRole: string; value={sort} onChange={(e) => { setSort(e.target.value as 'created' | 'updated') - // onItemsInit() }} > diff --git a/src/hooks/useSurvey.ts b/src/hooks/useSurvey.ts index d07cddf..043cfce 100644 --- a/src/hooks/useSurvey.ts +++ b/src/hooks/useSurvey.ts @@ -7,7 +7,7 @@ import { useSessionStore } from '@/store/session' import { useMemo } from 'react' import { AxiosResponse } from 'axios' -const requiredFields = [ +export const requiredFields = [ { field: 'installationSystem', name: '設置希望システム', @@ -67,7 +67,7 @@ export function useServey(id?: number): { createSurveyDetail: (params: { surveyId: number; surveyDetail: SurveyDetailCoverRequest }) => void updateSurvey: (survey: SurveyRegistRequest) => void deleteSurvey: () => Promise - submitSurvey: () => void + submitSurvey: (saveId?: number) => void validateSurveyDetail: (surveyDetail: SurveyDetailRequest) => string getZipCode: (zipCode: string) => Promise refetchSurveyList: () => void @@ -77,7 +77,7 @@ export function useServey(id?: number): { const { session } = useSessionStore() const { - data, + data: surveyListData, isLoading: isLoadingSurveyList, refetch: refetchSurveyList, } = useQuery({ @@ -100,18 +100,17 @@ export function useServey(id?: number): { enabled: session?.isLoggedIn, }) const surveyData = useMemo(() => { - if (!data) return {} + if (!surveyListData) return { count: 0, data: [] } return { - data: data.data, - count: data.count, + ...surveyListData, } - }, [data]) + }, [surveyListData]) const { data: surveyDetail, isLoading: isLoadingSurveyDetail } = useQuery({ queryKey: ['survey', id], queryFn: async () => { if (id === undefined) throw new Error('id is required') - if (id === null) return null + if (id === null || isNaN(id)) return null const resp = await axiosInstance(null).get(`/api/survey-sales/${id}`) return resp.data }, @@ -132,6 +131,7 @@ export function useServey(id?: number): { const { mutate: updateSurvey, isPending: isUpdatingSurvey } = useMutation({ mutationFn: async (survey: SurveyRegistRequest) => { + console.log('updateSurvey, survey:: ', survey) if (id === undefined) throw new Error('id is required') const resp = await axiosInstance(null).put(`/api/survey-sales/${id}`, survey) return resp.data @@ -166,9 +166,10 @@ export function useServey(id?: number): { }) const { mutateAsync: submitSurvey } = useMutation({ - mutationFn: async () => { - if (id === undefined) throw new Error('id is required') - const resp = await axiosInstance(null).patch(`/api/survey-sales/${id}`, { + mutationFn: async (saveId?: number) => { + const submitId = saveId ?? id + if (!submitId) throw new Error('id is required') + const resp = await axiosInstance(null).patch(`/api/survey-sales/${submitId}`, { submit: true, }) return resp.data @@ -180,12 +181,22 @@ export function useServey(id?: number): { }) const validateSurveyDetail = (surveyDetail: SurveyDetailRequest) => { - const etcFields = ['installationSystem', 'constructionYear', 'rafterSize', 'rafterPitch', 'waterproofMaterial', 'structureOrder'] as const + const etcFields = [ + 'installationSystem', + 'constructionYear', + 'rafterSize', + 'rafterPitch', + 'waterproofMaterial', + 'structureOrder', + 'insulationPresence', + ] as const const emptyField = requiredFields.find((field) => { if (etcFields.includes(field.field as (typeof etcFields)[number])) { return ( - surveyDetail[field.field as keyof SurveyDetailRequest] === null && surveyDetail[`${field.field}_ETC` as keyof SurveyDetailRequest] === '' + surveyDetail[field.field as keyof SurveyDetailRequest] === null && + (surveyDetail[`${field.field}Etc` as keyof SurveyDetailRequest] === null || + surveyDetail[`${field.field}Etc` as keyof SurveyDetailRequest]?.toString().trim() === '') ) } else { return surveyDetail[field.field as keyof SurveyDetailRequest] === null @@ -197,7 +208,7 @@ export function useServey(id?: number): { return 'contractCapacityUnit' } - return emptyField?.name || '' + return emptyField?.field || '' } const getZipCode = async (zipCode: string): Promise => { @@ -213,7 +224,7 @@ export function useServey(id?: number): { } return { - surveyList: surveyData, + surveyList: surveyData.data, surveyDetail: surveyDetail as SurveyBasicInfo | null, isLoadingSurveyList, isLoadingSurveyDetail, diff --git a/src/libs/axios.ts b/src/libs/axios.ts index d973f9d..6b5fbbc 100644 --- a/src/libs/axios.ts +++ b/src/libs/axios.ts @@ -84,10 +84,6 @@ const transformObjectKeys = (obj: any): any => { return obj } -export const camelToSnake = (str: string): string => { - return str.replace(/([A-Z])/g, (group) => `_${group.toLowerCase()}`) -} - const snakeToCamel = (str: string): string => { return str.toLowerCase().replace(/([-_][a-z])/g, (group) => group.toUpperCase().replace('-', '').replace('_', '')) } From 2d1184e1c09c9d53878e5cb28952cc4dd179d916 Mon Sep 17 00:00:00 2001 From: yoosangwook Date: Tue, 20 May 2025 13:59:58 +0900 Subject: [PATCH 19/36] 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 d21865ca65bd580acbaa338b8749cdd9f64d2677 Mon Sep 17 00:00:00 2001 From: keyy1315 Date: Tue, 20 May 2025 14:01:22 +0900 Subject: [PATCH 20/36] fix: convert camelcase function move to util.js --- src/app/api/survey-sales/[id]/route.ts | 2 +- src/app/api/survey-sales/route.ts | 26 +------------------------- src/utils/common-utils.js | 25 +++++++++++++++++++++++++ 3 files changed, 27 insertions(+), 26 deletions(-) diff --git a/src/app/api/survey-sales/[id]/route.ts b/src/app/api/survey-sales/[id]/route.ts index 3b88e86..415d1e6 100644 --- a/src/app/api/survey-sales/[id]/route.ts +++ b/src/app/api/survey-sales/[id]/route.ts @@ -1,6 +1,6 @@ import { NextRequest, NextResponse } from 'next/server' import { prisma } from '@/libs/prisma' -import { convertToSnakeCase } from '../route' +import { convertToSnakeCase } from '@/utils/common-utils' export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { try { diff --git a/src/app/api/survey-sales/route.ts b/src/app/api/survey-sales/route.ts index aa9a549..3298f5d 100644 --- a/src/app/api/survey-sales/route.ts +++ b/src/app/api/survey-sales/route.ts @@ -1,6 +1,6 @@ import { NextResponse } from 'next/server' import { prisma } from '@/libs/prisma' - +import { convertToSnakeCase } from '@/utils/common-utils' /** * 검색 파라미터 */ @@ -219,30 +219,6 @@ export async function PUT(request: Request) { } } -// 카멜케이스를 스네이크케이스로 변환하는 함수 -export const toSnakeCase = (str: string) => { - return str.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`); -} - -// 객체의 키를 스네이크케이스로 변환하는 함수 -export const convertToSnakeCase = (obj: any): Record => { - if (obj === null || obj === undefined) return obj; - - if (Array.isArray(obj)) { - return obj.map((item: any) => convertToSnakeCase(item)) - } - - if (typeof obj === 'object') { - return Object.keys(obj).reduce((acc, key) => { - const snakeKey = toSnakeCase(key).toUpperCase(); - acc[snakeKey] = convertToSnakeCase(obj[key]); - return acc; - }, {} as Record); - } - - return obj; -} - export async function POST(request: Request) { try { const body = await request.json() diff --git a/src/utils/common-utils.js b/src/utils/common-utils.js index 0a1265f..d0c7f65 100644 --- a/src/utils/common-utils.js +++ b/src/utils/common-utils.js @@ -185,3 +185,28 @@ export const isEqualObjects = (obj1, obj2) => { function isObject(value) { return value !== null && typeof value === 'object' } + + +// 카멜케이스를 스네이크케이스로 변환하는 함수 +export const toSnakeCase = (str) => { + return str.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`); +} + +// 객체의 키를 스네이크케이스로 변환하는 함수 +export const convertToSnakeCase = (obj) => { + if (obj === null || obj === undefined) return obj; + + if (Array.isArray(obj)) { + return obj.map((item) => convertToSnakeCase(item)) + } + + if (typeof obj === 'object') { + return Object.keys(obj).reduce((acc, key) => { + const snakeKey = toSnakeCase(key).toUpperCase(); + acc[snakeKey] = convertToSnakeCase(obj[key]); + return acc; + }, {}); + } + + return obj; +} From fcd80cbe3b0e763fd03887d5150afbf2c6371163 Mon Sep 17 00:00:00 2001 From: nalpari Date: Tue, 20 May 2025 14:16:19 +0900 Subject: [PATCH 21/36] ... --- .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 22/36] ...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 23/36] 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('/') + } + } + /** * 사용자 이벤트 트래킹 처리 * From 73b042b0acdf6c7a978ebda48a5361b301e18dda Mon Sep 17 00:00:00 2001 From: yoosangwook Date: Tue, 20 May 2025 18:55:24 +0900 Subject: [PATCH 24/36] refactor: Enhance authentication API to streamline session data handling and role assignment; update login response structure and conditionally render UI elements based on user role --- src/app/api/auth/route.ts | 51 +++++++++++++++++-- src/app/api/partner/route.ts | 2 + src/components/Login.tsx | 2 - .../popup/MemberInformationPopup.tsx | 8 +-- src/components/ui/common/Header.tsx | 8 +-- src/libs/axios.ts | 15 +++++- 6 files changed, 72 insertions(+), 14 deletions(-) diff --git a/src/app/api/auth/route.ts b/src/app/api/auth/route.ts index 8caef42..c595ecd 100644 --- a/src/app/api/auth/route.ts +++ b/src/app/api/auth/route.ts @@ -12,7 +12,7 @@ export async function POST(request: Request) { loginId, pwd, }) - // console.log('🚀 ~ result ~ result:', result) + console.log('🚀 ~ result ~ result:', result.data) if (result.data.result.code === 200) { const cookieStore = await cookies() @@ -57,8 +57,6 @@ export async function POST(request: Request) { session.role = 'Admin_Sub' } else if (result.data.data.groupId === '70000' && result.data.data.builderNo !== null) { session.role = 'Builder' - } else if (result.data.data.groupId === '90000' && result.data.data.builderNo !== null) { - session.role = 'Partner' } else { session.role = 'User' } @@ -68,5 +66,50 @@ export async function POST(request: Request) { await session.save() } - return NextResponse.json({ code: 200, message: 'Login is Succecss!!', result: result.data.data }) + const resultForSession = { + LANG_CD: result.data.data.langCd, + CURR_PAGE: result.data.data.currPage, + ROW_COUNT: result.data.data.rowCount, + START_ROW: result.data.data.startRow, + END_ROW: result.data.data.endRow, + COMP_CD: result.data.data.compCd, + AGENCY_STORE_ID: result.data.data.agencyStoreId, + STORE_ID: result.data.data.storeId, + STORE_NM: result.data.data.storeNm, + USER_ID: result.data.data.userId, + CATEGORY: result.data.data.category, + USER_NM: result.data.data.userNm, + USER_NM_KANA: result.data.data.userNmKana, + TEL_NO: result.data.data.telNo, + FAX: result.data.data.fax, + EMAIL: result.data.data.email, + LAST_EDIT_USER: result.data.data.lastEditUser, + STORE_GUBUN: result.data.data.storeGubun, + PW_CURR: result.data.data.pwCurr, + PWD_INIT_YN: result.data.data.pwdInitYn, + APPR_STAT_CD: result.data.data.apprStatCd, + LOGIN_FAIL_CNT: result.data.data.loginFailCnt, + LOGIN_FAIL_MIN_YN: result.data.data.loginFailMinYn, + PRICE_VIEW_STAT_CD: result.data.data.priceViewStatCd, + GROUP_ID: result.data.data.groupId, + STORE_LVL: result.data.data.storeLvl, + CUST_CD: result.data.data.custCd, + BUILDER_NO: result.data.data.builderNo, + IS_LOGGED_IN: true, + ROLE: '', + } + + if (result.data.data.userId === 'T01') { + resultForSession.ROLE = 'T01' + } else if (result.data.data.groupId === '60000') { + resultForSession.ROLE = 'Admin' + } else if (result.data.data.groupId === '70000' && result.data.data.builderNo === null) { + resultForSession.ROLE = 'Admin_Sub' + } else if (result.data.data.groupId === '70000' && result.data.data.builderNo !== null) { + resultForSession.ROLE = 'Builder' + } else { + resultForSession.ROLE = 'User' + } + + return NextResponse.json({ code: 200, message: 'Login is Succecss!!', result: resultForSession }) } diff --git a/src/app/api/partner/route.ts b/src/app/api/partner/route.ts index abb63df..7d02959 100644 --- a/src/app/api/partner/route.ts +++ b/src/app/api/partner/route.ts @@ -123,6 +123,8 @@ export async function POST(request: Request) { STORE_LVL: null, CUST_CD: null, BUILDER_NO: data[0].user_seko_id, + IS_LOGGED_IN: true, + ROLE: 'Partner', } return NextResponse.json({ code: 200, message: 'Partner Login is Succecss!!', result }) diff --git a/src/components/Login.tsx b/src/components/Login.tsx index d5edc00..24b7219 100644 --- a/src/components/Login.tsx +++ b/src/components/Login.tsx @@ -3,10 +3,8 @@ import type { SessionData } from '@/types/Auth' import { useEffect, useReducer, useState } from 'react' import { useRouter } from 'next/navigation' - import { useLocalStorage } from 'usehooks-ts' import { useQuery } from '@tanstack/react-query' - import { axiosInstance } from '@/libs/axios' import { useSessionStore } from '@/store/session' diff --git a/src/components/popup/MemberInformationPopup.tsx b/src/components/popup/MemberInformationPopup.tsx index 87a8829..8cf0bae 100644 --- a/src/components/popup/MemberInformationPopup.tsx +++ b/src/components/popup/MemberInformationPopup.tsx @@ -57,9 +57,11 @@ export default function MemberInformationPopup() {
- + {session.role !== 'Partner' && ( + + )} diff --git a/src/components/ui/common/Header.tsx b/src/components/ui/common/Header.tsx index da3cf49..3a2ca15 100644 --- a/src/components/ui/common/Header.tsx +++ b/src/components/ui/common/Header.tsx @@ -114,9 +114,11 @@ export default function Header() {
  • -
  • - -
  • + {session.role !== 'Partner' && ( +
  • + +
  • + )}
    diff --git a/src/libs/axios.ts b/src/libs/axios.ts index a5d355c..15127ec 100644 --- a/src/libs/axios.ts +++ b/src/libs/axios.ts @@ -75,8 +75,19 @@ export const transformObjectKeys = (obj: any): any => { if (obj !== null && typeof obj === 'object') { return Object.keys(obj).reduce((acc: any, key: string) => { - const camelKey = snakeToCamel(key) - acc[camelKey] = transformObjectKeys(obj[key]) + let transformedKey = key + + // Handle uppercase snake_case (e.g., USER_NAME -> userName) + if (/^[A-Z_]+$/.test(key)) { + transformedKey = snakeToCamel(key) + } + // Handle single uppercase word (e.g., ROLE -> role) + else if (/^[A-Z]+$/.test(key)) { + transformedKey = key.toLowerCase() + } + // Preserve existing camelCase + + acc[transformedKey] = transformObjectKeys(obj[key]) return acc }, {}) } From aa4292789a4726dafe3c8d002bcb8c22bb04b372 Mon Sep 17 00:00:00 2001 From: Daseul Kim Date: Wed, 21 May 2025 10:46:57 +0900 Subject: [PATCH 25/36] =?UTF-8?q?fix:=20=EC=B9=B4=EB=A9=9C=EC=BC=80?= =?UTF-8?q?=EC=9D=B4=EC=8A=A4=20=EB=B3=80=ED=99=98=20=ED=95=A8=EC=88=98=20?= =?UTF-8?q?=EB=82=B4=20=EC=86=8C=EB=AC=B8=EC=9E=90=20=EC=8A=A4=EB=84=A4?= =?UTF-8?q?=EC=9D=B4=ED=81=AC=EC=BC=80=EC=9D=B4=EC=8A=A4=EB=8F=84=20?= =?UTF-8?q?=EB=8C=80=EC=9D=91=EB=90=98=EB=8F=84=EB=A1=9D=20=EC=A1=B0?= =?UTF-8?q?=EA=B1=B4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/libs/axios.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/libs/axios.ts b/src/libs/axios.ts index 15127ec..b75e989 100644 --- a/src/libs/axios.ts +++ b/src/libs/axios.ts @@ -78,7 +78,8 @@ export const transformObjectKeys = (obj: any): any => { let transformedKey = key // Handle uppercase snake_case (e.g., USER_NAME -> userName) - if (/^[A-Z_]+$/.test(key)) { + // Handle lowercase snake_case (e.g., user_name -> userName) + if (/^[A-Z_]+$/.test(key) || /^[a-z_]+$/.test(key)) { transformedKey = snakeToCamel(key) } // Handle single uppercase word (e.g., ROLE -> role) From c10d1c3b5c61feb07d483bc2515594dd9d979880 Mon Sep 17 00:00:00 2001 From: yoosangwook Date: Wed, 21 May 2025 11:08:07 +0900 Subject: [PATCH 26/36] refactor: Update SD_SURVEY_SALES_BASIC_INFO model to include SRL_NO and SUBMISSION_TARGET_ID fields for enhanced data tracking --- prisma/schema.prisma | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 50cdf7d..e2d5cc1 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -58,21 +58,23 @@ model MS_SUITABLE { } model SD_SURVEY_SALES_BASIC_INFO { - ID Int @id @default(autoincrement()) - REPRESENTATIVE String @db.VarChar(200) - STORE String? @db.VarChar(200) - CONSTRUCTION_POINT String? @db.VarChar(200) - INVESTIGATION_DATE String? @db.VarChar(10) - BUILDING_NAME String? @db.VarChar(200) - CUSTOMER_NAME String? @db.VarChar(200) - POST_CODE String? @db.VarChar(10) - ADDRESS String? @db.VarChar(200) - ADDRESS_DETAIL String? @db.VarChar(300) - SUBMISSION_STATUS Boolean @default(false) - SUBMISSION_DATE DateTime? @db.Date - REG_DT DateTime @default(now()) - UPT_DT DateTime @updatedAt - DETAIL_INFO SD_SURVEY_SALES_DETAIL_INFO? + ID Int @id @default(autoincrement()) + SRL_NO String @db.VarChar(20) + REPRESENTATIVE String @db.VarChar(200) + STORE String? @db.VarChar(200) + CONSTRUCTION_POINT String? @db.VarChar(200) + INVESTIGATION_DATE String? @db.VarChar(10) + BUILDING_NAME String? @db.VarChar(200) + CUSTOMER_NAME String? @db.VarChar(200) + POST_CODE String? @db.VarChar(10) + ADDRESS String? @db.VarChar(200) + ADDRESS_DETAIL String? @db.VarChar(300) + SUBMISSION_STATUS Boolean @default(false) + SUBMISSION_DATE DateTime? @db.Date + SUBMISSION_TARGET_ID String? @db.VarChar(200) + REG_DT DateTime @default(now()) + UPT_DT DateTime @updatedAt + DETAIL_INFO SD_SURVEY_SALES_DETAIL_INFO? } model SD_SURVEY_SALES_DETAIL_INFO { From e66b009dd3f377ab9fb92ef7bda417aa337c2840 Mon Sep 17 00:00:00 2001 From: yoosangwook Date: Wed, 21 May 2025 11:16:47 +0900 Subject: [PATCH 27/36] fix: Update session data display in Header component to use store name instead of category; add additional role mapping in README --- README.md | 3 ++- src/components/ui/common/Header.tsx | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 72a29f7..6d332e0 100644 --- a/README.md +++ b/README.md @@ -54,12 +54,13 @@ session에 있는 role 키로 구분한다 session.role === 'Admin_Sub' - constA03_01 / 1234 -> 시공사\ session.role === 'Builder' +- teshg44 / 1234 -> 시공사\ + session.role === 'Builder' - partners -> Q.Partners 계정\ session.role === 'Partner' - 이외의 경우 -> 굳이 체크할 필요 없어보임\ session.role === 'User' - # 지붕재 적합성 TODO ``` diff --git a/src/components/ui/common/Header.tsx b/src/components/ui/common/Header.tsx index 3a2ca15..bd92675 100644 --- a/src/components/ui/common/Header.tsx +++ b/src/components/ui/common/Header.tsx @@ -71,7 +71,7 @@ export default function Header() {
    {session.userNm}
    -
    {session.category}
    +
    {session.storeNm}
    From 4e8f698f88e25c7e844aa4784b28da4cfe39d223 Mon Sep 17 00:00:00 2001 From: yoosangwook Date: Wed, 21 May 2025 11:17:14 +0900 Subject: [PATCH 28/36] feat: Integrate tracking functionality in authentication API routes for enhanced user activity monitoring; update tracking data structure in tracking route --- src/app/api/auth/logout/route.ts | 7 +++++++ src/app/api/auth/route.ts | 8 ++++++++ src/app/api/tracking/route.ts | 24 ++++++++++++++++-------- src/libs/axios.ts | 3 +-- src/providers/EdgeProvider.tsx | 2 +- 5 files changed, 33 insertions(+), 11 deletions(-) diff --git a/src/app/api/auth/logout/route.ts b/src/app/api/auth/logout/route.ts index 78f0a58..ad4b365 100644 --- a/src/app/api/auth/logout/route.ts +++ b/src/app/api/auth/logout/route.ts @@ -3,11 +3,18 @@ import { cookies } from 'next/headers' import { NextResponse } from 'next/server' import { getIronSession } from 'iron-session' import { sessionOptions } from '@/libs/session' +import { tracking } from '@/libs/tracking' export async function GET(request: Request) { const cookieStore = await cookies() const session = await getIronSession(cookieStore, sessionOptions) + tracking({ + url: '/api/auth/logout', + data: JSON.stringify({ + userId: session.userId, + }), + }) session.destroy() // return redirect('/login') diff --git a/src/app/api/auth/route.ts b/src/app/api/auth/route.ts index c595ecd..d5149a9 100644 --- a/src/app/api/auth/route.ts +++ b/src/app/api/auth/route.ts @@ -4,6 +4,7 @@ import { NextResponse } from 'next/server' import { getIronSession } from 'iron-session' import { axiosInstance } from '@/libs/axios' import { sessionOptions } from '@/libs/session' +import { tracking } from '@/libs/tracking' export async function POST(request: Request) { const { loginId, pwd } = await request.json() @@ -15,6 +16,13 @@ export async function POST(request: Request) { console.log('🚀 ~ result ~ result:', result.data) if (result.data.result.code === 200) { + tracking({ + url: `/api/auth/login`, + data: JSON.stringify({ + loginId, + pwd, + }), + }) const cookieStore = await cookies() const session = await getIronSession(cookieStore, sessionOptions) diff --git a/src/app/api/tracking/route.ts b/src/app/api/tracking/route.ts index d5f5607..913b4c5 100644 --- a/src/app/api/tracking/route.ts +++ b/src/app/api/tracking/route.ts @@ -1,17 +1,25 @@ import type { SessionData } from '@/types/Auth' import { NextResponse } from 'next/server' import { cookies } from 'next/headers' -import { Prisma } from '@prisma/client' +import { prisma } from '@/libs/prisma' import { getIronSession } from 'iron-session' import { sessionOptions } from '@/libs/session' -export const POST = async (request: Request) => { +export async function POST(request: Request) { const { url, data } = await request.json() const cookieStore = await cookies() const session = await getIronSession(cookieStore, sessionOptions) - let owner = session.userId + let owner = '' + if (url === '/api/auth/login') { + owner = 'Login' + } else if (url === '/api/auth/logout') { + owner = 'Logout' + } else { + owner = session.userId ?? 'Direct' + } + let type = '' if (url.includes('api')) { type = 'api' @@ -20,12 +28,12 @@ export const POST = async (request: Request) => { } // @ts-ignore - const result = await Prisma.MS_USR_TRK.create({ + const result = await prisma.MS_USR_TRK.create({ data: { - owner, - type, - url, - data: JSON.stringify(data), + OWNER: owner, + TYPE: type, + URL: url, + DATA: JSON.stringify(data), }, }) diff --git a/src/libs/axios.ts b/src/libs/axios.ts index b75e989..0abc6ab 100644 --- a/src/libs/axios.ts +++ b/src/libs/axios.ts @@ -1,5 +1,4 @@ import axios from 'axios' -import { tracking } from './tracking' export const axiosInstance = (url: string | null | undefined) => { const baseURL = url || process.env.NEXT_PUBLIC_API_URL @@ -12,7 +11,7 @@ export const axiosInstance = (url: string | null | undefined) => { instance.interceptors.request.use( (config) => { - console.log('🚀 ~ config:', config) + // console.log('🚀 ~ config:', config) return config }, (error) => { diff --git a/src/providers/EdgeProvider.tsx b/src/providers/EdgeProvider.tsx index de3c00f..7bba179 100644 --- a/src/providers/EdgeProvider.tsx +++ b/src/providers/EdgeProvider.tsx @@ -108,7 +108,7 @@ export default function EdgeProvider({ children, sessionData }: EdgeProviderProp //사이드바 초기화 reset() // 페이지 이벤트 트래킹 - // handlePageEvent(pathname) + handlePageEvent(pathname) }, [pathname]) return <>{children} From 45f523447a103e38aa21fa4598436ae852c3bc4d Mon Sep 17 00:00:00 2001 From: yoosangwook Date: Wed, 21 May 2025 11:31:19 +0900 Subject: [PATCH 29/36] feat: Add email validation in Login component and implement database connection utility; enhance login logic with partner status check --- src/components/Login.tsx | 15 ++++++++++++++- src/libs/{partner.tsx => partner.ts} | 0 2 files changed, 14 insertions(+), 1 deletion(-) rename src/libs/{partner.tsx => partner.ts} (100%) diff --git a/src/components/Login.tsx b/src/components/Login.tsx index 24b7219..6322ec4 100644 --- a/src/components/Login.tsx +++ b/src/components/Login.tsx @@ -33,6 +33,11 @@ export default function Login() { pwd: '', }) + const isValidEmail = (email: string) => { + const emailRegex = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/ + return emailRegex.test(email) + } + interface LoginData { code: number message: string | null @@ -82,6 +87,14 @@ export default function Login() { } }, [loginData]) + useEffect(() => { + if (isValidEmail(account.loginId)) { + setIsPartners(true) + } else { + setIsPartners(false) + } + }, [account.loginId]) + return ( <>
    @@ -94,7 +107,7 @@ export default function Login() { value={account.loginId} onChange={(e) => setAccount({ loginId: e.target.value })} /> -
    diff --git a/src/libs/partner.tsx b/src/libs/partner.ts similarity index 100% rename from src/libs/partner.tsx rename to src/libs/partner.ts From dd34c51dc167cf945c5056f30e230600a01dbbf5 Mon Sep 17 00:00:00 2001 From: yoosangwook Date: Wed, 21 May 2025 11:33:18 +0900 Subject: [PATCH 30/36] refactor: Remove console log from tracking function to clean up output and improve code clarity --- src/libs/tracking.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libs/tracking.ts b/src/libs/tracking.ts index 1fc2d06..2f8833f 100644 --- a/src/libs/tracking.ts +++ b/src/libs/tracking.ts @@ -6,5 +6,4 @@ export const tracking = async (params: { url: string; data: string }) => { url, data, }) - console.log('🚀 ~ result ~ result:', result) } From 0d96d4928109b4ff90550dd7be005995a4cd57a8 Mon Sep 17 00:00:00 2001 From: yoosangwook Date: Wed, 21 May 2025 13:26:59 +0900 Subject: [PATCH 31/36] refactor: Update BC_COMM_H model fields to be optional for improved flexibility in data handling --- prisma/schema.prisma | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index e2d5cc1..00e4bc6 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -122,28 +122,28 @@ model SD_SURVEY_SALES_DETAIL_INFO { model BC_COMM_H { HEAD_CD String @id(map: "PK_BC_COMM_H") @db.NVarChar(6) - HEAD_ID String @db.NVarChar(100) - HEAD_NM String @db.NVarChar(100) - HEAD_JP String @db.NVarChar(100) - HEAD_4TH String @db.NVarChar(100) - REF_CHR1 String @db.NVarChar(100) - REF_CHR2 String @db.NVarChar(100) - REF_CHR3 String @db.NVarChar(100) - REF_CHR4 String @db.NVarChar(100) - REF_CHR5 String @db.NVarChar(100) - REF_NUM1 String @db.NVarChar(100) - REF_NUM2 String @db.NVarChar(100) - REF_NUM3 String @db.NVarChar(100) - REF_NUM4 String @db.NVarChar(100) - REF_NUM5 String @db.NVarChar(100) - REMARKS String @db.NVarChar(200) - SAP_YN String @db.NVarChar(1) - STAT_CD String @db.NVarChar(1) - DEL_YN String @db.NVarChar(1) + HEAD_ID String? @db.NVarChar(100) + HEAD_NM String? @db.NVarChar(100) + HEAD_JP String? @db.NVarChar(100) + HEAD_4TH String? @db.NVarChar(100) + REF_CHR1 String? @db.NVarChar(100) + REF_CHR2 String? @db.NVarChar(100) + REF_CHR3 String? @db.NVarChar(100) + REF_CHR4 String? @db.NVarChar(100) + REF_CHR5 String? @db.NVarChar(100) + REF_NUM1 String? @db.NVarChar(100) + REF_NUM2 String? @db.NVarChar(100) + REF_NUM3 String? @db.NVarChar(100) + REF_NUM4 String? @db.NVarChar(100) + REF_NUM5 String? @db.NVarChar(100) + REMARKS String? @db.NVarChar(200) + SAP_YN String? @db.NVarChar(1) + STAT_CD String? @db.NVarChar(1) + DEL_YN String? @db.NVarChar(1) REG_DT DateTime? @db.DateTime - REG_ID String @db.NVarChar(50) + REG_ID String? @db.NVarChar(50) UPT_DT DateTime? @db.DateTime - UPT_ID String @db.NVarChar(50) + UPT_ID String? @db.NVarChar(50) QC_COMM_YN String? @default("N", map: "DF__BC_COMM_H__QC_CO__48CFD27E") @db.NVarChar(1) BC_COMM_L BC_COMM_L[] From ac59d626ab919cdbf62985442aa59f57bd75fa06 Mon Sep 17 00:00:00 2001 From: yoosangwook Date: Wed, 21 May 2025 14:10:57 +0900 Subject: [PATCH 32/36] refactor: Remove User model from Prisma schema to streamline database structure and improve maintainability --- prisma/schema.prisma | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 00e4bc6..46bd335 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -7,18 +7,6 @@ datasource db { url = env("DATABASE_URL") } -model User { - id Int @id @default(autoincrement()) - username String @unique - phone String? - email String? - password String? - kakao_id String? - avatar String? - created_at DateTime @default(now()) - updated_at DateTime @updatedAt -} - model MS_SUITABLE { id Int @id @default(autoincrement()) product_name String @db.VarChar(200) From a964e31dee27b23ccacaa015122384786d77241e Mon Sep 17 00:00:00 2001 From: yoosangwook Date: Wed, 21 May 2025 14:12:16 +0900 Subject: [PATCH 33/36] fix: Enable login redirection in middleware to ensure authenticated access to protected routes --- src/middleware.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/middleware.ts b/src/middleware.ts index 1bcfd96..115fd8a 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -10,9 +10,9 @@ export async function middleware(request: NextRequest) { const session = await getIronSession(cookieStore, sessionOptions) // todo: 로그인 기능 추가 시 주석 해제 - // if (!session.isLoggedIn) { - // return NextResponse.redirect(new URL('/login', request.url)) - // } + if (!session.isLoggedIn) { + return NextResponse.redirect(new URL('/login', request.url)) + } return NextResponse.next() } From 0cd95c6affa166c455dd069ee4abac0715560ca3 Mon Sep 17 00:00:00 2001 From: yoosangwook Date: Wed, 21 May 2025 14:14:37 +0900 Subject: [PATCH 34/36] refactor: Make UPT_DT field optional in MS_SUITABLE_ROOF_MATERIAL_GROUP model for improved data flexibility --- prisma/schema.prisma | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 46bd335..17cc1f4 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -169,11 +169,11 @@ model BC_COMM_L { } model MS_SUITABLE_ROOF_MATERIAL_GROUP { - ID Int @id @default(autoincrement()) - ROOF_MATERIAL_GROUP String @db.VarChar(200) - ROOF_MT_CD String @db.VarChar(200) - REG_DT DateTime @default(now(), map: "DF__MS_SUITAB__creat__4F7CD00D") - UPT_DT DateTime + ID Int @id @default(autoincrement()) + ROOF_MATERIAL_GROUP String @db.VarChar(200) + ROOF_MT_CD String @db.VarChar(200) + REG_DT DateTime @default(now(), map: "DF__MS_SUITAB__creat__4F7CD00D") + UPT_DT DateTime? } model MS_SUITABLE_DETAIL { From 13eb23863b991f3961020c30afcce213a9fd7436 Mon Sep 17 00:00:00 2001 From: yoosangwook Date: Wed, 21 May 2025 15:39:10 +0900 Subject: [PATCH 35/36] style: Add minWidth to DownloadPDF component for consistent layout --- src/components/DownloadPDF.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/DownloadPDF.tsx b/src/components/DownloadPDF.tsx index 5df8d87..570b926 100644 --- a/src/components/DownloadPDF.tsx +++ b/src/components/DownloadPDF.tsx @@ -35,7 +35,7 @@ export default function DownloadPdf() { <>
    -
    +
    HWJ 現地調査シート1/2 From c4ed298db51ca0a8a80a0ede43995924a3b53b9f Mon Sep 17 00:00:00 2001 From: yoosangwook Date: Wed, 21 May 2025 15:49:50 +0900 Subject: [PATCH 36/36] refactor: Remove user-related API routes to streamline codebase and improve maintainability --- src/app/api/user/create/route.ts | 23 ------------------- src/app/api/user/list/route.ts | 7 ------ src/app/api/user/route.ts | 38 -------------------------------- 3 files changed, 68 deletions(-) delete mode 100644 src/app/api/user/create/route.ts delete mode 100644 src/app/api/user/list/route.ts delete mode 100644 src/app/api/user/route.ts diff --git a/src/app/api/user/create/route.ts b/src/app/api/user/create/route.ts deleted file mode 100644 index c4e4060..0000000 --- a/src/app/api/user/create/route.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { NextResponse } from 'next/server' -import { prisma } from '@/libs/prisma' - -export async function POST(request: Request) { - try { - const body = await request.json() - const { username, email, password } = body - - const user = await prisma.user.create({ - data: { - username, - email, - password, - updated_at: new Date(), - }, - }) - - return NextResponse.json(user) - } catch (error) { - console.error('Error creating user:', error) - return NextResponse.json({ error: 'Error creating user' }, { status: 500 }) - } -} diff --git a/src/app/api/user/list/route.ts b/src/app/api/user/list/route.ts deleted file mode 100644 index f84af78..0000000 --- a/src/app/api/user/list/route.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { NextResponse } from 'next/server' -import { prisma } from '@/libs/prisma' - -export const GET = async () => { - const users = await prisma.user.findMany() - return NextResponse.json(users) -} diff --git a/src/app/api/user/route.ts b/src/app/api/user/route.ts deleted file mode 100644 index 0249fd6..0000000 --- a/src/app/api/user/route.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { NextResponse } from 'next/server' -import { prisma } from '@/libs/prisma' -import { getIronSession } from 'iron-session' -import { cookies } from 'next/headers' -import { sessionOptions } from '@/libs/session' -import type { SessionData } from '@/types/Auth' - -export async function POST(request: Request) { - const { username, password } = await request.json() - - console.log('🚀 ~ POST ~ username:', username) - console.log('🚀 ~ POST ~ password:', password) - - const user = await prisma.user.findFirst({ - where: { - username: username, - password: password, - }, - }) - console.log('🚀 ~ POST ~ user:', user) - - if (!user) { - return NextResponse.json({ error: 'User not found' }, { status: 404 }) - } - - const cookieStore = await cookies() - const session = await getIronSession(cookieStore, sessionOptions) - console.log('start session edit!') - // session.username = user.username! - // session.email = user.email! - session.isLoggedIn = true - console.log('end session edit!') - await session.save() - console.log('🚀 ~ POST ~ session:', session) - - // return NextResponse.redirect(new URL(process.env.NEXT_PUBLIC_URL!, request.url)) - return NextResponse.json(user) -}