From d718012f6f520ccccde792f38bd54706f9925a19 Mon Sep 17 00:00:00 2001 From: Daseul Kim Date: Fri, 23 May 2025 13:31:42 +0900 Subject: [PATCH 01/15] =?UTF-8?q?feat:=20=EC=A7=80=EB=B6=95=EC=9E=AC?= =?UTF-8?q?=EC=A0=81=ED=95=A9=EC=84=B1=20=EC=83=81=EC=84=B8=ED=8C=9D?= =?UTF-8?q?=EC=97=85=20=EB=A7=88=ED=81=AC=EC=97=85=20=EC=A0=81=EC=9A=A9=20?= =?UTF-8?q?=EB=B0=8F=20=ED=8C=9D=EC=97=85=20on/off=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/popup/SuitableDetailPopup.tsx | 215 ++++++++++++++++++- src/components/suitable/SuitableButton.tsx | 6 +- 2 files changed, 212 insertions(+), 9 deletions(-) diff --git a/src/components/popup/SuitableDetailPopup.tsx b/src/components/popup/SuitableDetailPopup.tsx index 59b7838..2d4095d 100644 --- a/src/components/popup/SuitableDetailPopup.tsx +++ b/src/components/popup/SuitableDetailPopup.tsx @@ -1,4 +1,11 @@ +'use client' + +import Image from 'next/image' +import { usePopupController } from '@/store/popupController' + export default function SuitableDetailPopup() { + const popupController = usePopupController() + return (
@@ -7,22 +14,214 @@ export default function SuitableDetailPopup() {
- +
屋根材適合性詳細
- +
-
-
-
アースティ40
-
- +
+
+
+
アースティ40
+
+ +
+
+
+
+
屋根技研 支持瓦
+
㈱ダイトー
+
+
+
屋根材
+
+
+
+
金具タイプ
+
木ねじ打ち込み式
+
+
+
+
+
屋根技研 支持瓦
+
+
+ +
+
+ +
+
+
+
Dで設置可
+
+
備考
+
+ 桟木なしの場合は支持金具平ー1で設置可能。その場合水返しが高い為、レベルプレート使用。桟木ありの場合は支持金具平ー2で設置可能 +
+
+
+
+
+
屋根技研支持金具
+
+
+ +
+
+ +
+
+
+
設置不可
+
+
備考
+
入手困難
+
+
+
+
+
屋根技研YGアンカー
+
+
+ +
+
+
+
お問い合わせください
+
+
備考
+
入手困難
+
+
+
+
+
ダイドーハント支持瓦Ⅱ
+
+
+ +
+
+
+
Ⅳ (D) で設置可
+
+
備考
+
入手困難
+
+
+
-
+
+
+
アースティ40
+
+ +
+
+
+
+
屋根技研 支持瓦
+
㈱ダイトー
+
+
+
屋根材
+
+
+
+
金具タイプ
+
木ねじ打ち込み式
+
+
+
+
+
屋根技研 支持瓦
+
+
+ +
+
+ +
+
+
+
Dで設置可
+
+
備考
+
入手困難
+
+
+
+
+
屋根技研支持金具
+
+
+ +
+
+ +
+
+
+
設置不可
+
+
備考
+
入手困難
+
+
+
+
+
屋根技研YGアンカー
+
+
+ +
+
+
+
お問い合わせください
+
+
備考
+
入手困難
+
+
+
+
+
ダイドーハント支持瓦Ⅱ
+
+
+ +
+
+
+
Ⅳ (D) で設置可
+
+
備考
+
入手困難
+
+
+
+
+
+
+
+
+ +
+
+ +
+
+ +
diff --git a/src/components/suitable/SuitableButton.tsx b/src/components/suitable/SuitableButton.tsx index f412c89..1aa6be5 100644 --- a/src/components/suitable/SuitableButton.tsx +++ b/src/components/suitable/SuitableButton.tsx @@ -1,6 +1,10 @@ 'use client' +import { usePopupController } from '@/store/popupController' + export default function SuitableButton() { + const popupController = usePopupController() + return (
@@ -10,7 +14,7 @@ export default function SuitableButton() {
-
From 36bcdd00a1c14c72cd151e33c84f87f5970f6d97 Mon Sep 17 00:00:00 2001 From: Daseul Kim Date: Fri, 23 May 2025 17:14:07 +0900 Subject: [PATCH 02/15] =?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=A0=84=EC=B2=B4=EC=84=A0?= =?UTF-8?q?=ED=83=9D/=EC=A0=84=EC=B2=B4=ED=95=B4=EC=A0=9C=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EB=B0=8F=20=EA=B4=80=EB=A0=A8=20=EA=B8=B0=EB=8A=A5?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 지붕재 적합성 전체선택/전체해제 기능 추가 - 검색조건에 따른 데이터 조회 api 추가 - 선택된 데이터 조회 api 추가 --- src/app/api/suitable/pick/route.ts | 47 ++++++ src/app/api/suitable/route.ts | 67 ++++++++- src/components/popup/SuitableDetailPopup.tsx | 141 ++++-------------- .../popup/SuitableDetailPopupButton.tsx | 23 +++ src/components/suitable/SuitableButton.tsx | 30 +++- src/hooks/useSuitable.ts | 48 +++++- src/store/useSuitableStore.ts | 16 +- src/types/Suitable.ts | 5 + 8 files changed, 250 insertions(+), 127 deletions(-) create mode 100644 src/app/api/suitable/pick/route.ts create mode 100644 src/components/popup/SuitableDetailPopupButton.tsx diff --git a/src/app/api/suitable/pick/route.ts b/src/app/api/suitable/pick/route.ts new file mode 100644 index 0000000..54db249 --- /dev/null +++ b/src/app/api/suitable/pick/route.ts @@ -0,0 +1,47 @@ +import { NextRequest, NextResponse } from 'next/server' +import { prisma } from '@/libs/prisma' +import { type Suitable } from '@/types/Suitable' + +export async function GET(request: NextRequest) { + try { + const searchParams = request.nextUrl.searchParams + const category = searchParams.get('category') + const keyword = searchParams.get('keyword') + + let query = ` + SELECT + msm.id + , details.detail_id + FROM ms_suitable_main msm + LEFT JOIN ( + SELECT + msd.main_id + , STRING_AGG(msd.id, ',') AS detail_id + FROM ms_suitable_detail msd + GROUP BY msd.main_id + ) AS details + ON msm.id = details.main_id + WHERE 1=1 + --roofMtCd AND msm.roof_mt_cd = ':roofMtCd' + --productName AND msm.product_name LIKE '%:productName%' + ; + ` + + // 검색 조건 설정 + if (category) { + query = query.replace('--roofMtCd ', '') + query = query.replace(':roofMtCd', category) + } + if (keyword) { + query = query.replace('--productName ', '') + query = query.replace(':productName', keyword) + } + + const suitableIdSet = await prisma.$queryRawUnsafe(query) + + return NextResponse.json(suitableIdSet) + } catch (error) { + console.error('❌ 데이터 조회 중 오류가 발생했습니다:', error) + return NextResponse.json({ error: '데이터 조회 중 오류가 발생했습니다' }, { status: 500 }) + } +} diff --git a/src/app/api/suitable/route.ts b/src/app/api/suitable/route.ts index 7f7be9c..df42e1e 100644 --- a/src/app/api/suitable/route.ts +++ b/src/app/api/suitable/route.ts @@ -1,12 +1,63 @@ -import { NextResponse } from 'next/server' +import { NextRequest, NextResponse } from 'next/server' import { prisma } from '@/libs/prisma' +import { Suitable } from '@/types/Suitable' -export async function POST(request: Request) { - const body = await request.json() - // @ts-ignore - const suitables = await prisma.MS_SUITABLE.createMany({ - data: body, - }) +export async function GET(request: NextRequest) { + try { + const searchParams = request.nextUrl.searchParams - return NextResponse.json({ message: 'Suitable created successfully' }) + const ids = searchParams.get('ids') + const detailIds = searchParams.get('subIds') + + let query = ` + SELECT + msm.id + , msm.product_name + , msm.manu_ft_cd + , msm.roof_mt_cd + , msm.roof_sh_cd + , details.detail + FROM ms_suitable_main msm + LEFT JOIN ( + SELECT + msd.main_id + , ( + SELECT + msd_json.id + , msd_json.trestle_mfpc_cd + , msd_json.trestle_manufacturer_product_name + , msd_json.memo + FROM ms_suitable_detail msd_json + WHERE msd.main_id = msd_json.main_id + FOR JSON PATH + ) AS detail + FROM ms_suitable_detail msd + GROUP BY msd.main_id + ) AS details + ON msm.id = details.main_id + --ids AND details.main_id IN (:mainIds) + --detailIds AND details.id IN (:detailIds) + WHERE 1=1 + --ids AND msm.id IN (:mainIds) + ORDER BY msm.product_name; + ` + + // 검색 조건 설정 + if (ids) { + query = query.replaceAll('--ids ', '') + query = query.replaceAll(':mainIds', ids) + if (detailIds) { + query = query.replaceAll('--detailIds ', '') + query = query.replaceAll(':detailIds', detailIds) + } + } + + const suitable: Suitable[] = await prisma.$queryRawUnsafe(query) + + return NextResponse.json(suitable) + } catch (error) { + console.error('❌ 데이터 조회 중 오류가 발생했습니다:', error) + return NextResponse.json({ error: '데이터 조회 중 오류가 발생했습니다' }, { status: 500 }) + } } + diff --git a/src/components/popup/SuitableDetailPopup.tsx b/src/components/popup/SuitableDetailPopup.tsx index 2d4095d..80524d0 100644 --- a/src/components/popup/SuitableDetailPopup.tsx +++ b/src/components/popup/SuitableDetailPopup.tsx @@ -1,10 +1,39 @@ 'use client' import Image from 'next/image' +import { useCallback, useEffect, useState } from 'react' import { usePopupController } from '@/store/popupController' +import { useSuitableStore } from '@/store/useSuitableStore' +import SuitableDetailPopupButton from './SuitableDetailPopupButton' +import { useSuitable } from '@/hooks/useSuitable' +import { Suitable } from '@/types/Suitable' export default function SuitableDetailPopup() { const popupController = usePopupController() + const { getSuitableDetails, serializeSelectedItems } = useSuitable() + const { selectedItems } = useSuitableStore() + + const [openItems, setOpenItems] = useState>(new Set()) + const [suitableDetails, setSuitableDetails] = useState([]) + + // 아이템 열기/닫기 + const toggleItemOpen = useCallback((itemId: number) => { + setOpenItems((prev) => { + const newOpenItems = new Set(prev) + newOpenItems.has(itemId) ? newOpenItems.delete(itemId) : newOpenItems.add(itemId) + return newOpenItems + }) + }, []) + + // 선택된 아이템 상세 데이터 가져오기 + const getSelectedItemsData = async () => { + const serialized: Map = serializeSelectedItems() + setSuitableDetails(await getSuitableDetails(serialized.get('ids') ?? '', serialized.get('detailIds') ?? '')) + } + + useEffect(() => { + getSelectedItemsData() + }, []) return (
@@ -23,11 +52,11 @@ export default function SuitableDetailPopup() {
-
+
アースティ40
- +
@@ -115,114 +144,8 @@ export default function SuitableDetailPopup() {
-
-
-
アースティ40
-
- -
-
-
-
-
屋根技研 支持瓦
-
㈱ダイトー
-
-
-
屋根材
-
-
-
-
金具タイプ
-
木ねじ打ち込み式
-
-
-
-
-
屋根技研 支持瓦
-
-
- -
-
- -
-
-
-
Dで設置可
-
-
備考
-
入手困難
-
-
-
-
-
屋根技研支持金具
-
-
- -
-
- -
-
-
-
設置不可
-
-
備考
-
入手困難
-
-
-
-
-
屋根技研YGアンカー
-
-
- -
-
-
-
お問い合わせください
-
-
備考
-
入手困難
-
-
-
-
-
ダイドーハント支持瓦Ⅱ
-
-
- -
-
-
-
Ⅳ (D) で設置可
-
-
備考
-
入手困難
-
-
-
-
-
-
-
-
- -
-
- -
-
- -
+
diff --git a/src/components/popup/SuitableDetailPopupButton.tsx b/src/components/popup/SuitableDetailPopupButton.tsx new file mode 100644 index 0000000..52e1bae --- /dev/null +++ b/src/components/popup/SuitableDetailPopupButton.tsx @@ -0,0 +1,23 @@ +'use client' + +export default function SuitableDetailPopupButton() { + return ( +
+
+ +
+
+ +
+
+ +
+
+ ) +} diff --git a/src/components/suitable/SuitableButton.tsx b/src/components/suitable/SuitableButton.tsx index 1aa6be5..20191d2 100644 --- a/src/components/suitable/SuitableButton.tsx +++ b/src/components/suitable/SuitableButton.tsx @@ -1,20 +1,42 @@ 'use client' import { usePopupController } from '@/store/popupController' +import { useSuitable } from '@/hooks/useSuitable' +import { useSuitableStore } from '@/store/useSuitableStore' export default function SuitableButton() { const popupController = usePopupController() + const { getSuitableIds } = useSuitable() + const { selectedItems, addAllSelectedItem, clearSelectedItems } = useSuitableStore() + + const handleSelectAll = async () => { + addAllSelectedItem(await getSuitableIds()) + } + + const handleOpenPopup = () => { + if (selectedItems.size === 0) { + alert('屋根材を選択してください。') + return + } + popupController.setSuitableDetailPopup(true) + } return (
- + {selectedItems.size === 0 ? ( + + ) : ( + + )}
-
diff --git a/src/hooks/useSuitable.ts b/src/hooks/useSuitable.ts index f651b4d..94e61a0 100644 --- a/src/hooks/useSuitable.ts +++ b/src/hooks/useSuitable.ts @@ -3,21 +3,19 @@ import { transformObjectKeys } from '@/libs/axios' import { useSuitableStore } from '@/store/useSuitableStore' import { useAxios } from './useAxios' import { useCommCode } from './useCommCode' -import { SUITABLE_HEAD_CODE, type Suitable, type SuitableDetail } from '@/types/Suitable' +import { SUITABLE_HEAD_CODE, type Suitable, type SuitableDetail, type SuitableIds } from '@/types/Suitable' export function useSuitable() { const { axiosInstance } = useAxios() const { getCommCode } = useCommCode() - const { itemPerPage, selectedCategory, searchValue, suitableCommCode, setSuitableCommCode, isSearch } = useSuitableStore() + const { itemPerPage, selectedCategory, searchValue, suitableCommCode, setSuitableCommCode, isSearch, selectedItems } = useSuitableStore() const getSuitables = async ({ pageNumber, - ids, category, keyword, }: { pageNumber?: number - ids?: string category?: string keyword?: string }): Promise => { @@ -26,7 +24,6 @@ export function useSuitable() { pageNumber: pageNumber || 1, itemPerPage: itemPerPage, } - if (ids) params.ids = ids if (category) params.category = category if (keyword) params.keyword = keyword @@ -38,6 +35,31 @@ export function useSuitable() { } } + const getSuitableIds = async (): Promise => { + try { + const params: Record = {} + if (selectedCategory) params.category = selectedCategory + if (searchValue) params.keyword = searchValue + const response = await axiosInstance(null).get('/api/suitable/pick', { params }) + return response.data + } catch (error) { + console.error('지붕재 아이디 로드 실패:', error) + return [] + } + } + + const getSuitableDetails = async (ids: string, detailIds?: string): Promise => { + try { + const params: Record = { ids: ids } + if (detailIds) params.detailIds = detailIds + const response = await axiosInstance(null).get('/api/suitable', { params }) + return response.data + } catch (error) { + console.error('지붕재 상세 데이터 로드 실패:', error) + return [] + } + } + const getSuitableCommCode = () => { const headCodes = Object.values(SUITABLE_HEAD_CODE) as SUITABLE_HEAD_CODE[] for (const code of headCodes) { @@ -100,8 +122,23 @@ export function useSuitable() { gcTime: 1000 * 60 * 10, }) + const serializeSelectedItems = (): Map => { + const ids: string[] = [] + const detailIds: string[] = [] + for (const [key, value] of selectedItems) { + ids.push(String(key)) + for (const id of value) detailIds.push(String(id)) + } + return new Map([ + ['ids', ids.join(',')], + ['detailIds', detailIds.join(',')], + ]) + } + return { getSuitables, + getSuitableIds, + getSuitableDetails, getSuitableCommCode, toCodeName, toSuitableDetail, @@ -111,5 +148,6 @@ export function useSuitable() { hasNextPage, isFetchingNextPage, isLoading, + serializeSelectedItems, } } diff --git a/src/store/useSuitableStore.ts b/src/store/useSuitableStore.ts index 74f4d0b..77ddfaa 100644 --- a/src/store/useSuitableStore.ts +++ b/src/store/useSuitableStore.ts @@ -1,5 +1,6 @@ import { create } from 'zustand' import type { CommCode } from '@/types/CommCode' +import type { SuitableIds } from '@/types/Suitable' interface SuitableState { /* 초기 데이터 로드 개수*/ @@ -27,8 +28,10 @@ interface SuitableState { /* 선택된 아이템 리스트 */ selectedItems: Map> - /* 선택된 아이템 추가 */ + /* 선택 아이템 추가 */ addSelectedItem: (mainId: number, detailId?: number, detailIds?: Set) => void + /* 아이템 전체 추가 */ + addAllSelectedItem: (suitableIds: SuitableIds[]) => void /* 선택된 아이템 제거 */ removeSelectedItem: (mainId: number, detailId?: number) => void /* 선택된 아이템 모두 제거 */ @@ -77,6 +80,17 @@ export const useSuitableStore = create((set) => ({ } }, + /* 아이템 전체 추가 */ + addAllSelectedItem: (suitableIds: SuitableIds[]) => { + set(() => { + const newSelectedItems = new Map() + suitableIds.forEach((suitableId) => { + newSelectedItems.set(suitableId.id, new Set(suitableId.detailId.split(',').map(Number))) + }) + return { selectedItems: newSelectedItems } + }) + }, + /* 선택된 아이템 제거 */ removeSelectedItem: (mainId: number, detailId?: number) => { set((state) => { diff --git a/src/types/Suitable.ts b/src/types/Suitable.ts index 270dd46..b8fbf72 100644 --- a/src/types/Suitable.ts +++ b/src/types/Suitable.ts @@ -34,3 +34,8 @@ export type Suitable = { detailCnt: number detail: string } + +export type SuitableIds = { + id: number + detailId: string +} From 0fbb8025f203fb645a4802bb56e51e4c411b3355 Mon Sep 17 00:00:00 2001 From: keyy1315 Date: Mon, 26 May 2025 10:58:25 +0900 Subject: [PATCH 03/15] fix: change SRL_NO save rules --- src/app/api/survey-sales/[id]/route.ts | 12 ++++++---- src/app/api/survey-sales/route.ts | 18 +++++++++++---- .../survey-sale/detail/ButtonForm.tsx | 6 ++--- .../survey-sale/detail/DetailForm.tsx | 4 ++-- src/hooks/useSurvey.ts | 23 +++++++------------ 5 files changed, 34 insertions(+), 29 deletions(-) diff --git a/src/app/api/survey-sales/[id]/route.ts b/src/app/api/survey-sales/[id]/route.ts index 07ecd6a..7c03530 100644 --- a/src/app/api/survey-sales/[id]/route.ts +++ b/src/app/api/survey-sales/[id]/route.ts @@ -19,14 +19,16 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ } } -const getNewSrlNo = async (srlNo: string, storeId: string) => { +const getNewSrlNo = async (srlNo: string, storeId: string, role: string) => { + const srlRole = role === 'T01' || role === 'Admin' ? 'HO' : role === 'Admin_Sub' || role === 'Builder' ? 'HM' : '' + let newSrlNo = srlNo if (srlNo.startsWith('一時保存')) { //@ts-ignore const lastSurvey = await prisma.SD_SURVEY_SALES_BASIC_INFO.findFirst({ where: { SRL_NO: { - startsWith: storeId, + startsWith: srlRole + storeId, }, }, orderBy: { @@ -34,7 +36,9 @@ const getNewSrlNo = async (srlNo: string, storeId: string) => { }, }) const lastNo = lastSurvey ? parseInt(lastSurvey.SRL_NO.slice(-3)) : 0 + newSrlNo = + srlRole + storeId + new Date().getFullYear().toString().slice(-2) + (new Date().getMonth() + 1).toString().padStart(2, '0') + @@ -51,7 +55,7 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{ const { detailInfo, ...basicInfo } = body.survey // PUT 요청 시 임시저장 여부 확인 후 임시저장 시 기존 SRL_NO 사용, 기본 저장 시 새로운 SRL_NO 생성 - const newSrlNo = body.isTemporary ? body.survey.srlNo : await getNewSrlNo(body.survey.srlNo, body.storeId) + const newSrlNo = body.isTemporary ? body.survey.srlNo : await getNewSrlNo(body.survey.srlNo, body.storeId, body.role) // @ts-ignore const survey = await prisma.SD_SURVEY_SALES_BASIC_INFO.update({ where: { ID: Number(id) }, @@ -113,7 +117,7 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise< const body = await request.json() // 제출 시 기존 SRL_NO 확인 후 '임시저장'으로 시작하면 새로운 SRL_NO 생성 - const newSrlNo = await getNewSrlNo(body.srlNo, body.storeId) + const newSrlNo = await getNewSrlNo(body.srlNo, body.storeId, body.role) if (body.targetId) { // @ts-ignore diff --git a/src/app/api/survey-sales/route.ts b/src/app/api/survey-sales/route.ts index d3160b6..a5d60c8 100644 --- a/src/app/api/survey-sales/route.ts +++ b/src/app/api/survey-sales/route.ts @@ -227,11 +227,21 @@ export async function POST(request: Request) { try { const body = await request.json() - // 임시 저장 시 임시저장 + 000 으로 저장 - // 기본 저장 시 판매점ID + yyMMdd + 000 으로 저장 + const role = + body.role === 'T01' || body.role === 'Admin' + ? 'HO' + : body.role === 'Admin_Sub' || body.role === 'Builder' + ? 'HM' + : body.role === 'Partner' + ? '' + : null + + // 임시 저장 시 임시저장으로 저장 + // 기본 저장 시 (HO/HM) + 판매점ID + yyMMdd + 000 으로 저장 const baseSrlNo = body.survey.srlNo ?? - body.storeId + + role + + body.storeId + new Date().getFullYear().toString().slice(-2) + (new Date().getMonth() + 1).toString().padStart(2, '0') + new Date().getDate().toString().padStart(2, '0') @@ -240,7 +250,7 @@ export async function POST(request: Request) { const lastSurvey = await prisma.SD_SURVEY_SALES_BASIC_INFO.findFirst({ where: { SRL_NO: { - startsWith: body.storeId, + startsWith: role + body.storeId, }, }, orderBy: { diff --git a/src/components/survey-sale/detail/ButtonForm.tsx b/src/components/survey-sale/detail/ButtonForm.tsx index 8f8e3dd..fd3e339 100644 --- a/src/components/survey-sale/detail/ButtonForm.tsx +++ b/src/components/survey-sale/detail/ButtonForm.tsx @@ -29,7 +29,7 @@ export default function ButtonForm(props: { }) // !!!!!!!!!! - const [tempTargetId, setTempTargetId] = useState('') + const [tempTargetId, setTempTargetId] = useState('TEST') // -------------------------------------------------------------- // 권한 @@ -158,6 +158,7 @@ export default function ButtonForm(props: { if (routeId) { window.neoConfirm('削除しますか?', async () => { await deleteSurvey() + alert('削除されました。') router.push('/survey-sale') }) } @@ -269,9 +270,6 @@ function SubmitButton(props: { handleSubmit: () => void; setTempTargetId: (targe 提出
-
- setTempTargetId(e.target.value)} /> -
) } diff --git a/src/components/survey-sale/detail/DetailForm.tsx b/src/components/survey-sale/detail/DetailForm.tsx index 340b13d..7204135 100644 --- a/src/components/survey-sale/detail/DetailForm.tsx +++ b/src/components/survey-sale/detail/DetailForm.tsx @@ -1,11 +1,11 @@ 'use client' -import type { Mode, SurveyBasicInfo, SurveyBasicRequest, SurveyDetailRequest } from '@/types/Survey' +import type { Mode, SurveyBasicRequest, SurveyDetailRequest } from '@/types/Survey' import { useEffect, useState } from 'react' import ButtonForm from './ButtonForm' import BasicForm from './BasicForm' import RoofForm from './RoofForm' -import { useParams, useRouter, useSearchParams } from 'next/navigation' +import { useParams, useSearchParams } from 'next/navigation' import { useServey } from '@/hooks/useSurvey' const roofInfoForm: SurveyDetailRequest = { diff --git a/src/hooks/useSurvey.ts b/src/hooks/useSurvey.ts index 0f897ff..7149828 100644 --- a/src/hooks/useSurvey.ts +++ b/src/hooks/useSurvey.ts @@ -1,4 +1,4 @@ -import type { SurveyBasicInfo, SurveyDetailInfo, SurveyDetailRequest, SurveyDetailCoverRequest, SurveyRegistRequest } from '@/types/Survey' +import type { SurveyBasicInfo, SurveyDetailRequest, SurveyRegistRequest } from '@/types/Survey' import { useMemo } from 'react' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { useSurveyFilterStore } from '@/store/surveyFilterStore' @@ -63,7 +63,6 @@ export function useServey(id?: number): { isUpdatingSurvey: boolean isDeletingSurvey: boolean createSurvey: (survey: SurveyRegistRequest) => Promise - createSurveyDetail: (params: { surveyId: number; surveyDetail: SurveyDetailCoverRequest }) => void updateSurvey: ({ survey, isTemporary, storeId }: { survey: SurveyRegistRequest; isTemporary: boolean; storeId?: string }) => void deleteSurvey: () => Promise submitSurvey: (params: { saveId?: number; targetId?: string; storeId?: string; srlNo?: string }) => void @@ -119,7 +118,11 @@ export function useServey(id?: number): { const { mutateAsync: createSurvey, isPending: isCreatingSurvey } = useMutation({ mutationFn: async (survey: SurveyRegistRequest) => { - const resp = await axiosInstance(null).post('/api/survey-sales', { survey: survey, storeId: session?.storeId ?? null }) + const resp = await axiosInstance(null).post('/api/survey-sales', { + survey: survey, + storeId: session?.storeId ?? null, + role: session?.role ?? null, + }) return resp.data.id ?? 0 }, onSuccess: (data) => { @@ -137,6 +140,7 @@ export function useServey(id?: number): { survey: survey, isTemporary: isTemporary, storeId: storeId, + role: session?.role ?? null, }) return resp.data }, @@ -158,17 +162,6 @@ export function useServey(id?: number): { }, }) - const { mutateAsync: createSurveyDetail } = useMutation({ - mutationFn: async ({ surveyId, surveyDetail }: { surveyId: number; surveyDetail: SurveyDetailCoverRequest }) => { - const resp = await axiosInstance(null).patch(`/api/survey-sales/${surveyId}`, surveyDetail) - return resp.data - }, - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['survey', 'list'] }) - queryClient.invalidateQueries({ queryKey: ['survey', id] }) - }, - }) - const { mutateAsync: submitSurvey } = useMutation({ mutationFn: async ({ saveId, targetId, storeId, srlNo }: { saveId?: number; targetId?: string; storeId?: string; srlNo?: string }) => { const submitId = saveId ?? id @@ -177,6 +170,7 @@ export function useServey(id?: number): { targetId, storeId, srlNo, + role: session?.role ?? null, }) return resp.data }, @@ -240,7 +234,6 @@ export function useServey(id?: number): { createSurvey, updateSurvey, deleteSurvey, - createSurveyDetail, submitSurvey, validateSurveyDetail, getZipCode, From 8bcffd43bbe89770afca1739bc6be2172c8e89a8 Mon Sep 17 00:00:00 2001 From: Daseul Kim Date: Mon, 26 May 2025 16:31:21 +0900 Subject: [PATCH 04/15] =?UTF-8?q?feat:=20=EC=A7=80=EB=B6=95=EC=9E=AC?= =?UTF-8?q?=EC=A0=81=ED=95=A9=EC=84=B1=20=EC=B4=88=EA=B8=B0=ED=99=94?= =?UTF-8?q?=EB=A9=B4=20=EC=A0=84=EC=B2=B4=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20?= =?UTF-8?q?=EB=AF=B8=ED=91=9C=EC=B6=9C,=20=EA=B2=80=EC=83=89=EC=A1=B0?= =?UTF-8?q?=EA=B1=B4=20=EA=B7=B8=EB=A3=B9=EC=BD=94=EB=93=9C=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/api/suitable/list/route.ts | 11 ++++++++-- src/components/suitable/Suitable.tsx | 15 +++++++------- src/components/suitable/SuitableList.tsx | 2 +- src/hooks/useSuitable.ts | 26 ++++++++++++++++++++---- src/store/useSuitableStore.ts | 25 +++++++++++------------ src/types/Suitable.ts | 4 +++- 6 files changed, 54 insertions(+), 29 deletions(-) diff --git a/src/app/api/suitable/list/route.ts b/src/app/api/suitable/list/route.ts index 005d556..61df570 100644 --- a/src/app/api/suitable/list/route.ts +++ b/src/app/api/suitable/list/route.ts @@ -41,7 +41,7 @@ export async function GET(request: NextRequest) { ) AS details ON msm.id = details.main_id WHERE 1=1 - --roofMtCd AND msm.roof_mt_cd = ':roofMtCd' + --roofMtCd AND msm.roof_mt_cd IN (:roofMtCd) --productName AND msm.product_name LIKE '%:productName%' ORDER BY msm.product_name OFFSET (@P1 - 1) * @P2 ROWS @@ -50,8 +50,13 @@ export async function GET(request: NextRequest) { // 검색 조건 설정 if (category) { + let roofMtQuery = ` + SELECT roof_mt_cd + FROM ms_suitable_roof_material_group + WHERE roof_matl_grp_cd = ':roofMtGrpCd' + ` query = query.replace('--roofMtCd ', '') - query = query.replace(':roofMtCd', category) + query = query.replace(':roofMtCd', roofMtQuery.replace(':roofMtGrpCd', category)) } if (keyword) { query = query.replace('--productName ', '') @@ -60,6 +65,8 @@ export async function GET(request: NextRequest) { const suitable: Suitable[] = await prisma.$queryRawUnsafe(query, pageNumber, itemPerPage) + // console.log(`검색 조건 :::: 카테고리: ${category}, 키워드: ${keyword}`) + return NextResponse.json(suitable) } catch (error) { console.error('❌ 데이터 조회 중 오류가 발생했습니다:', error) diff --git a/src/components/suitable/Suitable.tsx b/src/components/suitable/Suitable.tsx index f5f22b2..35fe94a 100644 --- a/src/components/suitable/Suitable.tsx +++ b/src/components/suitable/Suitable.tsx @@ -10,29 +10,28 @@ import { SUITABLE_HEAD_CODE } from '@/types/Suitable' export default function Suitable() { const [reference, setReference] = useState(true) + const [searchValue, setSearchValue] = useState('') - const { getSuitableCommCode } = useSuitable() - const { suitableCommCode, selectedCategory, setSelectedCategory, searchValue, setSearchValue, setIsSearch, clearSelectedItems } = useSuitableStore() + const { getSuitableCommCode, clearSuitableSearch } = useSuitable() + const { suitableCommCode, selectedCategory, setSelectedCategory, setSearchKeyword, clearSearchKeyword } = useSuitableStore() const handleInputSearch = async () => { if (!searchValue.trim()) { alert('屋根材の製品名を入力してください。') return } - setIsSearch(true) + setSearchKeyword(searchValue) } const handleInputClear = () => { setSearchValue('') - setIsSearch(false) + clearSearchKeyword() } useEffect(() => { getSuitableCommCode() return () => { - setSelectedCategory('') - setSearchValue('') - clearSelectedItems() + clearSuitableSearch() } }, []) @@ -41,7 +40,7 @@ export default function Suitable() {
setSelectedCategory(e.target.value)}> - - {suitableCommCode.get(SUITABLE_HEAD_CODE.ROOF_MATERIAL_GROUP)?.map((category: CommCode, index: number) => ( - - ))} - -
-
-
- setSearchValue(e.target.value)} - onKeyDown={(e) => { - if (e.key === 'Enter') { - handleInputSearch() - } - }} - /> - {searchValue &&
-
+
diff --git a/src/components/suitable/SuitableSearch.tsx b/src/components/suitable/SuitableSearch.tsx new file mode 100644 index 0000000..1337ced --- /dev/null +++ b/src/components/suitable/SuitableSearch.tsx @@ -0,0 +1,67 @@ +'use client' + +import { useEffect, useState } from 'react' +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 SuitableSearch() { + const [searchValue, setSearchValue] = useState('') + + const { getSuitableCommCode, clearSuitableSearch } = useSuitable() + const { suitableCommCode, selectedCategory, setSelectedCategory, setSearchKeyword } = useSuitableStore() + + const handleInputSearch = async () => { + if (!searchValue.trim()) { + alert('屋根材の製品名を入力してください。') + return + } + setSearchKeyword(searchValue) + } + + const handleInputClear = () => { + setSearchValue('') + clearSuitableSearch({ items: true, keyword: true }) + } + + useEffect(() => { + getSuitableCommCode() + return () => { + clearSuitableSearch({ items: true, category: true, keyword: true }) + } + }, []) + + return ( + <> +
+ +
+
+
+ setSearchValue(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + handleInputSearch() + } + }} + /> + {searchValue &&
+
+ + ) +} From 35b1002908fb7d12632b44e65f2d0d4d77381850 Mon Sep 17 00:00:00 2001 From: keyy1315 Date: Tue, 27 May 2025 09:08:45 +0900 Subject: [PATCH 08/15] feat: add submit popup page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 제출 팝업 페이지 추가 - 조사매물 작성 시 숫자 입력 항목 모바일에서 숫자 키패드만 나오도록 설정 - 제출 필수값 validation 구현 --- .env.development | 2 +- .env.localhost | 2 +- src/app/api/survey-sales/[id]/route.ts | 6 +- .../popup/SurveySaleSubmitPopup.tsx | 151 ++++++++++++++++++ src/components/popup/ZipCodePopup.tsx | 1 - .../survey-sale/detail/BasicForm.tsx | 14 +- .../survey-sale/detail/ButtonForm.tsx | 84 ++++------ .../survey-sale/detail/DataTable.tsx | 5 +- .../survey-sale/detail/RoofForm.tsx | 24 +-- src/components/ui/PopupController.tsx | 4 +- src/hooks/useSurvey.ts | 12 +- src/store/popupController.ts | 5 + 12 files changed, 225 insertions(+), 85 deletions(-) create mode 100644 src/components/popup/SurveySaleSubmitPopup.tsx diff --git a/.env.development b/.env.development index 47cfa78..f9cbda5 100644 --- a/.env.development +++ b/.env.development @@ -2,7 +2,7 @@ NEXT_PUBLIC_RUN_MODE=development # 모바일 디바이스로 로컬 서버 확인하려면 자신 IP 주소로 변경 # 다시 로컬에서 개발할때는 localhost로 변경 #route handler -NEXT_PUBLIC_API_URL=http://localhost:3000 +NEXT_PUBLIC_API_URL=http://172.30.1.65:3000 #qsp 로그인 api NEXT_PUBLIC_QSP_API_URL=http://1.248.227.176:8120 diff --git a/.env.localhost b/.env.localhost index 966e366..944ee6f 100644 --- a/.env.localhost +++ b/.env.localhost @@ -2,7 +2,7 @@ NEXT_PUBLIC_RUN_MODE=local # 모바일 디바이스로 로컬 서버 확인하려면 자신 IP 주소로 변경 # 다시 로컬에서 개발할때는 localhost로 변경 #route handler -NEXT_PUBLIC_API_URL=http://localhost:3000 +NEXT_PUBLIC_API_URL=http://172.30.1.65:3000 #qsp 로그인 api NEXT_PUBLIC_QSP_API_URL=http://1.248.227.176:8120 diff --git a/src/app/api/survey-sales/[id]/route.ts b/src/app/api/survey-sales/[id]/route.ts index 7c03530..eb718f4 100644 --- a/src/app/api/survey-sales/[id]/route.ts +++ b/src/app/api/survey-sales/[id]/route.ts @@ -116,9 +116,6 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise< const { id } = await params const body = await request.json() - // 제출 시 기존 SRL_NO 확인 후 '임시저장'으로 시작하면 새로운 SRL_NO 생성 - const newSrlNo = await getNewSrlNo(body.srlNo, body.storeId, body.role) - if (body.targetId) { // @ts-ignore const survey = await prisma.SD_SURVEY_SALES_BASIC_INFO.update({ @@ -128,10 +125,9 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise< SUBMISSION_DATE: new Date(), SUBMISSION_TARGET_ID: body.targetId, UPT_DT: new Date(), - SRL_NO: newSrlNo, }, }) - return NextResponse.json({ message: 'Survey confirmed successfully' }) + return NextResponse.json({ message: 'Survey confirmed successfully', data: survey }) } } catch (error) { console.error('Error updating survey:', error) diff --git a/src/components/popup/SurveySaleSubmitPopup.tsx b/src/components/popup/SurveySaleSubmitPopup.tsx new file mode 100644 index 0000000..1d61da1 --- /dev/null +++ b/src/components/popup/SurveySaleSubmitPopup.tsx @@ -0,0 +1,151 @@ +import Image from 'next/image' +import { usePopupController } from '@/store/popupController' +import { useParams } from 'next/navigation' +import { useServey } from '@/hooks/useSurvey' +import { useState } from 'react' +import { useSessionStore } from '@/store/session' + +interface SubmitFormData { + store: string + sender: string + receiver: string + reference: string + title: string + contents: string +} + +interface FormField { + id: keyof SubmitFormData + name: string + required: boolean +} + +const FORM_FIELDS: FormField[] = [ + { id: 'store', name: '提出販売店', required: true }, + { id: 'sender', name: '発送者', required: true }, + { id: 'receiver', name: '受信者', required: true }, + { id: 'reference', name: '参考', required: false }, + { id: 'title', name: 'タイトル', required: true }, + { id: 'contents', name: '内容', required: true }, +] + +export default function SurveySaleSubmitPopup() { + const popupController = usePopupController() + const { session } = useSessionStore() + const params = useParams() + const routeId = params.id + + const [submitData, setSubmitData] = useState({ + store: '', + sender: session?.email ?? '', + receiver: '', + reference: '', + title: '[HANASYS現地調査] 調査物件が提出.', + contents: '', + }) + + const { submitSurvey, isSubmittingSurvey } = useServey(Number(routeId)) + + const handleInputChange = (field: keyof SubmitFormData, value: string) => { + setSubmitData((prev) => ({ ...prev, [field]: value })) + } + + const validateData = (data: SubmitFormData): boolean => { + const requiredFields = FORM_FIELDS.filter((field) => field.required) + + for (const field of requiredFields) { + if (!data[field.id].trim()) { + const element = document.getElementById(field.id) + if (element) { + element.focus() + } + alert(`${field.name}は必須入力項目です。`) + return false + } + } + + return true + } + + const handleSubmit = () => { + if (validateData(submitData)) { + window.neoConfirm('送信しますか? 送信後は変更・修正することはできません。', () => { + submitSurvey({ targetId: submitData.store }) + if (!isSubmittingSurvey) { + popupController.setSurveySaleSubmitPopup(false) + } + }) + } + } + + const handleClose = () => { + popupController.setSurveySaleSubmitPopup(false) + } + + const renderFormField = (field: FormField) => { + // const isReadOnly = (field.id === 'store' && session?.role !== 'Partner') || (field.id === 'receiver' && session?.role !== 'Partner') + const isReadOnly = false + + return ( +
+
+ {field.name} {field.required && *} +
+
+ {field.id === 'contents' ? ( +