feature/survey : 조사매물 지붕정보 공통코드 값 추가 #122

Merged
seul merged 3 commits from feature/survey into dev 2025-08-06 09:43:10 +09:00
3 changed files with 284 additions and 32 deletions

View File

@ -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: {
<SelectedBox mode={mode} column="openFieldPlateKind" detailInfoData={roofInfo as SurveyDetailInfo} setRoofInfo={setRoofInfo} />
</div>
</div>
{roofInfo.openFieldPlateKind === '4' && (
{roofInfo.openFieldPlateKind === 'S' && (
<div className="data-input-form-bx">
{/* 노지판 두께 */}
<div className="data-input-form-tit">
@ -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>(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<HTMLSelectElement>) => {
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) => (
<option key={item.id} value={item.id}>
<option key={item.code ?? String(item.id)} value={item.code ?? String(item.id)}>
{item.name}
</option>
))}
@ -395,6 +401,7 @@ const RadioSelected = ({
const [etcChecked, setEtcChecked] = useState<boolean>(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) => (
<div className="radio-form-box mb10" key={item.id}>
<div className="radio-form-box mb10" key={item.code ?? String(item.id)}>
<input
type="radio"
name={column}
id={`${column}_${item.id}`}
id={`${column}_${item.code ?? item.id}`}
disabled={mode === 'READ'}
checked={Number(selectedId) === item.id}
checked={Number(selectedId) === (item.code ?? item.id)}
onChange={handleRadioChange}
value={item.id}
value={item.code ?? item.id}
/>
<label htmlFor={`${column}_${item.id}`}>{item.label}</label>
<label htmlFor={`${column}_${item.code ?? item.id}`}>{item.label}</label>
</div>
))}
{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>(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 = ({
<input
type="checkbox"
id={`${column}_${item.id}`}
checked={selectedValues.includes(String(item.id))}
checked={selectedValues.includes(item.code ?? String(item.id))}
disabled={mode === 'READ'}
onChange={() => handleCheckbox(item.id)}
onChange={() => handleCheckbox(item)}
/>
<label htmlFor={`${column}_${item.id}`}>{item.name}</label>
</div>

View File

@ -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<void>
}
export const useSurveyOptionStore = create<SurveyOptionState>((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<CommCode[]>('/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 })
}
},
}))

View File

@ -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<SelectBoxKeys, { id: number; name: string }[]> = {
export const selectBoxOptions: Record<SelectBoxKeys, { id: number; code: string | null; name: string }[]> = {
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<RadioEtcKeys, { id: number; label: string }[]> = {
export const radioEtcData: Record<RadioEtcKeys, { id: number; code: string | null; label: string }[]> = {
structureOrder: [
{
/** 지붕재 - 방수재 - 지붕의기초 - 서까래 */
id: 1,
code: null,
label: '屋根材 > 防水材 > 屋根の基礎 > 垂木',
},
],
@ -497,6 +529,7 @@ export const radioEtcData: Record<RadioEtcKeys, { id: number; label: string }[]>
{
/** 목재 */
id: 1,
code: null,
label: '木製',
},
],
@ -504,11 +537,13 @@ export const radioEtcData: Record<RadioEtcKeys, { id: number; label: string }[]>
{
/** 목재 */
id: 1,
code: null,
label: '木製',
},
{
/** 강재 */
id: 2,
code: null,
label: '強制',
},
],
@ -516,6 +551,7 @@ export const radioEtcData: Record<RadioEtcKeys, { id: number; label: string }[]>
{
/** 아스팔트 지붕 940(22kg 이상) */
id: 1,
code: null,
label: 'アスファルト屋根94022kg以上',
},
],
@ -523,11 +559,13 @@ export const radioEtcData: Record<RadioEtcKeys, { id: number; label: string }[]>
{
/** 없음 */
id: 1,
code: null,
label: 'なし',
},
{
/** 있음 */
id: 2,
code: null,
label: 'あり',
},
],
@ -535,11 +573,13 @@ export const radioEtcData: Record<RadioEtcKeys, { id: number; label: string }[]>
{
/** 수직 */
id: 1,
code: null,
label: '垂直垂木',
},
{
/** 수평 */
id: 2,
code: null,
label: '水平垂木',
},
],
@ -547,12 +587,14 @@ export const radioEtcData: Record<RadioEtcKeys, { id: number; label: string }[]>
{
/** 있음 */
id: 1,
code: null,
label: 'あり',
},
{
/** 없음 */
id: 2,
code: null,
label: 'なし',
},
],
}
}