summaryrefslogtreecommitdiff
path: root/public/wsdl/_util/analyze_mdz_wsdl.py
blob: 216d867b84bf14b219fd779ce53a43481f49a362 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
#!/usr/bin/env python3
"""
MDZ WSDL 파일 분석 스크립트
- WSDL 파일에서 테이블 구조 추출
- 현재 Drizzle 스키마와 비교
- 누락된 테이블/필드 확인
"""

import os
import re
import xml.etree.ElementTree as ET
from pathlib import Path
from typing import Dict, List, Set, Tuple
from collections import defaultdict
import sys
from datetime import datetime

class ColorLogger:
    """컬러 로깅을 위한 클래스"""
    
    # ANSI 컬러 코드
    COLORS = {
        'RESET': '\033[0m',
        'BOLD': '\033[1m',
        'DIM': '\033[2m',
        
        # 기본 컬러
        'BLACK': '\033[30m',
        'RED': '\033[31m',
        'GREEN': '\033[32m',
        'YELLOW': '\033[33m',
        'BLUE': '\033[34m',
        'MAGENTA': '\033[35m',
        'CYAN': '\033[36m',
        'WHITE': '\033[37m',
        
        # 밝은 컬러
        'BRIGHT_BLACK': '\033[90m',
        'BRIGHT_RED': '\033[91m',
        'BRIGHT_GREEN': '\033[92m',
        'BRIGHT_YELLOW': '\033[93m',
        'BRIGHT_BLUE': '\033[94m',
        'BRIGHT_MAGENTA': '\033[95m',
        'BRIGHT_CYAN': '\033[96m',
        'BRIGHT_WHITE': '\033[97m',
        
        # 배경 컬러
        'BG_RED': '\033[41m',
        'BG_GREEN': '\033[42m',
        'BG_YELLOW': '\033[43m',
        'BG_BLUE': '\033[44m',
    }
    
    def __init__(self, enable_colors: bool = True):
        """
        컬러 로거 초기화
        Args:
            enable_colors: Windows CMD에서는 False로 설정 가능
        """
        self.enable_colors = enable_colors and self._supports_color()
    
    def _supports_color(self) -> bool:
        """컬러 지원 여부 확인"""
        # Windows에서 colorama가 없으면 컬러 비활성화
        if os.name == 'nt':
            try:
                import colorama
                colorama.init()
                return True
            except ImportError:
                return False
        return True
    
    def _colorize(self, text: str, color: str) -> str:
        """텍스트에 컬러 적용"""
        if not self.enable_colors:
            return text
        return f"{self.COLORS.get(color, '')}{text}{self.COLORS['RESET']}"
    
    def header(self, text: str):
        """헤더 로그 (굵은 파란색)"""
        colored_text = self._colorize(text, 'BOLD')
        colored_text = self._colorize(colored_text, 'BRIGHT_BLUE')
        print(colored_text)
    
    def info(self, text: str):
        """정보 로그 (파란색)"""
        colored_text = self._colorize(text, 'BLUE')
        print(colored_text)
    
    def success(self, text: str):
        """성공 로그 (초록색)"""
        colored_text = self._colorize(text, 'BRIGHT_GREEN')
        print(colored_text)
    
    def warning(self, text: str):
        """경고 로그 (노란색)"""
        colored_text = self._colorize(text, 'BRIGHT_YELLOW')
        print(colored_text)
    
    def error(self, text: str):
        """에러 로그 (빨간색)"""
        colored_text = self._colorize(text, 'BRIGHT_RED')
        print(colored_text)
    
    def debug(self, text: str):
        """디버그 로그 (회색)"""
        colored_text = self._colorize(text, 'BRIGHT_BLACK')
        print(colored_text)
    
    def table_info(self, text: str):
        """테이블 정보 로그 (시안색)"""
        colored_text = self._colorize(text, 'CYAN')
        print(colored_text)
    
    def field_info(self, text: str):
        """필드 정보 로그 (마젠타)"""
        colored_text = self._colorize(text, 'MAGENTA')
        print(colored_text)
    
    def separator(self, char: str = "=", length: int = 80):
        """구분선 출력 (굵은 흰색)"""
        line = char * length
        colored_line = self._colorize(line, 'BOLD')
        print(colored_line)

# 전역 로거 인스턴스
logger = ColorLogger()

