@charzing/vehicle-utils 0.2.4 → 0.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +313 -56
- package/dist/cjs/index.d.ts +13 -1
- package/dist/cjs/index.js +93 -0
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/report/cellMap.d.ts +106 -0
- package/dist/cjs/report/cellMap.js +119 -0
- package/dist/cjs/report/cellMap.js.map +1 -0
- package/dist/esm/index.d.mts +13 -1
- package/dist/esm/index.mjs +87 -0
- package/dist/esm/index.mjs.map +1 -1
- package/dist/esm/report/cellMap.d.mts +106 -0
- package/dist/esm/report/cellMap.mjs +89 -0
- package/dist/esm/report/cellMap.mjs.map +1 -0
- package/package.json +21 -1
- package/dist/index.d.mts +0 -714
- package/dist/index.d.ts +0 -714
- package/dist/index.js +0 -357
- package/dist/index.js.map +0 -1
package/dist/cjs/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/index.ts","../../src/brand/mapping.ts","../../src/brand/dynamic-mapping.ts","../../src/model/mapping.ts","../../src/model/dynamic-mapping.ts","../../src/image/url-generator.ts"],"sourcesContent":["/**\n * @charzing/vehicle-utils\n *\n * 차징 플랫폼 전체에서 사용하는 차량 데이터 유틸리티\n */\n\n// ===== 타입 =====\nexport type * from './types';\n\n// ===== 브랜드 관련 (정적) =====\nexport {\n normalizeBrandId,\n getBrandNameKorean,\n getBrandNameEnglish,\n getStorageBrandName,\n getBrandMapping,\n SUPPORTED_BRANDS\n} from './brand/mapping';\n\n// ===== 브랜드 관련 (동적) ⭐ NEW =====\nexport {\n initializeBrandMappings,\n getBrandMapping as getDynamicBrandMapping,\n isInitialized as isBrandMappingInitialized,\n resetCache as resetBrandCache,\n getCachedBrandCount\n} from './brand/dynamic-mapping';\n\n// ===== 모델 관련 (정적) =====\nexport {\n toSlug,\n convertKoreanToEnglish,\n normalizeModelId,\n findBestModelMatch,\n getModelSlug,\n getModelMapping\n} from './model/mapping';\n\n// ===== 모델 관련 (동적) ⭐ NEW =====\nexport {\n initializeModelMappings,\n getModelMapping as getDynamicModelMapping,\n getBrandModels,\n isInitialized as isModelMappingInitialized,\n resetCache as resetModelCache,\n getCachedModelCount\n} from './model/dynamic-mapping';\n\n// ===== 이미지 URL/경로 생성 (순수 함수만) ⭐ UPDATED =====\nexport {\n generateImageFilename,\n generateStoragePath,\n generateVehicleImageUrl\n} from './image/url-generator';\n","/**\n * 브랜드 매핑 유틸리티\n *\n * 한글/영문 브랜드명 변환, Firestore ID 정규화, Storage 브랜드명 매핑\n */\n\nimport type { BrandId, BrandMapping, StorageBrandName } from '../types/brand';\n\n/**\n * 한글 → Firestore ID 매핑 테이블\n */\nconst KOREAN_TO_ID: Record<string, BrandId> = {\n '현대': 'hyundai',\n 'HYUNDAI': 'hyundai',\n 'Hyundai': 'hyundai',\n\n '기아': 'kia',\n 'KIA': 'kia',\n 'Kia': 'kia',\n\n '테슬라': 'tesla',\n 'TESLA': 'tesla',\n 'Tesla': 'tesla',\n\n 'BMW': 'bmw',\n 'bmw': 'bmw',\n '비엠더블유': 'bmw',\n\n '메르세데스-벤츠': 'mercedes-benz',\n 'Mercedes-Benz': 'mercedes-benz',\n 'MERCEDES-BENZ': 'mercedes-benz',\n '벤츠': 'mercedes-benz',\n 'Benz': 'mercedes-benz',\n 'BENZ': 'mercedes-benz',\n\n '메르세데스-마이바흐': 'mercedes-maybach',\n 'Mercedes-Maybach': 'mercedes-maybach',\n 'MERCEDES-MAYBACH': 'mercedes-maybach',\n '마이바흐': 'mercedes-maybach',\n 'Maybach': 'mercedes-maybach',\n 'MAYBACH': 'mercedes-maybach',\n\n '아우디': 'audi',\n 'AUDI': 'audi',\n 'Audi': 'audi',\n\n '포르쉐': 'porsche',\n '포르셰': 'porsche',\n 'PORSCHE': 'porsche',\n 'Porsche': 'porsche',\n\n 'MINI': 'mini',\n 'Mini': 'mini',\n 'mini': 'mini',\n '미니': 'mini'\n};\n\n/**\n * Firestore ID → 한글 브랜드명 매핑\n */\nconst ID_TO_KOREAN: Record<BrandId, string> = {\n 'hyundai': '현대',\n 'kia': '기아',\n 'tesla': '테슬라',\n 'bmw': 'BMW',\n 'mercedes-benz': '메르세데스-벤츠',\n 'mercedes-maybach': '메르세데스-마이바흐',\n 'audi': '아우디',\n 'porsche': '포르쉐',\n 'mini': '미니'\n};\n\n/**\n * Firestore ID → 영문 브랜드명 매핑\n */\nconst ID_TO_ENGLISH: Record<BrandId, string> = {\n 'hyundai': 'HYUNDAI',\n 'kia': 'KIA',\n 'tesla': 'TESLA',\n 'bmw': 'BMW',\n 'mercedes-benz': 'Mercedes-Benz',\n 'mercedes-maybach': 'Mercedes-Maybach',\n 'audi': 'AUDI',\n 'porsche': 'PORSCHE',\n 'mini': 'MINI'\n};\n\n/**\n * Firestore ID → Firebase Storage 브랜드명 매핑\n * Mercedes-Benz와 Mercedes-Maybach는 'BENZ'로 통합\n */\nconst ID_TO_STORAGE: Record<BrandId, StorageBrandName> = {\n 'hyundai': 'HYUNDAI',\n 'kia': 'KIA',\n 'tesla': 'TESLA',\n 'bmw': 'BMW',\n 'mercedes-benz': 'BENZ', // ⚠️ BENZ로 통합\n 'mercedes-maybach': 'BENZ', // ⚠️ BENZ로 통합\n 'audi': 'AUDI',\n 'porsche': 'PORSCHE',\n 'mini': 'MINI'\n};\n\n/**\n * 브랜드 ID 정규화 (소문자 통일)\n *\n * @param brandInput - 브랜드명 (한글, 영문, 대소문자 무관)\n * @returns Firestore 브랜드 ID (소문자, 하이픈)\n *\n * @example\n * ```typescript\n * normalizeBrandId('현대'); // 'hyundai'\n * normalizeBrandId('BMW'); // 'bmw'\n * normalizeBrandId('Mercedes-Benz'); // 'mercedes-benz'\n * ```\n */\nexport function normalizeBrandId(brandInput: string): BrandId {\n // 1. 매핑 테이블에서 직접 찾기\n if (brandInput in KOREAN_TO_ID) {\n return KOREAN_TO_ID[brandInput];\n }\n\n // 2. 소문자 변환 후 확인\n const lowercased = brandInput.toLowerCase().trim();\n if (lowercased in KOREAN_TO_ID) {\n return KOREAN_TO_ID[lowercased];\n }\n\n // 3. 이미 올바른 Firestore ID인 경우\n if (Object.values(KOREAN_TO_ID).includes(lowercased as BrandId)) {\n return lowercased as BrandId;\n }\n\n // 4. 매칭 실패 시 소문자 변환 결과 반환 (fallback)\n console.warn(`[normalizeBrandId] 알 수 없는 브랜드: \"${brandInput}\", fallback: \"${lowercased}\"`);\n return lowercased as BrandId;\n}\n\n/**\n * 한글 브랜드명 가져오기\n *\n * @param brandId - Firestore 브랜드 ID\n * @returns 한글 브랜드명\n *\n * @example\n * ```typescript\n * getBrandNameKorean('hyundai'); // '현대'\n * getBrandNameKorean('bmw'); // 'BMW'\n * ```\n */\nexport function getBrandNameKorean(brandId: BrandId): string {\n return ID_TO_KOREAN[brandId] || brandId;\n}\n\n/**\n * 영문 브랜드명 가져오기\n *\n * @param brandId - Firestore 브랜드 ID\n * @returns 영문 브랜드명\n *\n * @example\n * ```typescript\n * getBrandNameEnglish('hyundai'); // 'HYUNDAI'\n * getBrandNameEnglish('mercedes-benz'); // 'Mercedes-Benz'\n * ```\n */\nexport function getBrandNameEnglish(brandId: BrandId): string {\n return ID_TO_ENGLISH[brandId] || brandId.toUpperCase();\n}\n\n/**\n * Firebase Storage 브랜드명 가져오기\n *\n * @param brandId - Firestore 브랜드 ID\n * @returns Storage 브랜드명 (대문자)\n *\n * @example\n * ```typescript\n * getStorageBrandName('hyundai'); // 'HYUNDAI'\n * getStorageBrandName('mercedes-benz'); // 'BENZ'\n * getStorageBrandName('mercedes-maybach'); // 'BENZ'\n * ```\n */\nexport function getStorageBrandName(brandId: BrandId): StorageBrandName {\n return ID_TO_STORAGE[brandId] || brandId.toUpperCase() as StorageBrandName;\n}\n\n/**\n * 브랜드 매핑 정보 가져오기 (통합 함수)\n *\n * @param brandInput - 브랜드명 (한글, 영문, 대소문자 무관)\n * @returns 브랜드 매핑 정보\n *\n * @example\n * ```typescript\n * getBrandMapping('현대');\n * // {\n * // firestoreId: 'hyundai',\n * // koreanName: '현대',\n * // englishName: 'HYUNDAI',\n * // storageName: 'HYUNDAI'\n * // }\n * ```\n */\nexport function getBrandMapping(brandInput: string): BrandMapping {\n const firestoreId = normalizeBrandId(brandInput);\n\n return {\n firestoreId,\n koreanName: getBrandNameKorean(firestoreId),\n englishName: getBrandNameEnglish(firestoreId),\n storageName: getStorageBrandName(firestoreId)\n };\n}\n\n/**\n * 지원되는 모든 브랜드 ID 목록\n */\nexport const SUPPORTED_BRANDS: BrandId[] = [\n 'hyundai',\n 'kia',\n 'tesla',\n 'bmw',\n 'mini',\n 'audi',\n 'mercedes-benz',\n 'mercedes-maybach',\n 'porsche'\n];\n","/**\n * 동적 브랜드 매핑 시스템\n *\n * Firestore에서 실시간으로 브랜드 정보를 조회하여 캐싱\n * 새로운 브랜드 추가 시 코드 수정 불필요\n */\n\nimport type { Firestore } from 'firebase/firestore';\nimport { collection, getDocs } from 'firebase/firestore';\nimport type { BrandId, BrandMapping } from '../types/brand';\n\n/**\n * 브랜드 정보 캐시\n *\n * Key: 브랜드명 (한글/영문/ID 모두)\n * Value: 브랜드 매핑 정보\n */\nconst brandCache = new Map<string, BrandMapping>();\n\n/**\n * 초기화 상태\n */\nlet initialized = false;\n\n/**\n * Firestore에서 브랜드 정보를 조회하여 캐시 초기화\n *\n * @param firestore - Firestore 인스턴스\n *\n * @example\n * ```typescript\n * import { initializeBrandMappings } from '@charzing/vehicle-utils';\n * import { db } from './firebase';\n *\n * // 앱 시작 시 한 번만 호출\n * await initializeBrandMappings(db);\n * ```\n */\nexport async function initializeBrandMappings(firestore: Firestore): Promise<void> {\n if (initialized) {\n console.log('[Brand Mapping] Already initialized');\n return;\n }\n\n console.log('[Brand Mapping] Initializing from Firestore...');\n\n try {\n const vehiclesRef = collection(firestore, 'vehicles');\n const snapshot = await getDocs(vehiclesRef);\n\n snapshot.docs.forEach(doc => {\n const data = doc.data();\n\n // Storage 브랜드명 특별 처리\n let storageName = data.englishName?.toUpperCase() || doc.id.toUpperCase();\n\n // Mercedes-Benz/Maybach는 'BENZ'로 통합\n if (doc.id === 'mercedes-benz' || doc.id === 'mercedes-maybach') {\n storageName = 'BENZ';\n }\n\n const mapping: BrandMapping = {\n firestoreId: doc.id as BrandId,\n koreanName: data.name || doc.id,\n englishName: data.englishName || doc.id.toUpperCase(),\n storageName: storageName as any\n };\n\n // 여러 키로 등록 (대소문자 구분 없이 조회 가능)\n brandCache.set(doc.id, mapping); // 'hyundai'\n brandCache.set(doc.id.toUpperCase(), mapping); // 'HYUNDAI'\n brandCache.set(doc.id.toLowerCase(), mapping); // 'hyundai'\n brandCache.set(data.name, mapping); // '현대'\n brandCache.set(data.englishName, mapping); // 'HYUNDAI'\n brandCache.set(data.englishName?.toUpperCase(), mapping); // 'HYUNDAI'\n brandCache.set(data.englishName?.toLowerCase(), mapping); // 'hyundai'\n });\n\n initialized = true;\n console.log(`[Brand Mapping] ✅ Initialized with ${snapshot.docs.length} brands`);\n } catch (error) {\n console.error('[Brand Mapping] ❌ Initialization failed:', error);\n throw error;\n }\n}\n\n/**\n * 브랜드 매핑 정보 조회\n *\n * @param brandInput - 브랜드명 (한글/영문/ID 모두 가능)\n * @returns 브랜드 매핑 정보 또는 null\n *\n * @throws {Error} 초기화되지 않은 경우\n *\n * @example\n * ```typescript\n * getBrandMapping('현대'); // { firestoreId: 'hyundai', ... }\n * getBrandMapping('HYUNDAI'); // { firestoreId: 'hyundai', ... }\n * getBrandMapping('hyundai'); // { firestoreId: 'hyundai', ... }\n * ```\n */\nexport function getBrandMapping(brandInput: string): BrandMapping | null {\n if (!initialized) {\n throw new Error(\n '[Brand Mapping] Not initialized. Call initializeBrandMappings() first.'\n );\n }\n\n // 직접 매칭 시도\n let mapping = brandCache.get(brandInput);\n if (mapping) return mapping;\n\n // 대소문자 구분 없이 재시도\n mapping = brandCache.get(brandInput.toLowerCase());\n if (mapping) return mapping;\n\n mapping = brandCache.get(brandInput.toUpperCase());\n if (mapping) return mapping;\n\n // 공백 제거 후 재시도\n const trimmed = brandInput.trim();\n mapping = brandCache.get(trimmed);\n if (mapping) return mapping;\n\n console.warn(`[Brand Mapping] Unknown brand: \"${brandInput}\"`);\n return null;\n}\n\n/**\n * 초기화 상태 확인\n */\nexport function isInitialized(): boolean {\n return initialized;\n}\n\n/**\n * 캐시 초기화 (테스트용)\n */\nexport function resetCache(): void {\n brandCache.clear();\n initialized = false;\n console.log('[Brand Mapping] Cache reset');\n}\n\n/**\n * 캐시된 브랜드 수\n */\nexport function getCachedBrandCount(): number {\n return brandCache.size;\n}\n","/**\n * 모델 매핑 유틸리티\n *\n * 한글/영문 모델명 변환, 모델 ID 정규화, 유사도 기반 매칭\n */\n\nimport type { ModelId, ModelSlug, ModelMapping } from '../types/model';\n\n/**\n * 한글 → 영문 모델명 매핑 테이블\n */\nconst KOREAN_TO_ENGLISH_MODEL: Record<string, string> = {\n // MINI\n '쿠퍼': 'COOPER',\n '컨트리맨': 'COUNTRYMAN',\n '에이스맨': 'ACEMAN',\n\n // Hyundai\n '아이오닉': 'IONIQ',\n '코나': 'KONA',\n '넥소': 'NEXO',\n '캐스퍼': 'CASPER',\n\n // Kia\n '니로': 'NIRO',\n '레이': 'RAY',\n\n // Tesla\n '모델': 'MODEL',\n\n // Common words\n '일렉트릭': 'ELECTRIC',\n 'E': 'E',\n 'EV': 'EV'\n};\n\n/**\n * 문자열을 슬러그로 변환\n * - 소문자 변환\n * - 공백/언더바 → 하이픈\n * - 연속 하이픈 제거\n * - 앞뒤 하이픈 제거\n *\n * @param str - 변환할 문자열\n * @returns 슬러그 문자열\n *\n * @example\n * ```typescript\n * toSlug('IONIQ 5'); // 'ioniq-5'\n * toSlug('Model S'); // 'model-s'\n * toSlug('Cooper JCW'); // 'cooper-jcw'\n * ```\n */\nexport function toSlug(str: string): string {\n return str\n .toLowerCase()\n .trim()\n .replace(/[\\s_]+/g, '-') // 공백/언더바 → 하이픈\n .replace(/-+/g, '-') // 연속 하이픈 제거\n .replace(/^-+|-+$/g, ''); // 앞뒤 하이픈 제거\n}\n\n/**\n * 한글 모델명을 영문으로 변환\n *\n * @param koreanModel - 한글 모델명\n * @returns 영문 모델명\n *\n * @example\n * ```typescript\n * convertKoreanToEnglish('아이오닉 5'); // 'IONIQ 5'\n * convertKoreanToEnglish('니로 EV'); // 'NIRO EV'\n * convertKoreanToEnglish('쿠퍼'); // 'COOPER'\n * ```\n */\nexport function convertKoreanToEnglish(koreanModel: string): string {\n let result = koreanModel;\n\n // 각 한글 단어를 영문으로 치환\n for (const [korean, english] of Object.entries(KOREAN_TO_ENGLISH_MODEL)) {\n const regex = new RegExp(korean, 'gi');\n result = result.replace(regex, english);\n }\n\n return result;\n}\n\n/**\n * 모델 ID 정규화 (소문자, 하이픈)\n *\n * @param modelInput - 모델명 (한글, 영문, 대소문자 무관)\n * @returns Firestore 모델 ID (소문자, 하이픈)\n *\n * @example\n * ```typescript\n * normalizeModelId('아이오닉 5'); // 'ioniq-5'\n * normalizeModelId('Model S'); // 'model-s'\n * normalizeModelId('COOPER JCW'); // 'cooper-jcw'\n * ```\n */\nexport function normalizeModelId(modelInput: string): ModelId {\n // 1. 한글 → 영문 변환 시도\n const englishModel = convertKoreanToEnglish(modelInput);\n\n // 2. 슬러그 변환\n return toSlug(englishModel);\n}\n\n/**\n * 두 문자열의 유사도 점수 계산\n * - 정확한 매칭: 2점\n * - 부분 매칭: 1점\n *\n * @param input - 입력 문자열\n * @param target - 비교 대상 문자열\n * @returns 유사도 점수 (높을수록 유사)\n */\nfunction calculateSimilarity(input: string, target: string): number {\n const inputWords = input.toLowerCase().replace(/[\\s\\-]/g, ' ').split(' ').filter(w => w.length > 0);\n const targetWords = target.toLowerCase().replace(/[\\s\\-]/g, ' ').split(' ').filter(w => w.length > 0);\n\n let score = 0;\n\n for (const inputWord of inputWords) {\n for (const targetWord of targetWords) {\n if (inputWord === targetWord) {\n score += 2; // 정확한 단어 매칭\n } else if (inputWord.includes(targetWord) || targetWord.includes(inputWord)) {\n score += 1; // 부분 매칭\n }\n }\n }\n\n return score;\n}\n\n/**\n * 가능한 모델 목록에서 최적 매칭 찾기\n * - 정확한 매칭 우선\n * - 없으면 유사도 점수 기반 매칭\n *\n * @param inputModel - 입력 모델명\n * @param availableModels - 가능한 모델 ID 목록\n * @returns 매칭된 모델 ID 또는 null\n *\n * @example\n * ```typescript\n * findBestModelMatch('아이오닉5', ['ioniq-5', 'ioniq-6', 'kona-electric']);\n * // 'ioniq-5'\n *\n * findBestModelMatch('NEXSO', ['ioniq-5', 'nexo', 'kona-electric']);\n * // 'nexo' (유사도 매칭)\n * ```\n */\nexport function findBestModelMatch(inputModel: string, availableModels: string[]): string | null {\n // 1. 정확한 매칭 시도\n const normalizedInput = normalizeModelId(inputModel);\n\n const exactMatch = availableModels.find(model => {\n const normalizedModel = normalizeModelId(model);\n return normalizedModel === normalizedInput;\n });\n\n if (exactMatch) {\n return exactMatch;\n }\n\n // 2. 유사도 점수 기반 매칭\n let bestMatch: string | null = null;\n let bestScore = 0;\n\n for (const model of availableModels) {\n const score = calculateSimilarity(inputModel, model);\n\n if (score > bestScore) {\n bestScore = score;\n bestMatch = model;\n }\n }\n\n // 최소 점수 임계값 (0보다 커야 함)\n return (bestMatch && bestScore > 0) ? bestMatch : null;\n}\n\n/**\n * 모델 슬러그 생성 (URL/파일명용)\n *\n * @param modelInput - 모델명\n * @returns 모델 슬러그\n *\n * @example\n * ```typescript\n * getModelSlug('아이오닉 5'); // 'ioniq-5'\n * getModelSlug('Model 3'); // 'model-3'\n * ```\n */\nexport function getModelSlug(modelInput: string): ModelSlug {\n return normalizeModelId(modelInput);\n}\n\n/**\n * 모델 매핑 정보 가져오기\n *\n * @param modelInput - 모델명 (한글 또는 영문)\n * @param englishName - 영문 모델명 (선택적)\n * @param koreanName - 한글 모델명 (선택적)\n * @returns 모델 매핑 정보\n *\n * @example\n * ```typescript\n * getModelMapping('아이오닉 5', 'IONIQ-5', '아이오닉 5');\n * // {\n * // firestoreId: 'ioniq-5',\n * // koreanName: '아이오닉 5',\n * // englishName: 'IONIQ-5',\n * // slug: 'ioniq-5'\n * // }\n * ```\n */\nexport function getModelMapping(\n modelInput: string,\n englishName?: string,\n koreanName?: string\n): ModelMapping {\n const firestoreId = normalizeModelId(modelInput);\n const slug = getModelSlug(modelInput);\n\n return {\n firestoreId,\n koreanName: koreanName || modelInput,\n englishName: englishName || convertKoreanToEnglish(modelInput),\n slug\n };\n}\n","/**\n * 동적 모델 매핑 시스템\n *\n * Firestore에서 실시간으로 모델 정보를 조회하여 캐싱\n * 새로운 모델 추가 시 코드 수정 불필요\n */\n\nimport type { Firestore } from 'firebase/firestore';\nimport { collection, getDocs } from 'firebase/firestore';\n\n/**\n * 모델 매핑 정보\n */\nexport interface ModelMapping {\n firestoreId: string; // Firestore 모델 ID (예: \"ioniq-5\")\n koreanName: string; // 한글 모델명 (예: \"아이오닉 5\")\n englishName: string; // 영문 모델명 (예: \"IONIQ-5\")\n storageName: string; // Storage 모델명 (예: \"IONIQ-5\")\n brandId: string; // 소속 브랜드 ID\n}\n\n/**\n * 모델 정보 캐시\n *\n * Key: `{brandId}:{modelName}` 형식\n * Value: 모델 매핑 정보\n */\nconst modelCache = new Map<string, ModelMapping>();\n\n/**\n * 브랜드별 모델 목록 캐시\n *\n * Key: brandId\n * Value: 해당 브랜드의 모든 모델 매핑 배열\n */\nconst brandModelsCache = new Map<string, ModelMapping[]>();\n\n/**\n * 초기화 상태\n */\nlet initialized = false;\n\n/**\n * Firestore에서 모델 정보를 조회하여 캐시 초기화\n *\n * @param firestore - Firestore 인스턴스\n * @param brandIds - 초기화할 브랜드 ID 목록 (생략 시 모든 브랜드)\n *\n * @example\n * ```typescript\n * // 모든 브랜드의 모델 초기화\n * await initializeModelMappings(db);\n *\n * // 특정 브랜드만 초기화\n * await initializeModelMappings(db, ['hyundai', 'kia']);\n * ```\n */\nexport async function initializeModelMappings(\n firestore: Firestore,\n brandIds?: string[]\n): Promise<void> {\n console.log('[Model Mapping] Initializing from Firestore...');\n\n try {\n // 브랜드 목록 조회\n const vehiclesRef = collection(firestore, 'vehicles');\n const brandsSnapshot = await getDocs(vehiclesRef);\n\n let processedBrands = 0;\n let processedModels = 0;\n\n for (const brandDoc of brandsSnapshot.docs) {\n const brandId = brandDoc.id;\n\n // 특정 브랜드만 초기화하는 경우\n if (brandIds && !brandIds.includes(brandId)) {\n continue;\n }\n\n // 해당 브랜드의 모델 조회\n const modelsRef = collection(firestore, 'vehicles', brandId, 'models');\n const modelsSnapshot = await getDocs(modelsRef);\n\n const brandModels: ModelMapping[] = [];\n\n modelsSnapshot.docs.forEach(modelDoc => {\n const data = modelDoc.data();\n\n const mapping: ModelMapping = {\n firestoreId: modelDoc.id,\n koreanName: data.name || modelDoc.id,\n englishName: data.englishName || modelDoc.id.toUpperCase(),\n storageName: data.englishName?.toUpperCase() || modelDoc.id.toUpperCase(),\n brandId: brandId\n };\n\n brandModels.push(mapping);\n processedModels++;\n\n // 여러 키로 등록\n const keys = [\n `${brandId}:${modelDoc.id}`, // \"hyundai:ioniq-5\"\n `${brandId}:${data.name}`, // \"hyundai:아이오닉 5\"\n `${brandId}:${data.englishName}`, // \"hyundai:IONIQ-5\"\n `${brandId}:${data.englishName?.toLowerCase()}`, // \"hyundai:ioniq-5\"\n `${brandId}:${modelDoc.id.toUpperCase()}`, // \"hyundai:IONIQ-5\"\n ];\n\n keys.forEach(key => {\n if (key && !key.includes('undefined')) {\n modelCache.set(key, mapping);\n }\n });\n });\n\n brandModelsCache.set(brandId, brandModels);\n processedBrands++;\n }\n\n initialized = true;\n console.log(\n `[Model Mapping] ✅ Initialized: ${processedBrands} brands, ${processedModels} models`\n );\n } catch (error) {\n console.error('[Model Mapping] ❌ Initialization failed:', error);\n throw error;\n }\n}\n\n/**\n * 모델 매핑 정보 조회\n *\n * @param brandId - 브랜드 ID\n * @param modelInput - 모델명 (한글/영문/ID 모두 가능)\n * @returns 모델 매핑 정보 또는 null\n *\n * @throws {Error} 초기화되지 않은 경우\n *\n * @example\n * ```typescript\n * getModelMapping('hyundai', '아이오닉 5'); // { firestoreId: 'ioniq-5', ... }\n * getModelMapping('hyundai', 'IONIQ-5'); // { firestoreId: 'ioniq-5', ... }\n * getModelMapping('hyundai', 'ioniq-5'); // { firestoreId: 'ioniq-5', ... }\n * ```\n */\nexport function getModelMapping(\n brandId: string,\n modelInput: string\n): ModelMapping | null {\n if (!initialized) {\n throw new Error(\n '[Model Mapping] Not initialized. Call initializeModelMappings() first.'\n );\n }\n\n // 직접 매칭 시도\n const keys = [\n `${brandId}:${modelInput}`,\n `${brandId}:${modelInput.toLowerCase()}`,\n `${brandId}:${modelInput.toUpperCase()}`,\n `${brandId}:${modelInput.trim()}`,\n ];\n\n for (const key of keys) {\n const mapping = modelCache.get(key);\n if (mapping) return mapping;\n }\n\n // 유사도 매칭 (Fallback)\n const brandModels = brandModelsCache.get(brandId);\n if (brandModels) {\n const normalizedInput = modelInput.toLowerCase().replace(/[\\s\\-]/g, '');\n\n for (const model of brandModels) {\n const normalizedModel = model.englishName.toLowerCase().replace(/[\\s\\-]/g, '');\n if (normalizedModel === normalizedInput) {\n return model;\n }\n }\n }\n\n console.warn(`[Model Mapping] Unknown model: ${brandId}:${modelInput}`);\n return null;\n}\n\n/**\n * 브랜드의 모든 모델 조회\n *\n * @param brandId - 브랜드 ID\n * @returns 모델 매핑 배열\n */\nexport function getBrandModels(brandId: string): ModelMapping[] {\n if (!initialized) {\n throw new Error(\n '[Model Mapping] Not initialized. Call initializeModelMappings() first.'\n );\n }\n\n return brandModelsCache.get(brandId) || [];\n}\n\n/**\n * 초기화 상태 확인\n */\nexport function isInitialized(): boolean {\n return initialized;\n}\n\n/**\n * 캐시 초기화 (테스트용)\n */\nexport function resetCache(): void {\n modelCache.clear();\n brandModelsCache.clear();\n initialized = false;\n console.log('[Model Mapping] Cache reset');\n}\n\n/**\n * 캐시된 모델 수\n */\nexport function getCachedModelCount(): number {\n return modelCache.size;\n}\n","/**\n * Firebase Storage 차량 이미지 URL/경로 생성 유틸리티\n *\n * ⚠️ 주의: 이 파일은 순수 함수만 포함합니다.\n * Firebase SDK나 업로드 로직은 포함하지 않습니다.\n */\n\nimport { VehicleImageParams } from '../types/image';\n\nconst FIREBASE_STORAGE_BASE = 'https://firebasestorage.googleapis.com/v0/b/charzing-d1600.firebasestorage.app/o';\n\n/**\n * 브랜드명을 Storage 경로용으로 정규화\n * ✅ 완전 동적 처리: Firestore ID → Storage 폴더\n *\n * 규칙: 하이픈(-)을 언더스코어(_)로 변환 후 대문자\n * - mercedes-benz → MERCEDES_BENZ\n * - mercedes-maybach → MERCEDES_MAYBACH\n * - hyundai → HYUNDAI\n * - kia → KIA\n */\nfunction normalizeStorageBrand(firestoreBrandId: string): string {\n // ✅ 완전 동적 변환: 하이픈 → 언더스코어, 대문자\n return firestoreBrandId.toUpperCase().replace(/-/g, '_');\n}\n\n/**\n * 모델명을 Storage 경로용으로 정규화\n * ✅ 동적 처리: Firestore ID → Storage 폴더 (대문자)\n */\nfunction normalizeStorageModel(firestoreModelId: string): string {\n return firestoreModelId.toUpperCase();\n}\n\n/**\n * 트림명을 파일명용으로 정규화\n */\nfunction normalizeStorageTrim(trim?: string): string {\n if (!trim) return '';\n return trim.toLowerCase().replace(/[\\s\\-]/g, '_').replace(/[^a-z0-9_]/g, '');\n}\n\n/**\n * 차량 이미지 파일명 생성\n *\n * @example\n * generateImageFilename({\n * brandId: 'kia',\n * modelId: 'niro-ev',\n * year: 2022,\n * trim: 'standard'\n * })\n * // Returns: 'kia__niro-ev__2022__standard.png'\n */\nexport function generateImageFilename(params: VehicleImageParams): string {\n const { brandId, modelId, year, trim } = params;\n const brandLower = brandId.toLowerCase();\n const modelLower = modelId.toLowerCase().replace(/[\\s]/g, '-');\n const normalizedTrim = trim ? normalizeStorageTrim(trim) : '';\n const trimPart = normalizedTrim ? `__${normalizedTrim}` : '';\n return `${brandLower}__${modelLower}__${year}${trimPart}.png`;\n}\n\n/**\n * Storage 경로 생성 (업로드용)\n *\n * @example\n * generateStoragePath({\n * brandId: 'kia',\n * modelId: 'niro-ev',\n * year: 2022,\n * trim: 'standard'\n * })\n * // Returns: 'vehicle-images/KIA/NIRO-EV/2022/kia__niro-ev__2022__standard.png'\n */\nexport function generateStoragePath(params: VehicleImageParams): string {\n const { brandId, modelId, year } = params;\n const storageBrand = normalizeStorageBrand(brandId);\n const storageModel = normalizeStorageModel(modelId);\n const filename = generateImageFilename(params);\n return `vehicle-images/${storageBrand}/${storageModel}/${year}/${filename}`;\n}\n\n/**\n * 완전한 Firebase Storage URL 생성 (읽기용)\n *\n * ✅ 파라미터 검증: brandId, modelId, year 필수\n * ❌ Fallback 금지: 잘못된 파라미터 시 에러 throw\n *\n * @throws {Error} brandId, modelId, year가 없으면 에러\n *\n * @example\n * generateVehicleImageUrl({\n * brandId: 'kia',\n * modelId: 'niro-ev',\n * year: 2022\n * })\n * // Returns: 'https://firebasestorage.googleapis.com/.../vehicle-images%2FKIA%2FNIRO-EV%2F2022%2Fkia__niro-ev__2022.png?alt=media'\n */\nexport function generateVehicleImageUrl(params: VehicleImageParams): string {\n // ✅ 필수 파라미터 검증 (fallback 금지)\n if (!params.brandId) {\n throw new Error('brandId is required');\n }\n if (!params.modelId) {\n throw new Error('modelId is required');\n }\n if (!params.year) {\n throw new Error('year is required');\n }\n\n const path = generateStoragePath(params);\n const encodedPath = encodeURIComponent(path);\n return `${FIREBASE_STORAGE_BASE}/${encodedPath}?alt=media`;\n}\n"],"mappings":"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","names":["getBrandMapping","getModelMapping","isInitialized","resetCache","getBrandMapping","import_firestore","initialized","getModelMapping","isInitialized","resetCache"]}
|
|
1
|
+
{"version":3,"sources":["../../src/index.ts","../../src/brand/mapping.ts","../../src/brand/dynamic-mapping.ts","../../src/model/mapping.ts","../../src/model/dynamic-mapping.ts","../../src/image/url-generator.ts","../../src/report/cellMap.ts"],"sourcesContent":["/**\n * @charzing/vehicle-utils\n *\n * 차징 플랫폼 전체에서 사용하는 차량 데이터 유틸리티\n */\n\n// ===== 타입 =====\nexport type * from './types';\n\n// ===== 브랜드 관련 (정적) =====\nexport {\n normalizeBrandId,\n getBrandNameKorean,\n getBrandNameEnglish,\n getStorageBrandName,\n getBrandMapping,\n SUPPORTED_BRANDS\n} from './brand/mapping';\n\n// ===== 브랜드 관련 (동적) ⭐ NEW =====\nexport {\n initializeBrandMappings,\n getBrandMapping as getDynamicBrandMapping,\n isInitialized as isBrandMappingInitialized,\n resetCache as resetBrandCache,\n getCachedBrandCount\n} from './brand/dynamic-mapping';\n\n// ===== 모델 관련 (정적) =====\nexport {\n toSlug,\n convertKoreanToEnglish,\n normalizeModelId,\n findBestModelMatch,\n getModelSlug,\n getModelMapping\n} from './model/mapping';\n\n// ===== 모델 관련 (동적) ⭐ NEW =====\nexport {\n initializeModelMappings,\n getModelMapping as getDynamicModelMapping,\n getBrandModels,\n isInitialized as isModelMappingInitialized,\n resetCache as resetModelCache,\n getCachedModelCount\n} from './model/dynamic-mapping';\n\n// ===== 이미지 URL/경로 생성 (순수 함수만) ⭐ UPDATED =====\nexport {\n generateImageFilename,\n generateStoragePath,\n generateVehicleImageUrl\n} from './image/url-generator';\n\n// ===== 리포트 — 셀맵 미지원 차량 판정 =====\nexport {\n CELL_MAP_UNSUPPORTED_GROUPS,\n CELL_MAP_UNSUPPORTED_NOTICE,\n CELL_VOLTAGE_DELTA_THRESHOLDS,\n findCellMapUnsupportedGroup,\n isCellMapSupported,\n judgeCellVoltageByDelta\n} from './report/cellMap';\nexport type {\n CellMapUnsupportedGroup,\n CellMapReportRef,\n CellVoltageVerdict\n} from './report/cellMap';\n","/**\n * 브랜드 매핑 유틸리티\n *\n * 한글/영문 브랜드명 변환, Firestore ID 정규화, Storage 브랜드명 매핑\n */\n\nimport type { BrandId, BrandMapping, StorageBrandName } from '../types/brand';\n\n/**\n * 한글 → Firestore ID 매핑 테이블\n */\nconst KOREAN_TO_ID: Record<string, BrandId> = {\n '현대': 'hyundai',\n 'HYUNDAI': 'hyundai',\n 'Hyundai': 'hyundai',\n\n '기아': 'kia',\n 'KIA': 'kia',\n 'Kia': 'kia',\n\n '테슬라': 'tesla',\n 'TESLA': 'tesla',\n 'Tesla': 'tesla',\n\n 'BMW': 'bmw',\n 'bmw': 'bmw',\n '비엠더블유': 'bmw',\n\n '메르세데스-벤츠': 'mercedes-benz',\n 'Mercedes-Benz': 'mercedes-benz',\n 'MERCEDES-BENZ': 'mercedes-benz',\n '벤츠': 'mercedes-benz',\n 'Benz': 'mercedes-benz',\n 'BENZ': 'mercedes-benz',\n\n '메르세데스-마이바흐': 'mercedes-maybach',\n 'Mercedes-Maybach': 'mercedes-maybach',\n 'MERCEDES-MAYBACH': 'mercedes-maybach',\n '마이바흐': 'mercedes-maybach',\n 'Maybach': 'mercedes-maybach',\n 'MAYBACH': 'mercedes-maybach',\n\n '아우디': 'audi',\n 'AUDI': 'audi',\n 'Audi': 'audi',\n\n '포르쉐': 'porsche',\n '포르셰': 'porsche',\n 'PORSCHE': 'porsche',\n 'Porsche': 'porsche',\n\n 'MINI': 'mini',\n 'Mini': 'mini',\n 'mini': 'mini',\n '미니': 'mini'\n};\n\n/**\n * Firestore ID → 한글 브랜드명 매핑\n */\nconst ID_TO_KOREAN: Record<BrandId, string> = {\n 'hyundai': '현대',\n 'kia': '기아',\n 'tesla': '테슬라',\n 'bmw': 'BMW',\n 'mercedes-benz': '메르세데스-벤츠',\n 'mercedes-maybach': '메르세데스-마이바흐',\n 'audi': '아우디',\n 'porsche': '포르쉐',\n 'mini': '미니'\n};\n\n/**\n * Firestore ID → 영문 브랜드명 매핑\n */\nconst ID_TO_ENGLISH: Record<BrandId, string> = {\n 'hyundai': 'HYUNDAI',\n 'kia': 'KIA',\n 'tesla': 'TESLA',\n 'bmw': 'BMW',\n 'mercedes-benz': 'Mercedes-Benz',\n 'mercedes-maybach': 'Mercedes-Maybach',\n 'audi': 'AUDI',\n 'porsche': 'PORSCHE',\n 'mini': 'MINI'\n};\n\n/**\n * Firestore ID → Firebase Storage 브랜드명 매핑\n * Mercedes-Benz와 Mercedes-Maybach는 'BENZ'로 통합\n */\nconst ID_TO_STORAGE: Record<BrandId, StorageBrandName> = {\n 'hyundai': 'HYUNDAI',\n 'kia': 'KIA',\n 'tesla': 'TESLA',\n 'bmw': 'BMW',\n 'mercedes-benz': 'BENZ', // ⚠️ BENZ로 통합\n 'mercedes-maybach': 'BENZ', // ⚠️ BENZ로 통합\n 'audi': 'AUDI',\n 'porsche': 'PORSCHE',\n 'mini': 'MINI'\n};\n\n/**\n * 브랜드 ID 정규화 (소문자 통일)\n *\n * @param brandInput - 브랜드명 (한글, 영문, 대소문자 무관)\n * @returns Firestore 브랜드 ID (소문자, 하이픈)\n *\n * @example\n * ```typescript\n * normalizeBrandId('현대'); // 'hyundai'\n * normalizeBrandId('BMW'); // 'bmw'\n * normalizeBrandId('Mercedes-Benz'); // 'mercedes-benz'\n * ```\n */\nexport function normalizeBrandId(brandInput: string): BrandId {\n // 1. 매핑 테이블에서 직접 찾기\n if (brandInput in KOREAN_TO_ID) {\n return KOREAN_TO_ID[brandInput];\n }\n\n // 2. 소문자 변환 후 확인\n const lowercased = brandInput.toLowerCase().trim();\n if (lowercased in KOREAN_TO_ID) {\n return KOREAN_TO_ID[lowercased];\n }\n\n // 3. 이미 올바른 Firestore ID인 경우\n if (Object.values(KOREAN_TO_ID).includes(lowercased as BrandId)) {\n return lowercased as BrandId;\n }\n\n // 4. 매칭 실패 시 소문자 변환 결과 반환 (fallback)\n console.warn(`[normalizeBrandId] 알 수 없는 브랜드: \"${brandInput}\", fallback: \"${lowercased}\"`);\n return lowercased as BrandId;\n}\n\n/**\n * 한글 브랜드명 가져오기\n *\n * @param brandId - Firestore 브랜드 ID\n * @returns 한글 브랜드명\n *\n * @example\n * ```typescript\n * getBrandNameKorean('hyundai'); // '현대'\n * getBrandNameKorean('bmw'); // 'BMW'\n * ```\n */\nexport function getBrandNameKorean(brandId: BrandId): string {\n return ID_TO_KOREAN[brandId] || brandId;\n}\n\n/**\n * 영문 브랜드명 가져오기\n *\n * @param brandId - Firestore 브랜드 ID\n * @returns 영문 브랜드명\n *\n * @example\n * ```typescript\n * getBrandNameEnglish('hyundai'); // 'HYUNDAI'\n * getBrandNameEnglish('mercedes-benz'); // 'Mercedes-Benz'\n * ```\n */\nexport function getBrandNameEnglish(brandId: BrandId): string {\n return ID_TO_ENGLISH[brandId] || brandId.toUpperCase();\n}\n\n/**\n * Firebase Storage 브랜드명 가져오기\n *\n * @param brandId - Firestore 브랜드 ID\n * @returns Storage 브랜드명 (대문자)\n *\n * @example\n * ```typescript\n * getStorageBrandName('hyundai'); // 'HYUNDAI'\n * getStorageBrandName('mercedes-benz'); // 'BENZ'\n * getStorageBrandName('mercedes-maybach'); // 'BENZ'\n * ```\n */\nexport function getStorageBrandName(brandId: BrandId): StorageBrandName {\n return ID_TO_STORAGE[brandId] || brandId.toUpperCase() as StorageBrandName;\n}\n\n/**\n * 브랜드 매핑 정보 가져오기 (통합 함수)\n *\n * @param brandInput - 브랜드명 (한글, 영문, 대소문자 무관)\n * @returns 브랜드 매핑 정보\n *\n * @example\n * ```typescript\n * getBrandMapping('현대');\n * // {\n * // firestoreId: 'hyundai',\n * // koreanName: '현대',\n * // englishName: 'HYUNDAI',\n * // storageName: 'HYUNDAI'\n * // }\n * ```\n */\nexport function getBrandMapping(brandInput: string): BrandMapping {\n const firestoreId = normalizeBrandId(brandInput);\n\n return {\n firestoreId,\n koreanName: getBrandNameKorean(firestoreId),\n englishName: getBrandNameEnglish(firestoreId),\n storageName: getStorageBrandName(firestoreId)\n };\n}\n\n/**\n * 지원되는 모든 브랜드 ID 목록\n */\nexport const SUPPORTED_BRANDS: BrandId[] = [\n 'hyundai',\n 'kia',\n 'tesla',\n 'bmw',\n 'mini',\n 'audi',\n 'mercedes-benz',\n 'mercedes-maybach',\n 'porsche'\n];\n","/**\n * 동적 브랜드 매핑 시스템\n *\n * Firestore에서 실시간으로 브랜드 정보를 조회하여 캐싱\n * 새로운 브랜드 추가 시 코드 수정 불필요\n */\n\nimport type { Firestore } from 'firebase/firestore';\nimport { collection, getDocs } from 'firebase/firestore';\nimport type { BrandId, BrandMapping } from '../types/brand';\n\n/**\n * 브랜드 정보 캐시\n *\n * Key: 브랜드명 (한글/영문/ID 모두)\n * Value: 브랜드 매핑 정보\n */\nconst brandCache = new Map<string, BrandMapping>();\n\n/**\n * 초기화 상태\n */\nlet initialized = false;\n\n/**\n * Firestore에서 브랜드 정보를 조회하여 캐시 초기화\n *\n * @param firestore - Firestore 인스턴스\n *\n * @example\n * ```typescript\n * import { initializeBrandMappings } from '@charzing/vehicle-utils';\n * import { db } from './firebase';\n *\n * // 앱 시작 시 한 번만 호출\n * await initializeBrandMappings(db);\n * ```\n */\nexport async function initializeBrandMappings(firestore: Firestore): Promise<void> {\n if (initialized) {\n console.log('[Brand Mapping] Already initialized');\n return;\n }\n\n console.log('[Brand Mapping] Initializing from Firestore...');\n\n try {\n const vehiclesRef = collection(firestore, 'vehicles');\n const snapshot = await getDocs(vehiclesRef);\n\n snapshot.docs.forEach(doc => {\n const data = doc.data();\n\n // Storage 브랜드명 특별 처리\n let storageName = data.englishName?.toUpperCase() || doc.id.toUpperCase();\n\n // Mercedes-Benz/Maybach는 'BENZ'로 통합\n if (doc.id === 'mercedes-benz' || doc.id === 'mercedes-maybach') {\n storageName = 'BENZ';\n }\n\n const mapping: BrandMapping = {\n firestoreId: doc.id as BrandId,\n koreanName: data.name || doc.id,\n englishName: data.englishName || doc.id.toUpperCase(),\n storageName: storageName as any\n };\n\n // 여러 키로 등록 (대소문자 구분 없이 조회 가능)\n brandCache.set(doc.id, mapping); // 'hyundai'\n brandCache.set(doc.id.toUpperCase(), mapping); // 'HYUNDAI'\n brandCache.set(doc.id.toLowerCase(), mapping); // 'hyundai'\n brandCache.set(data.name, mapping); // '현대'\n brandCache.set(data.englishName, mapping); // 'HYUNDAI'\n brandCache.set(data.englishName?.toUpperCase(), mapping); // 'HYUNDAI'\n brandCache.set(data.englishName?.toLowerCase(), mapping); // 'hyundai'\n });\n\n initialized = true;\n console.log(`[Brand Mapping] ✅ Initialized with ${snapshot.docs.length} brands`);\n } catch (error) {\n console.error('[Brand Mapping] ❌ Initialization failed:', error);\n throw error;\n }\n}\n\n/**\n * 브랜드 매핑 정보 조회\n *\n * @param brandInput - 브랜드명 (한글/영문/ID 모두 가능)\n * @returns 브랜드 매핑 정보 또는 null\n *\n * @throws {Error} 초기화되지 않은 경우\n *\n * @example\n * ```typescript\n * getBrandMapping('현대'); // { firestoreId: 'hyundai', ... }\n * getBrandMapping('HYUNDAI'); // { firestoreId: 'hyundai', ... }\n * getBrandMapping('hyundai'); // { firestoreId: 'hyundai', ... }\n * ```\n */\nexport function getBrandMapping(brandInput: string): BrandMapping | null {\n if (!initialized) {\n throw new Error(\n '[Brand Mapping] Not initialized. Call initializeBrandMappings() first.'\n );\n }\n\n // 직접 매칭 시도\n let mapping = brandCache.get(brandInput);\n if (mapping) return mapping;\n\n // 대소문자 구분 없이 재시도\n mapping = brandCache.get(brandInput.toLowerCase());\n if (mapping) return mapping;\n\n mapping = brandCache.get(brandInput.toUpperCase());\n if (mapping) return mapping;\n\n // 공백 제거 후 재시도\n const trimmed = brandInput.trim();\n mapping = brandCache.get(trimmed);\n if (mapping) return mapping;\n\n console.warn(`[Brand Mapping] Unknown brand: \"${brandInput}\"`);\n return null;\n}\n\n/**\n * 초기화 상태 확인\n */\nexport function isInitialized(): boolean {\n return initialized;\n}\n\n/**\n * 캐시 초기화 (테스트용)\n */\nexport function resetCache(): void {\n brandCache.clear();\n initialized = false;\n console.log('[Brand Mapping] Cache reset');\n}\n\n/**\n * 캐시된 브랜드 수\n */\nexport function getCachedBrandCount(): number {\n return brandCache.size;\n}\n","/**\n * 모델 매핑 유틸리티\n *\n * 한글/영문 모델명 변환, 모델 ID 정규화, 유사도 기반 매칭\n */\n\nimport type { ModelId, ModelSlug, ModelMapping } from '../types/model';\n\n/**\n * 한글 → 영문 모델명 매핑 테이블\n */\nconst KOREAN_TO_ENGLISH_MODEL: Record<string, string> = {\n // MINI\n '쿠퍼': 'COOPER',\n '컨트리맨': 'COUNTRYMAN',\n '에이스맨': 'ACEMAN',\n\n // Hyundai\n '아이오닉': 'IONIQ',\n '코나': 'KONA',\n '넥소': 'NEXO',\n '캐스퍼': 'CASPER',\n\n // Kia\n '니로': 'NIRO',\n '레이': 'RAY',\n\n // Tesla\n '모델': 'MODEL',\n\n // Common words\n '일렉트릭': 'ELECTRIC',\n 'E': 'E',\n 'EV': 'EV'\n};\n\n/**\n * 문자열을 슬러그로 변환\n * - 소문자 변환\n * - 공백/언더바 → 하이픈\n * - 연속 하이픈 제거\n * - 앞뒤 하이픈 제거\n *\n * @param str - 변환할 문자열\n * @returns 슬러그 문자열\n *\n * @example\n * ```typescript\n * toSlug('IONIQ 5'); // 'ioniq-5'\n * toSlug('Model S'); // 'model-s'\n * toSlug('Cooper JCW'); // 'cooper-jcw'\n * ```\n */\nexport function toSlug(str: string): string {\n return str\n .toLowerCase()\n .trim()\n .replace(/[\\s_]+/g, '-') // 공백/언더바 → 하이픈\n .replace(/-+/g, '-') // 연속 하이픈 제거\n .replace(/^-+|-+$/g, ''); // 앞뒤 하이픈 제거\n}\n\n/**\n * 한글 모델명을 영문으로 변환\n *\n * @param koreanModel - 한글 모델명\n * @returns 영문 모델명\n *\n * @example\n * ```typescript\n * convertKoreanToEnglish('아이오닉 5'); // 'IONIQ 5'\n * convertKoreanToEnglish('니로 EV'); // 'NIRO EV'\n * convertKoreanToEnglish('쿠퍼'); // 'COOPER'\n * ```\n */\nexport function convertKoreanToEnglish(koreanModel: string): string {\n let result = koreanModel;\n\n // 각 한글 단어를 영문으로 치환\n for (const [korean, english] of Object.entries(KOREAN_TO_ENGLISH_MODEL)) {\n const regex = new RegExp(korean, 'gi');\n result = result.replace(regex, english);\n }\n\n return result;\n}\n\n/**\n * 모델 ID 정규화 (소문자, 하이픈)\n *\n * @param modelInput - 모델명 (한글, 영문, 대소문자 무관)\n * @returns Firestore 모델 ID (소문자, 하이픈)\n *\n * @example\n * ```typescript\n * normalizeModelId('아이오닉 5'); // 'ioniq-5'\n * normalizeModelId('Model S'); // 'model-s'\n * normalizeModelId('COOPER JCW'); // 'cooper-jcw'\n * ```\n */\nexport function normalizeModelId(modelInput: string): ModelId {\n // 1. 한글 → 영문 변환 시도\n const englishModel = convertKoreanToEnglish(modelInput);\n\n // 2. 슬러그 변환\n return toSlug(englishModel);\n}\n\n/**\n * 두 문자열의 유사도 점수 계산\n * - 정확한 매칭: 2점\n * - 부분 매칭: 1점\n *\n * @param input - 입력 문자열\n * @param target - 비교 대상 문자열\n * @returns 유사도 점수 (높을수록 유사)\n */\nfunction calculateSimilarity(input: string, target: string): number {\n const inputWords = input.toLowerCase().replace(/[\\s\\-]/g, ' ').split(' ').filter(w => w.length > 0);\n const targetWords = target.toLowerCase().replace(/[\\s\\-]/g, ' ').split(' ').filter(w => w.length > 0);\n\n let score = 0;\n\n for (const inputWord of inputWords) {\n for (const targetWord of targetWords) {\n if (inputWord === targetWord) {\n score += 2; // 정확한 단어 매칭\n } else if (inputWord.includes(targetWord) || targetWord.includes(inputWord)) {\n score += 1; // 부분 매칭\n }\n }\n }\n\n return score;\n}\n\n/**\n * 가능한 모델 목록에서 최적 매칭 찾기\n * - 정확한 매칭 우선\n * - 없으면 유사도 점수 기반 매칭\n *\n * @param inputModel - 입력 모델명\n * @param availableModels - 가능한 모델 ID 목록\n * @returns 매칭된 모델 ID 또는 null\n *\n * @example\n * ```typescript\n * findBestModelMatch('아이오닉5', ['ioniq-5', 'ioniq-6', 'kona-electric']);\n * // 'ioniq-5'\n *\n * findBestModelMatch('NEXSO', ['ioniq-5', 'nexo', 'kona-electric']);\n * // 'nexo' (유사도 매칭)\n * ```\n */\nexport function findBestModelMatch(inputModel: string, availableModels: string[]): string | null {\n // 1. 정확한 매칭 시도\n const normalizedInput = normalizeModelId(inputModel);\n\n const exactMatch = availableModels.find(model => {\n const normalizedModel = normalizeModelId(model);\n return normalizedModel === normalizedInput;\n });\n\n if (exactMatch) {\n return exactMatch;\n }\n\n // 2. 유사도 점수 기반 매칭\n let bestMatch: string | null = null;\n let bestScore = 0;\n\n for (const model of availableModels) {\n const score = calculateSimilarity(inputModel, model);\n\n if (score > bestScore) {\n bestScore = score;\n bestMatch = model;\n }\n }\n\n // 최소 점수 임계값 (0보다 커야 함)\n return (bestMatch && bestScore > 0) ? bestMatch : null;\n}\n\n/**\n * 모델 슬러그 생성 (URL/파일명용)\n *\n * @param modelInput - 모델명\n * @returns 모델 슬러그\n *\n * @example\n * ```typescript\n * getModelSlug('아이오닉 5'); // 'ioniq-5'\n * getModelSlug('Model 3'); // 'model-3'\n * ```\n */\nexport function getModelSlug(modelInput: string): ModelSlug {\n return normalizeModelId(modelInput);\n}\n\n/**\n * 모델 매핑 정보 가져오기\n *\n * @param modelInput - 모델명 (한글 또는 영문)\n * @param englishName - 영문 모델명 (선택적)\n * @param koreanName - 한글 모델명 (선택적)\n * @returns 모델 매핑 정보\n *\n * @example\n * ```typescript\n * getModelMapping('아이오닉 5', 'IONIQ-5', '아이오닉 5');\n * // {\n * // firestoreId: 'ioniq-5',\n * // koreanName: '아이오닉 5',\n * // englishName: 'IONIQ-5',\n * // slug: 'ioniq-5'\n * // }\n * ```\n */\nexport function getModelMapping(\n modelInput: string,\n englishName?: string,\n koreanName?: string\n): ModelMapping {\n const firestoreId = normalizeModelId(modelInput);\n const slug = getModelSlug(modelInput);\n\n return {\n firestoreId,\n koreanName: koreanName || modelInput,\n englishName: englishName || convertKoreanToEnglish(modelInput),\n slug\n };\n}\n","/**\n * 동적 모델 매핑 시스템\n *\n * Firestore에서 실시간으로 모델 정보를 조회하여 캐싱\n * 새로운 모델 추가 시 코드 수정 불필요\n */\n\nimport type { Firestore } from 'firebase/firestore';\nimport { collection, getDocs } from 'firebase/firestore';\n\n/**\n * 모델 매핑 정보\n */\nexport interface ModelMapping {\n firestoreId: string; // Firestore 모델 ID (예: \"ioniq-5\")\n koreanName: string; // 한글 모델명 (예: \"아이오닉 5\")\n englishName: string; // 영문 모델명 (예: \"IONIQ-5\")\n storageName: string; // Storage 모델명 (예: \"IONIQ-5\")\n brandId: string; // 소속 브랜드 ID\n}\n\n/**\n * 모델 정보 캐시\n *\n * Key: `{brandId}:{modelName}` 형식\n * Value: 모델 매핑 정보\n */\nconst modelCache = new Map<string, ModelMapping>();\n\n/**\n * 브랜드별 모델 목록 캐시\n *\n * Key: brandId\n * Value: 해당 브랜드의 모든 모델 매핑 배열\n */\nconst brandModelsCache = new Map<string, ModelMapping[]>();\n\n/**\n * 초기화 상태\n */\nlet initialized = false;\n\n/**\n * Firestore에서 모델 정보를 조회하여 캐시 초기화\n *\n * @param firestore - Firestore 인스턴스\n * @param brandIds - 초기화할 브랜드 ID 목록 (생략 시 모든 브랜드)\n *\n * @example\n * ```typescript\n * // 모든 브랜드의 모델 초기화\n * await initializeModelMappings(db);\n *\n * // 특정 브랜드만 초기화\n * await initializeModelMappings(db, ['hyundai', 'kia']);\n * ```\n */\nexport async function initializeModelMappings(\n firestore: Firestore,\n brandIds?: string[]\n): Promise<void> {\n console.log('[Model Mapping] Initializing from Firestore...');\n\n try {\n // 브랜드 목록 조회\n const vehiclesRef = collection(firestore, 'vehicles');\n const brandsSnapshot = await getDocs(vehiclesRef);\n\n let processedBrands = 0;\n let processedModels = 0;\n\n for (const brandDoc of brandsSnapshot.docs) {\n const brandId = brandDoc.id;\n\n // 특정 브랜드만 초기화하는 경우\n if (brandIds && !brandIds.includes(brandId)) {\n continue;\n }\n\n // 해당 브랜드의 모델 조회\n const modelsRef = collection(firestore, 'vehicles', brandId, 'models');\n const modelsSnapshot = await getDocs(modelsRef);\n\n const brandModels: ModelMapping[] = [];\n\n modelsSnapshot.docs.forEach(modelDoc => {\n const data = modelDoc.data();\n\n const mapping: ModelMapping = {\n firestoreId: modelDoc.id,\n koreanName: data.name || modelDoc.id,\n englishName: data.englishName || modelDoc.id.toUpperCase(),\n storageName: data.englishName?.toUpperCase() || modelDoc.id.toUpperCase(),\n brandId: brandId\n };\n\n brandModels.push(mapping);\n processedModels++;\n\n // 여러 키로 등록\n const keys = [\n `${brandId}:${modelDoc.id}`, // \"hyundai:ioniq-5\"\n `${brandId}:${data.name}`, // \"hyundai:아이오닉 5\"\n `${brandId}:${data.englishName}`, // \"hyundai:IONIQ-5\"\n `${brandId}:${data.englishName?.toLowerCase()}`, // \"hyundai:ioniq-5\"\n `${brandId}:${modelDoc.id.toUpperCase()}`, // \"hyundai:IONIQ-5\"\n ];\n\n keys.forEach(key => {\n if (key && !key.includes('undefined')) {\n modelCache.set(key, mapping);\n }\n });\n });\n\n brandModelsCache.set(brandId, brandModels);\n processedBrands++;\n }\n\n initialized = true;\n console.log(\n `[Model Mapping] ✅ Initialized: ${processedBrands} brands, ${processedModels} models`\n );\n } catch (error) {\n console.error('[Model Mapping] ❌ Initialization failed:', error);\n throw error;\n }\n}\n\n/**\n * 모델 매핑 정보 조회\n *\n * @param brandId - 브랜드 ID\n * @param modelInput - 모델명 (한글/영문/ID 모두 가능)\n * @returns 모델 매핑 정보 또는 null\n *\n * @throws {Error} 초기화되지 않은 경우\n *\n * @example\n * ```typescript\n * getModelMapping('hyundai', '아이오닉 5'); // { firestoreId: 'ioniq-5', ... }\n * getModelMapping('hyundai', 'IONIQ-5'); // { firestoreId: 'ioniq-5', ... }\n * getModelMapping('hyundai', 'ioniq-5'); // { firestoreId: 'ioniq-5', ... }\n * ```\n */\nexport function getModelMapping(\n brandId: string,\n modelInput: string\n): ModelMapping | null {\n if (!initialized) {\n throw new Error(\n '[Model Mapping] Not initialized. Call initializeModelMappings() first.'\n );\n }\n\n // 직접 매칭 시도\n const keys = [\n `${brandId}:${modelInput}`,\n `${brandId}:${modelInput.toLowerCase()}`,\n `${brandId}:${modelInput.toUpperCase()}`,\n `${brandId}:${modelInput.trim()}`,\n ];\n\n for (const key of keys) {\n const mapping = modelCache.get(key);\n if (mapping) return mapping;\n }\n\n // 유사도 매칭 (Fallback)\n const brandModels = brandModelsCache.get(brandId);\n if (brandModels) {\n const normalizedInput = modelInput.toLowerCase().replace(/[\\s\\-]/g, '');\n\n for (const model of brandModels) {\n const normalizedModel = model.englishName.toLowerCase().replace(/[\\s\\-]/g, '');\n if (normalizedModel === normalizedInput) {\n return model;\n }\n }\n }\n\n console.warn(`[Model Mapping] Unknown model: ${brandId}:${modelInput}`);\n return null;\n}\n\n/**\n * 브랜드의 모든 모델 조회\n *\n * @param brandId - 브랜드 ID\n * @returns 모델 매핑 배열\n */\nexport function getBrandModels(brandId: string): ModelMapping[] {\n if (!initialized) {\n throw new Error(\n '[Model Mapping] Not initialized. Call initializeModelMappings() first.'\n );\n }\n\n return brandModelsCache.get(brandId) || [];\n}\n\n/**\n * 초기화 상태 확인\n */\nexport function isInitialized(): boolean {\n return initialized;\n}\n\n/**\n * 캐시 초기화 (테스트용)\n */\nexport function resetCache(): void {\n modelCache.clear();\n brandModelsCache.clear();\n initialized = false;\n console.log('[Model Mapping] Cache reset');\n}\n\n/**\n * 캐시된 모델 수\n */\nexport function getCachedModelCount(): number {\n return modelCache.size;\n}\n","/**\n * Firebase Storage 차량 이미지 URL/경로 생성 유틸리티\n *\n * ⚠️ 주의: 이 파일은 순수 함수만 포함합니다.\n * Firebase SDK나 업로드 로직은 포함하지 않습니다.\n */\n\nimport { VehicleImageParams } from '../types/image';\n\nconst FIREBASE_STORAGE_BASE = 'https://firebasestorage.googleapis.com/v0/b/charzing-d1600.firebasestorage.app/o';\n\n/**\n * 브랜드명을 Storage 경로용으로 정규화\n * ✅ 완전 동적 처리: Firestore ID → Storage 폴더\n *\n * 규칙: 하이픈(-)을 언더스코어(_)로 변환 후 대문자\n * - mercedes-benz → MERCEDES_BENZ\n * - mercedes-maybach → MERCEDES_MAYBACH\n * - hyundai → HYUNDAI\n * - kia → KIA\n */\nfunction normalizeStorageBrand(firestoreBrandId: string): string {\n // ✅ 완전 동적 변환: 하이픈 → 언더스코어, 대문자\n return firestoreBrandId.toUpperCase().replace(/-/g, '_');\n}\n\n/**\n * 모델명을 Storage 경로용으로 정규화\n * ✅ 동적 처리: Firestore ID → Storage 폴더 (대문자)\n */\nfunction normalizeStorageModel(firestoreModelId: string): string {\n return firestoreModelId.toUpperCase();\n}\n\n/**\n * 트림명을 파일명용으로 정규화\n */\nfunction normalizeStorageTrim(trim?: string): string {\n if (!trim) return '';\n return trim.toLowerCase().replace(/[\\s\\-]/g, '_').replace(/[^a-z0-9_]/g, '');\n}\n\n/**\n * 차량 이미지 파일명 생성\n *\n * @example\n * generateImageFilename({\n * brandId: 'kia',\n * modelId: 'niro-ev',\n * year: 2022,\n * trim: 'standard'\n * })\n * // Returns: 'kia__niro-ev__2022__standard.png'\n */\nexport function generateImageFilename(params: VehicleImageParams): string {\n const { brandId, modelId, year, trim } = params;\n const brandLower = brandId.toLowerCase();\n const modelLower = modelId.toLowerCase().replace(/[\\s]/g, '-');\n const normalizedTrim = trim ? normalizeStorageTrim(trim) : '';\n const trimPart = normalizedTrim ? `__${normalizedTrim}` : '';\n return `${brandLower}__${modelLower}__${year}${trimPart}.png`;\n}\n\n/**\n * Storage 경로 생성 (업로드용)\n *\n * @example\n * generateStoragePath({\n * brandId: 'kia',\n * modelId: 'niro-ev',\n * year: 2022,\n * trim: 'standard'\n * })\n * // Returns: 'vehicle-images/KIA/NIRO-EV/2022/kia__niro-ev__2022__standard.png'\n */\nexport function generateStoragePath(params: VehicleImageParams): string {\n const { brandId, modelId, year } = params;\n const storageBrand = normalizeStorageBrand(brandId);\n const storageModel = normalizeStorageModel(modelId);\n const filename = generateImageFilename(params);\n return `vehicle-images/${storageBrand}/${storageModel}/${year}/${filename}`;\n}\n\n/**\n * 완전한 Firebase Storage URL 생성 (읽기용)\n *\n * ✅ 파라미터 검증: brandId, modelId, year 필수\n * ❌ Fallback 금지: 잘못된 파라미터 시 에러 throw\n *\n * @throws {Error} brandId, modelId, year가 없으면 에러\n *\n * @example\n * generateVehicleImageUrl({\n * brandId: 'kia',\n * modelId: 'niro-ev',\n * year: 2022\n * })\n * // Returns: 'https://firebasestorage.googleapis.com/.../vehicle-images%2FKIA%2FNIRO-EV%2F2022%2Fkia__niro-ev__2022.png?alt=media'\n */\nexport function generateVehicleImageUrl(params: VehicleImageParams): string {\n // ✅ 필수 파라미터 검증 (fallback 금지)\n if (!params.brandId) {\n throw new Error('brandId is required');\n }\n if (!params.modelId) {\n throw new Error('modelId is required');\n }\n if (!params.year) {\n throw new Error('year is required');\n }\n\n const path = generateStoragePath(params);\n const encodedPath = encodeURIComponent(path);\n return `${FIREBASE_STORAGE_BASE}/${encodedPath}?alt=media`;\n}\n","/**\n * 셀맵(모듈별 전압) 미지원 차량 판정\n *\n * 일부 차량은 진단기(vsNew·aviloo)가 brick 단위 전압을 내려주지 않고 최소/최대만 준다.\n * 그 차량의 리포트는 셀맵을 그리지 않고 안내 문구를 대신 노출하며,\n * 셀 관련 파이프라인 검사도 면제한다(= 정상 리포트로 취급, 크레딧 정상 차감).\n *\n * ⚠️ \"진단 미지원\"이 아니다. SOH·용량·주행거리·min/max 전압은 정상으로 나온다.\n *\n * 근거·설계: charzing/operation/2026-09-07-cellmap-unsupported-template-research.md\n */\n\n/** 셀맵 미지원 차량군 1건 */\nexport interface CellMapUnsupportedGroup {\n /** 사람이 읽는 이름 (로그·디버깅용) */\n readonly label: string;\n /** Firestore yearTemplate 문서 ID 목록 */\n readonly templateIds: readonly string[];\n /**\n * 해당 연식.\n * templateId와 **AND 조건**이다 — 파이프라인의 템플릿 폴백이 연식 불일치 시\n * 임의의 템플릿을 집을 수 있어(lookupDbVehicleSpecs), templateId만으로 판정하면\n * 멀쩡한 차의 셀맵이 숨겨진다.\n */\n readonly years: readonly number[];\n /**\n * templateId가 없는 리포트용 이름 폴백.\n * VIN 디코드가 트림을 못 풀면 vehicleRef가 만들어지지 않고,\n * 그때 차량명은 vsNew 원문(영문)이 들어온다 — 한글·영문을 모두 적는다.\n */\n readonly names: {\n readonly brand: readonly string[];\n readonly model: readonly string[];\n readonly trim?: readonly string[];\n };\n /** 왜 미지원인지 (운영 기록용) */\n readonly reason: string;\n}\n\n/**\n * 셀맵을 받을 수 없는 차량군.\n *\n * **새 차량이 생기면 여기에 블록 하나만 추가한다.** 웹·어드민·앱·파이프라인이\n * 모두 이 배열을 본다.\n */\nexport const CELL_MAP_UNSUPPORTED_GROUPS: readonly CellMapUnsupportedGroup[] = [\n {\n label: 'MINI 쿠퍼 SE (2022~2023)',\n templateIds: ['YsOQDxElF5P0thuUHKhE'], // vehicles/mini/models/COOPER — name: cooper-se_2022_2023\n years: [2022, 2023],\n names: {\n brand: ['미니', 'MINI', 'Mini'],\n model: ['쿠퍼', 'COOPER', 'Cooper', 'Cooper SE', '쿠퍼 SE'],\n trim: ['SE'],\n },\n reason: '진단기가 모듈별 전압을 내려주지 않음 (min/max만 제공)',\n },\n];\n\n/** 셀맵 자리에 노출하는 안내 문구 — 모든 화면이 같은 말을 하도록 여기서만 정의한다 */\nexport const CELL_MAP_UNSUPPORTED_NOTICE = {\n /** 항상 노출 */\n main: '이 차량은 모듈별 전압을 제공하지 않습니다',\n /** min/max가 있을 때만 덧붙인다 — 없는 값을 있다고 말하지 않기 위해 */\n sub: '최소·최대 전압만 측정됩니다',\n /** 어드민 수동 입력 화면에서 셀 입력을 잠글 때 */\n inputLocked: '이 차량은 모듈별 전압을 제공하지 않습니다. 최소·최대 전압만 입력하세요.',\n} as const;\n\n/** 판정에 필요한 리포트 필드만 추린 모양 — 웹/어드민/앱/CF의 리포트 타입이 모두 이 모양을 만족한다 */\nexport interface CellMapReportRef {\n vehicleRef?: { templateId?: string | null } | null;\n marketplaceData?: { vehicleRef?: { templateId?: string | null } | null } | null;\n vehicleBrand?: string | null;\n vehicleModel?: string | null;\n vehicleTrim?: string | null;\n vehicleYear?: string | number | null;\n}\n\n/** 비교용 정규화 — 공백 제거 + 소문자 */\nfunction normalizeName(value: string): string {\n return value.replace(/\\s+/g, '').toLowerCase();\n}\n\nfunction matchesAnyName(value: string | null | undefined, candidates: readonly string[]): boolean {\n if (!value) return false;\n const target = normalizeName(value);\n if (!target) return false;\n return candidates.some((candidate) => normalizeName(candidate) === target);\n}\n\n/** 문자열/숫자 어느 쪽으로 저장돼 있어도 연식을 숫자로 읽는다 */\nfunction parseYear(value: string | number | null | undefined): number | null {\n if (typeof value === 'number') return Number.isFinite(value) ? value : null;\n if (typeof value === 'string') {\n const parsed = Number.parseInt(value, 10);\n return Number.isNaN(parsed) ? null : parsed;\n }\n return null;\n}\n\n/** 리포트에서 yearTemplate ID를 꺼낸다 — marketplaceData 우선(파이프라인이 여기에 쓴다) */\nfunction readTemplateId(report: CellMapReportRef): string | null {\n return (\n report.marketplaceData?.vehicleRef?.templateId ??\n report.vehicleRef?.templateId ??\n null\n );\n}\n\n/**\n * 이 리포트의 차량이 셀맵 미지원 차량군에 속하면 해당 그룹을, 아니면 null을 반환한다.\n *\n * 판정 순서\n * 1. templateId가 목록에 있고 연식도 맞으면 미지원\n * (연식을 모르는 과거 문서는 templateId만으로 인정 — 그 문서엔 폴백 위험이 없다)\n * 2. templateId가 없으면 브랜드/모델(+트림) 이름 + 연식으로 대조\n */\nexport function findCellMapUnsupportedGroup(\n report: CellMapReportRef | null | undefined\n): CellMapUnsupportedGroup | null {\n if (!report) return null;\n\n const templateId = readTemplateId(report);\n const year = parseYear(report.vehicleYear);\n\n for (const group of CELL_MAP_UNSUPPORTED_GROUPS) {\n if (templateId && group.templateIds.includes(templateId)) {\n // 연식을 알면 반드시 일치해야 한다 (템플릿 폴백 오탐 차단)\n if (year === null || group.years.includes(year)) return group;\n continue;\n }\n\n if (!templateId) {\n const brandHit = matchesAnyName(report.vehicleBrand, group.names.brand);\n const modelHit = matchesAnyName(report.vehicleModel, group.names.model);\n const trimHit =\n group.names.trim === undefined || group.names.trim.length === 0\n ? true\n : matchesAnyName(report.vehicleTrim, group.names.trim);\n const yearHit = year === null ? false : group.years.includes(year);\n\n if (brandHit && modelHit && trimHit && yearHit) return group;\n }\n }\n\n return null;\n}\n\n/**\n * 셀맵(모듈별 전압)을 신뢰할 수 있는 차량인가.\n *\n * `false`면 화면은 셀맵 대신 안내 문구를 그리고,\n * 파이프라인은 셀 관련 검사를 면제한다.\n */\nexport function isCellMapSupported(report: CellMapReportRef | null | undefined): boolean {\n return findCellMapUnsupportedGroup(report) === null;\n}\n\n// ─── 셀 전압 판정 (min/max만 있을 때) ─────────────────────────────────────────\n\n/** 셀 전압 진단 항목 3단계 */\nexport type CellVoltageVerdict = 'normal' | 'warning' | 'danger';\n\n/**\n * 전압차(최대-최소) 임계값.\n *\n * 셀맵이 있는 차량은 \"셀별 평균 대비 편차\"로 판정하지만, 셀맵이 없으면 쓸 수 있는 건\n * 최대-최소 폭뿐이다. 환산 범위가 분포에 따라 [1배, 2배]로 갈리므로 오탐 비용으로 갈랐다.\n * - 주의는 하한(0.03) — 넉넉히 띄워 한 번 더 보게 한다\n * - 위험은 0.10 — 업계 통용 기준(100mV 초과 = 불균형·약한 셀·접점 문제)과 겹친다\n */\nexport const CELL_VOLTAGE_DELTA_THRESHOLDS = {\n warning: 0.03,\n danger: 0.1,\n} as const;\n\n/** 기존 통계칸과 같은 방식으로 소수 둘째 자리에서 반올림 */\nfunction round2(value: number): number {\n return Math.round(value * 100) / 100;\n}\n\n/**\n * 최소/최대 전압만으로 셀 전압 진단 항목을 판정한다.\n *\n * 값이 없으면 `null` — 호출부는 그 행을 숨긴다(근거 없이 \"정상\"이라고 쓰지 않는다).\n */\nexport function judgeCellVoltageByDelta(\n minVoltage: number | null | undefined,\n maxVoltage: number | null | undefined\n): CellVoltageVerdict | null {\n if (typeof minVoltage !== 'number' || typeof maxVoltage !== 'number') return null;\n if (!Number.isFinite(minVoltage) || !Number.isFinite(maxVoltage)) return null;\n\n const delta = round2(round2(maxVoltage) - round2(minVoltage));\n if (delta >= CELL_VOLTAGE_DELTA_THRESHOLDS.danger) return 'danger';\n if (delta >= CELL_VOLTAGE_DELTA_THRESHOLDS.warning) return 'warning';\n return 'normal';\n}\n"],"mappings":"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","names":["getBrandMapping","getModelMapping","isInitialized","resetCache","getBrandMapping","import_firestore","initialized","getModelMapping","isInitialized","resetCache"]}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 셀맵(모듈별 전압) 미지원 차량 판정
|
|
3
|
+
*
|
|
4
|
+
* 일부 차량은 진단기(vsNew·aviloo)가 brick 단위 전압을 내려주지 않고 최소/최대만 준다.
|
|
5
|
+
* 그 차량의 리포트는 셀맵을 그리지 않고 안내 문구를 대신 노출하며,
|
|
6
|
+
* 셀 관련 파이프라인 검사도 면제한다(= 정상 리포트로 취급, 크레딧 정상 차감).
|
|
7
|
+
*
|
|
8
|
+
* ⚠️ "진단 미지원"이 아니다. SOH·용량·주행거리·min/max 전압은 정상으로 나온다.
|
|
9
|
+
*
|
|
10
|
+
* 근거·설계: charzing/operation/2026-09-07-cellmap-unsupported-template-research.md
|
|
11
|
+
*/
|
|
12
|
+
/** 셀맵 미지원 차량군 1건 */
|
|
13
|
+
interface CellMapUnsupportedGroup {
|
|
14
|
+
/** 사람이 읽는 이름 (로그·디버깅용) */
|
|
15
|
+
readonly label: string;
|
|
16
|
+
/** Firestore yearTemplate 문서 ID 목록 */
|
|
17
|
+
readonly templateIds: readonly string[];
|
|
18
|
+
/**
|
|
19
|
+
* 해당 연식.
|
|
20
|
+
* templateId와 **AND 조건**이다 — 파이프라인의 템플릿 폴백이 연식 불일치 시
|
|
21
|
+
* 임의의 템플릿을 집을 수 있어(lookupDbVehicleSpecs), templateId만으로 판정하면
|
|
22
|
+
* 멀쩡한 차의 셀맵이 숨겨진다.
|
|
23
|
+
*/
|
|
24
|
+
readonly years: readonly number[];
|
|
25
|
+
/**
|
|
26
|
+
* templateId가 없는 리포트용 이름 폴백.
|
|
27
|
+
* VIN 디코드가 트림을 못 풀면 vehicleRef가 만들어지지 않고,
|
|
28
|
+
* 그때 차량명은 vsNew 원문(영문)이 들어온다 — 한글·영문을 모두 적는다.
|
|
29
|
+
*/
|
|
30
|
+
readonly names: {
|
|
31
|
+
readonly brand: readonly string[];
|
|
32
|
+
readonly model: readonly string[];
|
|
33
|
+
readonly trim?: readonly string[];
|
|
34
|
+
};
|
|
35
|
+
/** 왜 미지원인지 (운영 기록용) */
|
|
36
|
+
readonly reason: string;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* 셀맵을 받을 수 없는 차량군.
|
|
40
|
+
*
|
|
41
|
+
* **새 차량이 생기면 여기에 블록 하나만 추가한다.** 웹·어드민·앱·파이프라인이
|
|
42
|
+
* 모두 이 배열을 본다.
|
|
43
|
+
*/
|
|
44
|
+
declare const CELL_MAP_UNSUPPORTED_GROUPS: readonly CellMapUnsupportedGroup[];
|
|
45
|
+
/** 셀맵 자리에 노출하는 안내 문구 — 모든 화면이 같은 말을 하도록 여기서만 정의한다 */
|
|
46
|
+
declare const CELL_MAP_UNSUPPORTED_NOTICE: {
|
|
47
|
+
/** 항상 노출 */
|
|
48
|
+
readonly main: "이 차량은 모듈별 전압을 제공하지 않습니다";
|
|
49
|
+
/** min/max가 있을 때만 덧붙인다 — 없는 값을 있다고 말하지 않기 위해 */
|
|
50
|
+
readonly sub: "최소·최대 전압만 측정됩니다";
|
|
51
|
+
/** 어드민 수동 입력 화면에서 셀 입력을 잠글 때 */
|
|
52
|
+
readonly inputLocked: "이 차량은 모듈별 전압을 제공하지 않습니다. 최소·최대 전압만 입력하세요.";
|
|
53
|
+
};
|
|
54
|
+
/** 판정에 필요한 리포트 필드만 추린 모양 — 웹/어드민/앱/CF의 리포트 타입이 모두 이 모양을 만족한다 */
|
|
55
|
+
interface CellMapReportRef {
|
|
56
|
+
vehicleRef?: {
|
|
57
|
+
templateId?: string | null;
|
|
58
|
+
} | null;
|
|
59
|
+
marketplaceData?: {
|
|
60
|
+
vehicleRef?: {
|
|
61
|
+
templateId?: string | null;
|
|
62
|
+
} | null;
|
|
63
|
+
} | null;
|
|
64
|
+
vehicleBrand?: string | null;
|
|
65
|
+
vehicleModel?: string | null;
|
|
66
|
+
vehicleTrim?: string | null;
|
|
67
|
+
vehicleYear?: string | number | null;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* 이 리포트의 차량이 셀맵 미지원 차량군에 속하면 해당 그룹을, 아니면 null을 반환한다.
|
|
71
|
+
*
|
|
72
|
+
* 판정 순서
|
|
73
|
+
* 1. templateId가 목록에 있고 연식도 맞으면 미지원
|
|
74
|
+
* (연식을 모르는 과거 문서는 templateId만으로 인정 — 그 문서엔 폴백 위험이 없다)
|
|
75
|
+
* 2. templateId가 없으면 브랜드/모델(+트림) 이름 + 연식으로 대조
|
|
76
|
+
*/
|
|
77
|
+
declare function findCellMapUnsupportedGroup(report: CellMapReportRef | null | undefined): CellMapUnsupportedGroup | null;
|
|
78
|
+
/**
|
|
79
|
+
* 셀맵(모듈별 전압)을 신뢰할 수 있는 차량인가.
|
|
80
|
+
*
|
|
81
|
+
* `false`면 화면은 셀맵 대신 안내 문구를 그리고,
|
|
82
|
+
* 파이프라인은 셀 관련 검사를 면제한다.
|
|
83
|
+
*/
|
|
84
|
+
declare function isCellMapSupported(report: CellMapReportRef | null | undefined): boolean;
|
|
85
|
+
/** 셀 전압 진단 항목 3단계 */
|
|
86
|
+
type CellVoltageVerdict = 'normal' | 'warning' | 'danger';
|
|
87
|
+
/**
|
|
88
|
+
* 전압차(최대-최소) 임계값.
|
|
89
|
+
*
|
|
90
|
+
* 셀맵이 있는 차량은 "셀별 평균 대비 편차"로 판정하지만, 셀맵이 없으면 쓸 수 있는 건
|
|
91
|
+
* 최대-최소 폭뿐이다. 환산 범위가 분포에 따라 [1배, 2배]로 갈리므로 오탐 비용으로 갈랐다.
|
|
92
|
+
* - 주의는 하한(0.03) — 넉넉히 띄워 한 번 더 보게 한다
|
|
93
|
+
* - 위험은 0.10 — 업계 통용 기준(100mV 초과 = 불균형·약한 셀·접점 문제)과 겹친다
|
|
94
|
+
*/
|
|
95
|
+
declare const CELL_VOLTAGE_DELTA_THRESHOLDS: {
|
|
96
|
+
readonly warning: 0.03;
|
|
97
|
+
readonly danger: 0.1;
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* 최소/최대 전압만으로 셀 전압 진단 항목을 판정한다.
|
|
101
|
+
*
|
|
102
|
+
* 값이 없으면 `null` — 호출부는 그 행을 숨긴다(근거 없이 "정상"이라고 쓰지 않는다).
|
|
103
|
+
*/
|
|
104
|
+
declare function judgeCellVoltageByDelta(minVoltage: number | null | undefined, maxVoltage: number | null | undefined): CellVoltageVerdict | null;
|
|
105
|
+
|
|
106
|
+
export { CELL_MAP_UNSUPPORTED_GROUPS, CELL_MAP_UNSUPPORTED_NOTICE, CELL_VOLTAGE_DELTA_THRESHOLDS, type CellMapReportRef, type CellMapUnsupportedGroup, type CellVoltageVerdict, findCellMapUnsupportedGroup, isCellMapSupported, judgeCellVoltageByDelta };
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/report/cellMap.ts
|
|
21
|
+
var cellMap_exports = {};
|
|
22
|
+
__export(cellMap_exports, {
|
|
23
|
+
CELL_MAP_UNSUPPORTED_GROUPS: () => CELL_MAP_UNSUPPORTED_GROUPS,
|
|
24
|
+
CELL_MAP_UNSUPPORTED_NOTICE: () => CELL_MAP_UNSUPPORTED_NOTICE,
|
|
25
|
+
CELL_VOLTAGE_DELTA_THRESHOLDS: () => CELL_VOLTAGE_DELTA_THRESHOLDS,
|
|
26
|
+
findCellMapUnsupportedGroup: () => findCellMapUnsupportedGroup,
|
|
27
|
+
isCellMapSupported: () => isCellMapSupported,
|
|
28
|
+
judgeCellVoltageByDelta: () => judgeCellVoltageByDelta
|
|
29
|
+
});
|
|
30
|
+
module.exports = __toCommonJS(cellMap_exports);
|
|
31
|
+
var CELL_MAP_UNSUPPORTED_GROUPS = [
|
|
32
|
+
{
|
|
33
|
+
label: "MINI \uCFE0\uD37C SE (2022~2023)",
|
|
34
|
+
templateIds: ["YsOQDxElF5P0thuUHKhE"],
|
|
35
|
+
// vehicles/mini/models/COOPER — name: cooper-se_2022_2023
|
|
36
|
+
years: [2022, 2023],
|
|
37
|
+
names: {
|
|
38
|
+
brand: ["\uBBF8\uB2C8", "MINI", "Mini"],
|
|
39
|
+
model: ["\uCFE0\uD37C", "COOPER", "Cooper", "Cooper SE", "\uCFE0\uD37C SE"],
|
|
40
|
+
trim: ["SE"]
|
|
41
|
+
},
|
|
42
|
+
reason: "\uC9C4\uB2E8\uAE30\uAC00 \uBAA8\uB4C8\uBCC4 \uC804\uC555\uC744 \uB0B4\uB824\uC8FC\uC9C0 \uC54A\uC74C (min/max\uB9CC \uC81C\uACF5)"
|
|
43
|
+
}
|
|
44
|
+
];
|
|
45
|
+
var CELL_MAP_UNSUPPORTED_NOTICE = {
|
|
46
|
+
/** 항상 노출 */
|
|
47
|
+
main: "\uC774 \uCC28\uB7C9\uC740 \uBAA8\uB4C8\uBCC4 \uC804\uC555\uC744 \uC81C\uACF5\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4",
|
|
48
|
+
/** min/max가 있을 때만 덧붙인다 — 없는 값을 있다고 말하지 않기 위해 */
|
|
49
|
+
sub: "\uCD5C\uC18C\xB7\uCD5C\uB300 \uC804\uC555\uB9CC \uCE21\uC815\uB429\uB2C8\uB2E4",
|
|
50
|
+
/** 어드민 수동 입력 화면에서 셀 입력을 잠글 때 */
|
|
51
|
+
inputLocked: "\uC774 \uCC28\uB7C9\uC740 \uBAA8\uB4C8\uBCC4 \uC804\uC555\uC744 \uC81C\uACF5\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. \uCD5C\uC18C\xB7\uCD5C\uB300 \uC804\uC555\uB9CC \uC785\uB825\uD558\uC138\uC694."
|
|
52
|
+
};
|
|
53
|
+
function normalizeName(value) {
|
|
54
|
+
return value.replace(/\s+/g, "").toLowerCase();
|
|
55
|
+
}
|
|
56
|
+
function matchesAnyName(value, candidates) {
|
|
57
|
+
if (!value) return false;
|
|
58
|
+
const target = normalizeName(value);
|
|
59
|
+
if (!target) return false;
|
|
60
|
+
return candidates.some((candidate) => normalizeName(candidate) === target);
|
|
61
|
+
}
|
|
62
|
+
function parseYear(value) {
|
|
63
|
+
if (typeof value === "number") return Number.isFinite(value) ? value : null;
|
|
64
|
+
if (typeof value === "string") {
|
|
65
|
+
const parsed = Number.parseInt(value, 10);
|
|
66
|
+
return Number.isNaN(parsed) ? null : parsed;
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
function readTemplateId(report) {
|
|
71
|
+
return report.marketplaceData?.vehicleRef?.templateId ?? report.vehicleRef?.templateId ?? null;
|
|
72
|
+
}
|
|
73
|
+
function findCellMapUnsupportedGroup(report) {
|
|
74
|
+
if (!report) return null;
|
|
75
|
+
const templateId = readTemplateId(report);
|
|
76
|
+
const year = parseYear(report.vehicleYear);
|
|
77
|
+
for (const group of CELL_MAP_UNSUPPORTED_GROUPS) {
|
|
78
|
+
if (templateId && group.templateIds.includes(templateId)) {
|
|
79
|
+
if (year === null || group.years.includes(year)) return group;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (!templateId) {
|
|
83
|
+
const brandHit = matchesAnyName(report.vehicleBrand, group.names.brand);
|
|
84
|
+
const modelHit = matchesAnyName(report.vehicleModel, group.names.model);
|
|
85
|
+
const trimHit = group.names.trim === void 0 || group.names.trim.length === 0 ? true : matchesAnyName(report.vehicleTrim, group.names.trim);
|
|
86
|
+
const yearHit = year === null ? false : group.years.includes(year);
|
|
87
|
+
if (brandHit && modelHit && trimHit && yearHit) return group;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
function isCellMapSupported(report) {
|
|
93
|
+
return findCellMapUnsupportedGroup(report) === null;
|
|
94
|
+
}
|
|
95
|
+
var CELL_VOLTAGE_DELTA_THRESHOLDS = {
|
|
96
|
+
warning: 0.03,
|
|
97
|
+
danger: 0.1
|
|
98
|
+
};
|
|
99
|
+
function round2(value) {
|
|
100
|
+
return Math.round(value * 100) / 100;
|
|
101
|
+
}
|
|
102
|
+
function judgeCellVoltageByDelta(minVoltage, maxVoltage) {
|
|
103
|
+
if (typeof minVoltage !== "number" || typeof maxVoltage !== "number") return null;
|
|
104
|
+
if (!Number.isFinite(minVoltage) || !Number.isFinite(maxVoltage)) return null;
|
|
105
|
+
const delta = round2(round2(maxVoltage) - round2(minVoltage));
|
|
106
|
+
if (delta >= CELL_VOLTAGE_DELTA_THRESHOLDS.danger) return "danger";
|
|
107
|
+
if (delta >= CELL_VOLTAGE_DELTA_THRESHOLDS.warning) return "warning";
|
|
108
|
+
return "normal";
|
|
109
|
+
}
|
|
110
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
111
|
+
0 && (module.exports = {
|
|
112
|
+
CELL_MAP_UNSUPPORTED_GROUPS,
|
|
113
|
+
CELL_MAP_UNSUPPORTED_NOTICE,
|
|
114
|
+
CELL_VOLTAGE_DELTA_THRESHOLDS,
|
|
115
|
+
findCellMapUnsupportedGroup,
|
|
116
|
+
isCellMapSupported,
|
|
117
|
+
judgeCellVoltageByDelta
|
|
118
|
+
});
|
|
119
|
+
//# sourceMappingURL=cellMap.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/report/cellMap.ts"],"sourcesContent":["/**\n * 셀맵(모듈별 전압) 미지원 차량 판정\n *\n * 일부 차량은 진단기(vsNew·aviloo)가 brick 단위 전압을 내려주지 않고 최소/최대만 준다.\n * 그 차량의 리포트는 셀맵을 그리지 않고 안내 문구를 대신 노출하며,\n * 셀 관련 파이프라인 검사도 면제한다(= 정상 리포트로 취급, 크레딧 정상 차감).\n *\n * ⚠️ \"진단 미지원\"이 아니다. SOH·용량·주행거리·min/max 전압은 정상으로 나온다.\n *\n * 근거·설계: charzing/operation/2026-09-07-cellmap-unsupported-template-research.md\n */\n\n/** 셀맵 미지원 차량군 1건 */\nexport interface CellMapUnsupportedGroup {\n /** 사람이 읽는 이름 (로그·디버깅용) */\n readonly label: string;\n /** Firestore yearTemplate 문서 ID 목록 */\n readonly templateIds: readonly string[];\n /**\n * 해당 연식.\n * templateId와 **AND 조건**이다 — 파이프라인의 템플릿 폴백이 연식 불일치 시\n * 임의의 템플릿을 집을 수 있어(lookupDbVehicleSpecs), templateId만으로 판정하면\n * 멀쩡한 차의 셀맵이 숨겨진다.\n */\n readonly years: readonly number[];\n /**\n * templateId가 없는 리포트용 이름 폴백.\n * VIN 디코드가 트림을 못 풀면 vehicleRef가 만들어지지 않고,\n * 그때 차량명은 vsNew 원문(영문)이 들어온다 — 한글·영문을 모두 적는다.\n */\n readonly names: {\n readonly brand: readonly string[];\n readonly model: readonly string[];\n readonly trim?: readonly string[];\n };\n /** 왜 미지원인지 (운영 기록용) */\n readonly reason: string;\n}\n\n/**\n * 셀맵을 받을 수 없는 차량군.\n *\n * **새 차량이 생기면 여기에 블록 하나만 추가한다.** 웹·어드민·앱·파이프라인이\n * 모두 이 배열을 본다.\n */\nexport const CELL_MAP_UNSUPPORTED_GROUPS: readonly CellMapUnsupportedGroup[] = [\n {\n label: 'MINI 쿠퍼 SE (2022~2023)',\n templateIds: ['YsOQDxElF5P0thuUHKhE'], // vehicles/mini/models/COOPER — name: cooper-se_2022_2023\n years: [2022, 2023],\n names: {\n brand: ['미니', 'MINI', 'Mini'],\n model: ['쿠퍼', 'COOPER', 'Cooper', 'Cooper SE', '쿠퍼 SE'],\n trim: ['SE'],\n },\n reason: '진단기가 모듈별 전압을 내려주지 않음 (min/max만 제공)',\n },\n];\n\n/** 셀맵 자리에 노출하는 안내 문구 — 모든 화면이 같은 말을 하도록 여기서만 정의한다 */\nexport const CELL_MAP_UNSUPPORTED_NOTICE = {\n /** 항상 노출 */\n main: '이 차량은 모듈별 전압을 제공하지 않습니다',\n /** min/max가 있을 때만 덧붙인다 — 없는 값을 있다고 말하지 않기 위해 */\n sub: '최소·최대 전압만 측정됩니다',\n /** 어드민 수동 입력 화면에서 셀 입력을 잠글 때 */\n inputLocked: '이 차량은 모듈별 전압을 제공하지 않습니다. 최소·최대 전압만 입력하세요.',\n} as const;\n\n/** 판정에 필요한 리포트 필드만 추린 모양 — 웹/어드민/앱/CF의 리포트 타입이 모두 이 모양을 만족한다 */\nexport interface CellMapReportRef {\n vehicleRef?: { templateId?: string | null } | null;\n marketplaceData?: { vehicleRef?: { templateId?: string | null } | null } | null;\n vehicleBrand?: string | null;\n vehicleModel?: string | null;\n vehicleTrim?: string | null;\n vehicleYear?: string | number | null;\n}\n\n/** 비교용 정규화 — 공백 제거 + 소문자 */\nfunction normalizeName(value: string): string {\n return value.replace(/\\s+/g, '').toLowerCase();\n}\n\nfunction matchesAnyName(value: string | null | undefined, candidates: readonly string[]): boolean {\n if (!value) return false;\n const target = normalizeName(value);\n if (!target) return false;\n return candidates.some((candidate) => normalizeName(candidate) === target);\n}\n\n/** 문자열/숫자 어느 쪽으로 저장돼 있어도 연식을 숫자로 읽는다 */\nfunction parseYear(value: string | number | null | undefined): number | null {\n if (typeof value === 'number') return Number.isFinite(value) ? value : null;\n if (typeof value === 'string') {\n const parsed = Number.parseInt(value, 10);\n return Number.isNaN(parsed) ? null : parsed;\n }\n return null;\n}\n\n/** 리포트에서 yearTemplate ID를 꺼낸다 — marketplaceData 우선(파이프라인이 여기에 쓴다) */\nfunction readTemplateId(report: CellMapReportRef): string | null {\n return (\n report.marketplaceData?.vehicleRef?.templateId ??\n report.vehicleRef?.templateId ??\n null\n );\n}\n\n/**\n * 이 리포트의 차량이 셀맵 미지원 차량군에 속하면 해당 그룹을, 아니면 null을 반환한다.\n *\n * 판정 순서\n * 1. templateId가 목록에 있고 연식도 맞으면 미지원\n * (연식을 모르는 과거 문서는 templateId만으로 인정 — 그 문서엔 폴백 위험이 없다)\n * 2. templateId가 없으면 브랜드/모델(+트림) 이름 + 연식으로 대조\n */\nexport function findCellMapUnsupportedGroup(\n report: CellMapReportRef | null | undefined\n): CellMapUnsupportedGroup | null {\n if (!report) return null;\n\n const templateId = readTemplateId(report);\n const year = parseYear(report.vehicleYear);\n\n for (const group of CELL_MAP_UNSUPPORTED_GROUPS) {\n if (templateId && group.templateIds.includes(templateId)) {\n // 연식을 알면 반드시 일치해야 한다 (템플릿 폴백 오탐 차단)\n if (year === null || group.years.includes(year)) return group;\n continue;\n }\n\n if (!templateId) {\n const brandHit = matchesAnyName(report.vehicleBrand, group.names.brand);\n const modelHit = matchesAnyName(report.vehicleModel, group.names.model);\n const trimHit =\n group.names.trim === undefined || group.names.trim.length === 0\n ? true\n : matchesAnyName(report.vehicleTrim, group.names.trim);\n const yearHit = year === null ? false : group.years.includes(year);\n\n if (brandHit && modelHit && trimHit && yearHit) return group;\n }\n }\n\n return null;\n}\n\n/**\n * 셀맵(모듈별 전압)을 신뢰할 수 있는 차량인가.\n *\n * `false`면 화면은 셀맵 대신 안내 문구를 그리고,\n * 파이프라인은 셀 관련 검사를 면제한다.\n */\nexport function isCellMapSupported(report: CellMapReportRef | null | undefined): boolean {\n return findCellMapUnsupportedGroup(report) === null;\n}\n\n// ─── 셀 전압 판정 (min/max만 있을 때) ─────────────────────────────────────────\n\n/** 셀 전압 진단 항목 3단계 */\nexport type CellVoltageVerdict = 'normal' | 'warning' | 'danger';\n\n/**\n * 전압차(최대-최소) 임계값.\n *\n * 셀맵이 있는 차량은 \"셀별 평균 대비 편차\"로 판정하지만, 셀맵이 없으면 쓸 수 있는 건\n * 최대-최소 폭뿐이다. 환산 범위가 분포에 따라 [1배, 2배]로 갈리므로 오탐 비용으로 갈랐다.\n * - 주의는 하한(0.03) — 넉넉히 띄워 한 번 더 보게 한다\n * - 위험은 0.10 — 업계 통용 기준(100mV 초과 = 불균형·약한 셀·접점 문제)과 겹친다\n */\nexport const CELL_VOLTAGE_DELTA_THRESHOLDS = {\n warning: 0.03,\n danger: 0.1,\n} as const;\n\n/** 기존 통계칸과 같은 방식으로 소수 둘째 자리에서 반올림 */\nfunction round2(value: number): number {\n return Math.round(value * 100) / 100;\n}\n\n/**\n * 최소/최대 전압만으로 셀 전압 진단 항목을 판정한다.\n *\n * 값이 없으면 `null` — 호출부는 그 행을 숨긴다(근거 없이 \"정상\"이라고 쓰지 않는다).\n */\nexport function judgeCellVoltageByDelta(\n minVoltage: number | null | undefined,\n maxVoltage: number | null | undefined\n): CellVoltageVerdict | null {\n if (typeof minVoltage !== 'number' || typeof maxVoltage !== 'number') return null;\n if (!Number.isFinite(minVoltage) || !Number.isFinite(maxVoltage)) return null;\n\n const delta = round2(round2(maxVoltage) - round2(minVoltage));\n if (delta >= CELL_VOLTAGE_DELTA_THRESHOLDS.danger) return 'danger';\n if (delta >= CELL_VOLTAGE_DELTA_THRESHOLDS.warning) return 'warning';\n return 'normal';\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA6CO,IAAM,8BAAkE;AAAA,EAC7E;AAAA,IACE,OAAO;AAAA,IACP,aAAa,CAAC,sBAAsB;AAAA;AAAA,IACpC,OAAO,CAAC,MAAM,IAAI;AAAA,IAClB,OAAO;AAAA,MACL,OAAO,CAAC,gBAAM,QAAQ,MAAM;AAAA,MAC5B,OAAO,CAAC,gBAAM,UAAU,UAAU,aAAa,iBAAO;AAAA,MACtD,MAAM,CAAC,IAAI;AAAA,IACb;AAAA,IACA,QAAQ;AAAA,EACV;AACF;AAGO,IAAM,8BAA8B;AAAA;AAAA,EAEzC,MAAM;AAAA;AAAA,EAEN,KAAK;AAAA;AAAA,EAEL,aAAa;AACf;AAaA,SAAS,cAAc,OAAuB;AAC5C,SAAO,MAAM,QAAQ,QAAQ,EAAE,EAAE,YAAY;AAC/C;AAEA,SAAS,eAAe,OAAkC,YAAwC;AAChG,MAAI,CAAC,MAAO,QAAO;AACnB,QAAM,SAAS,cAAc,KAAK;AAClC,MAAI,CAAC,OAAQ,QAAO;AACpB,SAAO,WAAW,KAAK,CAAC,cAAc,cAAc,SAAS,MAAM,MAAM;AAC3E;AAGA,SAAS,UAAU,OAA0D;AAC3E,MAAI,OAAO,UAAU,SAAU,QAAO,OAAO,SAAS,KAAK,IAAI,QAAQ;AACvE,MAAI,OAAO,UAAU,UAAU;AAC7B,UAAM,SAAS,OAAO,SAAS,OAAO,EAAE;AACxC,WAAO,OAAO,MAAM,MAAM,IAAI,OAAO;AAAA,EACvC;AACA,SAAO;AACT;AAGA,SAAS,eAAe,QAAyC;AAC/D,SACE,OAAO,iBAAiB,YAAY,cACpC,OAAO,YAAY,cACnB;AAEJ;AAUO,SAAS,4BACd,QACgC;AAChC,MAAI,CAAC,OAAQ,QAAO;AAEpB,QAAM,aAAa,eAAe,MAAM;AACxC,QAAM,OAAO,UAAU,OAAO,WAAW;AAEzC,aAAW,SAAS,6BAA6B;AAC/C,QAAI,cAAc,MAAM,YAAY,SAAS,UAAU,GAAG;AAExD,UAAI,SAAS,QAAQ,MAAM,MAAM,SAAS,IAAI,EAAG,QAAO;AACxD;AAAA,IACF;AAEA,QAAI,CAAC,YAAY;AACf,YAAM,WAAW,eAAe,OAAO,cAAc,MAAM,MAAM,KAAK;AACtE,YAAM,WAAW,eAAe,OAAO,cAAc,MAAM,MAAM,KAAK;AACtE,YAAM,UACJ,MAAM,MAAM,SAAS,UAAa,MAAM,MAAM,KAAK,WAAW,IAC1D,OACA,eAAe,OAAO,aAAa,MAAM,MAAM,IAAI;AACzD,YAAM,UAAU,SAAS,OAAO,QAAQ,MAAM,MAAM,SAAS,IAAI;AAEjE,UAAI,YAAY,YAAY,WAAW,QAAS,QAAO;AAAA,IACzD;AAAA,EACF;AAEA,SAAO;AACT;AAQO,SAAS,mBAAmB,QAAsD;AACvF,SAAO,4BAA4B,MAAM,MAAM;AACjD;AAeO,IAAM,gCAAgC;AAAA,EAC3C,SAAS;AAAA,EACT,QAAQ;AACV;AAGA,SAAS,OAAO,OAAuB;AACrC,SAAO,KAAK,MAAM,QAAQ,GAAG,IAAI;AACnC;AAOO,SAAS,wBACd,YACA,YAC2B;AAC3B,MAAI,OAAO,eAAe,YAAY,OAAO,eAAe,SAAU,QAAO;AAC7E,MAAI,CAAC,OAAO,SAAS,UAAU,KAAK,CAAC,OAAO,SAAS,UAAU,EAAG,QAAO;AAEzE,QAAM,QAAQ,OAAO,OAAO,UAAU,IAAI,OAAO,UAAU,CAAC;AAC5D,MAAI,SAAS,8BAA8B,OAAQ,QAAO;AAC1D,MAAI,SAAS,8BAA8B,QAAS,QAAO;AAC3D,SAAO;AACT;","names":[]}
|
package/dist/esm/index.d.mts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Firestore } from 'firebase/firestore';
|
|
2
|
+
export { CELL_MAP_UNSUPPORTED_GROUPS, CELL_MAP_UNSUPPORTED_NOTICE, CELL_VOLTAGE_DELTA_THRESHOLDS, CellMapReportRef, CellMapUnsupportedGroup, CellVoltageVerdict, findCellMapUnsupportedGroup, isCellMapSupported, judgeCellVoltageByDelta } from './report/cellMap.mjs';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* 브랜드 관련 타입 정의
|
|
@@ -105,6 +106,15 @@ type TrimId = string;
|
|
|
105
106
|
* 구동 방식
|
|
106
107
|
*/
|
|
107
108
|
type DriveType = 'FWD' | 'RWD' | 'AWD' | '4WD';
|
|
109
|
+
/**
|
|
110
|
+
* 배터리 옵션 (복수 배터리 제조사 지원)
|
|
111
|
+
*/
|
|
112
|
+
interface BatteryOption {
|
|
113
|
+
/** 배터리 제조사 */
|
|
114
|
+
supplier: string;
|
|
115
|
+
/** VIN 패턴, 생산 시기 등 조건 (선택사항) */
|
|
116
|
+
condition?: string;
|
|
117
|
+
}
|
|
108
118
|
/**
|
|
109
119
|
* 차량 변형 (연식별 스펙)
|
|
110
120
|
*/
|
|
@@ -115,8 +125,10 @@ interface VehicleVariant {
|
|
|
115
125
|
batteryCapacity: number;
|
|
116
126
|
/** 주행거리 (km) */
|
|
117
127
|
range: number;
|
|
118
|
-
/** 배터리 공급사 */
|
|
128
|
+
/** 배터리 공급사 (단일, 기존 호환) */
|
|
119
129
|
supplier?: string;
|
|
130
|
+
/** 복수 배터리 공급사 (optional, supplier와 상호 배타적) */
|
|
131
|
+
batteryOptions?: BatteryOption[];
|
|
120
132
|
/** 배터리 타입 */
|
|
121
133
|
batteryType?: string;
|
|
122
134
|
/** 이미지 URL (트림별 디자인 차이가 있는 경우) */
|
package/dist/esm/index.mjs
CHANGED
|
@@ -422,10 +422,95 @@ function generateVehicleImageUrl(params) {
|
|
|
422
422
|
const encodedPath = encodeURIComponent(path);
|
|
423
423
|
return `${FIREBASE_STORAGE_BASE}/${encodedPath}?alt=media`;
|
|
424
424
|
}
|
|
425
|
+
|
|
426
|
+
// src/report/cellMap.ts
|
|
427
|
+
var CELL_MAP_UNSUPPORTED_GROUPS = [
|
|
428
|
+
{
|
|
429
|
+
label: "MINI \uCFE0\uD37C SE (2022~2023)",
|
|
430
|
+
templateIds: ["YsOQDxElF5P0thuUHKhE"],
|
|
431
|
+
// vehicles/mini/models/COOPER — name: cooper-se_2022_2023
|
|
432
|
+
years: [2022, 2023],
|
|
433
|
+
names: {
|
|
434
|
+
brand: ["\uBBF8\uB2C8", "MINI", "Mini"],
|
|
435
|
+
model: ["\uCFE0\uD37C", "COOPER", "Cooper", "Cooper SE", "\uCFE0\uD37C SE"],
|
|
436
|
+
trim: ["SE"]
|
|
437
|
+
},
|
|
438
|
+
reason: "\uC9C4\uB2E8\uAE30\uAC00 \uBAA8\uB4C8\uBCC4 \uC804\uC555\uC744 \uB0B4\uB824\uC8FC\uC9C0 \uC54A\uC74C (min/max\uB9CC \uC81C\uACF5)"
|
|
439
|
+
}
|
|
440
|
+
];
|
|
441
|
+
var CELL_MAP_UNSUPPORTED_NOTICE = {
|
|
442
|
+
/** 항상 노출 */
|
|
443
|
+
main: "\uC774 \uCC28\uB7C9\uC740 \uBAA8\uB4C8\uBCC4 \uC804\uC555\uC744 \uC81C\uACF5\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4",
|
|
444
|
+
/** min/max가 있을 때만 덧붙인다 — 없는 값을 있다고 말하지 않기 위해 */
|
|
445
|
+
sub: "\uCD5C\uC18C\xB7\uCD5C\uB300 \uC804\uC555\uB9CC \uCE21\uC815\uB429\uB2C8\uB2E4",
|
|
446
|
+
/** 어드민 수동 입력 화면에서 셀 입력을 잠글 때 */
|
|
447
|
+
inputLocked: "\uC774 \uCC28\uB7C9\uC740 \uBAA8\uB4C8\uBCC4 \uC804\uC555\uC744 \uC81C\uACF5\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. \uCD5C\uC18C\xB7\uCD5C\uB300 \uC804\uC555\uB9CC \uC785\uB825\uD558\uC138\uC694."
|
|
448
|
+
};
|
|
449
|
+
function normalizeName(value) {
|
|
450
|
+
return value.replace(/\s+/g, "").toLowerCase();
|
|
451
|
+
}
|
|
452
|
+
function matchesAnyName(value, candidates) {
|
|
453
|
+
if (!value) return false;
|
|
454
|
+
const target = normalizeName(value);
|
|
455
|
+
if (!target) return false;
|
|
456
|
+
return candidates.some((candidate) => normalizeName(candidate) === target);
|
|
457
|
+
}
|
|
458
|
+
function parseYear(value) {
|
|
459
|
+
if (typeof value === "number") return Number.isFinite(value) ? value : null;
|
|
460
|
+
if (typeof value === "string") {
|
|
461
|
+
const parsed = Number.parseInt(value, 10);
|
|
462
|
+
return Number.isNaN(parsed) ? null : parsed;
|
|
463
|
+
}
|
|
464
|
+
return null;
|
|
465
|
+
}
|
|
466
|
+
function readTemplateId(report) {
|
|
467
|
+
return report.marketplaceData?.vehicleRef?.templateId ?? report.vehicleRef?.templateId ?? null;
|
|
468
|
+
}
|
|
469
|
+
function findCellMapUnsupportedGroup(report) {
|
|
470
|
+
if (!report) return null;
|
|
471
|
+
const templateId = readTemplateId(report);
|
|
472
|
+
const year = parseYear(report.vehicleYear);
|
|
473
|
+
for (const group of CELL_MAP_UNSUPPORTED_GROUPS) {
|
|
474
|
+
if (templateId && group.templateIds.includes(templateId)) {
|
|
475
|
+
if (year === null || group.years.includes(year)) return group;
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
478
|
+
if (!templateId) {
|
|
479
|
+
const brandHit = matchesAnyName(report.vehicleBrand, group.names.brand);
|
|
480
|
+
const modelHit = matchesAnyName(report.vehicleModel, group.names.model);
|
|
481
|
+
const trimHit = group.names.trim === void 0 || group.names.trim.length === 0 ? true : matchesAnyName(report.vehicleTrim, group.names.trim);
|
|
482
|
+
const yearHit = year === null ? false : group.years.includes(year);
|
|
483
|
+
if (brandHit && modelHit && trimHit && yearHit) return group;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
return null;
|
|
487
|
+
}
|
|
488
|
+
function isCellMapSupported(report) {
|
|
489
|
+
return findCellMapUnsupportedGroup(report) === null;
|
|
490
|
+
}
|
|
491
|
+
var CELL_VOLTAGE_DELTA_THRESHOLDS = {
|
|
492
|
+
warning: 0.03,
|
|
493
|
+
danger: 0.1
|
|
494
|
+
};
|
|
495
|
+
function round2(value) {
|
|
496
|
+
return Math.round(value * 100) / 100;
|
|
497
|
+
}
|
|
498
|
+
function judgeCellVoltageByDelta(minVoltage, maxVoltage) {
|
|
499
|
+
if (typeof minVoltage !== "number" || typeof maxVoltage !== "number") return null;
|
|
500
|
+
if (!Number.isFinite(minVoltage) || !Number.isFinite(maxVoltage)) return null;
|
|
501
|
+
const delta = round2(round2(maxVoltage) - round2(minVoltage));
|
|
502
|
+
if (delta >= CELL_VOLTAGE_DELTA_THRESHOLDS.danger) return "danger";
|
|
503
|
+
if (delta >= CELL_VOLTAGE_DELTA_THRESHOLDS.warning) return "warning";
|
|
504
|
+
return "normal";
|
|
505
|
+
}
|
|
425
506
|
export {
|
|
507
|
+
CELL_MAP_UNSUPPORTED_GROUPS,
|
|
508
|
+
CELL_MAP_UNSUPPORTED_NOTICE,
|
|
509
|
+
CELL_VOLTAGE_DELTA_THRESHOLDS,
|
|
426
510
|
SUPPORTED_BRANDS,
|
|
427
511
|
convertKoreanToEnglish,
|
|
428
512
|
findBestModelMatch,
|
|
513
|
+
findCellMapUnsupportedGroup,
|
|
429
514
|
generateImageFilename,
|
|
430
515
|
generateStoragePath,
|
|
431
516
|
generateVehicleImageUrl,
|
|
@@ -443,7 +528,9 @@ export {
|
|
|
443
528
|
initializeBrandMappings,
|
|
444
529
|
initializeModelMappings,
|
|
445
530
|
isInitialized as isBrandMappingInitialized,
|
|
531
|
+
isCellMapSupported,
|
|
446
532
|
isInitialized2 as isModelMappingInitialized,
|
|
533
|
+
judgeCellVoltageByDelta,
|
|
447
534
|
normalizeBrandId,
|
|
448
535
|
normalizeModelId,
|
|
449
536
|
resetCache as resetBrandCache,
|