use anyhow::{Context, Result}; use log::{debug, error, info, warn}; use serde::Deserialize; use std::collections::HashMap; use std::fs; use std::path::{Path, PathBuf}; // 문자열 정리 및 기본값("N/A") 설정 함수 fn clean_string(s: Option<&str>) -> String { match s { Some(val) => { let trimmed = val.trim(); if trimmed.is_empty() { "N/A".to_string() // 비어 있으면 "N/A" 반환 } else { trimmed.to_string() // 앞뒤 공백 제거 } } None => "N/A".to_string(), // Option이 None이면 "N/A" 반환 } } // JSON 파일의 원본 데이터 구조체 #[derive(Deserialize, Debug, Clone)] #[serde(rename_all = "PascalCase")] pub struct RawHwInfo { pub hostname: Option, pub category: Option, pub manufacturer: Option, pub model: Option, pub serial: Option, // 실제 시리얼 (합성 키 생성에 사용될 수 있음) pub spec_value: Option, pub spec_unit: Option, pub port_or_slot: Option, // 합성 키 생성에 사용 } // 처리된 하드웨어 정보 구조체 (실제 사용될 데이터) #[derive(Debug, Clone)] pub struct ProcessedHwInfo { pub hostname: String, pub category: String, pub manufacturer: String, pub model: String, pub serial_key: String, // <-- 합성 키 (비교 및 DB 전달용) pub spec_value: String, pub spec_unit: String, } // 지정된 디렉토리에서 패턴에 맞는 JSON 파일 목록 찾기 pub fn find_json_files(dir_path: &str) -> Result> { let mut json_files = Vec::new(); let path = Path::new(dir_path); // 경로 유효성 검사 if !path.is_dir() { anyhow::bail!("제공된 JSON 경로가 디렉토리가 아닙니다: {}", dir_path); } // 디렉토리 순회하며 파일 찾기 for entry in fs::read_dir(path).with_context(|| format!("디렉토리 읽기 오류: {}", dir_path))? { let entry = entry.context("디렉토리 항목 읽기 실패")?; let path = entry.path(); if path.is_file() { // 파일 이름 패턴 확인 (HWInfo_*.json) if let Some(filename) = path.file_name().and_then(|n| n.to_str()) { if filename.starts_with("HWInfo_") && filename.ends_with(".json") { debug!("발견된 JSON 파일: {:?}", path); json_files.push(path); } } } } Ok(json_files) } // 단일 JSON 파일 파싱 및 데이터 처리 함수 fn parse_and_process_single_json(path: &Path) -> Result> { // 파일 읽기 및 BOM 처리 let bytes = fs::read(path).with_context(|| format!("JSON 파일 읽기 실패: {:?}", path))?; let json_content_without_bom = if bytes.starts_with(&[0xEF, 0xBB, 0xBF]) { std::str::from_utf8(&bytes[3..]).with_context(|| format!("UTF-8 변환 실패(BOM제거): {:?}", path))? } else { std::str::from_utf8(&bytes).with_context(|| format!("UTF-8 변환 실패: {:?}", path))? }; // 빈 파일 처리 if json_content_without_bom.trim().is_empty() { warn!("JSON 파일 내용이 비어 있습니다: {:?}", path); return Ok(Vec::new()); } // JSON 파싱 let raw_data: Vec = serde_json::from_str(json_content_without_bom) .with_context(|| format!("JSON 파싱 실패: {:?}", path))?; let mut processed_list = Vec::new(); // 처리할 카테고리 목록 (필요시 DB와 동기화 또는 설정 파일로 분리) let relevant_categories = ["CPU", "Mainboard", "Memory", "SSD", "HDD", "VGA"]; for raw_item in raw_data { // 필수 필드(카테고리, 호스트명) 확인 if let (Some(cat_str), Some(host_str)) = (raw_item.category.as_deref(), raw_item.hostname.as_deref()) { let category = clean_string(Some(cat_str)); let hostname = clean_string(Some(host_str)); // 관련 없는 카테고리 또는 유효하지 않은 호스트명 건너뛰기 if hostname == "N/A" || !relevant_categories.contains(&category.as_str()) { continue; } let original_serial = clean_string(raw_item.serial.as_deref()); let port_or_slot = clean_string(raw_item.port_or_slot.as_deref()); let model = clean_string(raw_item.model.as_deref()); // --- 합성 키 생성 로직 --- let serial_key: String; let mut key_parts: Vec = Vec::new(); // 실제 시리얼, 슬롯/포트, 호스트명을 조합하여 고유 키 생성 시도 if original_serial != "N/A" { key_parts.push(original_serial.replace('.', "")); } if port_or_slot != "N/A" { key_parts.push(port_or_slot.replace('.', "")); } if hostname != "N/A" { key_parts.push(hostname.replace('.', "")); } let combined_key = key_parts.iter().map(AsRef::as_ref).collect::>().join("_"); // 조합된 키가 비어있거나 호스트명만 있는 경우, 대체 키 생성 시도 (모델명+호스트명) if combined_key.is_empty() { let model_cleaned = model.replace('.', ""); let hostname_cleaned_fallback = hostname.replace('.', ""); if model != "N/A" && hostname != "N/A" { serial_key = format!("{}_{}", model_cleaned, hostname_cleaned_fallback); debug!("대체 Serial Key 생성 (모델+호스트): {}", serial_key); } else { // 대체 키 생성도 불가능하면 해당 항목 건너뛰기 warn!("고유 Key 생성 불가 (시리얼/슬롯/모델 정보 부족), 항목 건너<0xEB><0x9B><0x81>: {:?}", raw_item); continue; } } else { serial_key = combined_key; } // --- 합성 키 생성 로직 끝 --- // 처리된 데이터 구조체 생성 let processed = ProcessedHwInfo { hostname: hostname.clone(), category, // clean_string 이미 적용됨 manufacturer: clean_string(raw_item.manufacturer.as_deref()), model, // clean_string 이미 적용됨 serial_key, // 생성된 합성 키 spec_value: clean_string(raw_item.spec_value.as_deref()), spec_unit: clean_string(raw_item.spec_unit.as_deref()), }; processed_list.push(processed); } else { // 필수 필드 누락 시 경고 로그 warn!("필수 필드(Category 또는 Hostname) 누락 항목 건너<0xEB><0x9B><0x81>: {:?}", raw_item); } } Ok(processed_list) } // 모든 JSON 파일을 읽고 처리하여 호스트명 기준으로 그룹화하는 함수 pub fn read_and_process_json_files( json_dir_path: &str, ) -> Result>> { info!("'{}' 디렉토리에서 JSON 파일 검색 및 처리 시작...", json_dir_path); let json_files = find_json_files(json_dir_path)?; info!("총 {}개의 JSON 파일 발견.", json_files.len()); let mut all_processed_data: HashMap> = HashMap::new(); // 각 JSON 파일 처리 for json_path in json_files { debug!("처리 중인 파일: {:?}", json_path); match parse_and_process_single_json(&json_path) { Ok(processed_items) => { if !processed_items.is_empty() { // 처리된 데이터를 호스트명 기준으로 그룹화 for item in processed_items { all_processed_data.entry(item.hostname.clone()).or_default().push(item); } } else { // 처리할 데이터가 없는 경우 디버그 로그 debug!("파일에서 처리할 관련 카테고리 데이터가 없습니다: {:?}", json_path); } } Err(e) => { // 파일 처리 중 오류 발생 시 에러 로그 error!("파일 처리 중 오류 발생 {:?}: {}", json_path, e); } } } info!("JSON 파일 처리 완료. 총 {}개 호스트 데이터 생성.", all_processed_data.len()); Ok(all_processed_data) }