class WSDLAnalyzer:
    def __init__(self, wsdl_directory: str):
        self.wsdl_directory = Path(wsdl_directory)
        self.tables = defaultdict(dict)  # table_name -> {field_name: field_info}
        self.table_hierarchy = defaultdict(list)  # parent -> [children]
        self.table_sources = defaultdict(set)  # table_name -> {wsdl_file_names}
        
        # 필드명 매핑 규칙 정의 (개별 WSDL을 존중해 테이블 분리하기로 했으므로 사용하지 않음.)
        self.field_name_mappings = {}

        # 사용법
        # self.field_name_mappings = {
        #     'CUSTOMER_MASTER': {  # WSDL 파일명에 이 문자열이 포함되면
        #         'ADDRNO': 'ADR_NO'  # ADDRNO를 ADR_NO로 변경
        #     }
        # }
        
    def analyze_all_mdz_wsdls(self):
        """MDZ가 포함된 모든 WSDL 파일 분석"""
        wsdl_files = list(self.wsdl_directory.glob("*MDZ*.wsdl"))
        
        logger.info(f"Found {len(wsdl_files)} MDZ WSDL files:")
        for wsdl_file in wsdl_files:
            logger.table_info(f"  - {wsdl_file.name}")
        logger.info("")
        
        for wsdl_file in wsdl_files:
            self._analyze_wsdl_file(wsdl_file)
            
        # 테이블별 필드 합집합 처리
        self._merge_table_fields()
            
        return self.tables, self.table_hierarchy
    
    def _merge_table_fields(self):
        """테이블별 필드 합집합 처리 - 개선된 버전"""
        merged_tables = defaultdict(dict)
        
        for table_name, fields in self.tables.items():
            # MATL_PLNT 테이블의 경우 디버깅 정보 출력
            if table_name == 'MATL_PLNT':
                logger.debug(f"\n=== MATL_PLNT 테이블 디버깅 ===")
                logger.debug(f"  병합 전 필드 수: {len(fields)}")
                logger.debug(f"  필드 목록:")
                for field_key, field_info in fields.items():
                    logger.debug(f"    {field_key} -> {field_info['field_name']} (from {field_info['wsdl_source']})")
            
            # 테이블별 필드를 실제 필드명 기준으로 그룹화
            field_groups = defaultdict(list)  # actual_field_name -> [field_infos]
            
            for field_key, field_info in fields.items():
                # field_key에서 실제 필드명 추출 (|| 구분자 사용)
                actual_field_name = field_key.split('||')[0] if '||' in field_key else field_key
                field_groups[actual_field_name].append(field_info)
            
            # MATL_PLNT 테이블의 경우 그룹화 결과 출력
            if table_name == 'MATL_PLNT':
                logger.debug(f"  그룹화 후 필드 수: {len(field_groups)}")
                logger.debug(f"  그룹별 필드:")
                for actual_field_name, field_infos in field_groups.items():
                    sources = [info['wsdl_source'] for info in field_infos]
                    logger.debug(f"    {actual_field_name}: {len(field_infos)}개 소스 - {sources}")
            
            # 각 필드 그룹을 병합
            for actual_field_name, field_infos in field_groups.items():
                # 첫 번째 필드 정보를 기준으로 시작
                merged_field = field_infos[0].copy()
                
                # 모든 WSDL 소스 수집
                all_sources = set()
                all_descriptions = set()
                
                for field_info in field_infos:
                    all_sources.add(field_info['wsdl_source'])
                    if field_info['description'].strip():
                        all_descriptions.add(field_info['description'].strip())
                    
                    # 필수 필드인 경우 유지
                    if field_info['mandatory'] == 'M':
                        merged_field['mandatory'] = 'M'
                
                # 병합된 정보 설정
                merged_field['wsdl_sources'] = all_sources
                
                # 설명 병합 (첫 번째 설명 사용, WSDL 소스 정보는 주석에 추가)
                if all_descriptions:
                    merged_field['description'] = list(all_descriptions)[0]
                else:
                    merged_field['description'] = f'From multiple sources'
                
                # 테이블에 추가 (실제 필드명 사용)
                merged_tables[table_name][actual_field_name] = merged_field
        
        # 병합된 테이블 정보로 업데이트  
        self.tables = merged_tables
        
        # 테이블별 WSDL 소스 정보 출력
        logger.info("\n테이블별 WSDL 소스 정보 (필드 중복 제거 후):")
        for table_name, fields in self.tables.items():
            sources = set()
            for field_info in fields.values():
                sources.update(field_info['wsdl_sources'])
            logger.table_info(f"\n{table_name}:")
            for source in sorted(sources):
                logger.table_info(f"  - {source}")
            logger.table_info(f"  총 필드 수: {len(fields)}")
            
            # MATL_PLNT 테이블의 경우 최종 필드 목록 출력
            if table_name == 'MATL_PLNT':
                logger.debug(f"  최종 필드 목록:")
                for field_name in sorted(fields.keys()):
                    logger.debug(f"    - {field_name}")
    
    def _analyze_wsdl_file(self, wsdl_file: Path):
        """단일 WSDL 파일 분석"""
        logger.info(f"Analyzing {wsdl_file.name}...")
        
        try:
            with open(wsdl_file, 'r', encoding='utf-8') as f:
                content = f.read()
            
            # 우선 정규식으로 분석 시도 (주석에서 테이블 정보 추출)
            regex_count = self._extract_tables_from_regex(content, wsdl_file.name)
            
            # 정규식으로 찾지 못했을 때만 XML 파싱 시도
            if regex_count == 0:
                try:
                    # XML 네임스페이스 등록
                    namespaces = {
                        'xsd': 'http://www.w3.org/2001/XMLSchema',
                        'wsdl': 'http://schemas.xmlsoap.org/wsdl/'
                    }
                    
                    root = ET.fromstring(content)
                    self._extract_tables_from_xml(root, wsdl_file.name, namespaces)
                except ET.ParseError as e:
                    logger.error(f"  XML parsing failed: {e}")
                except Exception as e:
                    logger.error(f"  XML analysis error: {e}")
                
        except Exception as e:
            logger.error(f"  Error analyzing {wsdl_file.name}: {e}")
    
    def _extract_tables_from_xml(self, root: ET.Element, wsdl_name: str, namespaces: dict):
        """XML에서 테이블 정보 추출"""
        # complexType 요소들에서 테이블 구조 추출
        for complex_type in root.findall(".//xsd:complexType", namespaces):
            table_name = complex_type.get('name')
            if table_name:
                self._extract_fields_from_complex_type(complex_type, table_name, wsdl_name, namespaces)
    
    def _extract_tables_from_regex(self, content: str, wsdl_name: str) -> int:
        """정규식으로 테이블 정보 추출"""
        
        # Table 정보가 포함된 주석 패턴 (Description에서 --> 전까지 모든 문자 매칭)
        table_pattern = r'<!-- SEQ:\d+, Table:([^,]+), Field:([^,]+), M/O:([^,]*), Type:([^,]+), Size:([^,]+), Description:(.*?) -->'
        
        matches = re.findall(table_pattern, content)
        
        # # MATL/PLNT 관련 필드 디버깅
        # matl_plnt_matches = [match for match in matches if 'MATL/PLNT' in match[0]]
        # if matl_plnt_matches:
        #     print(f"  {wsdl_name}에서 MATL/PLNT 필드 발견: {len(matl_plnt_matches)}개")
        #     for match in matl_plnt_matches:
        #         table_path, field_name = match[0], match[1]
        #         print(f"    {field_name} (Table: {table_path})")
        
        for match in matches:
            table_path, field_name, mandatory, field_type, size, description = match
            
            # 필드명 매핑 적용
            original_field_name = field_name.strip()
            mapped_field_name = self._apply_field_name_mapping(original_field_name, wsdl_name)
            
            # 테이블 경로에서 실제 테이블명 추출
            # 예: "BP_HEADER/ADDRESS/AD_POSTAL" -> ["BP_HEADER", "ADDRESS", "AD_POSTAL"]
            table_parts = table_path.split('/')
            main_table = table_parts[0]
            
            # 계층 구조 기록
            if len(table_parts) > 1:
                for i in range(len(table_parts) - 1):
                    parent = '/'.join(table_parts[:i+1])
                    child = '/'.join(table_parts[:i+2])
                    if child not in self.table_hierarchy[parent]:
                        self.table_hierarchy[parent].append(child)
            
            # 필드 정보 저장 (매핑된 필드명 사용)
            field_info = {
                'field_name': mapped_field_name,  # 매핑된 필드명 사용
                'original_field_name': original_field_name,  # 원본 필드명도 보존
                'mandatory': mandatory.strip(),
                'type': field_type.strip(),
                'size': size.strip(),
                'description': description.strip(),
                'table_path': table_path,
                'wsdl_source': wsdl_name
            }
            
            # 테이블별로 필드 저장 (|| 구분자 사용으로 충돌 방지, 매핑된 필드명 사용)
            # CSV 파일명 기반 테이블 prefix 추가
            table_prefix = self._get_table_prefix_from_wsdl_name(wsdl_name)
            full_table_name = f"{table_prefix}_{table_path.replace('/', '_').upper()}"
            field_key = f"{mapped_field_name}||{table_path}"
            self.tables[full_table_name][field_key] = field_info
            
            # # MATL_PLNT 테이블에 필드 추가 시 디버깅
            # if 'MATL_PLNT' in full_table_name:
            #     print(f"    {full_table_name}에 필드 추가: {mapped_field_name} (from {wsdl_name})")
        
        logger.success(f"  Found {len(matches)} field definitions")
        return len(matches)
    
    def _extract_fields_from_complex_type(self, complex_type, table_name: str, wsdl_name: str, namespaces: dict):
        """complexType에서 필드 정보 추출"""
        for element in complex_type.findall(".//xsd:element", namespaces):
            field_name = element.get('name')
            field_type = element.get('type', 'unknown')
            min_occurs = element.get('minOccurs', '1')
            max_occurs = element.get('maxOccurs', '1')
            
            if field_name:
                field_info = {
                    'field_name': field_name,
                    'mandatory': 'M' if min_occurs != '0' else 'O',
                    'type': field_type,
                    'size': 'unknown',
                    'description': f'From {table_name}',
                    'table_path': table_name,
                    'wsdl_source': wsdl_name
                }
                
                field_key = f"{field_name}||{table_name}"
                self.tables[table_name.upper()][field_key] = field_info

    def _apply_field_name_mapping(self, field_name: str, wsdl_name: str) -> str:
        """특정 WSDL 파일의 필드명을 매핑 규칙에 따라 변경"""
        for wsdl_pattern, mappings in self.field_name_mappings.items():
            if wsdl_pattern in wsdl_name.upper():
                if field_name in mappings:
                    original_name = field_name
                    mapped_name = mappings[field_name]
                    logger.debug(f"  Field mapping: {original_name} -> {mapped_name} (from {wsdl_name})")
                    return mapped_name
        return field_name
    
    def _get_table_prefix_from_wsdl_name(self, wsdl_name: str) -> str:
        """WSDL 파일명에서 테이블 prefix 추출"""
        # 단순히 IF_MDZ_EVCP_ 접두사만 제거하고 나머지 그대로 사용
        # 예: IF_MDZ_EVCP_MATERIAL_PART_RETURN.wsdl -> MATERIAL_PART_RETURN
        prefix = wsdl_name.replace('IF_MDZ_EVCP_', '').replace('.wsdl', '')
        return prefix if prefix else 'COMMON'

