@charzing/vehicle-utils 0.1.1 → 0.2.0

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.
@@ -1,3 +1,5 @@
1
+ import { Firestore } from 'firebase/firestore';
2
+
1
3
  /**
2
4
  * 브랜드 관련 타입 정의
3
5
  */
@@ -79,7 +81,7 @@ type ModelSlug = string;
79
81
  /**
80
82
  * 모델 매핑 결과
81
83
  */
82
- interface ModelMapping {
84
+ interface ModelMapping$1 {
83
85
  /** Firestore ID (소문자, 하이픈) */
84
86
  firestoreId: ModelId;
85
87
  /** 한글 모델명 */
@@ -450,12 +452,63 @@ declare function getStorageBrandName(brandId: BrandId): StorageBrandName;
450
452
  * // }
451
453
  * ```
452
454
  */
453
- declare function getBrandMapping(brandInput: string): BrandMapping;
455
+ declare function getBrandMapping$1(brandInput: string): BrandMapping;
454
456
  /**
455
457
  * 지원되는 모든 브랜드 ID 목록
456
458
  */
457
459
  declare const SUPPORTED_BRANDS: BrandId[];
458
460
 
461
+ /**
462
+ * 동적 브랜드 매핑 시스템
463
+ *
464
+ * Firestore에서 실시간으로 브랜드 정보를 조회하여 캐싱
465
+ * 새로운 브랜드 추가 시 코드 수정 불필요
466
+ */
467
+
468
+ /**
469
+ * Firestore에서 브랜드 정보를 조회하여 캐시 초기화
470
+ *
471
+ * @param firestore - Firestore 인스턴스
472
+ *
473
+ * @example
474
+ * ```typescript
475
+ * import { initializeBrandMappings } from '@charzing/vehicle-utils';
476
+ * import { db } from './firebase';
477
+ *
478
+ * // 앱 시작 시 한 번만 호출
479
+ * await initializeBrandMappings(db);
480
+ * ```
481
+ */
482
+ declare function initializeBrandMappings(firestore: Firestore): Promise<void>;
483
+ /**
484
+ * 브랜드 매핑 정보 조회
485
+ *
486
+ * @param brandInput - 브랜드명 (한글/영문/ID 모두 가능)
487
+ * @returns 브랜드 매핑 정보 또는 null
488
+ *
489
+ * @throws {Error} 초기화되지 않은 경우
490
+ *
491
+ * @example
492
+ * ```typescript
493
+ * getBrandMapping('현대'); // { firestoreId: 'hyundai', ... }
494
+ * getBrandMapping('HYUNDAI'); // { firestoreId: 'hyundai', ... }
495
+ * getBrandMapping('hyundai'); // { firestoreId: 'hyundai', ... }
496
+ * ```
497
+ */
498
+ declare function getBrandMapping(brandInput: string): BrandMapping | null;
499
+ /**
500
+ * 초기화 상태 확인
501
+ */
502
+ declare function isInitialized$1(): boolean;
503
+ /**
504
+ * 캐시 초기화 (테스트용)
505
+ */
506
+ declare function resetCache$1(): void;
507
+ /**
508
+ * 캐시된 브랜드 수
509
+ */
510
+ declare function getCachedBrandCount(): number;
511
+
459
512
  /**
460
513
  * 모델 매핑 유틸리티
461
514
  *
@@ -559,7 +612,77 @@ declare function getModelSlug(modelInput: string): ModelSlug;
559
612
  * // }
560
613
  * ```
561
614
  */
562
- declare function getModelMapping(modelInput: string, englishName?: string, koreanName?: string): ModelMapping;
615
+ declare function getModelMapping$1(modelInput: string, englishName?: string, koreanName?: string): ModelMapping$1;
616
+
617
+ /**
618
+ * 동적 모델 매핑 시스템
619
+ *
620
+ * Firestore에서 실시간으로 모델 정보를 조회하여 캐싱
621
+ * 새로운 모델 추가 시 코드 수정 불필요
622
+ */
623
+
624
+ /**
625
+ * 모델 매핑 정보
626
+ */
627
+ interface ModelMapping {
628
+ firestoreId: string;
629
+ koreanName: string;
630
+ englishName: string;
631
+ storageName: string;
632
+ brandId: string;
633
+ }
634
+ /**
635
+ * Firestore에서 모델 정보를 조회하여 캐시 초기화
636
+ *
637
+ * @param firestore - Firestore 인스턴스
638
+ * @param brandIds - 초기화할 브랜드 ID 목록 (생략 시 모든 브랜드)
639
+ *
640
+ * @example
641
+ * ```typescript
642
+ * // 모든 브랜드의 모델 초기화
643
+ * await initializeModelMappings(db);
644
+ *
645
+ * // 특정 브랜드만 초기화
646
+ * await initializeModelMappings(db, ['hyundai', 'kia']);
647
+ * ```
648
+ */
649
+ declare function initializeModelMappings(firestore: Firestore, brandIds?: string[]): Promise<void>;
650
+ /**
651
+ * 모델 매핑 정보 조회
652
+ *
653
+ * @param brandId - 브랜드 ID
654
+ * @param modelInput - 모델명 (한글/영문/ID 모두 가능)
655
+ * @returns 모델 매핑 정보 또는 null
656
+ *
657
+ * @throws {Error} 초기화되지 않은 경우
658
+ *
659
+ * @example
660
+ * ```typescript
661
+ * getModelMapping('hyundai', '아이오닉 5'); // { firestoreId: 'ioniq-5', ... }
662
+ * getModelMapping('hyundai', 'IONIQ-5'); // { firestoreId: 'ioniq-5', ... }
663
+ * getModelMapping('hyundai', 'ioniq-5'); // { firestoreId: 'ioniq-5', ... }
664
+ * ```
665
+ */
666
+ declare function getModelMapping(brandId: string, modelInput: string): ModelMapping | null;
667
+ /**
668
+ * 브랜드의 모든 모델 조회
669
+ *
670
+ * @param brandId - 브랜드 ID
671
+ * @returns 모델 매핑 배열
672
+ */
673
+ declare function getBrandModels(brandId: string): ModelMapping[];
674
+ /**
675
+ * 초기화 상태 확인
676
+ */
677
+ declare function isInitialized(): boolean;
678
+ /**
679
+ * 캐시 초기화 (테스트용)
680
+ */
681
+ declare function resetCache(): void;
682
+ /**
683
+ * 캐시된 모델 수
684
+ */
685
+ declare function getCachedModelCount(): number;
563
686
 
564
687
  /**
565
688
  * 이미지 파일명 생성 유틸리티
@@ -711,4 +834,4 @@ declare function getVehicleImagePath(params: VehicleImageParams, options?: Image
711
834
  */
712
835
  declare function generateFallbackUrls(params: VehicleImageParams, options?: ImageUrlOptions): string[];
713
836
 
714
- export { type BatteryInfo, type Brand, type BrandId, type BrandMapping, type CompletedVehicle, type DriveType, type FirebaseStorageConfig, type ImageFallbackOptions, type ImageFallbackStrategy, type ImageFilename, type ImagePath, type ImageUrlOptions, type Model, type ModelId, type ModelMapping, type ModelSlug, SUPPORTED_BRANDS, type StorageBrandName, type TrimId, type TrimMapping, type TrimSlug, type UserVehicle, type VehicleDetails, type VehicleImageParams, type VehicleModel, type VehicleTrim, type VehicleVariant, convertKoreanToEnglish, findBestModelMatch, generateFallbackUrls, generateImageFilename, generateVehicleImageUrl, getBrandMapping, getBrandNameEnglish, getBrandNameKorean, getModelMapping, getModelSlug, getStorageBrandName, getVehicleImagePath, isValidImageFilename, normalizeBrandId, normalizeModelId, parseImageFilename, toSlug };
837
+ export { type BatteryInfo, type Brand, type BrandId, type BrandMapping, type CompletedVehicle, type DriveType, type FirebaseStorageConfig, type ImageFallbackOptions, type ImageFallbackStrategy, type ImageFilename, type ImagePath, type ImageUrlOptions, type Model, type ModelId, type ModelMapping$1 as ModelMapping, type ModelSlug, SUPPORTED_BRANDS, type StorageBrandName, type TrimId, type TrimMapping, type TrimSlug, type UserVehicle, type VehicleDetails, type VehicleImageParams, type VehicleModel, type VehicleTrim, type VehicleVariant, convertKoreanToEnglish, findBestModelMatch, generateFallbackUrls, generateImageFilename, generateVehicleImageUrl, getBrandMapping$1 as getBrandMapping, getBrandModels, getBrandNameEnglish, getBrandNameKorean, getCachedBrandCount, getCachedModelCount, getBrandMapping as getDynamicBrandMapping, getModelMapping as getDynamicModelMapping, getModelMapping$1 as getModelMapping, getModelSlug, getStorageBrandName, getVehicleImagePath, initializeBrandMappings, initializeModelMappings, isInitialized$1 as isBrandMappingInitialized, isInitialized as isModelMappingInitialized, isValidImageFilename, normalizeBrandId, normalizeModelId, parseImageFilename, resetCache$1 as resetBrandCache, resetCache as resetModelCache, toSlug };
package/dist/cjs/index.js CHANGED
@@ -27,16 +27,27 @@ __export(index_exports, {
27
27
  generateImageFilename: () => generateImageFilename,
28
28
  generateVehicleImageUrl: () => generateVehicleImageUrl,
29
29
  getBrandMapping: () => getBrandMapping,
30
+ getBrandModels: () => getBrandModels,
30
31
  getBrandNameEnglish: () => getBrandNameEnglish,
31
32
  getBrandNameKorean: () => getBrandNameKorean,
33
+ getCachedBrandCount: () => getCachedBrandCount,
34
+ getCachedModelCount: () => getCachedModelCount,
35
+ getDynamicBrandMapping: () => getBrandMapping2,
36
+ getDynamicModelMapping: () => getModelMapping2,
32
37
  getModelMapping: () => getModelMapping,
33
38
  getModelSlug: () => getModelSlug,
34
39
  getStorageBrandName: () => getStorageBrandName,
35
40
  getVehicleImagePath: () => getVehicleImagePath,
41
+ initializeBrandMappings: () => initializeBrandMappings,
42
+ initializeModelMappings: () => initializeModelMappings,
43
+ isBrandMappingInitialized: () => isInitialized,
44
+ isModelMappingInitialized: () => isInitialized2,
36
45
  isValidImageFilename: () => isValidImageFilename,
37
46
  normalizeBrandId: () => normalizeBrandId,
38
47
  normalizeModelId: () => normalizeModelId,
39
48
  parseImageFilename: () => parseImageFilename,
49
+ resetBrandCache: () => resetCache,
50
+ resetModelCache: () => resetCache2,
40
51
  toSlug: () => toSlug
41
52
  });
42
53
  module.exports = __toCommonJS(index_exports);
@@ -158,6 +169,76 @@ var SUPPORTED_BRANDS = [
158
169
  "porsche"
159
170
  ];
160
171
 
172
+ // src/brand/dynamic-mapping.ts
173
+ var import_firestore = require("firebase/firestore");
174
+ var brandCache = /* @__PURE__ */ new Map();
175
+ var initialized = false;
176
+ async function initializeBrandMappings(firestore) {
177
+ if (initialized) {
178
+ console.log("[Brand Mapping] Already initialized");
179
+ return;
180
+ }
181
+ console.log("[Brand Mapping] Initializing from Firestore...");
182
+ try {
183
+ const vehiclesRef = (0, import_firestore.collection)(firestore, "vehicles");
184
+ const snapshot = await (0, import_firestore.getDocs)(vehiclesRef);
185
+ snapshot.docs.forEach((doc) => {
186
+ const data = doc.data();
187
+ let storageName = data.englishName?.toUpperCase() || doc.id.toUpperCase();
188
+ if (doc.id === "mercedes-benz" || doc.id === "mercedes-maybach") {
189
+ storageName = "BENZ";
190
+ }
191
+ const mapping = {
192
+ firestoreId: doc.id,
193
+ koreanName: data.name || doc.id,
194
+ englishName: data.englishName || doc.id.toUpperCase(),
195
+ storageName
196
+ };
197
+ brandCache.set(doc.id, mapping);
198
+ brandCache.set(doc.id.toUpperCase(), mapping);
199
+ brandCache.set(doc.id.toLowerCase(), mapping);
200
+ brandCache.set(data.name, mapping);
201
+ brandCache.set(data.englishName, mapping);
202
+ brandCache.set(data.englishName?.toUpperCase(), mapping);
203
+ brandCache.set(data.englishName?.toLowerCase(), mapping);
204
+ });
205
+ initialized = true;
206
+ console.log(`[Brand Mapping] \u2705 Initialized with ${snapshot.docs.length} brands`);
207
+ } catch (error) {
208
+ console.error("[Brand Mapping] \u274C Initialization failed:", error);
209
+ throw error;
210
+ }
211
+ }
212
+ function getBrandMapping2(brandInput) {
213
+ if (!initialized) {
214
+ throw new Error(
215
+ "[Brand Mapping] Not initialized. Call initializeBrandMappings() first."
216
+ );
217
+ }
218
+ let mapping = brandCache.get(brandInput);
219
+ if (mapping) return mapping;
220
+ mapping = brandCache.get(brandInput.toLowerCase());
221
+ if (mapping) return mapping;
222
+ mapping = brandCache.get(brandInput.toUpperCase());
223
+ if (mapping) return mapping;
224
+ const trimmed = brandInput.trim();
225
+ mapping = brandCache.get(trimmed);
226
+ if (mapping) return mapping;
227
+ console.warn(`[Brand Mapping] Unknown brand: "${brandInput}"`);
228
+ return null;
229
+ }
230
+ function isInitialized() {
231
+ return initialized;
232
+ }
233
+ function resetCache() {
234
+ brandCache.clear();
235
+ initialized = false;
236
+ console.log("[Brand Mapping] Cache reset");
237
+ }
238
+ function getCachedBrandCount() {
239
+ return brandCache.size;
240
+ }
241
+
161
242
  // src/model/mapping.ts
162
243
  var KOREAN_TO_ENGLISH_MODEL = {
163
244
  // MINI
@@ -243,6 +324,117 @@ function getModelMapping(modelInput, englishName, koreanName) {
243
324
  };
244
325
  }
245
326
 
327
+ // src/model/dynamic-mapping.ts
328
+ var import_firestore2 = require("firebase/firestore");
329
+ var modelCache = /* @__PURE__ */ new Map();
330
+ var brandModelsCache = /* @__PURE__ */ new Map();
331
+ var initialized2 = false;
332
+ async function initializeModelMappings(firestore, brandIds) {
333
+ console.log("[Model Mapping] Initializing from Firestore...");
334
+ try {
335
+ const vehiclesRef = (0, import_firestore2.collection)(firestore, "vehicles");
336
+ const brandsSnapshot = await (0, import_firestore2.getDocs)(vehiclesRef);
337
+ let processedBrands = 0;
338
+ let processedModels = 0;
339
+ for (const brandDoc of brandsSnapshot.docs) {
340
+ const brandId = brandDoc.id;
341
+ if (brandIds && !brandIds.includes(brandId)) {
342
+ continue;
343
+ }
344
+ const modelsRef = (0, import_firestore2.collection)(firestore, "vehicles", brandId, "models");
345
+ const modelsSnapshot = await (0, import_firestore2.getDocs)(modelsRef);
346
+ const brandModels = [];
347
+ modelsSnapshot.docs.forEach((modelDoc) => {
348
+ const data = modelDoc.data();
349
+ const mapping = {
350
+ firestoreId: modelDoc.id,
351
+ koreanName: data.name || modelDoc.id,
352
+ englishName: data.englishName || modelDoc.id.toUpperCase(),
353
+ storageName: data.englishName?.toUpperCase() || modelDoc.id.toUpperCase(),
354
+ brandId
355
+ };
356
+ brandModels.push(mapping);
357
+ processedModels++;
358
+ const keys = [
359
+ `${brandId}:${modelDoc.id}`,
360
+ // "hyundai:ioniq-5"
361
+ `${brandId}:${data.name}`,
362
+ // "hyundai:아이오닉 5"
363
+ `${brandId}:${data.englishName}`,
364
+ // "hyundai:IONIQ-5"
365
+ `${brandId}:${data.englishName?.toLowerCase()}`,
366
+ // "hyundai:ioniq-5"
367
+ `${brandId}:${modelDoc.id.toUpperCase()}`
368
+ // "hyundai:IONIQ-5"
369
+ ];
370
+ keys.forEach((key) => {
371
+ if (key && !key.includes("undefined")) {
372
+ modelCache.set(key, mapping);
373
+ }
374
+ });
375
+ });
376
+ brandModelsCache.set(brandId, brandModels);
377
+ processedBrands++;
378
+ }
379
+ initialized2 = true;
380
+ console.log(
381
+ `[Model Mapping] \u2705 Initialized: ${processedBrands} brands, ${processedModels} models`
382
+ );
383
+ } catch (error) {
384
+ console.error("[Model Mapping] \u274C Initialization failed:", error);
385
+ throw error;
386
+ }
387
+ }
388
+ function getModelMapping2(brandId, modelInput) {
389
+ if (!initialized2) {
390
+ throw new Error(
391
+ "[Model Mapping] Not initialized. Call initializeModelMappings() first."
392
+ );
393
+ }
394
+ const keys = [
395
+ `${brandId}:${modelInput}`,
396
+ `${brandId}:${modelInput.toLowerCase()}`,
397
+ `${brandId}:${modelInput.toUpperCase()}`,
398
+ `${brandId}:${modelInput.trim()}`
399
+ ];
400
+ for (const key of keys) {
401
+ const mapping = modelCache.get(key);
402
+ if (mapping) return mapping;
403
+ }
404
+ const brandModels = brandModelsCache.get(brandId);
405
+ if (brandModels) {
406
+ const normalizedInput = modelInput.toLowerCase().replace(/[\s\-]/g, "");
407
+ for (const model of brandModels) {
408
+ const normalizedModel = model.englishName.toLowerCase().replace(/[\s\-]/g, "");
409
+ if (normalizedModel === normalizedInput) {
410
+ return model;
411
+ }
412
+ }
413
+ }
414
+ console.warn(`[Model Mapping] Unknown model: ${brandId}:${modelInput}`);
415
+ return null;
416
+ }
417
+ function getBrandModels(brandId) {
418
+ if (!initialized2) {
419
+ throw new Error(
420
+ "[Model Mapping] Not initialized. Call initializeModelMappings() first."
421
+ );
422
+ }
423
+ return brandModelsCache.get(brandId) || [];
424
+ }
425
+ function isInitialized2() {
426
+ return initialized2;
427
+ }
428
+ function resetCache2() {
429
+ modelCache.clear();
430
+ brandModelsCache.clear();
431
+ initialized2 = false;
432
+ console.log("[Model Mapping] Cache reset");
433
+ }
434
+ function getCachedModelCount() {
435
+ return modelCache.size;
436
+ }
437
+
246
438
  // src/image/filename-generator.ts
247
439
  function generateImageFilename(params, extension = "png") {
248
440
  const brandSlug = toSlug(params.brand);
@@ -342,16 +534,27 @@ function generateFallbackUrls(params, options = {}) {
342
534
  generateImageFilename,
343
535
  generateVehicleImageUrl,
344
536
  getBrandMapping,
537
+ getBrandModels,
345
538
  getBrandNameEnglish,
346
539
  getBrandNameKorean,
540
+ getCachedBrandCount,
541
+ getCachedModelCount,
542
+ getDynamicBrandMapping,
543
+ getDynamicModelMapping,
347
544
  getModelMapping,
348
545
  getModelSlug,
349
546
  getStorageBrandName,
350
547
  getVehicleImagePath,
548
+ initializeBrandMappings,
549
+ initializeModelMappings,
550
+ isBrandMappingInitialized,
551
+ isModelMappingInitialized,
351
552
  isValidImageFilename,
352
553
  normalizeBrandId,
353
554
  normalizeModelId,
354
555
  parseImageFilename,
556
+ resetBrandCache,
557
+ resetModelCache,
355
558
  toSlug
356
559
  });
357
560
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/index.ts","../../src/brand/mapping.ts","../../src/model/mapping.ts","../../src/image/filename-generator.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// ===== 모델 관련 =====\nexport {\n toSlug,\n convertKoreanToEnglish,\n normalizeModelId,\n findBestModelMatch,\n getModelSlug,\n getModelMapping\n} from './model/mapping';\n\n// ===== 이미지 파일명 =====\nexport {\n generateImageFilename,\n parseImageFilename,\n isValidImageFilename\n} from './image/filename-generator';\n\n// ===== 이미지 URL =====\nexport {\n generateVehicleImageUrl,\n getVehicleImagePath,\n generateFallbackUrls\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 * 한글/영문 모델명 변환, 모델 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 * 표준 파일명 규칙: {brandSlug}__{modelSlug}__{year}__{trimSlug}.png\n */\n\nimport type { VehicleImageParams, ImageFilename } from '../types/image';\nimport { toSlug } from '../model/mapping';\n\n/**\n * 차량 이미지 파일명 생성\n *\n * @param params - 차량 이미지 파라미터\n * @param extension - 파일 확장자 (기본: 'png')\n * @returns 이미지 파일명\n *\n * @example\n * ```typescript\n * generateImageFilename({\n * brand: 'hyundai',\n * model: 'ioniq-5',\n * year: 2024\n * });\n * // 'hyundai__ioniq-5__2024.png'\n *\n * generateImageFilename({\n * brand: 'mini',\n * model: 'cooper',\n * year: 2025,\n * trim: 'jcw'\n * });\n * // 'mini__cooper__2025__jcw.png'\n *\n * generateImageFilename({\n * brand: 'Tesla',\n * model: 'Model 3',\n * year: 2024,\n * trim: 'Highland'\n * }, 'webp');\n * // 'tesla__model-3__2024__highland.webp'\n * ```\n */\nexport function generateImageFilename(\n params: VehicleImageParams,\n extension: 'png' | 'jpg' | 'webp' = 'png'\n): ImageFilename {\n // 1. 브랜드 슬러그 (소문자)\n const brandSlug = toSlug(params.brand);\n\n // 2. 모델 슬러그 (소문자, 하이픈)\n const modelSlug = toSlug(params.model);\n\n // 3. 연도 (숫자)\n const year = params.year;\n\n // 4. 파일명 생성\n if (params.trim) {\n // 트림이 있는 경우\n const trimSlug = toSlug(params.trim);\n return `${brandSlug}__${modelSlug}__${year}__${trimSlug}.${extension}` as ImageFilename;\n } else {\n // 트림이 없는 경우\n return `${brandSlug}__${modelSlug}__${year}.${extension}` as ImageFilename;\n }\n}\n\n/**\n * 파일명에서 차량 정보 추출\n *\n * @param filename - 이미지 파일명\n * @returns 차량 정보 또는 null\n *\n * @example\n * ```typescript\n * parseImageFilename('hyundai__ioniq-5__2024.png');\n * // { brand: 'hyundai', model: 'ioniq-5', year: 2024 }\n *\n * parseImageFilename('mini__cooper__2025__jcw.png');\n * // { brand: 'mini', model: 'cooper', year: 2025, trim: 'jcw' }\n * ```\n */\nexport function parseImageFilename(filename: string): VehicleImageParams | null {\n // 확장자 제거\n const nameWithoutExt = filename.replace(/\\.(png|jpg|webp)$/, '');\n\n // 더블 언더바로 분리\n const parts = nameWithoutExt.split('__');\n\n if (parts.length < 3) {\n return null; // 최소 brand, model, year 필요\n }\n\n const brand = parts[0];\n const model = parts[1];\n const year = parseInt(parts[2], 10);\n\n if (isNaN(year)) {\n return null; // 연도가 숫자가 아님\n }\n\n const trim = parts[3] || undefined;\n\n return {\n brand,\n model,\n year,\n trim\n };\n}\n\n/**\n * 파일명 검증\n *\n * @param filename - 검증할 파일명\n * @returns 유효 여부\n *\n * @example\n * ```typescript\n * isValidImageFilename('hyundai__ioniq-5__2024.png'); // true\n * isValidImageFilename('invalid-filename.png'); // false\n * ```\n */\nexport function isValidImageFilename(filename: string): boolean {\n const pattern = /^[a-z0-9\\-]+__[a-z0-9\\-]+__\\d{4}(__[a-z0-9\\-]+)?\\.(png|jpg|webp)$/;\n return pattern.test(filename);\n}\n","/**\n * 이미지 URL 생성 유틸리티\n *\n * Firebase Storage URL 생성 및 Fallback 처리\n */\n\nimport type { VehicleImageParams, ImageUrlOptions, ImagePath } from '../types/image';\nimport { getStorageBrandName } from '../brand/mapping';\nimport { toSlug } from '../model/mapping';\nimport { generateImageFilename } from './filename-generator';\n\n/**\n * Firebase Storage 기본 설정\n */\nconst DEFAULT_BUCKET = 'charzing-d1600.firebasestorage.app';\nconst DEFAULT_BASE_PATH = 'vehicle-images';\n\n/**\n * URL 인코딩 (Firebase Storage용)\n *\n * @param path - 인코딩할 경로\n * @returns 인코딩된 경로\n */\nfunction encodeStoragePath(path: string): string {\n return path.split('/').map(segment => encodeURIComponent(segment)).join('%2F');\n}\n\n/**\n * Firebase Storage 다운로드 URL 생성\n *\n * @param bucket - Storage 버킷명\n * @param fullPath - 파일 전체 경로\n * @returns 다운로드 URL\n */\nfunction createDownloadUrl(bucket: string, fullPath: string): string {\n const encodedPath = encodeStoragePath(fullPath);\n return `https://firebasestorage.googleapis.com/v0/b/${bucket}/o/${encodedPath}?alt=media`;\n}\n\n/**\n * 차량 이미지 URL 생성\n *\n * @param params - 차량 이미지 파라미터\n * @param options - URL 생성 옵션\n * @returns 이미지 다운로드 URL\n *\n * @example\n * ```typescript\n * generateVehicleImageUrl({\n * brand: 'hyundai',\n * model: 'ioniq-5',\n * year: 2024\n * });\n * // 'https://firebasestorage.googleapis.com/v0/b/charzing-d1600.firebasestorage.app/o/\n * // vehicle-images%2FHYUNDAI%2FIONIQ-5%2F2024%2Fhyundai__ioniq-5__2024.png?alt=media'\n *\n * generateVehicleImageUrl({\n * brand: 'mini',\n * model: 'cooper',\n * year: 2025,\n * trim: 'jcw'\n * });\n * // 'https://firebasestorage.googleapis.com/v0/b/charzing-d1600.firebasestorage.app/o/\n * // vehicle-images%2FMINI%2FCOOPER%2F2025%2Fmini__cooper__2025__jcw.png?alt=media'\n * ```\n */\nexport function generateVehicleImageUrl(\n params: VehicleImageParams,\n options: ImageUrlOptions = {}\n): string {\n const {\n bucket = DEFAULT_BUCKET,\n extension = 'png'\n } = options;\n\n // 1. Storage 브랜드명 (대문자)\n const storageBrand = getStorageBrandName(params.brand as any);\n\n // 2. Storage 모델명 (대문자, 하이픈)\n const storageModel = toSlug(params.model).toUpperCase();\n\n // 3. 연도\n const year = params.year;\n\n // 4. 파일명 생성\n const filename = generateImageFilename(params, extension);\n\n // 5. 전체 경로 생성\n const fullPath = `${DEFAULT_BASE_PATH}/${storageBrand}/${storageModel}/${year}/${filename}`;\n\n // 6. 다운로드 URL 생성\n return createDownloadUrl(bucket, fullPath);\n}\n\n/**\n * 차량 이미지 경로 정보 가져오기\n *\n * @param params - 차량 이미지 파라미터\n * @param options - URL 생성 옵션\n * @returns 이미지 경로 정보\n *\n * @example\n * ```typescript\n * getVehicleImagePath({\n * brand: 'hyundai',\n * model: 'ioniq-5',\n * year: 2024\n * });\n * // {\n * // fullPath: 'vehicle-images/HYUNDAI/IONIQ-5/2024/hyundai__ioniq-5__2024.png',\n * // directory: 'vehicle-images/HYUNDAI/IONIQ-5/2024',\n * // filename: 'hyundai__ioniq-5__2024.png',\n * // downloadUrl: 'https://...'\n * // }\n * ```\n */\nexport function getVehicleImagePath(\n params: VehicleImageParams,\n options: ImageUrlOptions = {}\n): ImagePath {\n const {\n bucket = DEFAULT_BUCKET,\n extension = 'png'\n } = options;\n\n // 1. Storage 브랜드명 (대문자)\n const storageBrand = getStorageBrandName(params.brand as any);\n\n // 2. Storage 모델명 (대문자, 하이픈)\n const storageModel = toSlug(params.model).toUpperCase();\n\n // 3. 연도\n const year = params.year;\n\n // 4. 디렉토리 경로\n const directory = `${DEFAULT_BASE_PATH}/${storageBrand}/${storageModel}/${year}`;\n\n // 5. 파일명\n const filename = generateImageFilename(params, extension);\n\n // 6. 전체 경로\n const fullPath = `${directory}/${filename}`;\n\n // 7. 다운로드 URL\n const downloadUrl = createDownloadUrl(bucket, fullPath);\n\n return {\n fullPath,\n directory,\n filename,\n downloadUrl\n };\n}\n\n/**\n * Fallback 이미지 URL 목록 생성\n *\n * @param params - 차량 이미지 파라미터\n * @param options - URL 생성 옵션\n * @returns Fallback URL 목록 (우선순위 순)\n *\n * @example\n * ```typescript\n * generateFallbackUrls({\n * brand: 'mini',\n * model: 'cooper',\n * year: 2025,\n * trim: 'jcw'\n * });\n * // [\n * // 'https://.../mini__cooper__2025__jcw.png', // 트림 이미지\n * // 'https://.../mini__cooper__2025.png', // 기본 모델 이미지\n * // ]\n * ```\n */\nexport function generateFallbackUrls(\n params: VehicleImageParams,\n options: ImageUrlOptions = {}\n): string[] {\n const urls: string[] = [];\n\n // 1. 트림 이미지 (있는 경우)\n if (params.trim) {\n urls.push(generateVehicleImageUrl(params, options));\n }\n\n // 2. 기본 모델 이미지\n urls.push(generateVehicleImageUrl({\n brand: params.brand,\n model: params.model,\n year: params.year\n }, options));\n\n return urls;\n}\n"],"mappings":"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","names":[]}
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/filename-generator.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// ===== 이미지 파일명 =====\nexport {\n generateImageFilename,\n parseImageFilename,\n isValidImageFilename\n} from './image/filename-generator';\n\n// ===== 이미지 URL =====\nexport {\n generateVehicleImageUrl,\n getVehicleImagePath,\n generateFallbackUrls\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 * 이미지 파일명 생성 유틸리티\n *\n * 표준 파일명 규칙: {brandSlug}__{modelSlug}__{year}__{trimSlug}.png\n */\n\nimport type { VehicleImageParams, ImageFilename } from '../types/image';\nimport { toSlug } from '../model/mapping';\n\n/**\n * 차량 이미지 파일명 생성\n *\n * @param params - 차량 이미지 파라미터\n * @param extension - 파일 확장자 (기본: 'png')\n * @returns 이미지 파일명\n *\n * @example\n * ```typescript\n * generateImageFilename({\n * brand: 'hyundai',\n * model: 'ioniq-5',\n * year: 2024\n * });\n * // 'hyundai__ioniq-5__2024.png'\n *\n * generateImageFilename({\n * brand: 'mini',\n * model: 'cooper',\n * year: 2025,\n * trim: 'jcw'\n * });\n * // 'mini__cooper__2025__jcw.png'\n *\n * generateImageFilename({\n * brand: 'Tesla',\n * model: 'Model 3',\n * year: 2024,\n * trim: 'Highland'\n * }, 'webp');\n * // 'tesla__model-3__2024__highland.webp'\n * ```\n */\nexport function generateImageFilename(\n params: VehicleImageParams,\n extension: 'png' | 'jpg' | 'webp' = 'png'\n): ImageFilename {\n // 1. 브랜드 슬러그 (소문자)\n const brandSlug = toSlug(params.brand);\n\n // 2. 모델 슬러그 (소문자, 하이픈)\n const modelSlug = toSlug(params.model);\n\n // 3. 연도 (숫자)\n const year = params.year;\n\n // 4. 파일명 생성\n if (params.trim) {\n // 트림이 있는 경우\n const trimSlug = toSlug(params.trim);\n return `${brandSlug}__${modelSlug}__${year}__${trimSlug}.${extension}` as ImageFilename;\n } else {\n // 트림이 없는 경우\n return `${brandSlug}__${modelSlug}__${year}.${extension}` as ImageFilename;\n }\n}\n\n/**\n * 파일명에서 차량 정보 추출\n *\n * @param filename - 이미지 파일명\n * @returns 차량 정보 또는 null\n *\n * @example\n * ```typescript\n * parseImageFilename('hyundai__ioniq-5__2024.png');\n * // { brand: 'hyundai', model: 'ioniq-5', year: 2024 }\n *\n * parseImageFilename('mini__cooper__2025__jcw.png');\n * // { brand: 'mini', model: 'cooper', year: 2025, trim: 'jcw' }\n * ```\n */\nexport function parseImageFilename(filename: string): VehicleImageParams | null {\n // 확장자 제거\n const nameWithoutExt = filename.replace(/\\.(png|jpg|webp)$/, '');\n\n // 더블 언더바로 분리\n const parts = nameWithoutExt.split('__');\n\n if (parts.length < 3) {\n return null; // 최소 brand, model, year 필요\n }\n\n const brand = parts[0];\n const model = parts[1];\n const year = parseInt(parts[2], 10);\n\n if (isNaN(year)) {\n return null; // 연도가 숫자가 아님\n }\n\n const trim = parts[3] || undefined;\n\n return {\n brand,\n model,\n year,\n trim\n };\n}\n\n/**\n * 파일명 검증\n *\n * @param filename - 검증할 파일명\n * @returns 유효 여부\n *\n * @example\n * ```typescript\n * isValidImageFilename('hyundai__ioniq-5__2024.png'); // true\n * isValidImageFilename('invalid-filename.png'); // false\n * ```\n */\nexport function isValidImageFilename(filename: string): boolean {\n const pattern = /^[a-z0-9\\-]+__[a-z0-9\\-]+__\\d{4}(__[a-z0-9\\-]+)?\\.(png|jpg|webp)$/;\n return pattern.test(filename);\n}\n","/**\n * 이미지 URL 생성 유틸리티\n *\n * Firebase Storage URL 생성 및 Fallback 처리\n */\n\nimport type { VehicleImageParams, ImageUrlOptions, ImagePath } from '../types/image';\nimport { getStorageBrandName } from '../brand/mapping';\nimport { toSlug } from '../model/mapping';\nimport { generateImageFilename } from './filename-generator';\n\n/**\n * Firebase Storage 기본 설정\n */\nconst DEFAULT_BUCKET = 'charzing-d1600.firebasestorage.app';\nconst DEFAULT_BASE_PATH = 'vehicle-images';\n\n/**\n * URL 인코딩 (Firebase Storage용)\n *\n * @param path - 인코딩할 경로\n * @returns 인코딩된 경로\n */\nfunction encodeStoragePath(path: string): string {\n return path.split('/').map(segment => encodeURIComponent(segment)).join('%2F');\n}\n\n/**\n * Firebase Storage 다운로드 URL 생성\n *\n * @param bucket - Storage 버킷명\n * @param fullPath - 파일 전체 경로\n * @returns 다운로드 URL\n */\nfunction createDownloadUrl(bucket: string, fullPath: string): string {\n const encodedPath = encodeStoragePath(fullPath);\n return `https://firebasestorage.googleapis.com/v0/b/${bucket}/o/${encodedPath}?alt=media`;\n}\n\n/**\n * 차량 이미지 URL 생성\n *\n * @param params - 차량 이미지 파라미터\n * @param options - URL 생성 옵션\n * @returns 이미지 다운로드 URL\n *\n * @example\n * ```typescript\n * generateVehicleImageUrl({\n * brand: 'hyundai',\n * model: 'ioniq-5',\n * year: 2024\n * });\n * // 'https://firebasestorage.googleapis.com/v0/b/charzing-d1600.firebasestorage.app/o/\n * // vehicle-images%2FHYUNDAI%2FIONIQ-5%2F2024%2Fhyundai__ioniq-5__2024.png?alt=media'\n *\n * generateVehicleImageUrl({\n * brand: 'mini',\n * model: 'cooper',\n * year: 2025,\n * trim: 'jcw'\n * });\n * // 'https://firebasestorage.googleapis.com/v0/b/charzing-d1600.firebasestorage.app/o/\n * // vehicle-images%2FMINI%2FCOOPER%2F2025%2Fmini__cooper__2025__jcw.png?alt=media'\n * ```\n */\nexport function generateVehicleImageUrl(\n params: VehicleImageParams,\n options: ImageUrlOptions = {}\n): string {\n const {\n bucket = DEFAULT_BUCKET,\n extension = 'png'\n } = options;\n\n // 1. Storage 브랜드명 (대문자)\n const storageBrand = getStorageBrandName(params.brand as any);\n\n // 2. Storage 모델명 (대문자, 하이픈)\n const storageModel = toSlug(params.model).toUpperCase();\n\n // 3. 연도\n const year = params.year;\n\n // 4. 파일명 생성\n const filename = generateImageFilename(params, extension);\n\n // 5. 전체 경로 생성\n const fullPath = `${DEFAULT_BASE_PATH}/${storageBrand}/${storageModel}/${year}/${filename}`;\n\n // 6. 다운로드 URL 생성\n return createDownloadUrl(bucket, fullPath);\n}\n\n/**\n * 차량 이미지 경로 정보 가져오기\n *\n * @param params - 차량 이미지 파라미터\n * @param options - URL 생성 옵션\n * @returns 이미지 경로 정보\n *\n * @example\n * ```typescript\n * getVehicleImagePath({\n * brand: 'hyundai',\n * model: 'ioniq-5',\n * year: 2024\n * });\n * // {\n * // fullPath: 'vehicle-images/HYUNDAI/IONIQ-5/2024/hyundai__ioniq-5__2024.png',\n * // directory: 'vehicle-images/HYUNDAI/IONIQ-5/2024',\n * // filename: 'hyundai__ioniq-5__2024.png',\n * // downloadUrl: 'https://...'\n * // }\n * ```\n */\nexport function getVehicleImagePath(\n params: VehicleImageParams,\n options: ImageUrlOptions = {}\n): ImagePath {\n const {\n bucket = DEFAULT_BUCKET,\n extension = 'png'\n } = options;\n\n // 1. Storage 브랜드명 (대문자)\n const storageBrand = getStorageBrandName(params.brand as any);\n\n // 2. Storage 모델명 (대문자, 하이픈)\n const storageModel = toSlug(params.model).toUpperCase();\n\n // 3. 연도\n const year = params.year;\n\n // 4. 디렉토리 경로\n const directory = `${DEFAULT_BASE_PATH}/${storageBrand}/${storageModel}/${year}`;\n\n // 5. 파일명\n const filename = generateImageFilename(params, extension);\n\n // 6. 전체 경로\n const fullPath = `${directory}/${filename}`;\n\n // 7. 다운로드 URL\n const downloadUrl = createDownloadUrl(bucket, fullPath);\n\n return {\n fullPath,\n directory,\n filename,\n downloadUrl\n };\n}\n\n/**\n * Fallback 이미지 URL 목록 생성\n *\n * @param params - 차량 이미지 파라미터\n * @param options - URL 생성 옵션\n * @returns Fallback URL 목록 (우선순위 순)\n *\n * @example\n * ```typescript\n * generateFallbackUrls({\n * brand: 'mini',\n * model: 'cooper',\n * year: 2025,\n * trim: 'jcw'\n * });\n * // [\n * // 'https://.../mini__cooper__2025__jcw.png', // 트림 이미지\n * // 'https://.../mini__cooper__2025.png', // 기본 모델 이미지\n * // ]\n * ```\n */\nexport function generateFallbackUrls(\n params: VehicleImageParams,\n options: ImageUrlOptions = {}\n): string[] {\n const urls: string[] = [];\n\n // 1. 트림 이미지 (있는 경우)\n if (params.trim) {\n urls.push(generateVehicleImageUrl(params, options));\n }\n\n // 2. 기본 모델 이미지\n urls.push(generateVehicleImageUrl({\n brand: params.brand,\n model: params.model,\n year: params.year\n }, options));\n\n return urls;\n}\n"],"mappings":"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","names":["getBrandMapping","getModelMapping","isInitialized","resetCache","getBrandMapping","import_firestore","initialized","getModelMapping","isInitialized","resetCache"]}
@@ -1,3 +1,5 @@
1
+ import { Firestore } from 'firebase/firestore';
2
+
1
3
  /**
2
4
  * 브랜드 관련 타입 정의
3
5
  */
@@ -79,7 +81,7 @@ type ModelSlug = string;
79
81
  /**
80
82
  * 모델 매핑 결과
81
83
  */
82
- interface ModelMapping {
84
+ interface ModelMapping$1 {
83
85
  /** Firestore ID (소문자, 하이픈) */
84
86
  firestoreId: ModelId;
85
87
  /** 한글 모델명 */
@@ -450,12 +452,63 @@ declare function getStorageBrandName(brandId: BrandId): StorageBrandName;
450
452
  * // }
451
453
  * ```
452
454
  */
453
- declare function getBrandMapping(brandInput: string): BrandMapping;
455
+ declare function getBrandMapping$1(brandInput: string): BrandMapping;
454
456
  /**
455
457
  * 지원되는 모든 브랜드 ID 목록
456
458
  */
457
459
  declare const SUPPORTED_BRANDS: BrandId[];
458
460
 
461
+ /**
462
+ * 동적 브랜드 매핑 시스템
463
+ *
464
+ * Firestore에서 실시간으로 브랜드 정보를 조회하여 캐싱
465
+ * 새로운 브랜드 추가 시 코드 수정 불필요
466
+ */
467
+
468
+ /**
469
+ * Firestore에서 브랜드 정보를 조회하여 캐시 초기화
470
+ *
471
+ * @param firestore - Firestore 인스턴스
472
+ *
473
+ * @example
474
+ * ```typescript
475
+ * import { initializeBrandMappings } from '@charzing/vehicle-utils';
476
+ * import { db } from './firebase';
477
+ *
478
+ * // 앱 시작 시 한 번만 호출
479
+ * await initializeBrandMappings(db);
480
+ * ```
481
+ */
482
+ declare function initializeBrandMappings(firestore: Firestore): Promise<void>;
483
+ /**
484
+ * 브랜드 매핑 정보 조회
485
+ *
486
+ * @param brandInput - 브랜드명 (한글/영문/ID 모두 가능)
487
+ * @returns 브랜드 매핑 정보 또는 null
488
+ *
489
+ * @throws {Error} 초기화되지 않은 경우
490
+ *
491
+ * @example
492
+ * ```typescript
493
+ * getBrandMapping('현대'); // { firestoreId: 'hyundai', ... }
494
+ * getBrandMapping('HYUNDAI'); // { firestoreId: 'hyundai', ... }
495
+ * getBrandMapping('hyundai'); // { firestoreId: 'hyundai', ... }
496
+ * ```
497
+ */
498
+ declare function getBrandMapping(brandInput: string): BrandMapping | null;
499
+ /**
500
+ * 초기화 상태 확인
501
+ */
502
+ declare function isInitialized$1(): boolean;
503
+ /**
504
+ * 캐시 초기화 (테스트용)
505
+ */
506
+ declare function resetCache$1(): void;
507
+ /**
508
+ * 캐시된 브랜드 수
509
+ */
510
+ declare function getCachedBrandCount(): number;
511
+
459
512
  /**
460
513
  * 모델 매핑 유틸리티
461
514
  *
@@ -559,7 +612,77 @@ declare function getModelSlug(modelInput: string): ModelSlug;
559
612
  * // }
560
613
  * ```
561
614
  */
562
- declare function getModelMapping(modelInput: string, englishName?: string, koreanName?: string): ModelMapping;
615
+ declare function getModelMapping$1(modelInput: string, englishName?: string, koreanName?: string): ModelMapping$1;
616
+
617
+ /**
618
+ * 동적 모델 매핑 시스템
619
+ *
620
+ * Firestore에서 실시간으로 모델 정보를 조회하여 캐싱
621
+ * 새로운 모델 추가 시 코드 수정 불필요
622
+ */
623
+
624
+ /**
625
+ * 모델 매핑 정보
626
+ */
627
+ interface ModelMapping {
628
+ firestoreId: string;
629
+ koreanName: string;
630
+ englishName: string;
631
+ storageName: string;
632
+ brandId: string;
633
+ }
634
+ /**
635
+ * Firestore에서 모델 정보를 조회하여 캐시 초기화
636
+ *
637
+ * @param firestore - Firestore 인스턴스
638
+ * @param brandIds - 초기화할 브랜드 ID 목록 (생략 시 모든 브랜드)
639
+ *
640
+ * @example
641
+ * ```typescript
642
+ * // 모든 브랜드의 모델 초기화
643
+ * await initializeModelMappings(db);
644
+ *
645
+ * // 특정 브랜드만 초기화
646
+ * await initializeModelMappings(db, ['hyundai', 'kia']);
647
+ * ```
648
+ */
649
+ declare function initializeModelMappings(firestore: Firestore, brandIds?: string[]): Promise<void>;
650
+ /**
651
+ * 모델 매핑 정보 조회
652
+ *
653
+ * @param brandId - 브랜드 ID
654
+ * @param modelInput - 모델명 (한글/영문/ID 모두 가능)
655
+ * @returns 모델 매핑 정보 또는 null
656
+ *
657
+ * @throws {Error} 초기화되지 않은 경우
658
+ *
659
+ * @example
660
+ * ```typescript
661
+ * getModelMapping('hyundai', '아이오닉 5'); // { firestoreId: 'ioniq-5', ... }
662
+ * getModelMapping('hyundai', 'IONIQ-5'); // { firestoreId: 'ioniq-5', ... }
663
+ * getModelMapping('hyundai', 'ioniq-5'); // { firestoreId: 'ioniq-5', ... }
664
+ * ```
665
+ */
666
+ declare function getModelMapping(brandId: string, modelInput: string): ModelMapping | null;
667
+ /**
668
+ * 브랜드의 모든 모델 조회
669
+ *
670
+ * @param brandId - 브랜드 ID
671
+ * @returns 모델 매핑 배열
672
+ */
673
+ declare function getBrandModels(brandId: string): ModelMapping[];
674
+ /**
675
+ * 초기화 상태 확인
676
+ */
677
+ declare function isInitialized(): boolean;
678
+ /**
679
+ * 캐시 초기화 (테스트용)
680
+ */
681
+ declare function resetCache(): void;
682
+ /**
683
+ * 캐시된 모델 수
684
+ */
685
+ declare function getCachedModelCount(): number;
563
686
 
564
687
  /**
565
688
  * 이미지 파일명 생성 유틸리티
@@ -711,4 +834,4 @@ declare function getVehicleImagePath(params: VehicleImageParams, options?: Image
711
834
  */
712
835
  declare function generateFallbackUrls(params: VehicleImageParams, options?: ImageUrlOptions): string[];
713
836
 
714
- export { type BatteryInfo, type Brand, type BrandId, type BrandMapping, type CompletedVehicle, type DriveType, type FirebaseStorageConfig, type ImageFallbackOptions, type ImageFallbackStrategy, type ImageFilename, type ImagePath, type ImageUrlOptions, type Model, type ModelId, type ModelMapping, type ModelSlug, SUPPORTED_BRANDS, type StorageBrandName, type TrimId, type TrimMapping, type TrimSlug, type UserVehicle, type VehicleDetails, type VehicleImageParams, type VehicleModel, type VehicleTrim, type VehicleVariant, convertKoreanToEnglish, findBestModelMatch, generateFallbackUrls, generateImageFilename, generateVehicleImageUrl, getBrandMapping, getBrandNameEnglish, getBrandNameKorean, getModelMapping, getModelSlug, getStorageBrandName, getVehicleImagePath, isValidImageFilename, normalizeBrandId, normalizeModelId, parseImageFilename, toSlug };
837
+ export { type BatteryInfo, type Brand, type BrandId, type BrandMapping, type CompletedVehicle, type DriveType, type FirebaseStorageConfig, type ImageFallbackOptions, type ImageFallbackStrategy, type ImageFilename, type ImagePath, type ImageUrlOptions, type Model, type ModelId, type ModelMapping$1 as ModelMapping, type ModelSlug, SUPPORTED_BRANDS, type StorageBrandName, type TrimId, type TrimMapping, type TrimSlug, type UserVehicle, type VehicleDetails, type VehicleImageParams, type VehicleModel, type VehicleTrim, type VehicleVariant, convertKoreanToEnglish, findBestModelMatch, generateFallbackUrls, generateImageFilename, generateVehicleImageUrl, getBrandMapping$1 as getBrandMapping, getBrandModels, getBrandNameEnglish, getBrandNameKorean, getCachedBrandCount, getCachedModelCount, getBrandMapping as getDynamicBrandMapping, getModelMapping as getDynamicModelMapping, getModelMapping$1 as getModelMapping, getModelSlug, getStorageBrandName, getVehicleImagePath, initializeBrandMappings, initializeModelMappings, isInitialized$1 as isBrandMappingInitialized, isInitialized as isModelMappingInitialized, isValidImageFilename, normalizeBrandId, normalizeModelId, parseImageFilename, resetCache$1 as resetBrandCache, resetCache as resetModelCache, toSlug };
@@ -115,6 +115,76 @@ var SUPPORTED_BRANDS = [
115
115
  "porsche"
116
116
  ];
117
117
 
118
+ // src/brand/dynamic-mapping.ts
119
+ import { collection, getDocs } from "firebase/firestore";
120
+ var brandCache = /* @__PURE__ */ new Map();
121
+ var initialized = false;
122
+ async function initializeBrandMappings(firestore) {
123
+ if (initialized) {
124
+ console.log("[Brand Mapping] Already initialized");
125
+ return;
126
+ }
127
+ console.log("[Brand Mapping] Initializing from Firestore...");
128
+ try {
129
+ const vehiclesRef = collection(firestore, "vehicles");
130
+ const snapshot = await getDocs(vehiclesRef);
131
+ snapshot.docs.forEach((doc) => {
132
+ const data = doc.data();
133
+ let storageName = data.englishName?.toUpperCase() || doc.id.toUpperCase();
134
+ if (doc.id === "mercedes-benz" || doc.id === "mercedes-maybach") {
135
+ storageName = "BENZ";
136
+ }
137
+ const mapping = {
138
+ firestoreId: doc.id,
139
+ koreanName: data.name || doc.id,
140
+ englishName: data.englishName || doc.id.toUpperCase(),
141
+ storageName
142
+ };
143
+ brandCache.set(doc.id, mapping);
144
+ brandCache.set(doc.id.toUpperCase(), mapping);
145
+ brandCache.set(doc.id.toLowerCase(), mapping);
146
+ brandCache.set(data.name, mapping);
147
+ brandCache.set(data.englishName, mapping);
148
+ brandCache.set(data.englishName?.toUpperCase(), mapping);
149
+ brandCache.set(data.englishName?.toLowerCase(), mapping);
150
+ });
151
+ initialized = true;
152
+ console.log(`[Brand Mapping] \u2705 Initialized with ${snapshot.docs.length} brands`);
153
+ } catch (error) {
154
+ console.error("[Brand Mapping] \u274C Initialization failed:", error);
155
+ throw error;
156
+ }
157
+ }
158
+ function getBrandMapping2(brandInput) {
159
+ if (!initialized) {
160
+ throw new Error(
161
+ "[Brand Mapping] Not initialized. Call initializeBrandMappings() first."
162
+ );
163
+ }
164
+ let mapping = brandCache.get(brandInput);
165
+ if (mapping) return mapping;
166
+ mapping = brandCache.get(brandInput.toLowerCase());
167
+ if (mapping) return mapping;
168
+ mapping = brandCache.get(brandInput.toUpperCase());
169
+ if (mapping) return mapping;
170
+ const trimmed = brandInput.trim();
171
+ mapping = brandCache.get(trimmed);
172
+ if (mapping) return mapping;
173
+ console.warn(`[Brand Mapping] Unknown brand: "${brandInput}"`);
174
+ return null;
175
+ }
176
+ function isInitialized() {
177
+ return initialized;
178
+ }
179
+ function resetCache() {
180
+ brandCache.clear();
181
+ initialized = false;
182
+ console.log("[Brand Mapping] Cache reset");
183
+ }
184
+ function getCachedBrandCount() {
185
+ return brandCache.size;
186
+ }
187
+
118
188
  // src/model/mapping.ts
119
189
  var KOREAN_TO_ENGLISH_MODEL = {
120
190
  // MINI
@@ -200,6 +270,117 @@ function getModelMapping(modelInput, englishName, koreanName) {
200
270
  };
201
271
  }
202
272
 
273
+ // src/model/dynamic-mapping.ts
274
+ import { collection as collection2, getDocs as getDocs2 } from "firebase/firestore";
275
+ var modelCache = /* @__PURE__ */ new Map();
276
+ var brandModelsCache = /* @__PURE__ */ new Map();
277
+ var initialized2 = false;
278
+ async function initializeModelMappings(firestore, brandIds) {
279
+ console.log("[Model Mapping] Initializing from Firestore...");
280
+ try {
281
+ const vehiclesRef = collection2(firestore, "vehicles");
282
+ const brandsSnapshot = await getDocs2(vehiclesRef);
283
+ let processedBrands = 0;
284
+ let processedModels = 0;
285
+ for (const brandDoc of brandsSnapshot.docs) {
286
+ const brandId = brandDoc.id;
287
+ if (brandIds && !brandIds.includes(brandId)) {
288
+ continue;
289
+ }
290
+ const modelsRef = collection2(firestore, "vehicles", brandId, "models");
291
+ const modelsSnapshot = await getDocs2(modelsRef);
292
+ const brandModels = [];
293
+ modelsSnapshot.docs.forEach((modelDoc) => {
294
+ const data = modelDoc.data();
295
+ const mapping = {
296
+ firestoreId: modelDoc.id,
297
+ koreanName: data.name || modelDoc.id,
298
+ englishName: data.englishName || modelDoc.id.toUpperCase(),
299
+ storageName: data.englishName?.toUpperCase() || modelDoc.id.toUpperCase(),
300
+ brandId
301
+ };
302
+ brandModels.push(mapping);
303
+ processedModels++;
304
+ const keys = [
305
+ `${brandId}:${modelDoc.id}`,
306
+ // "hyundai:ioniq-5"
307
+ `${brandId}:${data.name}`,
308
+ // "hyundai:아이오닉 5"
309
+ `${brandId}:${data.englishName}`,
310
+ // "hyundai:IONIQ-5"
311
+ `${brandId}:${data.englishName?.toLowerCase()}`,
312
+ // "hyundai:ioniq-5"
313
+ `${brandId}:${modelDoc.id.toUpperCase()}`
314
+ // "hyundai:IONIQ-5"
315
+ ];
316
+ keys.forEach((key) => {
317
+ if (key && !key.includes("undefined")) {
318
+ modelCache.set(key, mapping);
319
+ }
320
+ });
321
+ });
322
+ brandModelsCache.set(brandId, brandModels);
323
+ processedBrands++;
324
+ }
325
+ initialized2 = true;
326
+ console.log(
327
+ `[Model Mapping] \u2705 Initialized: ${processedBrands} brands, ${processedModels} models`
328
+ );
329
+ } catch (error) {
330
+ console.error("[Model Mapping] \u274C Initialization failed:", error);
331
+ throw error;
332
+ }
333
+ }
334
+ function getModelMapping2(brandId, modelInput) {
335
+ if (!initialized2) {
336
+ throw new Error(
337
+ "[Model Mapping] Not initialized. Call initializeModelMappings() first."
338
+ );
339
+ }
340
+ const keys = [
341
+ `${brandId}:${modelInput}`,
342
+ `${brandId}:${modelInput.toLowerCase()}`,
343
+ `${brandId}:${modelInput.toUpperCase()}`,
344
+ `${brandId}:${modelInput.trim()}`
345
+ ];
346
+ for (const key of keys) {
347
+ const mapping = modelCache.get(key);
348
+ if (mapping) return mapping;
349
+ }
350
+ const brandModels = brandModelsCache.get(brandId);
351
+ if (brandModels) {
352
+ const normalizedInput = modelInput.toLowerCase().replace(/[\s\-]/g, "");
353
+ for (const model of brandModels) {
354
+ const normalizedModel = model.englishName.toLowerCase().replace(/[\s\-]/g, "");
355
+ if (normalizedModel === normalizedInput) {
356
+ return model;
357
+ }
358
+ }
359
+ }
360
+ console.warn(`[Model Mapping] Unknown model: ${brandId}:${modelInput}`);
361
+ return null;
362
+ }
363
+ function getBrandModels(brandId) {
364
+ if (!initialized2) {
365
+ throw new Error(
366
+ "[Model Mapping] Not initialized. Call initializeModelMappings() first."
367
+ );
368
+ }
369
+ return brandModelsCache.get(brandId) || [];
370
+ }
371
+ function isInitialized2() {
372
+ return initialized2;
373
+ }
374
+ function resetCache2() {
375
+ modelCache.clear();
376
+ brandModelsCache.clear();
377
+ initialized2 = false;
378
+ console.log("[Model Mapping] Cache reset");
379
+ }
380
+ function getCachedModelCount() {
381
+ return modelCache.size;
382
+ }
383
+
203
384
  // src/image/filename-generator.ts
204
385
  function generateImageFilename(params, extension = "png") {
205
386
  const brandSlug = toSlug(params.brand);
@@ -298,16 +479,27 @@ export {
298
479
  generateImageFilename,
299
480
  generateVehicleImageUrl,
300
481
  getBrandMapping,
482
+ getBrandModels,
301
483
  getBrandNameEnglish,
302
484
  getBrandNameKorean,
485
+ getCachedBrandCount,
486
+ getCachedModelCount,
487
+ getBrandMapping2 as getDynamicBrandMapping,
488
+ getModelMapping2 as getDynamicModelMapping,
303
489
  getModelMapping,
304
490
  getModelSlug,
305
491
  getStorageBrandName,
306
492
  getVehicleImagePath,
493
+ initializeBrandMappings,
494
+ initializeModelMappings,
495
+ isInitialized as isBrandMappingInitialized,
496
+ isInitialized2 as isModelMappingInitialized,
307
497
  isValidImageFilename,
308
498
  normalizeBrandId,
309
499
  normalizeModelId,
310
500
  parseImageFilename,
501
+ resetCache as resetBrandCache,
502
+ resetCache2 as resetModelCache,
311
503
  toSlug
312
504
  };
313
505
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/brand/mapping.ts","../../src/model/mapping.ts","../../src/image/filename-generator.ts","../../src/image/url-generator.ts"],"sourcesContent":["/**\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 * 한글/영문 모델명 변환, 모델 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 * 표준 파일명 규칙: {brandSlug}__{modelSlug}__{year}__{trimSlug}.png\n */\n\nimport type { VehicleImageParams, ImageFilename } from '../types/image';\nimport { toSlug } from '../model/mapping';\n\n/**\n * 차량 이미지 파일명 생성\n *\n * @param params - 차량 이미지 파라미터\n * @param extension - 파일 확장자 (기본: 'png')\n * @returns 이미지 파일명\n *\n * @example\n * ```typescript\n * generateImageFilename({\n * brand: 'hyundai',\n * model: 'ioniq-5',\n * year: 2024\n * });\n * // 'hyundai__ioniq-5__2024.png'\n *\n * generateImageFilename({\n * brand: 'mini',\n * model: 'cooper',\n * year: 2025,\n * trim: 'jcw'\n * });\n * // 'mini__cooper__2025__jcw.png'\n *\n * generateImageFilename({\n * brand: 'Tesla',\n * model: 'Model 3',\n * year: 2024,\n * trim: 'Highland'\n * }, 'webp');\n * // 'tesla__model-3__2024__highland.webp'\n * ```\n */\nexport function generateImageFilename(\n params: VehicleImageParams,\n extension: 'png' | 'jpg' | 'webp' = 'png'\n): ImageFilename {\n // 1. 브랜드 슬러그 (소문자)\n const brandSlug = toSlug(params.brand);\n\n // 2. 모델 슬러그 (소문자, 하이픈)\n const modelSlug = toSlug(params.model);\n\n // 3. 연도 (숫자)\n const year = params.year;\n\n // 4. 파일명 생성\n if (params.trim) {\n // 트림이 있는 경우\n const trimSlug = toSlug(params.trim);\n return `${brandSlug}__${modelSlug}__${year}__${trimSlug}.${extension}` as ImageFilename;\n } else {\n // 트림이 없는 경우\n return `${brandSlug}__${modelSlug}__${year}.${extension}` as ImageFilename;\n }\n}\n\n/**\n * 파일명에서 차량 정보 추출\n *\n * @param filename - 이미지 파일명\n * @returns 차량 정보 또는 null\n *\n * @example\n * ```typescript\n * parseImageFilename('hyundai__ioniq-5__2024.png');\n * // { brand: 'hyundai', model: 'ioniq-5', year: 2024 }\n *\n * parseImageFilename('mini__cooper__2025__jcw.png');\n * // { brand: 'mini', model: 'cooper', year: 2025, trim: 'jcw' }\n * ```\n */\nexport function parseImageFilename(filename: string): VehicleImageParams | null {\n // 확장자 제거\n const nameWithoutExt = filename.replace(/\\.(png|jpg|webp)$/, '');\n\n // 더블 언더바로 분리\n const parts = nameWithoutExt.split('__');\n\n if (parts.length < 3) {\n return null; // 최소 brand, model, year 필요\n }\n\n const brand = parts[0];\n const model = parts[1];\n const year = parseInt(parts[2], 10);\n\n if (isNaN(year)) {\n return null; // 연도가 숫자가 아님\n }\n\n const trim = parts[3] || undefined;\n\n return {\n brand,\n model,\n year,\n trim\n };\n}\n\n/**\n * 파일명 검증\n *\n * @param filename - 검증할 파일명\n * @returns 유효 여부\n *\n * @example\n * ```typescript\n * isValidImageFilename('hyundai__ioniq-5__2024.png'); // true\n * isValidImageFilename('invalid-filename.png'); // false\n * ```\n */\nexport function isValidImageFilename(filename: string): boolean {\n const pattern = /^[a-z0-9\\-]+__[a-z0-9\\-]+__\\d{4}(__[a-z0-9\\-]+)?\\.(png|jpg|webp)$/;\n return pattern.test(filename);\n}\n","/**\n * 이미지 URL 생성 유틸리티\n *\n * Firebase Storage URL 생성 및 Fallback 처리\n */\n\nimport type { VehicleImageParams, ImageUrlOptions, ImagePath } from '../types/image';\nimport { getStorageBrandName } from '../brand/mapping';\nimport { toSlug } from '../model/mapping';\nimport { generateImageFilename } from './filename-generator';\n\n/**\n * Firebase Storage 기본 설정\n */\nconst DEFAULT_BUCKET = 'charzing-d1600.firebasestorage.app';\nconst DEFAULT_BASE_PATH = 'vehicle-images';\n\n/**\n * URL 인코딩 (Firebase Storage용)\n *\n * @param path - 인코딩할 경로\n * @returns 인코딩된 경로\n */\nfunction encodeStoragePath(path: string): string {\n return path.split('/').map(segment => encodeURIComponent(segment)).join('%2F');\n}\n\n/**\n * Firebase Storage 다운로드 URL 생성\n *\n * @param bucket - Storage 버킷명\n * @param fullPath - 파일 전체 경로\n * @returns 다운로드 URL\n */\nfunction createDownloadUrl(bucket: string, fullPath: string): string {\n const encodedPath = encodeStoragePath(fullPath);\n return `https://firebasestorage.googleapis.com/v0/b/${bucket}/o/${encodedPath}?alt=media`;\n}\n\n/**\n * 차량 이미지 URL 생성\n *\n * @param params - 차량 이미지 파라미터\n * @param options - URL 생성 옵션\n * @returns 이미지 다운로드 URL\n *\n * @example\n * ```typescript\n * generateVehicleImageUrl({\n * brand: 'hyundai',\n * model: 'ioniq-5',\n * year: 2024\n * });\n * // 'https://firebasestorage.googleapis.com/v0/b/charzing-d1600.firebasestorage.app/o/\n * // vehicle-images%2FHYUNDAI%2FIONIQ-5%2F2024%2Fhyundai__ioniq-5__2024.png?alt=media'\n *\n * generateVehicleImageUrl({\n * brand: 'mini',\n * model: 'cooper',\n * year: 2025,\n * trim: 'jcw'\n * });\n * // 'https://firebasestorage.googleapis.com/v0/b/charzing-d1600.firebasestorage.app/o/\n * // vehicle-images%2FMINI%2FCOOPER%2F2025%2Fmini__cooper__2025__jcw.png?alt=media'\n * ```\n */\nexport function generateVehicleImageUrl(\n params: VehicleImageParams,\n options: ImageUrlOptions = {}\n): string {\n const {\n bucket = DEFAULT_BUCKET,\n extension = 'png'\n } = options;\n\n // 1. Storage 브랜드명 (대문자)\n const storageBrand = getStorageBrandName(params.brand as any);\n\n // 2. Storage 모델명 (대문자, 하이픈)\n const storageModel = toSlug(params.model).toUpperCase();\n\n // 3. 연도\n const year = params.year;\n\n // 4. 파일명 생성\n const filename = generateImageFilename(params, extension);\n\n // 5. 전체 경로 생성\n const fullPath = `${DEFAULT_BASE_PATH}/${storageBrand}/${storageModel}/${year}/${filename}`;\n\n // 6. 다운로드 URL 생성\n return createDownloadUrl(bucket, fullPath);\n}\n\n/**\n * 차량 이미지 경로 정보 가져오기\n *\n * @param params - 차량 이미지 파라미터\n * @param options - URL 생성 옵션\n * @returns 이미지 경로 정보\n *\n * @example\n * ```typescript\n * getVehicleImagePath({\n * brand: 'hyundai',\n * model: 'ioniq-5',\n * year: 2024\n * });\n * // {\n * // fullPath: 'vehicle-images/HYUNDAI/IONIQ-5/2024/hyundai__ioniq-5__2024.png',\n * // directory: 'vehicle-images/HYUNDAI/IONIQ-5/2024',\n * // filename: 'hyundai__ioniq-5__2024.png',\n * // downloadUrl: 'https://...'\n * // }\n * ```\n */\nexport function getVehicleImagePath(\n params: VehicleImageParams,\n options: ImageUrlOptions = {}\n): ImagePath {\n const {\n bucket = DEFAULT_BUCKET,\n extension = 'png'\n } = options;\n\n // 1. Storage 브랜드명 (대문자)\n const storageBrand = getStorageBrandName(params.brand as any);\n\n // 2. Storage 모델명 (대문자, 하이픈)\n const storageModel = toSlug(params.model).toUpperCase();\n\n // 3. 연도\n const year = params.year;\n\n // 4. 디렉토리 경로\n const directory = `${DEFAULT_BASE_PATH}/${storageBrand}/${storageModel}/${year}`;\n\n // 5. 파일명\n const filename = generateImageFilename(params, extension);\n\n // 6. 전체 경로\n const fullPath = `${directory}/${filename}`;\n\n // 7. 다운로드 URL\n const downloadUrl = createDownloadUrl(bucket, fullPath);\n\n return {\n fullPath,\n directory,\n filename,\n downloadUrl\n };\n}\n\n/**\n * Fallback 이미지 URL 목록 생성\n *\n * @param params - 차량 이미지 파라미터\n * @param options - URL 생성 옵션\n * @returns Fallback URL 목록 (우선순위 순)\n *\n * @example\n * ```typescript\n * generateFallbackUrls({\n * brand: 'mini',\n * model: 'cooper',\n * year: 2025,\n * trim: 'jcw'\n * });\n * // [\n * // 'https://.../mini__cooper__2025__jcw.png', // 트림 이미지\n * // 'https://.../mini__cooper__2025.png', // 기본 모델 이미지\n * // ]\n * ```\n */\nexport function generateFallbackUrls(\n params: VehicleImageParams,\n options: ImageUrlOptions = {}\n): string[] {\n const urls: string[] = [];\n\n // 1. 트림 이미지 (있는 경우)\n if (params.trim) {\n urls.push(generateVehicleImageUrl(params, options));\n }\n\n // 2. 기본 모델 이미지\n urls.push(generateVehicleImageUrl({\n brand: params.brand,\n model: params.model,\n year: params.year\n }, options));\n\n return urls;\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/brand/mapping.ts","../../src/brand/dynamic-mapping.ts","../../src/model/mapping.ts","../../src/model/dynamic-mapping.ts","../../src/image/filename-generator.ts","../../src/image/url-generator.ts"],"sourcesContent":["/**\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 * 이미지 파일명 생성 유틸리티\n *\n * 표준 파일명 규칙: {brandSlug}__{modelSlug}__{year}__{trimSlug}.png\n */\n\nimport type { VehicleImageParams, ImageFilename } from '../types/image';\nimport { toSlug } from '../model/mapping';\n\n/**\n * 차량 이미지 파일명 생성\n *\n * @param params - 차량 이미지 파라미터\n * @param extension - 파일 확장자 (기본: 'png')\n * @returns 이미지 파일명\n *\n * @example\n * ```typescript\n * generateImageFilename({\n * brand: 'hyundai',\n * model: 'ioniq-5',\n * year: 2024\n * });\n * // 'hyundai__ioniq-5__2024.png'\n *\n * generateImageFilename({\n * brand: 'mini',\n * model: 'cooper',\n * year: 2025,\n * trim: 'jcw'\n * });\n * // 'mini__cooper__2025__jcw.png'\n *\n * generateImageFilename({\n * brand: 'Tesla',\n * model: 'Model 3',\n * year: 2024,\n * trim: 'Highland'\n * }, 'webp');\n * // 'tesla__model-3__2024__highland.webp'\n * ```\n */\nexport function generateImageFilename(\n params: VehicleImageParams,\n extension: 'png' | 'jpg' | 'webp' = 'png'\n): ImageFilename {\n // 1. 브랜드 슬러그 (소문자)\n const brandSlug = toSlug(params.brand);\n\n // 2. 모델 슬러그 (소문자, 하이픈)\n const modelSlug = toSlug(params.model);\n\n // 3. 연도 (숫자)\n const year = params.year;\n\n // 4. 파일명 생성\n if (params.trim) {\n // 트림이 있는 경우\n const trimSlug = toSlug(params.trim);\n return `${brandSlug}__${modelSlug}__${year}__${trimSlug}.${extension}` as ImageFilename;\n } else {\n // 트림이 없는 경우\n return `${brandSlug}__${modelSlug}__${year}.${extension}` as ImageFilename;\n }\n}\n\n/**\n * 파일명에서 차량 정보 추출\n *\n * @param filename - 이미지 파일명\n * @returns 차량 정보 또는 null\n *\n * @example\n * ```typescript\n * parseImageFilename('hyundai__ioniq-5__2024.png');\n * // { brand: 'hyundai', model: 'ioniq-5', year: 2024 }\n *\n * parseImageFilename('mini__cooper__2025__jcw.png');\n * // { brand: 'mini', model: 'cooper', year: 2025, trim: 'jcw' }\n * ```\n */\nexport function parseImageFilename(filename: string): VehicleImageParams | null {\n // 확장자 제거\n const nameWithoutExt = filename.replace(/\\.(png|jpg|webp)$/, '');\n\n // 더블 언더바로 분리\n const parts = nameWithoutExt.split('__');\n\n if (parts.length < 3) {\n return null; // 최소 brand, model, year 필요\n }\n\n const brand = parts[0];\n const model = parts[1];\n const year = parseInt(parts[2], 10);\n\n if (isNaN(year)) {\n return null; // 연도가 숫자가 아님\n }\n\n const trim = parts[3] || undefined;\n\n return {\n brand,\n model,\n year,\n trim\n };\n}\n\n/**\n * 파일명 검증\n *\n * @param filename - 검증할 파일명\n * @returns 유효 여부\n *\n * @example\n * ```typescript\n * isValidImageFilename('hyundai__ioniq-5__2024.png'); // true\n * isValidImageFilename('invalid-filename.png'); // false\n * ```\n */\nexport function isValidImageFilename(filename: string): boolean {\n const pattern = /^[a-z0-9\\-]+__[a-z0-9\\-]+__\\d{4}(__[a-z0-9\\-]+)?\\.(png|jpg|webp)$/;\n return pattern.test(filename);\n}\n","/**\n * 이미지 URL 생성 유틸리티\n *\n * Firebase Storage URL 생성 및 Fallback 처리\n */\n\nimport type { VehicleImageParams, ImageUrlOptions, ImagePath } from '../types/image';\nimport { getStorageBrandName } from '../brand/mapping';\nimport { toSlug } from '../model/mapping';\nimport { generateImageFilename } from './filename-generator';\n\n/**\n * Firebase Storage 기본 설정\n */\nconst DEFAULT_BUCKET = 'charzing-d1600.firebasestorage.app';\nconst DEFAULT_BASE_PATH = 'vehicle-images';\n\n/**\n * URL 인코딩 (Firebase Storage용)\n *\n * @param path - 인코딩할 경로\n * @returns 인코딩된 경로\n */\nfunction encodeStoragePath(path: string): string {\n return path.split('/').map(segment => encodeURIComponent(segment)).join('%2F');\n}\n\n/**\n * Firebase Storage 다운로드 URL 생성\n *\n * @param bucket - Storage 버킷명\n * @param fullPath - 파일 전체 경로\n * @returns 다운로드 URL\n */\nfunction createDownloadUrl(bucket: string, fullPath: string): string {\n const encodedPath = encodeStoragePath(fullPath);\n return `https://firebasestorage.googleapis.com/v0/b/${bucket}/o/${encodedPath}?alt=media`;\n}\n\n/**\n * 차량 이미지 URL 생성\n *\n * @param params - 차량 이미지 파라미터\n * @param options - URL 생성 옵션\n * @returns 이미지 다운로드 URL\n *\n * @example\n * ```typescript\n * generateVehicleImageUrl({\n * brand: 'hyundai',\n * model: 'ioniq-5',\n * year: 2024\n * });\n * // 'https://firebasestorage.googleapis.com/v0/b/charzing-d1600.firebasestorage.app/o/\n * // vehicle-images%2FHYUNDAI%2FIONIQ-5%2F2024%2Fhyundai__ioniq-5__2024.png?alt=media'\n *\n * generateVehicleImageUrl({\n * brand: 'mini',\n * model: 'cooper',\n * year: 2025,\n * trim: 'jcw'\n * });\n * // 'https://firebasestorage.googleapis.com/v0/b/charzing-d1600.firebasestorage.app/o/\n * // vehicle-images%2FMINI%2FCOOPER%2F2025%2Fmini__cooper__2025__jcw.png?alt=media'\n * ```\n */\nexport function generateVehicleImageUrl(\n params: VehicleImageParams,\n options: ImageUrlOptions = {}\n): string {\n const {\n bucket = DEFAULT_BUCKET,\n extension = 'png'\n } = options;\n\n // 1. Storage 브랜드명 (대문자)\n const storageBrand = getStorageBrandName(params.brand as any);\n\n // 2. Storage 모델명 (대문자, 하이픈)\n const storageModel = toSlug(params.model).toUpperCase();\n\n // 3. 연도\n const year = params.year;\n\n // 4. 파일명 생성\n const filename = generateImageFilename(params, extension);\n\n // 5. 전체 경로 생성\n const fullPath = `${DEFAULT_BASE_PATH}/${storageBrand}/${storageModel}/${year}/${filename}`;\n\n // 6. 다운로드 URL 생성\n return createDownloadUrl(bucket, fullPath);\n}\n\n/**\n * 차량 이미지 경로 정보 가져오기\n *\n * @param params - 차량 이미지 파라미터\n * @param options - URL 생성 옵션\n * @returns 이미지 경로 정보\n *\n * @example\n * ```typescript\n * getVehicleImagePath({\n * brand: 'hyundai',\n * model: 'ioniq-5',\n * year: 2024\n * });\n * // {\n * // fullPath: 'vehicle-images/HYUNDAI/IONIQ-5/2024/hyundai__ioniq-5__2024.png',\n * // directory: 'vehicle-images/HYUNDAI/IONIQ-5/2024',\n * // filename: 'hyundai__ioniq-5__2024.png',\n * // downloadUrl: 'https://...'\n * // }\n * ```\n */\nexport function getVehicleImagePath(\n params: VehicleImageParams,\n options: ImageUrlOptions = {}\n): ImagePath {\n const {\n bucket = DEFAULT_BUCKET,\n extension = 'png'\n } = options;\n\n // 1. Storage 브랜드명 (대문자)\n const storageBrand = getStorageBrandName(params.brand as any);\n\n // 2. Storage 모델명 (대문자, 하이픈)\n const storageModel = toSlug(params.model).toUpperCase();\n\n // 3. 연도\n const year = params.year;\n\n // 4. 디렉토리 경로\n const directory = `${DEFAULT_BASE_PATH}/${storageBrand}/${storageModel}/${year}`;\n\n // 5. 파일명\n const filename = generateImageFilename(params, extension);\n\n // 6. 전체 경로\n const fullPath = `${directory}/${filename}`;\n\n // 7. 다운로드 URL\n const downloadUrl = createDownloadUrl(bucket, fullPath);\n\n return {\n fullPath,\n directory,\n filename,\n downloadUrl\n };\n}\n\n/**\n * Fallback 이미지 URL 목록 생성\n *\n * @param params - 차량 이미지 파라미터\n * @param options - URL 생성 옵션\n * @returns Fallback URL 목록 (우선순위 순)\n *\n * @example\n * ```typescript\n * generateFallbackUrls({\n * brand: 'mini',\n * model: 'cooper',\n * year: 2025,\n * trim: 'jcw'\n * });\n * // [\n * // 'https://.../mini__cooper__2025__jcw.png', // 트림 이미지\n * // 'https://.../mini__cooper__2025.png', // 기본 모델 이미지\n * // ]\n * ```\n */\nexport function generateFallbackUrls(\n params: VehicleImageParams,\n options: ImageUrlOptions = {}\n): string[] {\n const urls: string[] = [];\n\n // 1. 트림 이미지 (있는 경우)\n if (params.trim) {\n urls.push(generateVehicleImageUrl(params, options));\n }\n\n // 2. 기본 모델 이미지\n urls.push(generateVehicleImageUrl({\n brand: params.brand,\n model: params.model,\n year: params.year\n }, options));\n\n return urls;\n}\n"],"mappings":"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","names":["getBrandMapping","collection","getDocs","initialized","getModelMapping","isInitialized","resetCache"]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@charzing/vehicle-utils",
3
- "version": "0.1.1",
4
- "description": "Shared utilities for Charzing vehicle data management across all platforms",
3
+ "version": "0.2.0",
4
+ "description": "Shared utilities for Charzing vehicle data management across all platforms (with dynamic Firestore mapping)",
5
5
  "main": "./dist/cjs/index.js",
6
6
  "module": "./dist/esm/index.mjs",
7
7
  "types": "./dist/esm/index.d.mts",
@@ -37,6 +37,7 @@
37
37
  "@typescript-eslint/eslint-plugin": "^6.17.0",
38
38
  "@typescript-eslint/parser": "^6.17.0",
39
39
  "eslint": "^8.56.0",
40
+ "firebase": "^12.6.0",
40
41
  "jest": "^29.7.0",
41
42
  "ts-jest": "^29.1.1",
42
43
  "tsup": "^8.0.1",
@@ -45,5 +46,8 @@
45
46
  "files": [
46
47
  "dist",
47
48
  "README.md"
48
- ]
49
+ ],
50
+ "peerDependencies": {
51
+ "firebase": ">=10.0.0"
52
+ }
49
53
  }