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
+}