def analyze_current_drizzle_schema(schema_file: str) -> Set[str]:
    """현재 Drizzle 스키마에서 테이블 목록 추출"""
    try:
        with open(schema_file, 'r', encoding='utf-8') as f:
            content = f.read()
        
        # export const 테이블명 패턴 찾기
        table_pattern = r'export const (\w+) = mdgSchema\.table\('
        matches = re.findall(table_pattern, content)
        
        return set(matches)
    
    except FileNotFoundError:
        logger.error(f"Schema file not found: {schema_file}")
        return set()

def compare_wsdl_vs_schema(wsdl_tables: Dict, schema_tables: Set[str]):
    """WSDL 테이블과 스키마 테이블 비교"""
    logger.separator()
    logger.header("WSDL vs Drizzle Schema 비교 결과")
    logger.separator()
    
    # WSDL에서 추출한 테이블명 (이미 대문자로 변환됨)
    wsdl_table_names = set(wsdl_tables.keys())
    
    logger.info(f"\nWSDL에서 발견된 테이블: {len(wsdl_tables)}개")
    for table in sorted(wsdl_tables.keys()):
        field_count = len(wsdl_tables[table])
        logger.table_info(f"  - {table} ({field_count} fields)")
    
    logger.info(f"\nDrizzle 스키마의 테이블: {len(schema_tables)}개")
    for table in sorted(schema_tables):
        logger.table_info(f"  - {table}")
    
    # 테이블명 직접 비교 (대문자로 통일)
    schema_tables_upper = {table.upper() for table in schema_tables}
    wsdl_tables_upper = {table.upper() for table in wsdl_table_names}
    
    # 누락된 테이블 찾기
    missing_in_schema = wsdl_tables_upper - schema_tables_upper
    extra_in_schema = schema_tables_upper - wsdl_tables_upper
    
    if missing_in_schema:
        logger.warning(f"\n⚠️  스키마에 누락된 테이블 ({len(missing_in_schema)}개):")
        for table in sorted(missing_in_schema):
            logger.warning(f"  - {table}")
    
    if extra_in_schema:
        logger.success(f"\n✅ 스키마에 추가로 정의된 테이블 ({len(extra_in_schema)}개):")
        for table in sorted(extra_in_schema):
            logger.success(f"  - {table}")
    
    return missing_in_schema, extra_in_schema

