#!/usr/bin/env python3 import csv import re import shutil import os from datetime import datetime # 컬러 로그를 위한 색상 코드 추가 class Colors: RED = '\033[91m' GREEN = '\033[92m' YELLOW = '\033[93m' BLUE = '\033[94m' MAGENTA = '\033[95m' CYAN = '\033[96m' WHITE = '\033[97m' ENDC = '\033[0m' # End color BOLD = '\033[1m' def print_color(message, color=Colors.WHITE): """컬러 출력 함수""" print(f"{color}{message}{Colors.ENDC}") def print_error(message): """에러 메시지 출력""" print_color(f"❌ ERROR: {message}", Colors.RED) def print_warning(message): """경고 메시지 출력""" print_color(f"⚠️ WARNING: {message}", Colors.YELLOW) def print_success(message): """성공 메시지 출력""" print_color(f"✅ SUCCESS: {message}", Colors.GREEN) def print_info(message): """정보 메시지 출력""" print_color(f"ℹ️ INFO: {message}", Colors.CYAN) """ 실제 CSV 파일들 IF_MDZ_EVCP_CUSTOMER_MASTER.csv IF_MDZ_EVCP_EMPLOYEE_REFERENCE.csv IF_MDZ_EVCP_MATERIAL_MASTER_PART_RETURN.csv IF_MDZ_EVCP_PROJECT_MASTER.csv IF_MDZ_EVCP_DEPARTMENT_CODE.csv IF_MDZ_EVCP_EQUP_MASTER.csv IF_MDZ_EVCP_MODEL_MASTER.csv IF_MDZ_EVCP_VENDOR_MASTER.csv IF_MDZ_EVCP_EMPLOYEE_MASTER.csv IF_MDZ_EVCP_MATERIAL_MASTER_PART.csv IF_MDZ_EVCP_ORGANIZATION_MASTER.csv """ # ===== 설정 ===== CSV_DIR = './public/wsdl/_csv' WSDL_DIR = './public/wsdl' # 발견된 SAP 타입들을 수집하기 위한 전역 SET discovered_sap_types = set() type_size_combinations = set() # 타입-사이즈 조합도 수집 # 필드명 매핑 테이블 (CSV -> WSDL) FIELD_MAPPING = { # 개별 WSDL 별 테이블 만들기로 했으므로 사용하지 않고 WSDL 그대로 사용 # 'ADR_NO': 'ADDRNO', # 필요한 경우 더 추가 } # 테이블 매핑 테이블 (complexType -> CSV Table) TABLE_MAPPING = { # 'MATL': 'MATL', # 'UNIT': 'MATL/UNIT', # 필요한 경우 더 추가 } def normalize_sap_type_and_size(sap_type, size_str): """SAP 타입과 사이즈를 정규화""" global discovered_sap_types, type_size_combinations try: # 타입을 대문자로 변환 normalized_type = sap_type.upper().strip() if sap_type else 'CHAR' # 사이즈 처리 normalized_size = size_str.strip() if size_str else '' original_size = normalized_size # 원본 사이즈 보존 (로깅용) # 빈 사이즈인 경우 기본값 설정 if not normalized_size: normalized_size = '255' else: # 따옴표로 감싸진 경우 제거 (예: "1,0") quote_removed = False if normalized_size.startswith('"') and normalized_size.endswith('"'): before_quote_removal = normalized_size normalized_size = normalized_size[1:-1] quote_removed = True print_color(f"🔍 SIZE 파싱: 따옴표 제거 - '{before_quote_removal}' -> '{normalized_size}' (Type: {normalized_type})", Colors.YELLOW) # 로깅: 최종 결과 (따옴표가 없는 경우만) if not quote_removed and original_size: print_color(f"🔍 SIZE 파싱: 따옴표 없음 - '{original_size}' 그대로 사용 (Type: {normalized_type})", Colors.BLUE) # 발견된 타입들을 SET에 추가 discovered_sap_types.add(normalized_type) type_size_combinations.add(f"{normalized_type}({normalized_size})") # 컬럼 구분자나 특수문자가 있는 경우 그대로 유지 # DEC, QUAN, NUMB 등에서 "1,0" 형태의 사이즈는 정상 return normalized_type, normalized_size except Exception as e: print_error(f"타입/사이즈 정규화 실패 - Type: {sap_type}, Size: {size_str}, Error: {str(e)}") return 'CHAR', '255' # 기본값 반환 def safe_description_escape(description): """Description 필드의 특수문자를 안전하게 처리""" try: if not description: return '' # HTML/XML 특수문자 이스케이프 description = description.replace('&', '&') description = description.replace('<', '<') description = description.replace('>', '>') description = description.replace('"', '"') description = description.replace("'", ''') return description except Exception as e: print_error(f"Description 이스케이프 실패: {description}, Error: {str(e)}") return str(description) if description else '' def get_csv_files(): """CSV 디렉토리에서 모든 CSV 파일 목록을 가져옴""" csv_files = [] for file in os.listdir(CSV_DIR): if file.endswith('.csv'): csv_files.append(file.replace('.csv', '')) return csv_files def get_complex_type_info(wsdl_content): """WSDL 파일에서 complexType 정보를 추출""" complex_types = {} current_type = None current_fields = [] type_stack = [] # 중첩된 complexType을 추적하기 위한 스택 for line in wsdl_content: # complexType 시작 태그 찾기 type_match = re.search(r'' in line: if current_type: complex_types[current_type] = current_fields if type_stack: current_type = type_stack.pop() else: current_type = None continue # element 태그 찾기 element_match = re.search(r'" # complexType과 일치하는 테이블 정보 찾기 matching_data = None # 1. complexType 이름과 완전히 일치하는 테이블 찾기 for key in matching_keys: table_name = key.split('||', 1)[1] if complex_type.upper() == table_name.upper(): matching_data = csv_data[key] break # 2. CSV 테이블명을 '/'로 스플릿한 마지막 부분이 complexType과 일치하는 경우 if not matching_data: for key in matching_keys: table_name = key.split('||', 1)[1] if '/' in table_name: last_part = table_name.split('/')[-1] if complex_type.upper() == last_part.upper(): matching_data = csv_data[key] break # 3. 필드명만 일치하는 경우 (첫 번째 매칭 데이터 사용) if not matching_data: matching_data = csv_data[matching_keys[0]] # 4. 매칭된 데이터가 있으면 주석 생성, 없으면 매칭 실패 주석 if matching_data: indent = ' ' * indentation # CSV의 실제 타입과 사이즈 사용 comment = f"{indent}" print_info(f"주석 생성 완료: {field_name} -> Type:{matching_data['type']}, Size:{matching_data['size']}") return comment else: indent = ' ' * indentation print_warning(f"매칭 데이터를 찾을 수 없음: {field_name}") return f"{indent}" except Exception as e: indent = ' ' * indentation print_error(f"주석 생성 실패 - 필드: {field_name}, 에러: {str(e)}") return f"{indent}" def normalize_comment(comment_line): """주석을 정규화 (공백 제거, 소문자 변환 등)""" # 제거하고 내용만 추출 content = re.sub(r'^\s*\s*$', '', comment_line.strip()) # 여러 공백을 하나로 통합 content = re.sub(r'\s+', ' ', content) return content.strip() def comments_are_equal(existing_comment, expected_comment): """두 주석이 같은 내용인지 비교""" existing_normalized = normalize_comment(existing_comment) expected_normalized = normalize_comment(expected_comment) return existing_normalized == expected_normalized def should_process_line(line, csv_data): """라인이 처리 대상인지 확인""" # 네 조건을 모두 만족해야 함: # 1. str: """CSV 파일명에서 테이블 prefix 추출""" csv_upper = csv_name.upper() # CSV 파일명 패턴에서 마스터 타입 추출 if 'CUSTOMER_MASTER' in csv_upper: return 'CUSTOMER' elif 'VENDOR_MASTER' in csv_upper: return 'VENDOR' elif 'EMPLOYEE_MASTER' in csv_upper: return 'EMPLOYEE' elif 'PROJECT_MASTER' in csv_upper: return 'PROJECT' elif 'DEPARTMENT_CODE' in csv_upper: return 'DEPARTMENT' elif 'ORGANIZATION_MASTER' in csv_upper: return 'ORGANIZATION' elif 'EQUP_MASTER' in csv_upper: return 'EQUP' elif 'MODEL_MASTER' in csv_upper: return 'MODEL' elif 'MATERIAL_MASTER' in csv_upper: return 'MATERIAL' elif 'EMPLOYEE_REFERENCE' in csv_upper: return 'EMPLOYEE_REF' else: # 기본적으로 MDZ 부분 제거 후 첫 번째 단어 사용 parts = csv_name.replace('IF_MDZ_EVCP_', '').split('_') return parts[0] if parts else 'COMMON' def backup_file(filepath): """파일을 백업""" timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") backup_path = f"{filepath}.backup_{timestamp}" shutil.copy2(filepath, backup_path) print(f"백업 파일 생성: {backup_path}") return backup_path def process_wsdl_file(target): """WSDL 파일 처리""" csv_file_path = os.path.join(CSV_DIR, f'{target}.csv') wsdl_file_path = os.path.join(WSDL_DIR, f'{target}.wsdl') try: # 백업 생성 backup_path = backup_file(wsdl_file_path) print_color(f"\n🚀 처리 시작: {target}", Colors.BOLD) print_info("CSV 데이터 로딩 중...") csv_data = load_csv_data(target) print_success(f"CSV에서 {len(csv_data)}개 필드 정보 로드됨") # WSDL 파일 읽기 with open(wsdl_file_path, 'r', encoding='utf-8') as f: lines = f.readlines() # complexType 정보 추출 complex_types = get_complex_type_info(lines) print_success(f"WSDL에서 {len(complex_types)}개 complexType 정보 추출됨") except Exception as e: print_error(f"파일 초기화 실패 - {target}: {str(e)}") return # complexType 구조 출력 (디버깅용) for type_name, fields in complex_types.items(): print_color(f"\nComplexType: {type_name}", Colors.MAGENTA) for field in fields: print(f" - {field['name']} ({field['type']}) {'[Array]' if field['is_array'] else ''}") new_lines = [] i = 0 changes_made = 0 processed_fields = [] skipped_fields = [] skipped_array_fields = [] skipped_no_csv_fields = [] skipped_req_wrapper_fields = [] verified_correct = 0 corrected_seq = 0 error_count = 0 current_complex_type = None type_stack = [] # 중첩된 complexType을 추적하기 위한 스택 while i < len(lines): line = lines[i] line_processed = False try: # complexType 시작 태그 확인 type_match = re.search(r'' in line: if type_stack: current_complex_type = type_stack.pop() print_color(f"이전 complexType으로 복귀: {current_complex_type}", Colors.BLUE) else: current_complex_type = None # CSV에 있는 xsd:element 필드인지 확인 if should_process_line(line, csv_data): field_name = extract_field_name_from_line(line) if field_name and current_complex_type: processed_fields.append(field_name) print_color(f"처리 중인 필드: {field_name} (complexType: {current_complex_type})", Colors.CYAN) # 바로 위 라인이 주석인지 확인 (공백 라인 건너뛰면서) comment_line_index = -1 j = len(new_lines) - 1 while j >= 0: prev_line = new_lines[j].strip() if prev_line == '': j -= 1 continue elif prev_line.startswith(''): comment_line_index = j break else: break if comment_line_index >= 0: existing_comment = new_lines[comment_line_index] if has_seq_in_comment(existing_comment): indentation = get_indentation(line) expected_comment = create_comment(field_name, csv_data, indentation, current_complex_type) if expected_comment: if comments_are_equal(existing_comment, expected_comment): verified_correct += 1 print_success(f" 주석 검증 통과") else: new_lines[comment_line_index] = expected_comment + '\n' changes_made += 1 corrected_seq += 1 print_warning(f" SEQ 주석 수정: {field_name}") print(f" 기존: {existing_comment.strip()}") print(f" 수정: {expected_comment}") else: indentation = get_indentation(line) new_comment = create_comment(field_name, csv_data, indentation, current_complex_type) if new_comment: new_lines[comment_line_index] = new_comment + '\n' changes_made += 1 print_warning(f" 주석 교체: {field_name}") else: indentation = get_indentation(line) new_comment = create_comment(field_name, csv_data, indentation, current_complex_type) if new_comment: new_lines.append(new_comment + '\n') changes_made += 1 print_info(f" 주석 추가: {field_name}") line_processed = True elif ' 0 else Colors.WHITE) print(f" 검증 통과한 SEQ 주석: {verified_correct}개") print(f" 수정된 SEQ 주석: {corrected_seq}개") print(f" 오류 발생 횟수: {error_count}개") # CSV 누락 필드 상세 표시 if len(skipped_no_csv_fields) > 0: print_error(f"\n⚠️ CSV에 누락된 필드 목록 (확인 필요):") for field in skipped_no_csv_fields: print_error(f" - {field}") # 최종 결과 if error_count > 0: print_error(f"\n⚠️ {error_count}개의 오류가 발생했습니다. 로그를 확인해주세요.") if len(skipped_no_csv_fields) > 0: print_error(f"\n🚨 주의: {len(skipped_no_csv_fields)}개의 필드가 CSV에 누락되어 있습니다!") print_error("이 필드들은 WSDL에 정의되어 있지만 CSV 스펙에 없어 주석이 생성되지 않았습니다.") if changes_made == 0: print_success(f"\n모든 주석이 정확합니다! (검증된 SEQ 주석: {verified_correct}개)") else: print_success(f"\n{changes_made}개의 주석이 수정되었습니다.") if corrected_seq > 0: print(f" - 기존 SEQ 주석 수정: {corrected_seq}개") if changes_made - corrected_seq > 0: print(f" - 새로 추가/교체된 주석: {changes_made - corrected_seq}개") if __name__ == "__main__": try: csv_files = get_csv_files() print_color(f"\n🎯 발견된 CSV 파일: {len(csv_files)}개", Colors.BOLD) print_info(f"처리할 파일 목록: {csv_files}") total_files = len(csv_files) success_count = 0 error_count = 0 for i, target in enumerate(csv_files, 1): print_color(f"\n{'='*60}", Colors.BOLD) print_color(f"진행률: {i}/{total_files} - {target}", Colors.BOLD) print_color(f"{'='*60}", Colors.BOLD) try: process_wsdl_file(target) success_count += 1 except Exception as e: print_error(f"파일 처리 실패 - {target}: {str(e)}") error_count += 1 # 최종 통계 print_color(f"\n{'='*60}", Colors.BOLD) print_color("🏁 전체 처리 완료", Colors.BOLD) print_color(f"{'='*60}", Colors.BOLD) print_success(f"성공: {success_count}개 파일") if error_count > 0: print_error(f"실패: {error_count}개 파일") else: print_success("모든 파일이 성공적으로 처리되었습니다!") # 발견된 SAP 타입들 출력 (PostgreSQL 매핑용) print_color(f"\n{'='*60}", Colors.BOLD) print_color("📊 발견된 SAP 타입 통계 (PostgreSQL 매핑용)", Colors.MAGENTA) print_color(f"{'='*60}", Colors.BOLD) print_color(f"\n🔤 고유 SAP 타입 ({len(discovered_sap_types)}개):", Colors.CYAN) for sap_type in sorted(discovered_sap_types): print(f" - {sap_type}") print_color(f"\n📏 타입-사이즈 조합 ({len(type_size_combinations)}개):", Colors.YELLOW) for combination in sorted(type_size_combinations): print(f" - {combination}") print_color(f"\n💡 PostgreSQL 타입 매핑 가이드 (XML 파싱/조회용):", Colors.GREEN) print(" 🎯 실용적 접근법:") print(" - 대부분 → VARCHAR(500) 또는 TEXT (XML에서 모든 데이터가 문자열로 전송)") print(" - 숫자 검색/정렬이 필요한 경우만 → NUMERIC") print(" - 날짜 검색/정렬이 필요한 경우만 → DATE/TIMESTAMP") print("") print(" 📋 SAP 타입별 상세:") print(" - CHAR, VARC, LCHR → VARCHAR(해당사이즈) 또는 TEXT") print(" - DATS (날짜) → VARCHAR(8) 또는 DATE (YYYYMMDD 형식)") print(" - TIMS (시간) → VARCHAR(6) 또는 TIME (HHMMSS 형식)") print(" - CURR, DEC, QUAN, NUMB, NUMC, FLTP → VARCHAR 또는 NUMERIC") print(" - CUKY (통화), UNIT (단위), LANG (언어) → VARCHAR(10)") print("") print(" ⚡ 권장: 초기에는 모두 VARCHAR/TEXT로 시작하고 필요시 변환") except Exception as e: print_error(f"스크립트 실행 중 치명적 오류 발생: {str(e)}") exit(1)