"use client"; /** * 계층구조 코드 선택 컴포넌트 (1단계, 2단계, 3단계 셀렉트박스) * * @example * // 기본 사용 * setSelectedCode(code)} * /> * * @example * // 특정 depth까지만 선택 * setSelectedCode(code)} * /> */ import React, { useState, useEffect, useCallback, useMemo } from "react"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Label } from "@/components/ui/label"; import { commonCodeApi } from "@/lib/api/commonCode"; import { Loader2 } from "lucide-react"; import type { CodeInfo } from "@/types/commonCode"; export interface HierarchicalCodeSelectProps { /** 코드 카테고리 */ categoryCode: string; /** 최대 깊이 (1, 2, 3) */ maxDepth?: 1 | 2 | 3; /** 현재 선택된 값 (최종 선택된 코드값) */ value?: string; /** 값 변경 핸들러 */ onChange?: (codeValue: string, codeInfo?: CodeInfo, fullPath?: CodeInfo[]) => void; /** 각 단계별 라벨 */ labels?: [string, string?, string?]; /** 각 단계별 placeholder */ placeholders?: [string, string?, string?]; /** 비활성화 */ disabled?: boolean; /** 필수 입력 */ required?: boolean; /** 메뉴 OBJID (메뉴 기반 필터링) */ menuObjid?: number; /** 추가 클래스 */ className?: string; /** 인라인 표시 (가로 배열) */ inline?: boolean; } interface LoadingState { level1: boolean; level2: boolean; level3: boolean; } export function HierarchicalCodeSelect({ categoryCode, maxDepth = 3, value, onChange, labels = ["1단계", "2단계", "3단계"], placeholders = ["선택하세요", "선택하세요", "선택하세요"], disabled = false, required = false, menuObjid, className = "", inline = false, }: HierarchicalCodeSelectProps) { // 각 단계별 옵션 const [level1Options, setLevel1Options] = useState([]); const [level2Options, setLevel2Options] = useState([]); const [level3Options, setLevel3Options] = useState([]); // 각 단계별 선택값 const [selectedLevel1, setSelectedLevel1] = useState(""); const [selectedLevel2, setSelectedLevel2] = useState(""); const [selectedLevel3, setSelectedLevel3] = useState(""); // 로딩 상태 const [loading, setLoading] = useState({ level1: false, level2: false, level3: false, }); // 모든 코드 데이터 (경로 추적용) const [allCodes, setAllCodes] = useState([]); // 1단계 코드 로드 (최상위) const loadLevel1Codes = useCallback(async () => { if (!categoryCode) return; setLoading(prev => ({ ...prev, level1: true })); try { const response = await commonCodeApi.hierarchy.getHierarchicalCodes( categoryCode, null, // 부모 없음 (최상위) 1, // depth = 1 menuObjid ); if (response.success && response.data) { setLevel1Options(response.data); setAllCodes(prev => { const filtered = prev.filter(c => c.depth !== 1); return [...filtered, ...response.data]; }); } } catch (error) { console.error("1단계 코드 로드 실패:", error); } finally { setLoading(prev => ({ ...prev, level1: false })); } }, [categoryCode, menuObjid]); // 2단계 코드 로드 (1단계 선택값 기준) const loadLevel2Codes = useCallback(async (parentCodeValue: string) => { if (!categoryCode || !parentCodeValue) { setLevel2Options([]); return; } setLoading(prev => ({ ...prev, level2: true })); try { const response = await commonCodeApi.hierarchy.getHierarchicalCodes( categoryCode, parentCodeValue, undefined, menuObjid ); if (response.success && response.data) { setLevel2Options(response.data); setAllCodes(prev => { const filtered = prev.filter(c => c.depth !== 2 || (c.parentCodeValue || c.parent_code_value) !== parentCodeValue); return [...filtered, ...response.data]; }); } } catch (error) { console.error("2단계 코드 로드 실패:", error); } finally { setLoading(prev => ({ ...prev, level2: false })); } }, [categoryCode, menuObjid]); // 3단계 코드 로드 (2단계 선택값 기준) const loadLevel3Codes = useCallback(async (parentCodeValue: string) => { if (!categoryCode || !parentCodeValue) { setLevel3Options([]); return; } setLoading(prev => ({ ...prev, level3: true })); try { const response = await commonCodeApi.hierarchy.getHierarchicalCodes( categoryCode, parentCodeValue, undefined, menuObjid ); if (response.success && response.data) { setLevel3Options(response.data); setAllCodes(prev => { const filtered = prev.filter(c => c.depth !== 3 || (c.parentCodeValue || c.parent_code_value) !== parentCodeValue); return [...filtered, ...response.data]; }); } } catch (error) { console.error("3단계 코드 로드 실패:", error); } finally { setLoading(prev => ({ ...prev, level3: false })); } }, [categoryCode, menuObjid]); // 초기 로드 및 카테고리 변경 시 useEffect(() => { loadLevel1Codes(); setSelectedLevel1(""); setSelectedLevel2(""); setSelectedLevel3(""); setLevel2Options([]); setLevel3Options([]); }, [loadLevel1Codes]); // value prop 변경 시 역추적 (외부에서 값이 설정된 경우) useEffect(() => { if (!value || allCodes.length === 0) return; // 선택된 코드 찾기 const selectedCode = allCodes.find(c => (c.codeValue || c.code_value) === value ); if (!selectedCode) return; const depth = selectedCode.depth || 1; if (depth === 1) { setSelectedLevel1(value); setSelectedLevel2(""); setSelectedLevel3(""); } else if (depth === 2) { const parentValue = selectedCode.parentCodeValue || selectedCode.parent_code_value || ""; setSelectedLevel1(parentValue); setSelectedLevel2(value); setSelectedLevel3(""); loadLevel2Codes(parentValue); } else if (depth === 3) { const parentValue = selectedCode.parentCodeValue || selectedCode.parent_code_value || ""; // 2단계 부모 찾기 const level2Code = allCodes.find(c => (c.codeValue || c.code_value) === parentValue); const level1Value = level2Code?.parentCodeValue || level2Code?.parent_code_value || ""; setSelectedLevel1(level1Value); setSelectedLevel2(parentValue); setSelectedLevel3(value); loadLevel2Codes(level1Value); loadLevel3Codes(parentValue); } }, [value, allCodes]); // 1단계 선택 변경 const handleLevel1Change = (codeValue: string) => { setSelectedLevel1(codeValue); setSelectedLevel2(""); setSelectedLevel3(""); setLevel2Options([]); setLevel3Options([]); if (codeValue && maxDepth > 1) { loadLevel2Codes(codeValue); } // 최대 깊이가 1이면 즉시 onChange 호출 if (maxDepth === 1 && onChange) { const selectedCodeInfo = level1Options.find(c => (c.codeValue || c.code_value) === codeValue); onChange(codeValue, selectedCodeInfo, selectedCodeInfo ? [selectedCodeInfo] : []); } }; // 2단계 선택 변경 const handleLevel2Change = (codeValue: string) => { setSelectedLevel2(codeValue); setSelectedLevel3(""); setLevel3Options([]); if (codeValue && maxDepth > 2) { loadLevel3Codes(codeValue); } // 최대 깊이가 2이면 onChange 호출 if (maxDepth === 2 && onChange) { const level1Code = level1Options.find(c => (c.codeValue || c.code_value) === selectedLevel1); const level2Code = level2Options.find(c => (c.codeValue || c.code_value) === codeValue); const fullPath = [level1Code, level2Code].filter(Boolean) as CodeInfo[]; onChange(codeValue, level2Code, fullPath); } }; // 3단계 선택 변경 const handleLevel3Change = (codeValue: string) => { setSelectedLevel3(codeValue); if (onChange) { const level1Code = level1Options.find(c => (c.codeValue || c.code_value) === selectedLevel1); const level2Code = level2Options.find(c => (c.codeValue || c.code_value) === selectedLevel2); const level3Code = level3Options.find(c => (c.codeValue || c.code_value) === codeValue); const fullPath = [level1Code, level2Code, level3Code].filter(Boolean) as CodeInfo[]; onChange(codeValue, level3Code, fullPath); } }; // 최종 선택값 계산 const finalValue = useMemo(() => { if (maxDepth >= 3 && selectedLevel3) return selectedLevel3; if (maxDepth >= 2 && selectedLevel2) return selectedLevel2; if (selectedLevel1) return selectedLevel1; return ""; }, [maxDepth, selectedLevel1, selectedLevel2, selectedLevel3]); // 최종 선택값이 변경되면 onChange 호출 (maxDepth 제한 없이) useEffect(() => { if (!onChange) return; // 현재 선택된 깊이 확인 if (selectedLevel3 && maxDepth >= 3) { // 3단계까지 선택됨 return; // handleLevel3Change에서 처리 } if (selectedLevel2 && maxDepth >= 2 && !selectedLevel3 && level3Options.length === 0) { // 2단계까지 선택되고 3단계 옵션이 없음 const level1Code = level1Options.find(c => (c.codeValue || c.code_value) === selectedLevel1); const level2Code = level2Options.find(c => (c.codeValue || c.code_value) === selectedLevel2); const fullPath = [level1Code, level2Code].filter(Boolean) as CodeInfo[]; onChange(selectedLevel2, level2Code, fullPath); } else if (selectedLevel1 && maxDepth >= 1 && !selectedLevel2 && level2Options.length === 0) { // 1단계까지 선택되고 2단계 옵션이 없음 const level1Code = level1Options.find(c => (c.codeValue || c.code_value) === selectedLevel1); onChange(selectedLevel1, level1Code, level1Code ? [level1Code] : []); } }, [level2Options, level3Options]); const containerClass = inline ? "flex flex-wrap gap-4 items-end" : "space-y-4"; const selectItemClass = inline ? "flex-1 min-w-[150px] space-y-1" : "space-y-1"; return (
{/* 1단계 선택 */}
{/* 2단계 선택 */} {maxDepth >= 2 && (
)} {/* 3단계 선택 */} {maxDepth >= 3 && (
)}
); } export default HierarchicalCodeSelect;