def generate_missing_tables_schema(wsdl_tables: Dict, missing_tables: Set[str]):
    """누락된 테이블들의 Drizzle 스키마 코드 생성"""
    if not missing_tables:
        return
    
    logger.separator()
    logger.header("누락된 테이블들의 Drizzle 스키마 코드")
    logger.separator()
    
    for missing_table in sorted(missing_tables):
        # WSDL 테이블명에서 해당하는 테이블 찾기 (대문자로 직접 매칭)
        wsdl_table_key = missing_table.upper()
        
        if wsdl_table_key in wsdl_tables and wsdl_tables[wsdl_table_key]:
            logger.field_info(f"\n// {wsdl_table_key}")
            logger.field_info(f"export const {wsdl_table_key} = mdgSchema.table('{wsdl_table_key}', {{")
            logger.field_info("  id: integer('id').primaryKey().generatedByDefaultAsIdentity(),")
            
            for field_key, field_info in wsdl_tables[wsdl_table_key].items():
                # field_key에서 실제 필드명 추출 (|| 구분자 사용)
                if '||' in field_key:
                    actual_field_name = field_key.split('||')[0] 
                else:
                    actual_field_name = field_key
                    
                # 필드 타입 매핑
                drizzle_type = map_wsdl_type_to_drizzle(field_info['type'], field_info['size'])
                mandatory = ".notNull()" if field_info['mandatory'] == 'M' else ""
                # NOTE: WSDL별로 개별 테이블을 만들기로 했으므로 notNull() 제약조건 복구
                
                # 주석으로 설명 추가
                comment = f" // {field_info['description']}" if field_info['description'] else ""
                wsdl_source = f" // From: {field_info['wsdl_source']}"
                mandatory_comment = f" // WSDL에서 필수 필드" if field_info['mandatory'] == 'M' else ""
                
                logger.field_info(f"  {actual_field_name}: {drizzle_type}{mandatory},{comment}{wsdl_source}{mandatory_comment}")
            
            logger.field_info("  ")
            logger.field_info("  createdAt: timestamp('created_at').defaultNow().notNull(),")
            logger.field_info("  updatedAt: timestamp('updated_at').defaultNow().notNull(),")
            logger.field_info("});")

