diff --git a/src/components/survey-sale/detail/RoofForm.tsx b/src/components/survey-sale/detail/RoofForm.tsx index ff3d0c9..33aded8 100644 --- a/src/components/survey-sale/detail/RoofForm.tsx +++ b/src/components/survey-sale/detail/RoofForm.tsx @@ -1,7 +1,8 @@ -import { useState } from 'react' +import { useEffect, useState } from 'react' import type { Mode, SurveyDetailInfo, SurveyDetailRequest } from '@/types/Survey' import { useAlertMsg, WARNING_MESSAGE } from '@/hooks/useAlertMsg' -import { radioEtcData, selectBoxOptions, supplementaryFacilities, roofMaterial } from '@/types/Survey' +import { radioEtcData, supplementaryFacilities } from '@/types/Survey' +import { useSurveyOptionStore } from '@/store/surveyOptionStore' const makeNumArr = (value: string) => { return value @@ -205,7 +206,7 @@ export default function RoofForm(props: { - {roofInfo.openFieldPlateKind === '4' && ( + {roofInfo.openFieldPlateKind === 'S' && (
{/* 노지판 두께 */}
@@ -292,10 +293,15 @@ const SelectedBox = ({ detailInfoData: SurveyDetailInfo setRoofInfo: (roofInfo: SurveyDetailRequest) => void }) => { + const { selectBoxOptions, initialized, loading, loadOptions } = useSurveyOptionStore() const selectedId = detailInfoData?.[column as keyof SurveyDetailInfo] const etcValue = detailInfoData?.[`${column}Etc` as keyof SurveyDetailInfo] const [isEtcSelected, setIsEtcSelected] = useState(Boolean(etcValue)) + useEffect(() => { + if (!initialized && !loading) loadOptions() + }, [initialized, loading]) + const isSpecialCase = column === 'constructionYear' || column === 'installationAvailability' const showEtcOption = !isSpecialCase @@ -303,7 +309,7 @@ const SelectedBox = ({ const handleSelectChange = (e: React.ChangeEvent) => { const value = e.target.value const isEtc = value === 'etc' - const isSpecialEtc = isSpecialCase && value === '2' + const isSpecialEtc = isSpecialCase && value === 'O' const updatedData = { ...detailInfoData, @@ -333,7 +339,7 @@ const SelectedBox = ({ if (mode === 'READ') return true if (column === 'installationAvailability') return false if (column === 'constructionYear') { - return detailInfoData.constructionYear === '1' || detailInfoData.constructionYear === null + return detailInfoData.constructionYear === 'N' || detailInfoData.constructionYear === null } return !isEtcSelected && !etcValue } @@ -345,11 +351,11 @@ const SelectedBox = ({ name={column} id={column} disabled={mode === 'READ'} - value={selectedId ? Number(selectedId) : etcValue || isEtcSelected ? 'etc' : ''} + value={selectedId ? String(selectedId) : etcValue || isEtcSelected ? 'etc' : ''} onChange={handleSelectChange} > {selectBoxOptions[column as keyof typeof selectBoxOptions].map((item) => ( - ))} @@ -395,6 +401,7 @@ const RadioSelected = ({ const [etcChecked, setEtcChecked] = useState(Boolean(etcValue)) const selectedId = + /** 누수 흔적 boolean 타입이므로 number 타입으로 변환 - 값이 없을 경우 2(없음) 으로 초기화*/ column === 'leakTrace' ? Number(detailInfoData?.[column as keyof SurveyDetailInfo]) || 2 : detailInfoData?.[column as keyof SurveyDetailInfo] const isSpecialColumn = column === 'rafterDirection' || column === 'leakTrace' || column === 'insulationPresence' @@ -452,17 +459,17 @@ const RadioSelected = ({ return ( <> {radioEtcData[column as keyof typeof radioEtcData].map((item) => ( -
+
- +
))} {showEtcOption && ( @@ -509,20 +516,25 @@ const MultiCheck = ({ setRoofInfo: (roofInfo: SurveyDetailRequest) => void }) => { const { showErrorAlert } = useAlertMsg() + const { roofMaterial, initialized, loading, loadOptions } = useSurveyOptionStore() const multiCheckData = column === 'supplementaryFacilities' ? supplementaryFacilities : roofMaterial const etcValue = roofInfo?.[`${column}Etc` as keyof SurveyDetailInfo] const [isOtherCheck, setIsOtherCheck] = useState(Boolean(etcValue)) + useEffect(() => { + if (!initialized && !loading) loadOptions() + }, [initialized, loading]) + const isRoofMaterial = column === 'roofMaterial' const selectedValues = makeNumArr(String(roofInfo[column as keyof SurveyDetailInfo] ?? '')) /** 다중 선택 처리 */ - const handleCheckbox = (id: number) => { + const handleCheckbox = (item: { id: number; code: string | null; name: string }) => { const isOtherSelected = Boolean(etcValue) let newValue: string[] - if (selectedValues.includes(String(id))) { - newValue = selectedValues.filter((v) => v !== String(id)) + if (selectedValues.includes(item.code ?? String(item.id))) { + newValue = selectedValues.filter((v) => v !== item.code && v !== String(item.id)) } else { /** 지붕 재료 처리 - 최대 2개 선택 처리 */ if (isRoofMaterial) { @@ -532,7 +544,7 @@ const MultiCheck = ({ return } } - newValue = [...selectedValues, String(id)] + newValue = [...selectedValues, item.code ?? String(item.id)] } setRoofInfo({ ...roofInfo, [column]: newValue.join(',') }) } @@ -575,9 +587,9 @@ const MultiCheck = ({ handleCheckbox(item.id)} + onChange={() => handleCheckbox(item)} />
diff --git a/src/store/surveyOptionStore.ts b/src/store/surveyOptionStore.ts new file mode 100644 index 0000000..a8d03db --- /dev/null +++ b/src/store/surveyOptionStore.ts @@ -0,0 +1,198 @@ +import { create } from 'zustand' +import { axiosInstance } from '@/libs/axios' +import { selectBoxOptions as defaultSelectBoxOptions, roofMaterial as defaultRoofMaterial, type SelectBoxKeys } from '@/types/Survey' +import { type CommCode } from '@/types/CommCode' + +export const SURVEY_OPTION_HEAD_ID: { [key: string]: string } = { + /* 지붕재 종류 : HEAD_ID = 'ROOF_MATL' (HEAD_CD = 114200) */ + roofMaterial: 'ROOF_MATL', + /* 건축 연수 : HEAD_ID = 'BUILDING' (HEAD_CD = 114000) */ + constructionYear: 'BUILDING', + /* 서까래 피치 : HEAD_ID = 'RAFT_BASE_CD' (HEAD_CD = 203800) */ + rafterPitch: 'RAFT_BASE_CD', + /* 골목판 종류 : HEAD_ID = 'ROOF_BOARD' (HEAD_CD = 114600) */ + openFieldPlateKind: 'ROOF_BOARD', +} + +/* + * 2025-08-04 기준 공통코드 동기화 목록 + * + * 옵션 데이터 공통코드 동기화 목록 : SURVEY_OPTION_HEAD_ID -> roofMaterial, constructionYear, rafterPitch, openFieldPlateKind + * 제약사항 : BC_COMM_L.CODE 데이터 기준으로 code값 하드코딩 데이터 동기화 필요. 공통코드 테이블 내에 데이터가 없는 경우 null 처리. + * 설명 : 공통코드 테이블 데이터로 하드코딩된 code값을 기준으로 code_jp(name)값 업데이트. + * code가 null인 경우 name값 업데이트 하지 않음 (= 공통코드 데이터로 업데이트 하지 않고, 내부 데이터 유지) + * + * comm_cd : { + * // 지붕재 종류 : HEAD_ID = 'ROOF_MATL' (HEAD_CD = 114200) + * roofMaterial : [ + * // 슬레이트 + * { + * head_cd: '114200', + * code: '7', + * code_jp: 'スレート', + * }, + * // 아스팔트 싱글 + * { + * head_cd: '114200', + * code: '8', + * code_jp: 'アスファルトシングル', + * }, + * // 기와 + * { + * head_cd: '114200', + * code: null, + * code_jp: '瓦', + * }, + * // 금속지붕 + * { + * head_cd: '114200', + * code: null, + * code_jp: '金属屋根', + * }, + * ], + * // 건축 연수 : HEAD_ID = 'BUILDING' (HEAD_CD = 114000) + * constructionYear : [ + * // 신축 + * { + * head_cd: '114000', + * code: 'N', + * code_jp: '新築', + * }, + * // 기축 + * { + * head_cd: '114000', + * code: 'O', + * code_jp: '既築', + * }, + * ], + * // 서까래 피치 : HEAD_ID = 'RAFT_BASE_CD' (HEAD_CD = 203800) + * rafterPitch : [ + * // 455mm 이하 + * { + * head_cd: '203800', + * code: 'HEI_455', + * code_jp: '455mm以下', + * }, + * // 500mm 이하 + * { + * head_cd: '203800', + * code: 'HEI_500', + * code_jp: '500mm以下', + * }, + * // 606mm 이하 + * { + * head_cd: '203800', + * code: 'HEI_606', + * code_jp: '606mm以下', + * }, + * ], + * // 골목판 종류 : HEAD_ID = 'ROOF_BOARD' (HEAD_CD = 114600) + * openFieldPlateKind : [ + * // 구조용합판 + * { + * head_cd: '114600', + * code: null, + * code_jp: '構造用合板', + * }, + * // OSB + * { + * head_cd: '114600', + * code: 'O', + * code_jp: 'OSB', + * }, + * // 파티클보드 + * { + * head_cd: '114600', + * code: 'A', + * code_jp: 'パーティクルボード', + * }, + * // 소판 + * { + * head_cd: '114600', + * code: 'S', + * code_jp: '小幅板', + * }, + * ] + * } + */ + +interface SurveyOptionState { + /* 지붕재종류 데이터 */ + roofMaterial: typeof defaultRoofMaterial + /* 셀렉트박스 옵션 데이터 */ + selectBoxOptions: typeof defaultSelectBoxOptions + /* 로딩 여부 */ + loading: boolean + /* 옵션 로드 완료 여부 */ + initialized: boolean + /* 옵션 로드 */ + loadOptions: () => Promise +} + +export const useSurveyOptionStore = create((set, get) => ({ + roofMaterial: defaultRoofMaterial, + selectBoxOptions: defaultSelectBoxOptions, + loading: false, + initialized: false, + + loadOptions: async () => { + if (get().initialized || get().loading) return + + set({ loading: true }) + + try { + const promises = Object.entries(SURVEY_OPTION_HEAD_ID).map(async ([optionKey, headId]) => { + try { + const response = await axiosInstance(process.env.NEXT_PUBLIC_API_URL).get('/api/comm-code', { + params: { headId: headId }, + }) + const commCodeData = response.data + return { optionKey, commCodeData } + } catch (error) { + console.error(`${optionKey} 로드 실패:`, error) + return { optionKey, commCodeData: [] } + } + }) + const results: { optionKey: string; commCodeData: CommCode[] }[] = await Promise.all(promises) + + set((prev) => { + const newState = { + roofMaterial: [...prev.roofMaterial], + selectBoxOptions: { ...prev.selectBoxOptions }, + } + + results.forEach(({ optionKey, commCodeData }) => { + if (optionKey === 'roofMaterial') { + // 지붕재 종류 업데이트 + newState.roofMaterial = newState.roofMaterial.map((item) => { + if (item.code) { + const commCode = commCodeData.find((c) => c.code === item.code) + if (commCode) { + return { ...item, code: commCode.code, name: commCode.codeJp } + } + } + return item + }) + } else { + // 셀렉트박스 옵션 업데이트 + const key = optionKey as SelectBoxKeys + newState.selectBoxOptions[key] = newState.selectBoxOptions[key].map((item) => { + if (item.code) { + const commCode = commCodeData.find((c) => c.code === item.code) + if (commCode) { + return { ...item, code: commCode.code, name: commCode.codeJp } + } + } + return item + }) + } + }) + + return { ...newState, initialized: true, loading: false } + }) + } catch (error) { + console.error('옵션 데이터 로드 중 오류 발생:', error) + set({ loading: false }) + } + }, +})) diff --git a/src/types/Survey.ts b/src/types/Survey.ts index e45ccd3..efd2147 100644 --- a/src/types/Survey.ts +++ b/src/types/Survey.ts @@ -332,7 +332,7 @@ type RadioEtcKeys = | 'insulationPresence' | 'rafterDirection' | 'leakTrace' -type SelectBoxKeys = +export type SelectBoxKeys = | 'installationSystem' | 'constructionYear' | 'roofShape' @@ -341,155 +341,187 @@ type SelectBoxKeys = | 'openFieldPlateKind' | 'installationAvailability' -export const supplementaryFacilities = [ +export const supplementaryFacilities: { id: number; code: string | null; name: string }[] = [ /** 에코큐트 */ - { id: 1, name: 'エコキュート' }, + { id: 1, code: null, name: 'エコキュート' }, /** 에네팜 */ - { id: 2, name: 'エネパーム' }, + { id: 2, code: null, name: 'エネパーム' }, /** 축전지시스템 */ - { id: 3, name: '蓄電池システム' }, + { id: 3, code: null, name: '蓄電池システム' }, /** 태양광발전 */ - { id: 4, name: '太陽光発電' }, + { id: 4, code: null, name: '太陽光発電' }, ] -export const roofMaterial = [ +// 지붕재 종류 : HEAD_ID = 'ROOF_MATL' (114200) +export const roofMaterial: { id: number; code: string | null; name: string }[] = [ /** 슬레이트 */ - { id: 1, name: 'スレート' }, + { id: 1, code: '7', name: 'スレート' }, /** 아스팔트 싱글 */ - { id: 2, name: 'アスファルトシングル' }, + { id: 2, code: '8', name: 'アスファルトシングル' }, /** 기와 */ - { id: 3, name: '瓦' }, + { id: 3, code: null, name: '瓦' }, /** 금속지붕 */ - { id: 4, name: '金属屋根' }, + { id: 4, code: null, name: '金属屋根' }, ] -export const selectBoxOptions: Record = { +export const selectBoxOptions: Record = { installationSystem: [ { /** 태양광발전 */ id: 1, + code: null, name: '太陽光発電', }, { /** 하이브리드축전지시스템 */ id: 2, + code: null, name: 'ハイブリッド蓄電システム', }, { /** 축전지시스템 */ id: 3, + code: null, name: '蓄電池システム', }, ], + + // 건축 연수 : HEAD_ID = 'BUILDING' (114000) constructionYear: [ { /** 신축 */ id: 1, + code: 'N', name: '新築', }, { /** 기축 */ id: 2, + code: 'O', name: '既築', }, ], + roofShape: [ { /** 박공지붕 */ id: 1, + code: null, name: '切妻', }, { /** 기동 */ id: 2, + code: null, name: '寄棟', }, { /** 한쪽흐름 */ id: 3, + code: null, name: '片流れ', }, ], + rafterSize: [ { /** 35mm 이상×48mm 이상 */ id: 1, + code: null, name: '幅35mm以上×高さ48mm以上', }, { /** 36mm 이상×46mm 이상 */ id: 2, + code: null, name: '幅36mm以上×高さ46mm以上', }, { /** 37mm 이상×43mm 이상 */ id: 3, + code: null, name: '幅37mm以上×高さ43mm以上', }, { /** 38mm 이상×40mm 이상 */ id: 4, + code: null, name: '幅38mm以上×高さ40mm以上', }, ], + + // 서까래 피치 : HEAD_ID = 'RAFT_BASE_CD' (203800) rafterPitch: [ { /** 455mm 이하 */ id: 1, + code: 'HEI_455', name: '455mm以下', }, { /** 500mm 이하 */ id: 2, + code: 'HEI_500', name: '500mm以下', }, { /** 606mm 이하 */ id: 3, + code: 'HEI_606', name: '606mm以下', }, ], + + // 골목판 종류 : HEAD_ID = 'ROOF_BOARD' (114600) openFieldPlateKind: [ { /** 구조용합판 */ id: 1, + code: null, name: '構造用合板', }, { /** OSB */ id: 2, + code: 'O', name: 'OSB', }, { /** 파티클보드 */ id: 3, + code: 'A', name: 'パーティクルボード', }, { /** 소판 */ id: 4, + code: 'S', name: '小幅板', }, ], + installationAvailability: [ { /** 확인완료 */ id: 1, + code: null, name: '確認済み', }, { /** 미확인 */ id: 2, + code: null, name: '未確認', }, ], } -export const radioEtcData: Record = { +export const radioEtcData: Record = { structureOrder: [ { /** 지붕재 - 방수재 - 지붕의기초 - 서까래 */ id: 1, + code: null, label: '屋根材 > 防水材 > 屋根の基礎 > 垂木', }, ], @@ -497,6 +529,7 @@ export const radioEtcData: Record { /** 목재 */ id: 1, + code: null, label: '木製', }, ], @@ -504,11 +537,13 @@ export const radioEtcData: Record { /** 목재 */ id: 1, + code: null, label: '木製', }, { /** 강재 */ id: 2, + code: null, label: '強制', }, ], @@ -516,6 +551,7 @@ export const radioEtcData: Record { /** 아스팔트 지붕 940(22kg 이상) */ id: 1, + code: null, label: 'アスファルト屋根940(22kg以上)', }, ], @@ -523,11 +559,13 @@ export const radioEtcData: Record { /** 없음 */ id: 1, + code: null, label: 'なし', }, { /** 있음 */ id: 2, + code: null, label: 'あり', }, ], @@ -535,11 +573,13 @@ export const radioEtcData: Record { /** 수직 */ id: 1, + code: null, label: '垂直垂木', }, { /** 수평 */ id: 2, + code: null, label: '水平垂木', }, ], @@ -547,12 +587,14 @@ export const radioEtcData: Record { /** 있음 */ id: 1, + code: null, label: 'あり', }, { /** 없음 */ id: 2, + code: null, label: 'なし', }, ], -} \ No newline at end of file +}