From 905a309a9c0bb688852f5e9b5b2b4ca8a3f029e8 Mon Sep 17 00:00:00 2001 From: keyy1315 Date: Tue, 20 May 2025 17:56:09 +0900 Subject: [PATCH 1/8] fix: survey update, create, submit logic fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 조사매물 수정, 작성, 조회 시 각 컴포넌트에 데이터 적용 안되던 문제 해결 - 조사매물 제출 시 데이터 저장 안되는 문제 해결결 --- src/app/api/survey-sales/[id]/route.ts | 16 +- src/app/api/survey-sales/route.ts | 1 + .../survey-sale/detail/ButtonForm.tsx | 53 +++--- .../survey-sale/detail/DetailForm.tsx | 5 +- .../survey-sale/detail/RoofForm.tsx | 162 +++++++++--------- src/components/survey-sale/list/ListTable.tsx | 24 +-- .../survey-sale/list/SearchForm.tsx | 6 +- 7 files changed, 140 insertions(+), 127 deletions(-) diff --git a/src/app/api/survey-sales/[id]/route.ts b/src/app/api/survey-sales/[id]/route.ts index 415d1e6..fb48ec4 100644 --- a/src/app/api/survey-sales/[id]/route.ts +++ b/src/app/api/survey-sales/[id]/route.ts @@ -23,30 +23,22 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{ try { const { id } = await params const body = await request.json() - const { DETAIL_INFO, ...basicInfo } = body + const { detailInfo, ...basicInfo } = body - console.log('body:: ', body) // @ts-ignore const survey = await prisma.SD_SURVEY_SALES_BASIC_INFO.update({ where: { ID: Number(id) }, data: { ...convertToSnakeCase(basicInfo), UPT_DT: new Date(), - DETAIL_INFO: DETAIL_INFO ? { - upsert: { - create: convertToSnakeCase(DETAIL_INFO), - update: convertToSnakeCase(DETAIL_INFO), - where: { - BASIC_INFO_ID: Number(id) - } - } - } : undefined + DETAIL_INFO: { + update: convertToSnakeCase(detailInfo) + } }, include: { DETAIL_INFO: true } }) - console.log('survey:: ', survey) return NextResponse.json(survey) } catch (error) { console.error('Error updating survey:', error) diff --git a/src/app/api/survey-sales/route.ts b/src/app/api/survey-sales/route.ts index 3298f5d..cbcead5 100644 --- a/src/app/api/survey-sales/route.ts +++ b/src/app/api/survey-sales/route.ts @@ -236,6 +236,7 @@ export async function POST(request: Request) { } } }) + console.log('result:: ', result) return NextResponse.json(result) } catch (error) { console.error(error) diff --git a/src/components/survey-sale/detail/ButtonForm.tsx b/src/components/survey-sale/detail/ButtonForm.tsx index a22ac5c..29652a7 100644 --- a/src/components/survey-sale/detail/ButtonForm.tsx +++ b/src/components/survey-sale/detail/ButtonForm.tsx @@ -22,8 +22,13 @@ export default function ButtonForm(props: { const params = useParams() const routeId = params.id - const [isSubmitProcess, setIsSubmitProcess] = useState(false) // ------------------------------------------------------------ + const [isSubmitProcess, setIsSubmitProcess] = useState(false) + const [saveData, setSaveData] = useState({ + ...props.data.basic, + detailInfo: props.data.roof, + }) + // -------------------------------------------------------------- // 권한 // 제출권한 ㅇ @@ -37,6 +42,10 @@ export default function ButtonForm(props: { setIsSubmiter(session.storeNm === props.data.basic.store && session.builderNo === props.data.basic.constructionPoint) setIsWriter(session.userNm === props.data.basic.representative) } + setSaveData({ + ...props.data.basic, + detailInfo: props.data.roof, + }) }, [session, props.data]) // ------------------------------------------------------------ @@ -45,18 +54,13 @@ export default function ButtonForm(props: { 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 handleSave = (isTemporary: boolean, isSubmitProcess?: boolean) => { const emptyField = validateSurveyDetail(props.data.roof) - console.log('handleSave, emptyField:: ', emptyField) if (isTemporary) { tempSaveProcess() } else { - saveProcess(emptyField) + saveProcess(emptyField, isSubmitProcess ?? false) } } @@ -78,30 +82,38 @@ export default function ButtonForm(props: { } } - const saveProcess = async (emptyField: string) => { - if (emptyField.trim() === '') { + const saveProcess = async (emptyField: string | null, isSubmitProcess?: boolean) => { + if (emptyField?.trim() === '') { if (idParam) { - // 수정 페이지에서 작성 후 제출 if (isSubmitProcess) { - saveData = { + const updatedData = { ...saveData, submissionStatus: true, submissionDate: new Date().toISOString(), } + await updateSurvey(updatedData) + router.push(`/survey-sale/${idParam}`) + } else { + await updateSurvey(saveData) + router.push(`/survey-sale/${idParam}`) } - await updateSurvey(saveData) - router.push(`/survey-sale/${idParam}`) } else { - const id = await createSurvey(saveData) if (isSubmitProcess) { + const updatedData = { + ...saveData, + submissionStatus: true, + submissionDate: new Date().toISOString(), + } + const id = await createSurvey(updatedData) submitProcess(id) - return + } else { + const id = await createSurvey(saveData) + router.push(`/survey-sale/${id}`) } - router.push(`/survey-sale/${id}`) } alert('保存されました。') } else { - if (emptyField.includes('Unit')) { + if (emptyField?.includes('Unit')) { alert('電気契約容量の単位を入力してください。') focusInput(emptyField as keyof SurveyDetailInfo) } else { @@ -124,11 +136,10 @@ export default function ButtonForm(props: { const handleSubmit = async () => { window.neoConfirm('提出しますか?', async () => { - setIsSubmitProcess(true) - if (routeId) { + if (Number(routeId)) { submitProcess() } else { - handleSave(false) + handleSave(false, true) } }) } diff --git a/src/components/survey-sale/detail/DetailForm.tsx b/src/components/survey-sale/detail/DetailForm.tsx index 0467aec..f8761bc 100644 --- a/src/components/survey-sale/detail/DetailForm.tsx +++ b/src/components/survey-sale/detail/DetailForm.tsx @@ -81,11 +81,12 @@ export default function DetailForm() { setRoofInfoData(rest) } } - }, [surveyDetail, mode]) + }, [surveyDetail]) // console.log('mode:: ', mode) // console.log('surveyDetail:: ', surveyDetail) - // console.log('roofInfoData:: ', roofInfoData) + // console.log('basicInfoData:: ', basicInfoData) + console.log('roofInfoData:: ', roofInfoData) const data = { basic: basicInfoData, diff --git a/src/components/survey-sale/detail/RoofForm.tsx b/src/components/survey-sale/detail/RoofForm.tsx index 60399fc..7f011c9 100644 --- a/src/components/survey-sale/detail/RoofForm.tsx +++ b/src/components/survey-sale/detail/RoofForm.tsx @@ -464,17 +464,17 @@ const SelectedBox = ({ }) => { const selectedId = detailInfoData?.[column as keyof SurveyDetailInfo] const etcValue = detailInfoData?.[`${column}Etc` as keyof SurveyDetailInfo] + const [isEtcSelected, setIsEtcSelected] = useState(Boolean(etcValue)) - const [isEtcSelected, setIsEtcSelected] = useState(etcValue !== null && etcValue !== undefined && etcValue !== '') - const [etcVal, setEtcVal] = useState(etcValue?.toString() ?? '') + const isSpecialCase = column === 'constructionYear' || column === 'installationAvailability' + const showEtcOption = !isSpecialCase 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 = { + const updatedData = { ...detailInfoData, [column]: isEtc ? null : value, [`${column}Etc`]: isEtc ? '' : null, @@ -485,14 +485,20 @@ const SelectedBox = ({ } 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 }) + setRoofInfo({ ...detailInfoData, [`${column}Etc`]: e.target.value }) + } + + const isInputDisabled = () => { + if (mode === 'READ') return true + if (column === 'installationAvailability') return false + if (column === 'constructionYear') { + return detailInfoData.constructionYear === '1' || detailInfoData.constructionYear === null + } + return !isEtcSelected && !etcValue } return ( @@ -502,7 +508,7 @@ const SelectedBox = ({ name={column} id={column} disabled={mode === 'READ'} - value={selectedId ? Number(selectedId) : etcValue !== null ? 'etc' : ''} + value={selectedId ? Number(selectedId) : etcValue ? 'etc' : ''} onChange={handleSelectChange} > {selectBoxOptions[column as keyof typeof selectBoxOptions].map((item) => ( @@ -510,7 +516,7 @@ const SelectedBox = ({ {item.name} ))} - {column !== 'installationAvailability' && column !== 'constructionYear' && ( + {showEtcOption && ( @@ -524,17 +530,9 @@ const SelectedBox = ({ type="text" className="input-frame" placeholder="-" - value={etcVal} + value={detailInfoData[`${column}Etc` as keyof SurveyDetailInfo]?.toString() ?? ''} onChange={handleEtcInputChange} - disabled={ - mode === 'READ' - ? true - : column === 'installationAvailability' - ? false - : column === 'constructionYear' - ? detailInfoData.constructionYear === '1' || detailInfoData.constructionYear === null - : !isEtcSelected - } + disabled={isInputDisabled()} /> @@ -552,49 +550,52 @@ const RadioSelected = ({ detailInfoData: SurveyDetailInfo setRoofInfo: (roofInfo: SurveyDetailRequest) => void }) => { - let selectedId = detailInfoData?.[column as keyof SurveyDetailInfo] - if (column === 'leakTrace') { - selectedId = Number(selectedId) - if (!selectedId) selectedId = 2 - } + const etcValue = detailInfoData?.[`${column}Etc` as keyof SurveyDetailInfo] + const [etcChecked, setEtcChecked] = useState(Boolean(etcValue)) - let etcValue = null - if (column !== 'rafterDirection') { - etcValue = detailInfoData?.[`${column}Etc` as keyof SurveyDetailInfo] - } - const [etcChecked, setEtcChecked] = useState(etcValue !== null && etcValue !== undefined && etcValue !== '') - const [etcVal, setEtcVal] = useState(etcValue?.toString() ?? '') + const selectedId = column === 'leakTrace' + ? Number(detailInfoData?.[column as keyof SurveyDetailInfo]) || 2 + : detailInfoData?.[column as keyof SurveyDetailInfo] + + const isSpecialColumn = column === 'rafterDirection' || column === 'leakTrace' || column === 'insulationPresence' + const showEtcOption = !isSpecialColumn const handleRadioChange = (e: React.ChangeEvent) => { const value = e.target.value + if (column === 'leakTrace') { - handleBooleanRadioChange(value) + setRoofInfo({ ...detailInfoData, leakTrace: value === '1' }) + return } + 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 }) + return } - } - const handleBooleanRadioChange = (value: string) => { - if (value === '1') { - setRoofInfo({ ...detailInfoData, leakTrace: true }) - } else { - setRoofInfo({ ...detailInfoData, leakTrace: false }) - } + const isInsulationPresence = column === 'insulationPresence' + const isRafterDirection = column === 'rafterDirection' + + setEtcChecked(isInsulationPresence && value === '2') + + setRoofInfo({ + ...detailInfoData, + [column]: value, + [`${column}Etc`]: isRafterDirection ? detailInfoData[`${column}Etc` as keyof SurveyDetailInfo] : null + }) } const handleEtcInputChange = (e: React.ChangeEvent) => { - const value = e.target.value - setEtcVal(value) - setRoofInfo({ ...detailInfoData, [`${column}Etc`]: value }) + setRoofInfo({ ...detailInfoData, [`${column}Etc`]: e.target.value }) + } + + const isInputDisabled = () => { + if (mode === 'READ') return true + if (column === 'insulationPresence') { + return detailInfoData.insulationPresence !== '2' + } + return !etcChecked && !etcValue } return ( @@ -613,7 +614,7 @@ const RadioSelected = ({ ))} - {column !== 'rafterDirection' && column !== 'leakTrace' && column !== 'insulationPresence' && ( + {showEtcOption && (
)} - {column !== 'leakTrace' && column !== 'rafterDirection' && ( + {showEtcOption && (
)} @@ -655,51 +656,56 @@ const MultiCheck = ({ setRoofInfo: (roofInfo: SurveyDetailRequest) => void }) => { const multiCheckData = column === 'supplementaryFacilities' ? supplementaryFacilities : roofMaterial + const etcValue = roofInfo?.[`${column}Etc` as keyof SurveyDetailInfo] + const [isOtherCheck, setIsOtherCheck] = useState(Boolean(etcValue)) - const [isOtherCheck, setIsOtherCheck] = useState(false) - const [otherValue, setOtherValue] = useState(roofInfo?.[`${column}Etc` as keyof SurveyDetailInfo]?.toString() ?? '') + const isRoofMaterial = column === 'roofMaterial' + const selectedValues = makeNumArr(String(roofInfo[column as keyof SurveyDetailInfo] ?? '')) const handleCheckbox = (id: number) => { - const value = makeNumArr(String(roofInfo[column as keyof SurveyDetailInfo] ?? '')) - const isOtherSelected = roofInfo?.[`${column}Etc` as keyof SurveyDetailInfo] !== null - + const isOtherSelected = Boolean(etcValue) 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 (selectedValues.includes(String(id))) { + newValue = selectedValues.filter((v) => v !== String(id)) + } else { + if (isRoofMaterial) { + const totalSelected = selectedValues.length + (isOtherSelected ? 1 : 0) if (totalSelected >= 2) { alert('屋根材は最大2個まで選択できます。') return } } - newValue = [...value, String(id)] + newValue = [...selectedValues, 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 (isRoofMaterial) { + const currentSelected = selectedValues.length if (!isOtherCheck && currentSelected >= 2) { alert('屋根材は最大2個まで選択できます。') return } } + const newIsOtherCheck = !isOtherCheck setIsOtherCheck(newIsOtherCheck) - setOtherValue('') - - setRoofInfo({ ...roofInfo, [`${column}Etc`]: newIsOtherCheck ? '' : null }) + + // 기타 선택 해제 시 값도 null로 설정 + setRoofInfo({ + ...roofInfo, + [`${column}Etc`]: newIsOtherCheck ? '' : null + }) } const handleOtherInputChange = (e: React.ChangeEvent) => { - const value = e.target.value - setOtherValue(value) - setRoofInfo({ ...roofInfo, [`${column}Etc`]: value }) + setRoofInfo({ ...roofInfo, [`${column}Etc`]: e.target.value }) + } + + const isInputDisabled = () => { + return mode === 'READ' || (!isOtherCheck && !etcValue) } return ( @@ -710,7 +716,7 @@ const MultiCheck = ({ handleCheckbox(item.id)} /> @@ -721,7 +727,7 @@ const MultiCheck = ({ @@ -733,9 +739,9 @@ const MultiCheck = ({ type="text" className="input-frame" placeholder="-" - value={otherValue} + value={roofInfo[`${column}Etc` as keyof SurveyDetailInfo]?.toString() ?? ''} onChange={handleOtherInputChange} - disabled={mode === 'READ' || !isOtherCheck} + disabled={isInputDisabled()} /> diff --git a/src/components/survey-sale/list/ListTable.tsx b/src/components/survey-sale/list/ListTable.tsx index f1a3847..777ad78 100644 --- a/src/components/survey-sale/list/ListTable.tsx +++ b/src/components/survey-sale/list/ListTable.tsx @@ -2,8 +2,8 @@ import LoadMoreButton from '@/components/LoadMoreButton' import { useServey } from '@/hooks/useSurvey' -import { useEffect, useState } from 'react' -import { useRouter } from 'next/navigation' +import { useEffect, useState, useMemo, useRef } from 'react' +import { useRouter, usePathname } from 'next/navigation' import SearchForm from './SearchForm' import { useSurveyFilterStore } from '@/store/surveyFilterStore' import { useSessionStore } from '@/store/session' @@ -11,19 +11,26 @@ import type { SurveyBasicInfo } from '@/types/Survey' export default function ListTable() { const router = useRouter() + const pathname = usePathname() + const { surveyList, isLoadingSurveyList } = useServey() const { offset, setOffset } = useSurveyFilterStore() + const { session } = useSessionStore() + const [heldSurveyList, setHeldSurveyList] = useState([]) const [hasMore, setHasMore] = useState(false) - const { session } = useSessionStore() + useEffect(() => { + setOffset(0) + setHeldSurveyList([]) + }, [pathname]) useEffect(() => { if (!session.isLoggedIn || !('data' in surveyList)) return if ('count' in surveyList && surveyList.count > 0) { if (offset > 0) { - setHeldSurveyList((prev) => [...prev, ...surveyList.data]) + setHeldSurveyList(prev => [...prev, ...surveyList.data]) } else { setHeldSurveyList(surveyList.data) } @@ -32,22 +39,17 @@ export default function ListTable() { setHeldSurveyList([]) setHasMore(false) } - }, [surveyList, offset, session]) + }, [surveyList, offset, session.isLoggedIn]) const handleDetailClick = (id: number) => { router.push(`/survey-sale/${id}`) } - const handleItemsInit = () => { - setHeldSurveyList([]) - setOffset(0) - } - // TODO: 로딩 처리 필요 return ( <> - + {heldSurveyList.length > 0 ? (
    diff --git a/src/components/survey-sale/list/SearchForm.tsx b/src/components/survey-sale/list/SearchForm.tsx index 7f46e68..e0de061 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({ memberRole, userId }: { memberRole: string; userId: string }) { +export default function SearchForm({ memberRole, userNm }: { memberRole: string; userNm: string }) { const router = useRouter() const { setSearchOption, setSort, setIsMySurvey, setKeyword, isMySurvey, keyword, searchOption, sort } = useSurveyFilterStore() const [searchKeyword, setSearchKeyword] = useState(keyword) @@ -75,9 +75,9 @@ export default function SearchForm({ memberRole, userId }: { memberRole: string; { - setIsMySurvey(isMySurvey === userId ? null : userId) + setIsMySurvey(isMySurvey === userNm ? null : userNm) }} /> From 67d587acf5ff43dd691ba40ddfca312c879071e9 Mon Sep 17 00:00:00 2001 From: keyy1315 Date: Wed, 21 May 2025 10:19:38 +0900 Subject: [PATCH 2/8] feat: Implement Separate temporary save data logic --- src/app/api/survey-sales/route.ts | 37 +++++++++++++++++-- .../survey-sale/detail/ButtonForm.tsx | 14 +++++-- .../survey-sale/detail/DataTable.tsx | 12 +++--- .../survey-sale/detail/DetailForm.tsx | 14 +++---- 4 files changed, 56 insertions(+), 21 deletions(-) diff --git a/src/app/api/survey-sales/route.ts b/src/app/api/survey-sales/route.ts index cbcead5..5a34bb8 100644 --- a/src/app/api/survey-sales/route.ts +++ b/src/app/api/survey-sales/route.ts @@ -77,6 +77,33 @@ const createKeywordSearchCondition = (keyword: string, searchOption: string): Wh return where } +const filterTempData = (data: any) => { + const requiredFields = [ + 'INSTALLATION_SYSTEM', + 'CONSTRUCTION_YEAR', + 'RAFTER_SIZE', + 'RAFTER_PITCH', + 'RAFTER_PITH_ETC', + 'WATERPROOF_MATERIAL', + 'INSULATION_PRESENCE', + 'STRUCTURE_ORDER', + ] + + const where: WhereCondition = { AND: [] } + + // 각 필드에 대해 OR 조건을 만들고, 전체를 AND로 묶음 + where.AND.push( + ...requiredFields.map((field: string) => ({ + OR: [ + { [field]: { not: null } }, + { [`${field}_ETC`]: { not: null } } + ] + })) + ) + + return where +} + /** * 회원 역할별 검색 조건 생성 함수 * @param params 검색 파라미터 @@ -128,6 +155,8 @@ const createMemberRoleCondition = (params: SearchParams): WhereCondition => { break case 'T01': + where.AND.push(filterTempData(params)) + break case 'User': // 모든 매물 조회 가능 (추가 조건 없음) break @@ -219,7 +248,7 @@ export async function PUT(request: Request) { } } -export async function POST(request: Request) { +export async function POST(request: Request) { try { const body = await request.json() console.log('body:: ', body) @@ -232,9 +261,9 @@ export async function POST(request: Request) { data: { ...convertToSnakeCase(basicInfo), DETAIL_INFO: { - create: convertToSnakeCase(detailInfo) - } - } + create: convertToSnakeCase(detailInfo), + }, + }, }) console.log('result:: ', result) return NextResponse.json(result) diff --git a/src/components/survey-sale/detail/ButtonForm.tsx b/src/components/survey-sale/detail/ButtonForm.tsx index 29652a7..6eae13f 100644 --- a/src/components/survey-sale/detail/ButtonForm.tsx +++ b/src/components/survey-sale/detail/ButtonForm.tsx @@ -10,6 +10,7 @@ export default function ButtonForm(props: { mode: Mode setMode: (mode: Mode) => void data: { basic: SurveyBasicRequest; roof: SurveyDetailRequest } + isTemporarySave: boolean }) { // 라우터 const router = useRouter() @@ -23,7 +24,6 @@ export default function ButtonForm(props: { const routeId = params.id // ------------------------------------------------------------ - const [isSubmitProcess, setIsSubmitProcess] = useState(false) const [saveData, setSaveData] = useState({ ...props.data.basic, detailInfo: props.data.roof, @@ -55,12 +55,14 @@ export default function ButtonForm(props: { const { deleteSurvey, submitSurvey, updateSurvey } = useServey(Number(id)) const { validateSurveyDetail, createSurvey } = useServey() - const handleSave = (isTemporary: boolean, isSubmitProcess?: boolean) => { + const handleSave = (isTemporary: boolean, isSubmitProcess = false) => { const emptyField = validateSurveyDetail(props.data.roof) + const hasEmptyField = emptyField?.trim() !== '' + if (isTemporary) { - tempSaveProcess() + hasEmptyField ? tempSaveProcess() : saveProcess(emptyField, false) } else { - saveProcess(emptyField, isSubmitProcess ?? false) + saveProcess(emptyField, isSubmitProcess) } } @@ -135,6 +137,10 @@ export default function ButtonForm(props: { } const handleSubmit = async () => { + if (props.isTemporarySave) { + alert('一時保存されたデータは提出できません。') + return + } window.neoConfirm('提出しますか?', async () => { if (Number(routeId)) { submitProcess() diff --git a/src/components/survey-sale/detail/DataTable.tsx b/src/components/survey-sale/detail/DataTable.tsx index 210d80d..4fb370f 100644 --- a/src/components/survey-sale/detail/DataTable.tsx +++ b/src/components/survey-sale/detail/DataTable.tsx @@ -4,7 +4,6 @@ import { useServey } from '@/hooks/useSurvey' import { useParams, useSearchParams } from 'next/navigation' import { useEffect, useState } from 'react' import DetailForm from './DetailForm' -import type { SurveyBasicInfo } from '@/types/Survey' export default function DataTable() { const params = useParams() @@ -14,19 +13,22 @@ export default function DataTable() { const isTemp = searchParams.get('isTemporary') const { surveyDetail, isLoadingSurveyDetail } = useServey(Number(id)) - const [isTemporary, setIsTemporary] = useState(isTemp === 'true') + const [isTemporarySave, setIsTemporarySave] = useState(isTemp === 'true') const { validateSurveyDetail } = useServey(Number(id)) useEffect(() => { if (surveyDetail?.detailInfo) { const validate = validateSurveyDetail(surveyDetail.detailInfo) + console.log('validate:: ', validate) if (validate.trim() !== '') { - setIsTemporary(false) + setIsTemporarySave(true) } } }, [surveyDetail]) + console.log('isTemporarySave:: ', isTemporarySave) + if (isLoadingSurveyDetail) { return
    Loading...
    } @@ -42,7 +44,7 @@ export default function DataTable() { 登録番号 - {isTemporary ? ( + {isTemporarySave ? ( 仮保存 @@ -83,7 +85,7 @@ export default function DataTable() {
- + ) } diff --git a/src/components/survey-sale/detail/DetailForm.tsx b/src/components/survey-sale/detail/DetailForm.tsx index f8761bc..71b771e 100644 --- a/src/components/survey-sale/detail/DetailForm.tsx +++ b/src/components/survey-sale/detail/DetailForm.tsx @@ -60,13 +60,14 @@ const basicInfoForm: SurveyBasicRequest = { submissionDate: null, } -export default function DetailForm() { +export default function DetailForm(props: { isTemporarySave: boolean }) { + const { isTemporarySave } = props const idParam = useSearchParams().get('id') const routeId = useParams().id const id = idParam ?? routeId - const { surveyDetail } = useServey(Number(id)) + const { surveyDetail, validateSurveyDetail } = useServey(Number(id)) const [mode, setMode] = useState(idParam ? 'EDIT' : routeId ? 'READ' : 'CREATE') const [basicInfoData, setBasicInfoData] = useState(basicInfoForm) @@ -79,21 +80,18 @@ export default function DetailForm() { if (detailInfo) { const { id, uptDt, regDt, basicInfoId, ...rest } = detailInfo setRoofInfoData(rest) + if (validateSurveyDetail(rest).trim() !== '') { + } } } }, [surveyDetail]) - // console.log('mode:: ', mode) - // console.log('surveyDetail:: ', surveyDetail) - // console.log('basicInfoData:: ', basicInfoData) - console.log('roofInfoData:: ', roofInfoData) - const data = { basic: basicInfoData, roof: roofInfoData, } - const buttonFormProps = { mode, setMode, data } + const buttonFormProps = { mode, setMode, data, isTemporarySave } return ( <> From a58c58afca76189f17d3811f97b4f7435a0093a5 Mon Sep 17 00:00:00 2001 From: keyy1315 Date: Wed, 21 May 2025 10:29:08 +0900 Subject: [PATCH 3/8] fix: fix prisma syntax for filtering temporary save data for T01 to Read survey list --- src/app/api/survey-sales/route.ts | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/src/app/api/survey-sales/route.ts b/src/app/api/survey-sales/route.ts index 5a34bb8..64c520f 100644 --- a/src/app/api/survey-sales/route.ts +++ b/src/app/api/survey-sales/route.ts @@ -77,31 +77,27 @@ const createKeywordSearchCondition = (keyword: string, searchOption: string): Wh return where } -const filterTempData = (data: any) => { +// 임시저장 데이터 필터링 - T01 만 적용 +const filterTempData = () => { const requiredFields = [ 'INSTALLATION_SYSTEM', 'CONSTRUCTION_YEAR', 'RAFTER_SIZE', 'RAFTER_PITCH', - 'RAFTER_PITH_ETC', 'WATERPROOF_MATERIAL', 'INSULATION_PRESENCE', 'STRUCTURE_ORDER', ] - const where: WhereCondition = { AND: [] } - - // 각 필드에 대해 OR 조건을 만들고, 전체를 AND로 묶음 - where.AND.push( - ...requiredFields.map((field: string) => ({ - OR: [ - { [field]: { not: null } }, - { [`${field}_ETC`]: { not: null } } - ] - })) - ) - - return where + return { + DETAIL_INFO: { + is: { + AND: requiredFields.map((field: string) => ({ + OR: [{ [field]: { not: null } }, { [`${field}_ETC`]: { not: null } }], + })), + }, + }, + } } /** @@ -155,7 +151,7 @@ const createMemberRoleCondition = (params: SearchParams): WhereCondition => { break case 'T01': - where.AND.push(filterTempData(params)) + where.AND.push(filterTempData()) break case 'User': // 모든 매물 조회 가능 (추가 조건 없음) From 333943c651bbd35cc6011008a932ef8e12fae4ba Mon Sep 17 00:00:00 2001 From: keyy1315 Date: Wed, 21 May 2025 17:53:58 +0900 Subject: [PATCH 4/8] feat: add SUBMISSION_TARGET_ID, SRL_NO Column MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 제출 대상 판매점 ID, 일련번호 컬럼 추가 - 임시 저장 시 일련번호에 '임시저장000' 으로 저장 - 조사매물 목록 조회 필터링 조건 수정 - url 에러 핸들링 --- src/app/api/survey-sales/[id]/route.ts | 78 +++++++++--------- src/app/api/survey-sales/route.ts | 57 ++++++++++--- .../survey-sale/detail/BasicForm.tsx | 2 +- .../survey-sale/detail/ButtonForm.tsx | 80 ++++++++++++++----- .../survey-sale/detail/DataTable.tsx | 33 +++----- .../survey-sale/detail/DetailForm.tsx | 21 +++-- .../survey-sale/detail/RoofForm.tsx | 36 +++++---- src/components/survey-sale/list/ListTable.tsx | 24 +++--- src/components/ui/Main.tsx | 2 +- src/hooks/useSurvey.ts | 20 +++-- src/types/Survey.ts | 6 ++ 11 files changed, 225 insertions(+), 134 deletions(-) diff --git a/src/app/api/survey-sales/[id]/route.ts b/src/app/api/survey-sales/[id]/route.ts index fb48ec4..db5c9df 100644 --- a/src/app/api/survey-sales/[id]/route.ts +++ b/src/app/api/survey-sales/[id]/route.ts @@ -19,25 +19,54 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ } } +const getNewSrlNo = async (srlNo: string, storeId: string) => { + let newSrlNo = srlNo + console.log('srlNo:: ', srlNo) + if (srlNo.startsWith('一時保存')) { + //@ts-ignore + const lastSurvey = await prisma.SD_SURVEY_SALES_BASIC_INFO.findFirst({ + where: { + SRL_NO: { + startsWith: storeId, + }, + }, + orderBy: { + ID: 'desc', + }, + }) + const lastNo = lastSurvey ? parseInt(lastSurvey.SRL_NO.slice(-3)) : 0 + newSrlNo = + storeId + + new Date().getFullYear().toString().slice(-2) + + (new Date().getMonth() + 1).toString().padStart(2, '0') + + new Date().getDate().toString().padStart(2, '0') + + (lastNo + 1).toString().padStart(3, '0') + } + return newSrlNo +} + export async function PUT(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { try { const { id } = await params const body = await request.json() - const { detailInfo, ...basicInfo } = body + const { detailInfo, ...basicInfo } = body.survey + // PUT 요청 시 임시저장 여부 확인 후 임시저장 시 기존 SRL_NO 사용, 기본 저장 시 새로운 SRL_NO 생성 + const newSrlNo = body.isTemporary ? body.survey.srlNo : await getNewSrlNo(body.survey.srlNo, body.storeId) // @ts-ignore const survey = await prisma.SD_SURVEY_SALES_BASIC_INFO.update({ where: { ID: Number(id) }, data: { ...convertToSnakeCase(basicInfo), + SRL_NO: newSrlNo, UPT_DT: new Date(), DETAIL_INFO: { - update: convertToSnakeCase(detailInfo) - } + update: convertToSnakeCase(detailInfo), + }, }, include: { - DETAIL_INFO: true - } + DETAIL_INFO: true, + }, }) return NextResponse.json(survey) } catch (error) { @@ -84,49 +113,24 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise< const { id } = await params const body = await request.json() - if (body.submit) { + // 제출 시 기존 SRL_NO 확인 후 '임시저장'으로 시작하면 새로운 SRL_NO 생성 + const newSrlNo = await getNewSrlNo(body.srlNo, body.storeId) + + if (body.targetId) { // @ts-ignore const survey = await prisma.SD_SURVEY_SALES_BASIC_INFO.update({ where: { ID: Number(id) }, data: { SUBMISSION_STATUS: true, SUBMISSION_DATE: new Date(), + SUBMISSION_TARGET_ID: body.targetId, UPT_DT: new Date(), + SRL_NO: newSrlNo, }, }) + console.log(survey) 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: 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 64c520f..96cfce4 100644 --- a/src/app/api/survey-sales/route.ts +++ b/src/app/api/survey-sales/route.ts @@ -1,6 +1,7 @@ import { NextResponse } from 'next/server' import { prisma } from '@/libs/prisma' import { convertToSnakeCase } from '@/utils/common-utils' +import { equal } from 'assert' /** * 검색 파라미터 */ @@ -110,13 +111,14 @@ const createMemberRoleCondition = (params: SearchParams): WhereCondition => { switch (params.role) { case 'Admin': // 1차점 - // 같은 판매점에서 작성된 매물 + 2차점에서 제출받은 매물 where.OR = [ { + // 같은 판매점에서 작성한 제출/제출되지 않은 매물 AND: [{ STORE: { equals: params.store } }], }, { - AND: [{ STORE: { startsWith: params.store } }, { SUBMISSION_STATUS: { equals: true } }], + // MUSUBI (시공권한 X) 가 ORDER 에 제출한 매물 + AND: [{ SUBMISSION_TARGET_ID: { equals: params.store } }, { SUBMISSION_STATUS: { equals: true } }], }, ] break @@ -124,6 +126,7 @@ const createMemberRoleCondition = (params: SearchParams): WhereCondition => { case 'Admin_Sub': // 2차점 where.OR = [ { + // MUSUBI (시공권한 X) 같은 판매점에서 작성한 제출/제출되지 않은 매물 AND: [ { STORE: { equals: params.store } }, { @@ -132,8 +135,9 @@ const createMemberRoleCondition = (params: SearchParams): WhereCondition => { ], }, { + // MUSUBI (시공권한 O) 가 MUSUBI 에 제출한 매물 + PARTNER 가 제출한 매물 AND: [ - { STORE: { equals: params.store } }, + { SUBMISSION_TARGET_ID: { equals: params.store } }, { CONSTRUCTION_POINT: { not: null } }, { CONSTRUCTION_POINT: { not: '' } }, { SUBMISSION_STATUS: { equals: true } }, @@ -142,8 +146,8 @@ const createMemberRoleCondition = (params: SearchParams): WhereCondition => { ] break - case 'Builder': // 2차점 시공권한 - case 'Partner': // Partner + case 'Builder': // MUSUBI (시공권한 O) + case 'Partner': // PARTNER // 같은 시공ID에서 작성된 매물 where.AND?.push({ CONSTRUCTION_POINT: { equals: params.builderNo }, @@ -151,7 +155,14 @@ const createMemberRoleCondition = (params: SearchParams): WhereCondition => { break case 'T01': - where.AND.push(filterTempData()) + // where.AND.push(filterTempData()) + where.AND?.push({ + NOT: { + SRL_NO: { + startsWith: '一時保存', + }, + }, + }) break case 'User': // 모든 매물 조회 가능 (추가 조건 없음) @@ -247,21 +258,45 @@ export async function PUT(request: Request) { export async function POST(request: Request) { try { const body = await request.json() - console.log('body:: ', body) - const { detailInfo, ...basicInfo } = body + // 임시 저장 시 임시저장 + 000 으로 저장 + // 기본 저장 시 판매점ID + yyMMdd + 000 으로 저장 + const baseSrlNo = + body.survey.srlNo ?? + body.storeId + + new Date().getFullYear().toString().slice(-2) + + (new Date().getMonth() + 1).toString().padStart(2, '0') + + new Date().getDate().toString().padStart(2, '0') - // 기본 정보 생성 - //@ts-ignore + // @ts-ignore + const lastSurvey = await prisma.SD_SURVEY_SALES_BASIC_INFO.findFirst({ + where: { + SRL_NO: { + startsWith: body.storeId, + }, + }, + orderBy: { + SRL_NO: 'desc', + }, + }) + + // 마지막 번호 추출 + const lastNumber = lastSurvey ? parseInt(lastSurvey.SRL_NO.slice(-3)) : 0 + + // 새로운 srlNo 생성 + const newSrlNo = baseSrlNo + (lastNumber + 1).toString().padStart(3, '0') + + const { detailInfo, ...basicInfo } = body.survey + // @ts-ignore const result = await prisma.SD_SURVEY_SALES_BASIC_INFO.create({ data: { ...convertToSnakeCase(basicInfo), + SRL_NO: newSrlNo, DETAIL_INFO: { create: convertToSnakeCase(detailInfo), }, }, }) - console.log('result:: ', result) return NextResponse.json(result) } catch (error) { console.error(error) diff --git a/src/components/survey-sale/detail/BasicForm.tsx b/src/components/survey-sale/detail/BasicForm.tsx index 0942abb..b984101 100644 --- a/src/components/survey-sale/detail/BasicForm.tsx +++ b/src/components/survey-sale/detail/BasicForm.tsx @@ -25,7 +25,7 @@ export default function BasicForm(props: { basicInfo: SurveyBasicRequest; setBas setBasicInfo({ ...basicInfo, representative: session.userNm ?? '', - store: session.storeNm ?? null, + store: session.role === 'Partner' ? null : session.storeNm ?? null, constructionPoint: session.builderNo ?? null, }) } diff --git a/src/components/survey-sale/detail/ButtonForm.tsx b/src/components/survey-sale/detail/ButtonForm.tsx index 6eae13f..c179fb7 100644 --- a/src/components/survey-sale/detail/ButtonForm.tsx +++ b/src/components/survey-sale/detail/ButtonForm.tsx @@ -10,7 +10,6 @@ export default function ButtonForm(props: { mode: Mode setMode: (mode: Mode) => void data: { basic: SurveyBasicRequest; roof: SurveyDetailRequest } - isTemporarySave: boolean }) { // 라우터 const router = useRouter() @@ -28,6 +27,9 @@ export default function ButtonForm(props: { ...props.data.basic, detailInfo: props.data.roof, }) + + // !!!!!!!!!! + const [tempTargetId, setTempTargetId] = useState('') // -------------------------------------------------------------- // 권한 @@ -39,7 +41,26 @@ export default function ButtonForm(props: { useEffect(() => { if (session?.isLoggedIn) { - setIsSubmiter(session.storeNm === props.data.basic.store && session.builderNo === props.data.basic.constructionPoint) + switch (session?.role) { + // T01 제출권한 없음 + case 'T01': + setIsSubmiter(false) + break + // 1차 판매점(Order) + 2차 판매점(Musubi) => 같은 판매점 제출권한 + case 'Admin': + case 'Admin_Sub': + setIsSubmiter(session.storeNm === props.data.basic.store && session.builderNo === props.data.basic.constructionPoint) + break + // 시공권한 User(Musubi) + Partner => 같은 시공ID 제출권한 + case 'Builder': + case 'Partner': + setIsSubmiter(session.builderNo === props.data.basic.constructionPoint) + break + default: + setIsSubmiter(false) + break + } + setIsWriter(session.userNm === props.data.basic.representative) } setSaveData({ @@ -50,8 +71,8 @@ export default function ButtonForm(props: { // ------------------------------------------------------------ // 저장/임시저장/수정 + const id = Number(routeId) ? Number(routeId) : Number(idParam) - const id = routeId ? Number(routeId) : Number(idParam) const { deleteSurvey, submitSurvey, updateSurvey } = useServey(Number(id)) const { validateSurveyDetail, createSurvey } = useServey() @@ -68,11 +89,15 @@ export default function ButtonForm(props: { const tempSaveProcess = async () => { if (idParam) { - await updateSurvey(saveData) - router.push(`/survey-sale/detail?id=${idParam}&isTemporary=true`) + await updateSurvey({ survey: saveData, isTemporary: true }) + router.push(`/survey-sale/${idParam}`) } else { - const id = await createSurvey(saveData) - router.push(`/survey-sale/detail?id=${id}&isTemporary=true`) + const updatedData = { + ...saveData, + srlNo: '一時保存', + } + const id = await createSurvey(updatedData) + router.push(`/survey-sale/${id}`) } alert('一時保存されました。') } @@ -92,11 +117,12 @@ export default function ButtonForm(props: { ...saveData, submissionStatus: true, submissionDate: new Date().toISOString(), + submissionTargetId: tempTargetId, } - await updateSurvey(updatedData) + await updateSurvey({ survey: updatedData, isTemporary: false, storeId: session.storeId ?? '' }) router.push(`/survey-sale/${idParam}`) } else { - await updateSurvey(saveData) + await updateSurvey({ survey: saveData, isTemporary: false, storeId: session.storeId ?? '' }) router.push(`/survey-sale/${idParam}`) } } else { @@ -105,6 +131,7 @@ export default function ButtonForm(props: { ...saveData, submissionStatus: true, submissionDate: new Date().toISOString(), + submissionTargetId: tempTargetId, } const id = await createSurvey(updatedData) submitProcess(id) @@ -137,10 +164,14 @@ export default function ButtonForm(props: { } const handleSubmit = async () => { - if (props.isTemporarySave) { + if (props.data.basic.srlNo?.startsWith('一時保存')) { alert('一時保存されたデータは提出できません。') return } + if (tempTargetId.trim() === '') { + alert('提出対象店舗を入力してください。') + return + } window.neoConfirm('提出しますか?', async () => { if (Number(routeId)) { submitProcess() @@ -149,8 +180,9 @@ export default function ButtonForm(props: { } }) } + const submitProcess = async (saveId?: number) => { - await submitSurvey(saveId) + await submitSurvey({ saveId: saveId, targetId: tempTargetId, storeId: session.storeId ?? '', srlNo: '一時保存' }) alert('提出されました。') router.push('/survey-sale') } @@ -176,7 +208,7 @@ export default function ButtonForm(props: { {(isWriter || !isSubmiter) && } - {!isSubmit && isSubmiter && } + {!isSubmit && isSubmiter && } )} @@ -187,7 +219,7 @@ export default function ButtonForm(props: { - + )} @@ -227,15 +259,20 @@ function EditButton(props: { setMode: (mode: Mode) => void; id: string; mode: Mo ) } -function SubmitButton(props: { handleSubmit: () => void }) { - const { handleSubmit } = props +function SubmitButton(props: { handleSubmit: () => void; setTempTargetId: (targetId: string) => void }) { + const { handleSubmit, setTempTargetId } = props return ( -
- {/* 제출 */} - -
+ <> +
+ {/* 제출 */} + +
+
+ setTempTargetId(e.target.value)} /> +
+ ) } @@ -273,7 +310,6 @@ function TempButton(props: { setMode: (mode: Mode) => void; handleSave: (isTempo +
調査物件登録
diff --git a/src/hooks/useSurvey.ts b/src/hooks/useSurvey.ts index 043cfce..1bfa614 100644 --- a/src/hooks/useSurvey.ts +++ b/src/hooks/useSurvey.ts @@ -65,9 +65,9 @@ export function useServey(id?: number): { isDeletingSurvey: boolean createSurvey: (survey: SurveyRegistRequest) => Promise createSurveyDetail: (params: { surveyId: number; surveyDetail: SurveyDetailCoverRequest }) => void - updateSurvey: (survey: SurveyRegistRequest) => void + updateSurvey: ({ survey, isTemporary, storeId }: { survey: SurveyRegistRequest; isTemporary: boolean; storeId?: string }) => void deleteSurvey: () => Promise - submitSurvey: (saveId?: number) => void + submitSurvey: (params: { saveId?: number; targetId?: string; storeId?: string; srlNo?: string }) => void validateSurveyDetail: (surveyDetail: SurveyDetailRequest) => string getZipCode: (zipCode: string) => Promise refetchSurveyList: () => void @@ -119,7 +119,7 @@ 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) + const resp = await axiosInstance(null).post('/api/survey-sales', { survey: survey, storeId: session?.storeId ?? null }) return resp.data.id ?? 0 }, onSuccess: (data) => { @@ -130,10 +130,14 @@ export function useServey(id?: number): { }) const { mutate: updateSurvey, isPending: isUpdatingSurvey } = useMutation({ - mutationFn: async (survey: SurveyRegistRequest) => { + mutationFn: async ({ survey, isTemporary, storeId }: { survey: SurveyRegistRequest; isTemporary: boolean; storeId?: string }) => { 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) + const resp = await axiosInstance(null).put(`/api/survey-sales/${id}`, { + survey: survey, + isTemporary: isTemporary, + storeId: storeId, + }) return resp.data }, onSuccess: () => { @@ -166,11 +170,13 @@ export function useServey(id?: number): { }) const { mutateAsync: submitSurvey } = useMutation({ - mutationFn: async (saveId?: number) => { + mutationFn: async ({ saveId, targetId, storeId, srlNo }: { saveId?: number; targetId?: string; storeId?: string; srlNo?: string }) => { const submitId = saveId ?? id if (!submitId) throw new Error('id is required') const resp = await axiosInstance(null).patch(`/api/survey-sales/${submitId}`, { - submit: true, + targetId, + storeId, + srlNo, }) return resp.data }, diff --git a/src/types/Survey.ts b/src/types/Survey.ts index 36e7aa5..8066836 100644 --- a/src/types/Survey.ts +++ b/src/types/Survey.ts @@ -14,6 +14,8 @@ export type SurveyBasicInfo = { detailInfo: SurveyDetailInfo | null regDt: Date uptDt: Date + submissionTargetId: string | null + srlNo: string | null //판매점IDyyMMdd000 } export type SurveyDetailInfo = { @@ -70,6 +72,8 @@ export type SurveyBasicRequest = { addressDetail: string | null submissionStatus: boolean submissionDate: string | null + submissionTargetId: string | null + srlNo: string | null //판매점IDyyMMdd000 } export type SurveyDetailRequest = { @@ -127,6 +131,8 @@ export type SurveyRegistRequest = { submissionStatus: boolean submissionDate: string | null detailInfo: SurveyDetailRequest | null + submissionTargetId: string | null + srlNo: string | null //판매점IDyyMMdd000 } export type Mode = 'CREATE' | 'EDIT' | 'READ' | 'TEMP' // 등록 | 수정 | 상세 | 임시저장 From a483ffce445d555b9bf2ace112bdd7a0e0d845bf Mon Sep 17 00:00:00 2001 From: keyy1315 Date: Wed, 21 May 2025 18:21:22 +0900 Subject: [PATCH 5/8] fix: Detailed error resolution MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 단열재 유무 기타 input 창 나오지 않는 오류 해결 - T01 임시저장 데이터 조회 가능하도록 로직 수정정 --- src/app/api/survey-sales/route.ts | 41 ++++++------------- .../survey-sale/detail/ButtonForm.tsx | 2 +- .../survey-sale/detail/RoofForm.tsx | 2 +- 3 files changed, 14 insertions(+), 31 deletions(-) diff --git a/src/app/api/survey-sales/route.ts b/src/app/api/survey-sales/route.ts index 96cfce4..551ad0b 100644 --- a/src/app/api/survey-sales/route.ts +++ b/src/app/api/survey-sales/route.ts @@ -78,29 +78,6 @@ const createKeywordSearchCondition = (keyword: string, searchOption: string): Wh return where } -// 임시저장 데이터 필터링 - T01 만 적용 -const filterTempData = () => { - const requiredFields = [ - 'INSTALLATION_SYSTEM', - 'CONSTRUCTION_YEAR', - 'RAFTER_SIZE', - 'RAFTER_PITCH', - 'WATERPROOF_MATERIAL', - 'INSULATION_PRESENCE', - 'STRUCTURE_ORDER', - ] - - return { - DETAIL_INFO: { - is: { - AND: requiredFields.map((field: string) => ({ - OR: [{ [field]: { not: null } }, { [`${field}_ETC`]: { not: null } }], - })), - }, - }, - } -} - /** * 회원 역할별 검색 조건 생성 함수 * @param params 검색 파라미터 @@ -155,14 +132,20 @@ const createMemberRoleCondition = (params: SearchParams): WhereCondition => { break case 'T01': - // where.AND.push(filterTempData()) - where.AND?.push({ - NOT: { - SRL_NO: { - startsWith: '一時保存', + where.OR = [ + { + NOT: { + SRL_NO: { + startsWith: '一時保存', + }, }, }, - }) + { + STORE: { + equals: params.store, + }, + }, + ] break case 'User': // 모든 매물 조회 가능 (추가 조건 없음) diff --git a/src/components/survey-sale/detail/ButtonForm.tsx b/src/components/survey-sale/detail/ButtonForm.tsx index c179fb7..8f8e3dd 100644 --- a/src/components/survey-sale/detail/ButtonForm.tsx +++ b/src/components/survey-sale/detail/ButtonForm.tsx @@ -219,7 +219,7 @@ export default function ButtonForm(props: { - + {session?.role !== 'T01' && }{' '}
)} diff --git a/src/components/survey-sale/detail/RoofForm.tsx b/src/components/survey-sale/detail/RoofForm.tsx index 26228dd..3018296 100644 --- a/src/components/survey-sale/detail/RoofForm.tsx +++ b/src/components/survey-sale/detail/RoofForm.tsx @@ -636,7 +636,7 @@ const RadioSelected = ({ )} - {showEtcOption && ( + {(showEtcOption || column === 'insulationPresence') && (
Date: Thu, 22 May 2025 09:05:41 +0900 Subject: [PATCH 6/8] fix: change the SRL_NO at temporary save --- src/app/api/survey-sales/route.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/api/survey-sales/route.ts b/src/app/api/survey-sales/route.ts index 551ad0b..7d51802 100644 --- a/src/app/api/survey-sales/route.ts +++ b/src/app/api/survey-sales/route.ts @@ -266,8 +266,8 @@ export async function POST(request: Request) { // 마지막 번호 추출 const lastNumber = lastSurvey ? parseInt(lastSurvey.SRL_NO.slice(-3)) : 0 - // 새로운 srlNo 생성 - const newSrlNo = baseSrlNo + (lastNumber + 1).toString().padStart(3, '0') + // 새로운 srlNo 생성 - 임시저장일 경우 '임시저장' 으로 저장 + const newSrlNo = baseSrlNo.startsWith('一時保存') ? baseSrlNo : baseSrlNo + (lastNumber + 1).toString().padStart(3, '0') const { detailInfo, ...basicInfo } = body.survey // @ts-ignore From 0e7de68f2999c91f9235a2b38a415ad05f12d041 Mon Sep 17 00:00:00 2001 From: yoosangwook Date: Thu, 22 May 2025 13:24:39 +0900 Subject: [PATCH 7/8] feat: Add Spinner component and update styles - Introduced a new Spinner component for loading indicators. - Removed unused btn_arr_up.svg asset and related styles. - Updated radio button styles for improved UI consistency. - Added new PDF view styles for enhanced document presentation. - Included spinner styles for better loading visuals. --- public/assets/images/common/btn_arr_up.svg | 3 -- src/components/ui/common/Spinner.tsx | 7 +++ src/styles/base/_button.scss | 8 --- src/styles/base/_check-radio.scss | 2 +- src/styles/components/_index.scss | 4 +- src/styles/components/_pdfview.scss | 57 ++++++++++++++++++++++ src/styles/components/_pop-contents.scss | 6 +-- src/styles/components/_spinner.scss | 37 ++++++++++++++ 8 files changed, 106 insertions(+), 18 deletions(-) delete mode 100644 public/assets/images/common/btn_arr_up.svg create mode 100644 src/components/ui/common/Spinner.tsx create mode 100644 src/styles/components/_pdfview.scss create mode 100644 src/styles/components/_spinner.scss diff --git a/public/assets/images/common/btn_arr_up.svg b/public/assets/images/common/btn_arr_up.svg deleted file mode 100644 index b90389f..0000000 --- a/public/assets/images/common/btn_arr_up.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/src/components/ui/common/Spinner.tsx b/src/components/ui/common/Spinner.tsx new file mode 100644 index 0000000..4a2aa38 --- /dev/null +++ b/src/components/ui/common/Spinner.tsx @@ -0,0 +1,7 @@ +export default function Spinner() { + return ( +
+ +
+ ) +} diff --git a/src/styles/base/_button.scss b/src/styles/base/_button.scss index b4d61a4..c6d11ad 100644 --- a/src/styles/base/_button.scss +++ b/src/styles/base/_button.scss @@ -49,14 +49,6 @@ background-size: cover; margin-left: 12px; } - .btn-arr-up{ - display: block; - width: 10px; - height: 6px; - background: url(/assets/images/common/btn_arr_up.svg)no-repeat center; - background-size: cover; - margin-left: 12px; - } .btn-edit{ display: block; width: 10px; diff --git a/src/styles/base/_check-radio.scss b/src/styles/base/_check-radio.scss index 47c50cb..f6ff573 100644 --- a/src/styles/base/_check-radio.scss +++ b/src/styles/base/_check-radio.scss @@ -201,7 +201,7 @@ } } input:checked + .slider { - background-color: #A8B6C7; + background-color: #0081b5; &:after { content: ''; left: 10px; diff --git a/src/styles/components/_index.scss b/src/styles/components/_index.scss index 4ebdd02..3ae03f1 100644 --- a/src/styles/components/_index.scss +++ b/src/styles/components/_index.scss @@ -1,4 +1,6 @@ @forward 'main'; @forward 'login'; @forward 'pop-contents'; -@forward 'sub'; \ No newline at end of file +@forward 'sub'; +@forward 'pdfview'; +@forward 'spinner'; \ No newline at end of file diff --git a/src/styles/components/_pdfview.scss b/src/styles/components/_pdfview.scss new file mode 100644 index 0000000..8db6cf7 --- /dev/null +++ b/src/styles/components/_pdfview.scss @@ -0,0 +1,57 @@ +@use "../abstracts" as *; + +.pdf-contents{ + padding: 0 20px; + border-top: 1px solid #ececec; +} +.pdf-cont-head{ + align-items: center; + padding: 24px 0 15px; + border-bottom: 2px solid $black-1010; + .pdf-cont-head-tit{ + @include defaultFont($font-s-16, $font-w-600, $black-1010); + margin-bottom: 10px; + } +} +.pdf-cont-head-data-wrap{ + @include flex(20px); + align-items: center; + .pdf-cont-head-data-tit{ + @include defaultFont($font-s-13, $font-w-500, $black-1010); + } + .pdf-cont-head-data{ + @include defaultFont($font-s-13, $font-w-400, #FF5656); + } +} +.pdf-cont-body{ + padding: 24px 0 0; +} +.pdf-data-tit{ + @include defaultFont($font-s-13, $font-w-500, $black-1010); + margin-bottom: 5px; +} +.pdf-table{ + margin-bottom: 24px; + table{ + width: 100%; + table-layout: fixed; + border-collapse: collapse; + th{ + padding: 9.5px; + @include defaultFont($font-s-11, $font-w-500, $black-1010); + border: 1px solid #2E3A59; + background-color: #F5F6FA; + } + td{ + padding: 9.5px; + @include defaultFont($font-s-11, $font-w-400, #FF5656); + border: 1px solid #2E3A59; + } + } +} +.pdf-textarea-data{ + padding: 10px; + @include defaultFont($font-s-11, $font-w-400, #FF5656); + border: 1px solid $black-1010; + min-height: 150px; +} \ No newline at end of file diff --git a/src/styles/components/_pop-contents.scss b/src/styles/components/_pop-contents.scss index f0f18d5..b598397 100644 --- a/src/styles/components/_pop-contents.scss +++ b/src/styles/components/_pop-contents.scss @@ -103,16 +103,12 @@ @include defaultFont($font-s-13, $font-w-400, $font-c); } .pop-data-table-footer{ - @include flex(0px); .pop-data-table-footer-unit{ - flex: 1; padding: 10px; @include defaultFont($font-s-13, $font-w-500, $font-c); - border-right: 1px solid #2E3A59; + border-bottom: 1px solid #2E3A59; } .pop-data-table-footer-data{ - flex: none; - width: 104px; padding: 10px; @include defaultFont($font-s-13, $font-w-400, $font-c); } diff --git a/src/styles/components/_spinner.scss b/src/styles/components/_spinner.scss new file mode 100644 index 0000000..f37f12d --- /dev/null +++ b/src/styles/components/_spinner.scss @@ -0,0 +1,37 @@ +.spinner-wrap{ + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100vh; + display: flex; + align-items: center; + justify-content: center; + background-color: rgba($color: #101010, $alpha: 0.5); + z-index: 2000000; +} +.loader { + width: 16px; + height: 16px; + border-radius: 50%; + background-color: #fff; + box-shadow: 32px 0 #fff, -32px 0 #fff; + position: relative; + animation: flash 0.5s ease-out infinite alternate; +} + +@keyframes flash { + 0% { + background-color: #FFF2; + box-shadow: 32px 0 #FFF2, -32px 0 #FFF; + } + 50% { + background-color: #FFF; + box-shadow: 32px 0 #FFF2, -32px 0 #FFF2; + } + 100% { + background-color: #FFF2; + box-shadow: 32px 0 #FFF, -32px 0 #FFF2; + } +} + \ No newline at end of file From 2c5ddad29b39110e542808fde0e2360f7c45b042 Mon Sep 17 00:00:00 2001 From: yoosangwook Date: Thu, 22 May 2025 14:08:23 +0900 Subject: [PATCH 8/8] refactor: Enhance survey hook and integrate spinner functionality - Refactored useSurvey hook to utilize useAxios for API calls. - Added spinner visibility management in EdgeProvider to improve loading feedback. - Cleaned up imports and organized code structure for better readability. --- src/hooks/useAxios.ts | 93 ++++++++++++++++++++++++++++++++++ src/hooks/useSurvey.ts | 12 ++--- src/libs/axios.ts | 1 + src/providers/EdgeProvider.tsx | 10 +++- src/store/spinnerStore.ts | 21 ++++++++ 5 files changed, 130 insertions(+), 7 deletions(-) create mode 100644 src/hooks/useAxios.ts create mode 100644 src/store/spinnerStore.ts diff --git a/src/hooks/useAxios.ts b/src/hooks/useAxios.ts new file mode 100644 index 0000000..ac59166 --- /dev/null +++ b/src/hooks/useAxios.ts @@ -0,0 +1,93 @@ +import axios from 'axios' +import { useSpinnerStore } from '@/store/spinnerStore' + +export const useAxios = () => { + const { setIsShow } = useSpinnerStore() + + const axiosInstance = (url: string | null | undefined) => { + const baseURL = url || process.env.NEXT_PUBLIC_API_URL + const instance = axios.create({ + baseURL, + headers: { + Accept: 'application/json', + }, + }) + + instance.interceptors.request.use( + (config) => { + // console.log('🚀 ~ config:', config) + setIsShow(true) + return config + }, + (error) => { + return Promise.reject(error) + }, + ) + + instance.interceptors.response.use( + (response) => { + response.data = transferResponse(response) + setIsShow(false) + return response + }, + (error) => { + // 에러 처리 로직 + return Promise.reject(error) + }, + ) + + return instance + } + + // response데이터가 array, object에 따라 분기하여 키 변환 + const transferResponse = (response: any) => { + if (!response.data) return response.data + + // 배열인 경우 각 객체의 키를 변환 + if (Array.isArray(response.data)) { + return response.data.map((item: any) => transformObjectKeys(item)) + } + + // 단일 객체인 경우 + return transformObjectKeys(response.data) + } + + // camel case object 반환 + const transformObjectKeys = (obj: any): any => { + if (Array.isArray(obj)) { + return obj.map(transformObjectKeys) + } + + if (obj !== null && typeof obj === 'object') { + return Object.keys(obj).reduce((acc: any, key: string) => { + let transformedKey = key + + // Handle uppercase snake_case (e.g., USER_NAME -> userName) + // 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) + else if (/^[A-Z]+$/.test(key)) { + transformedKey = key.toLowerCase() + } + // Preserve existing camelCase + + acc[transformedKey] = transformObjectKeys(obj[key]) + return acc + }, {}) + } + + return obj + } + + const snakeToCamel = (str: string): string => { + return str.toLowerCase().replace(/([-_][a-z])/g, (group) => group.toUpperCase().replace('-', '').replace('_', '')) + } + + return { + axiosInstance, + transferResponse, + transformObjectKeys, + } +} diff --git a/src/hooks/useSurvey.ts b/src/hooks/useSurvey.ts index 1bfa614..6610cea 100644 --- a/src/hooks/useSurvey.ts +++ b/src/hooks/useSurvey.ts @@ -1,11 +1,10 @@ -import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' 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' +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import { useSurveyFilterStore } from '@/store/surveyFilterStore' +import { useSessionStore } from '@/store/session' +import { useAxios } from './useAxios' +import { queryStringFormatter } from '@/utils/common-utils' export const requiredFields = [ { @@ -75,6 +74,7 @@ export function useServey(id?: number): { const queryClient = useQueryClient() const { keyword, searchOption, isMySurvey, sort, offset } = useSurveyFilterStore() const { session } = useSessionStore() + const { axiosInstance } = useAxios() const { data: surveyListData, diff --git a/src/libs/axios.ts b/src/libs/axios.ts index 0abc6ab..6007c47 100644 --- a/src/libs/axios.ts +++ b/src/libs/axios.ts @@ -1,3 +1,4 @@ +import { useSpinnerStore } from '@/store/spinnerStore' import axios from 'axios' export const axiosInstance = (url: string | null | undefined) => { diff --git a/src/providers/EdgeProvider.tsx b/src/providers/EdgeProvider.tsx index 77b8edd..e04f0e5 100644 --- a/src/providers/EdgeProvider.tsx +++ b/src/providers/EdgeProvider.tsx @@ -8,6 +8,8 @@ import { usePopupController } from '@/store/popupController' import { useSideNavState } from '@/store/sideNavState' import { useSessionStore } from '@/store/session' import { tracking } from '@/libs/tracking' +import Spinner from '@/components/ui/common/Spinner' +import { useSpinnerStore } from '@/store/spinnerStore' declare global { interface Window { @@ -28,6 +30,7 @@ export default function EdgeProvider({ children, sessionData }: EdgeProviderProp const { reset } = useSideNavState() const { setAlertMsg, setAlertBtn, setAlert, setAlert2, setAlert2BtnYes, setAlert2BtnNo } = usePopupController() const { session, setSession } = useSessionStore() + const { isShow, setIsShow } = useSpinnerStore() /** * 사용자 이벤트 트래킹 처리 @@ -110,5 +113,10 @@ export default function EdgeProvider({ children, sessionData }: EdgeProviderProp handlePageEvent(pathname) }, [pathname]) - return <>{children} + return ( + <> + {children} + {isShow && } + + ) } diff --git a/src/store/spinnerStore.ts b/src/store/spinnerStore.ts new file mode 100644 index 0000000..976382e --- /dev/null +++ b/src/store/spinnerStore.ts @@ -0,0 +1,21 @@ +import { create } from 'zustand' + +type SpinnerState = { + isShow: boolean + setIsShow: (isShow: boolean) => void + resetCount: () => void +} + +type InitialState = { + isShow: boolean +} + +const initialState: InitialState = { + isShow: false, +} + +export const useSpinnerStore = create((set) => ({ + ...initialState, + setIsShow: (isShow: boolean) => set({ isShow }), + resetCount: () => set(initialState), +}))