def map_wsdl_type_to_drizzle(wsdl_type: str, size: str) -> str:
    """WSDL 타입을 Drizzle 타입으로 매핑 (모든 필드를 VARCHAR로 통일, 방어적 사이즈 계산)"""
    # 기본 길이 설정
    default_length = 100
    min_length = 10      # 최소 길이
    max_length = 2000    # 최대 길이 (PostgreSQL VARCHAR 권장 최대)
    
    # LCHR 타입은 text()로 처리 (큰 텍스트)
    if 'LCHR' in wsdl_type.upper():
        return "text()"
    
    # 사이즈 처리
    if size and size.strip():
        try:
            size_clean = size.strip()
            
            # "n,m" 형태 처리 (소수점 있는 숫자 타입)
            if ',' in size_clean:
                parts = size_clean.split(',')
                if len(parts) == 2 and parts[0].isdigit() and parts[1].isdigit():
                    total_digits = int(parts[0])  # 전체 자릿수
                    decimal_places = int(parts[1])  # 소수점 이하 자릿수
                    
                    # 방어적 계산: 전체 자릿수 + 부호(1) + 소수점(1) + 여유분(3) = +5
                    safe_length = total_digits + 5
                    logger.debug(f"  📏 소수점 타입 사이즈 계산: {size_clean} -> {safe_length} (원본: {total_digits}, 여유: +5)")
                    
                    # 최소/최대 길이 제한
                    safe_length = max(min_length, min(safe_length, max_length))
                    return f"varchar({{ length: {safe_length} }})"
            
            # 단순 숫자 처리
            elif size_clean.isdigit():
                original_length = int(size_clean)
                # 단순 숫자는 그대로 사용 (여유분 없음)
                safe_length = max(min_length, min(original_length, max_length))
                
                if safe_length != original_length:
                    logger.debug(f"  📏 단순 사이즈 조정: {original_length} -> {safe_length} (min/max 제한)")
                else:
                    logger.debug(f"  📏 단순 사이즈 사용: {safe_length}")
                
                return f"varchar({{ length: {safe_length} }})"
            
            # "n.m" 형태 처리 (점으로 구분된 경우도 있을 수 있음)
            elif '.' in size_clean:
                parts = size_clean.split('.')
                if len(parts) == 2 and parts[0].isdigit() and parts[1].isdigit():
                    total_digits = int(parts[0])
                    decimal_places = int(parts[1])
                    
                    # 방어적 계산
                    safe_length = total_digits + 5
                    logger.debug(f"  📏 소수점 타입 사이즈 계산 (점 구분): {size_clean} -> {safe_length} (원본: {total_digits}, 여유: +5)")
                    
                    safe_length = max(min_length, min(safe_length, max_length))
                    return f"varchar({{ length: {safe_length} }})"
            
            # 기타 형태는 기본값 사용
            else:
                logger.warning(f"  ⚠️  알 수 없는 사이즈 형태: '{size_clean}' -> 기본값 {default_length} 사용")
                return f"varchar({{ length: {default_length} }})"
                
        except Exception as e:
            logger.error(f"  ❌ 사이즈 파싱 오류: '{size}' -> 기본값 {default_length} 사용, 오류: {e}")
            return f"varchar({{ length: {default_length} }})"
    
    # 사이즈가 없거나 비어있는 경우 기본값
    return f"varchar({{ length: {default_length} }})"

def validate_schema(wsdl_tables: Dict, schema_tables: Set[str]) -> Dict[str, List[str]]:
    """스키마 검증"""
    validation_results = {
        'missing_tables': [],
        'missing_fields': [],
        'type_mismatches': [],
        'duplicate_fields': []
    }
    
    for table_name, fields in wsdl_tables.items():
        # 테이블 존재 여부 검증
        if table_name not in schema_tables:
            validation_results['missing_tables'].append(table_name)
            continue
        
        # 필드 검증
        field_names = set()
        for field_key, field_info in fields.items():
            # field_key에서 실제 필드명 추출 (|| 구분자 사용)
            actual_field_name = field_key.split('||')[0] if '||' in field_key else field_key
            
            # 중복 필드 검사
            if actual_field_name in field_names:
                validation_results['duplicate_fields'].append(f"{table_name}.{actual_field_name}")
            field_names.add(actual_field_name)
            
            # 누락된 필드 검증 (WSDL의 모든 필드가 스키마에 있는지 확인)
            if actual_field_name not in existing_fields:
                validation_results['missing_fields'].append(f"{table_name}.{actual_field_name}")
            
            # 타입 호환성 검증
            # ? 기존 스키마의 필드 타입과 비교
            # ! VARCHAR로 처리하기로 했으니 타입 호환성 검사는 필요 없음
    
    return validation_results

def analyze_existing_schema(schema_file: str) -> Dict[str, Dict[str, str]]:
    """기존 스키마 파일 분석"""
    existing_schema = {}
    try:
        with open(schema_file, 'r', encoding='utf-8') as f:
            content = f.read()
            
        # 테이블 정의 찾기 (변경된 패턴)
        table_pattern = r'export const (\w+) = mdgSchema\.table\([\'"](\w+)[\'"]'
        tables = re.findall(table_pattern, content)
        
        for table_const, table_name in tables:
            # 테이블의 필드 정의 찾기
            field_pattern = rf'{table_const} = mdgSchema\.table\([\'"]{table_name}[\'"].*?{{(.*?)}}'
            table_match = re.search(field_pattern, content, re.DOTALL)
            
            if table_match:
                fields = {}
                field_defs = table_match.group(1)
                
                # 각 필드 정의 파싱 (변경된 패턴)
                field_pattern = r'(\w+):\s*(\w+)\([\'"](\w+)[\'"]'
                field_matches = re.findall(field_pattern, field_defs)
                
                for field_name, field_type, field_db_name in field_matches:
                    fields[field_name] = {
                        'type': field_type,
                        'db_name': field_db_name
                    }
                
                existing_schema[table_name] = fields
    
    except Exception as e:
        logger.error(f"스키마 파일 분석 중 오류 발생: {e}")
    
    return existing_schema

def compare_field_types(wsdl_type: str, existing_type: str) -> bool:
    """필드 타입 호환성 검사"""
    type_mapping = {
        'varchar': ['CHAR', 'VARC', 'LCHR'],
        'integer': ['NUMB', 'NUMC'],
        'decimal': ['CURR'],
        'date': ['DATS'],
        'time': ['TIMS'],
        'text': ['LCHR']
    }
    
    wsdl_type = wsdl_type.upper()
    existing_type = existing_type.lower()
    
    # 타입 매핑 확인
    for drizzle_type, wsdl_types in type_mapping.items():
        if existing_type == drizzle_type:
            return any(t in wsdl_type for t in wsdl_types)
    
    return False

def validate_schema(wsdl_tables: Dict, schema_tables: Set[str], existing_schema: Dict[str, Dict[str, str]]) -> Dict[str, List[str]]:
    """스키마 검증 (개선된 버전)"""
    validation_results = {
        'missing_tables': [],
        'missing_fields': [],
        'type_mismatches': [],
        'duplicate_fields': []
    }
    
    for table_name, fields in wsdl_tables.items():
        # 테이블 존재 여부 검증
        if table_name not in schema_tables:
            validation_results['missing_tables'].append(table_name)
            continue
        
        # 기존 테이블의 필드 정보 가져오기
        existing_fields = existing_schema.get(table_name, {})
        
        # 필드 검증
        field_names = set()
        for field_key, field_info in fields.items():
            # field_key에서 실제 필드명 추출 (|| 구분자 사용)
            actual_field_name = field_key.split('||')[0] if '||' in field_key else field_key
            
            # 중복 필드 검사
            if actual_field_name in field_names:
                validation_results['duplicate_fields'].append(f"{table_name}.{actual_field_name}")
            field_names.add(actual_field_name)
            
            # 누락된 필드 검증 (WSDL의 모든 필드가 스키마에 있는지 확인)
            # Note: existing_fields는 기존 validate_schema에서는 정의되지 않았으므로 스킵
            
            # 타입 호환성 검증
            if actual_field_name in existing_fields:
                existing_type = existing_fields[actual_field_name]['type']
                if not compare_field_types(field_info['type'], existing_type):
                    validation_results['type_mismatches'].append(
                        f"{table_name}.{actual_field_name}: WSDL={field_info['type']}, Existing={existing_type}"
                    )
    
    return validation_results

def generate_schema_code(wsdl_tables: Dict, validation_results: Dict[str, List[str]], existing_schema: Dict[str, Dict[str, str]]) -> str:
    """스키마 코드 생성 (개선된 버전)"""
    schema_code = []
    
    # 누락된 테이블 생성
    for table_name in validation_results['missing_tables']:
        table_code = generate_table_code(wsdl_tables[table_name], table_name)
        schema_code.append(table_code)
    
    # 누락된 필드 추가
    for field_info in validation_results['missing_fields']:
        table_name, field_name = field_info.split('.')
        if table_name in existing_schema:
            field_code = generate_field_code(wsdl_tables[table_name][field_name])
            # 기존 테이블에 필드 추가하는 코드 생성
            table_code = f"// {table_name}에 추가할 필드:\n{field_code}"
            schema_code.append(table_code)
    
    return '\n\n'.join(schema_code)

def generate_table_code(fields: Dict, table_name: str) -> str:
    """테이블 코드 생성"""
    code = [
        f"export const {table_name} = mdgSchema.table('{table_name}', {{",
        "  id: integer('id').primaryKey().generatedByDefaultAsIdentity(),"
    ]
    
    # fields에서 실제 필드명과 필드 정보 가져오기
    # _merge_table_fields에서 이미 actual_field_name을 키로 사용하므로 그대로 사용
    for actual_field_name, field_info in sorted(fields.items()):
        # 필드 코드 생성 (actual_field_name 사용)
        field_code = generate_field_code(field_info)
        code.append(f"  {field_code}")
    
    code.extend([
        "  ",
        "  createdAt: timestamp('created_at').defaultNow().notNull(),",
        "  updatedAt: timestamp('updated_at').defaultNow().notNull(),",
        "});"
    ])
    
    return '\n'.join(code)

def generate_field_code(field_info: Dict) -> str:
    """필드 코드 생성"""
    drizzle_type = map_wsdl_type_to_drizzle(field_info['type'], field_info['size'])
    mandatory = ".notNull()" if field_info['mandatory'] == 'M' else ""
    # NOTE: WSDL별로 개별 테이블을 만들기로 했으므로 notNull() 제약조건 복구
    
    comment = f" // {field_info['description']}" if field_info['description'] else ""
    
    # 여러 WSDL 소스 정보 추가
    if 'wsdl_sources' in field_info and len(field_info['wsdl_sources']) > 1:
        sources_comment = f" // From: {', '.join(sorted(field_info['wsdl_sources']))}"
    else:
        wsdl_source = field_info.get('wsdl_source', list(field_info.get('wsdl_sources', ['Unknown']))[0])
        sources_comment = f" // From: {wsdl_source}"
    
    # 필수 필드 정보는 주석으로만 표시
    mandatory_comment = f" // WSDL에서 필수 필드" if field_info['mandatory'] == 'M' else ""
    
    # 필드명 매핑이 적용된 경우 원본 필드명 표시
    mapping_comment = ""
    if 'original_field_name' in field_info and field_info['original_field_name'] != field_info['field_name']:
        mapping_comment = f" // Original: {field_info['original_field_name']}"
    
    return f"{field_info['field_name']}: {drizzle_type}{mandatory},{comment}{sources_comment}{mandatory_comment}{mapping_comment}"

def generate_complete_schema(wsdl_tables: Dict) -> str:
    """완전한 스키마 코드 생성"""
    schema_code = [
        "import { integer, varchar, text, timestamp } from 'drizzle-orm/pg-core';",
        "import { mdgSchema } from '../../../db/schema/MDG/mdg';",
        "",
        "// WSDL 기반 자동 생성된 스키마",
        "// 생성일시: " + datetime.now().strftime("%Y-%m-%d %H:%M:%S") + " (UTC로 9시간 빼야 한국 시간)",
        "// 개선사항:",
        "// 1. WSDL별로 테이블 만들었음. 인터페이스 정의서에 문제가 많아서 어쩔 수 없었음.",
        "// 2. 타입은 varchar를 사용하도록 했음. 숫자관련된 건 부호, 소수점 대비 방어적으로 처리함 (사이즈)",
        "// 3. 테이블명에서 '/' 문자를 '_'로 변경하여 PostgreSQL/TypeScript 호환성 확보함",
        "",
    ]
    
    # 테이블 코드 생성
    for table_name, fields in sorted(wsdl_tables.items()):
        table_code = generate_table_code(fields, table_name)
        schema_code.append(table_code)
        schema_code.append("")  # 빈 줄 추가
    
    return '\n'.join(schema_code)

def main():
    # 현재 스크립트 위치에서 프로젝트 루트 찾기
    script_dir = Path(__file__).parent
    project_root = script_dir.parent.parent.parent  # public/wsdl/_util -> project_root
    
    wsdl_dir = script_dir.parent  # public/wsdl
    schema_file = project_root / "db" / "schema" / "MDG" / "mdg.ts"
    
    logger.header("MDZ WSDL 분석 시작...")
    logger.info(f"WSDL 디렉토리: {wsdl_dir}")
    logger.info(f"스키마 파일: {schema_file}")
    logger.info("")
    
    # WSDL 분석
    analyzer = WSDLAnalyzer(wsdl_dir)
    wsdl_tables, table_hierarchy = analyzer.analyze_all_mdz_wsdls()
    
    # 현재 스키마 분석
    schema_tables = analyze_current_drizzle_schema(schema_file)
    
    # 기존 스키마 분석
    existing_schema = analyze_existing_schema(schema_file)
    
    # 비교 결과 출력
    missing_tables, extra_tables = compare_wsdl_vs_schema(wsdl_tables, schema_tables)
    
    # 누락된 테이블 스키마 생성
    generate_missing_tables_schema(wsdl_tables, missing_tables)
    
    # 스키마 검증
    validation_results = validate_schema(wsdl_tables, schema_tables, existing_schema)
    
    # 검증 결과 출력
    logger.separator()
    logger.header("스키마 검증 결과")
    logger.separator()
    
    for category, items in validation_results.items():
        if items:
            logger.warning(f"\n{category}:")
            for item in items:
                logger.warning(f"  - {item}")
    
    # 완전한 스키마 코드 생성
    complete_schema = generate_complete_schema(wsdl_tables)
    
    # 스키마 파일 저장
    output_file = script_dir / "generated_schema.ts"
    with open(output_file, 'w', encoding='utf-8') as f:
        f.write(complete_schema)
    
    logger.success(f"\n생성된 스키마가 {output_file}에 저장되었습니다.")
    
    # 상세 필드 정보 출력 (옵션)
    if len(sys.argv) > 1 and sys.argv[1] == "--detailed":
        logger.separator()
        logger.header("상세 필드 정보")
        logger.separator()
        
        for table_name, fields in wsdl_tables.items():
            logger.table_info(f"\n### {table_name}")
            for field_name, field_info in fields.items():
                logger.field_info(f"  {field_name}: {field_info['type']}({field_info['size']}) - {field_info['description']}")
    
    logger.success("\n분석 완료!")
    logger.info(f"- 총 WSDL 테이블: {len(wsdl_tables)}개")
    logger.info(f"- 현재 스키마 테이블: {len(schema_tables)}개")
    logger.warning(f"- 누락 테이블: {len(missing_tables)}개") if missing_tables else logger.success(f"- 누락 테이블: {len(missing_tables)}개")
    logger.info(f"- 추가 테이블: {len(extra_tables)}개")

if __name__ == "__main__":
    main()