commit b91d8b8fac186168d08cc70db256972fd5c5d4e2 Author: user01 Date: Tue May 26 22:38:52 2026 +0900 Initial commit: Excel Address to Postal Code Converter diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..0e86fd8 --- /dev/null +++ b/.env.example @@ -0,0 +1,4 @@ +# 네이버 API 인증 정보 +# https://console.ncloud.com/ 에서 발급받을 수 있습니다 +NAVER_CLIENT_ID=your_client_id +NAVER_CLIENT_SECRET=your_client_secret diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..26d1a98 --- /dev/null +++ b/.gitignore @@ -0,0 +1,42 @@ +# 환경 변수 파일 +.env +.env.local + +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ +.DS_Store + +# 테스트 결과 +.pytest_cache/ +.coverage +htmlcov/ + +# 임시 엑셀 파일 (처리 중) +~$*.xlsx +*.tmp diff --git a/README.md b/README.md new file mode 100644 index 0000000..68409e0 --- /dev/null +++ b/README.md @@ -0,0 +1,170 @@ +# 엑셀 주소 → 우편번호 자동 변환 앱 + +엑셀 파일에서 "주소" 컬럼의 주소를 읽어 네이버 지도 API를 통해 우편번호를 조회하고, "우편번호" 컬럼에 자동으로 매칭하여 저장하는 Python 애플리케이션입니다. + +## 주요 기능 + +- 📋 엑셀 파일의 "주소" 컬럼 자동 인식 +- 🌐 네이버 지도 API를 사용한 정확한 우편번호 조회 +- 📝 "우편번호" 컬럼 자동 생성 및 데이터 추가 +- ✅ 처리 결과 통계 표시 +- 🔄 배치 처리 지원 + +## 설치 방법 + +### 1. 필수 요구사항 +- Python 3.7 이상 +- pip (Python 패키지 관리자) + +### 2. 라이브러리 설치 +```bash +pip install -r requirements.txt +``` + +### 3. 네이버 API 등록 + +1. [네이버 클라우드 콘솔](https://console.ncloud.com/)에 접속 +2. 회원가입 또는 로그인 +3. "Services" → "AI · Application Service" → "Naver API" 선택 +4. "Application" 메뉴에서 새 애플리케이션 등록 +5. Maps 서비스에서 "Geocoding API" 권한 추가 +6. API 인증 정보 확인: + - Client ID + - Client Secret + +### 4. 환경 변수 설정 + +`.env.example` 파일을 참고하여 `.env` 파일을 생성합니다: + +```bash +# .env 파일 +NAVER_CLIENT_ID=your_client_id_here +NAVER_CLIENT_SECRET=your_client_secret_here +``` + +## 사용 방법 + +### 기본 사용법 + +```bash +python main.py 파일경로.xlsx +``` + +또는 대화형 모드: + +```bash +python main.py +# 그러면 파일 경로를 입력하도록 요청합니다 +``` + +### 예시 + +```bash +python main.py customers.xlsx +``` + +## 엑셀 파일 형식 + +### 필수 요구사항 +- 첫 번째 행은 헤더 행 +- **"주소"** 라는 이름의 컬럼 필수 존재 + +### 입력 예시 + +| 이름 | 주소 | +|------|------| +| 홍길동 | 서울시 강남구 테헤란로 123 | +| 김철수 | 부산시 중구 중앙대로 456 | + +### 출력 예시 + +| 이름 | 주소 | 우편번호 | +|------|------|---------| +| 홍길동 | 서울시 강남구 테헤란로 123 | 06234 | +| 김철수 | 부산시 중구 중앙대로 456 | 48058 | + +## 주소 입력 형식 + +네이버 API는 다양한 주소 형식을 지원합니다: + +### 도로명 주소 (권장) +``` +서울시 강남구 테헤란로 123 +경기도 성남시 분당구 정자동 100-1 +``` + +### 지번 주소 +``` +서울시 강남구 역삼동 123 +부산시 중구 중앙동 456-7 +``` + +## 트러블슈팅 + +### "네이버 API 인증 정보가 없습니다" 오류 + +**.env 파일이 제대로 설정되었는지 확인하세요:** + +```bash +# .env 파일이 존재하는지 확인 +ls -la .env + +# 파일 내용 확인 (값이 비어있지 않은지 확인) +cat .env +``` + +### "주소 검색 결과 없음" 경고 + +- 주소 형식이 정확한지 확인하세요 +- 존재하지 않는 주소일 수 있습니다 +- 도로명 주소 또는 정확한 주소로 다시 시도해보세요 + +### API 요청 타임아웃 + +네이버 API 응답이 느린 경우입니다: +- 잠시 후 다시 시도하세요 +- 인터넷 연결 상태를 확인하세요 + +### "X-NCP-APIGW-API-KEY-ID" 관련 오류 + +API 인증 정보가 잘못되었을 가능성이 있습니다: +1. 네이버 클라우드 콘솔에서 클라이언트 ID/Secret 재확인 +2. `.env` 파일에 정확히 입력했는지 확인 +3. 파일을 저장한 후 다시 실행 + +## 처리 시간 + +주소 개수에 따라 처리 시간이 달라집니다: +- 100개 주소: ~1-2분 +- 1,000개 주소: ~10-20분 +- 10,000개 주소: ~100-200분 + +## API 사용 제한 + +네이버 API는 무료로 일정량의 요청을 제공합니다: +- 월 25,000건의 무료 쿼터 제공 +- 초과 시 추가 요금 발생 가능 + +자세한 내용은 [네이버 Geocoding API 문서](https://api.ncloud-docs.com/docs/ai-geocoding-geocode)를 참고하세요. + +## 파일 구조 + +``` +excelpostchanger/ +├── main.py # 메인 애플리케이션 +├── address_handler.py # 우편번호 조회 로직 +├── excel_handler.py # 엑셀 파일 처리 로직 +├── config.py # 설정 파일 +├── .env.example # 환경 변수 예시 +├── .env # 환경 변수 (실제 사용) +├── requirements.txt # Python 의존성 +└── README.md # 이 파일 +``` + +## 라이선스 + +MIT License + +## 지원 + +문제가 발생하거나 기능 요청이 있으면 이슈를 등록해주세요. diff --git a/address_handler.py b/address_handler.py new file mode 100644 index 0000000..f3e0368 --- /dev/null +++ b/address_handler.py @@ -0,0 +1,92 @@ +import requests +from config import NAVER_CLIENT_ID, NAVER_CLIENT_SECRET, NAVER_GEOCODING_URL + + +class AddressLookup: + """네이버 지도 API를 사용하여 주소에서 우편번호를 조회""" + + def __init__(self): + self.client_id = NAVER_CLIENT_ID + self.client_secret = NAVER_CLIENT_SECRET + self.api_url = NAVER_GEOCODING_URL + + if not self.client_id or not self.client_secret: + raise ValueError( + "네이버 API 인증 정보가 없습니다. " + ".env 파일에 NAVER_CLIENT_ID와 NAVER_CLIENT_SECRET을 설정하세요." + ) + + def get_postal_code(self, address): + """ + 주소를 입력받아 우편번호를 조회합니다. + + Args: + address (str): 조회할 주소 + + Returns: + str: 우편번호 (조회 실패 시 빈 문자열) + """ + try: + headers = { + 'X-NCP-APIGW-API-KEY-ID': self.client_id, + 'X-NCP-APIGW-API-KEY': self.client_secret + } + + params = { + 'query': address + } + + response = requests.get( + self.api_url, + headers=headers, + params=params, + timeout=5 + ) + + if response.status_code == 200: + data = response.json() + + # 응답에서 주소 정보 추출 + if data.get('addresses') and len(data['addresses']) > 0: + first_result = data['addresses'][0] + # 우편번호는 'addressElements'에서 '법정동' 정보와 함께 제공됨 + postal_code = first_result.get('postalCode', '') + + if postal_code: + return postal_code + else: + print(f"⚠ 경고: '{address}' - 우편번호 정보 없음") + return '' + else: + print(f"✗ 실패: '{address}' - 주소 검색 결과 없음") + return '' + else: + print(f"✗ API 오류: {response.status_code} - {address}") + return '' + + except requests.exceptions.Timeout: + print(f"✗ 타임아웃: '{address}' - 요청 시간 초과") + return '' + except requests.exceptions.RequestException as e: + print(f"✗ 네트워크 오류: '{address}' - {str(e)}") + return '' + except Exception as e: + print(f"✗ 오류: '{address}' - {str(e)}") + return '' + + def get_postal_codes_batch(self, addresses): + """ + 여러 주소의 우편번호를 일괄 조회합니다. + + Args: + addresses (list): 주소 리스트 + + Returns: + dict: {주소: 우편번호} 형태의 딕셔너리 + """ + results = {} + for i, address in enumerate(addresses, 1): + print(f"진행 중... ({i}/{len(addresses)})") + results[address] = self.get_postal_code(address) + + return results diff --git a/config.py b/config.py new file mode 100644 index 0000000..24c8200 --- /dev/null +++ b/config.py @@ -0,0 +1,16 @@ +import os +from dotenv import load_dotenv + +# .env 파일에서 환경 변수 로드 +load_dotenv() + +# 네이버 API 설정 +NAVER_CLIENT_ID = os.getenv('NAVER_CLIENT_ID', 'urpaarosee') +NAVER_CLIENT_SECRET = os.getenv('NAVER_CLIENT_SECRET', 'edHfPK139KCg8YsxigQmyhiE3YAuwsn70cWfyZFi') + +# API 엔드포인트 +NAVER_GEOCODING_URL = 'https://naveropenapi.apigw.ntruss.com/map-geocode/v2/geocode' + +# 엑셀 설정 +ADDRESS_COLUMN = '주소' +POSTAL_CODE_COLUMN = '우편번호' diff --git a/excel_handler.py b/excel_handler.py new file mode 100644 index 0000000..26e9f42 --- /dev/null +++ b/excel_handler.py @@ -0,0 +1,107 @@ +from openpyxl import load_workbook +from config import ADDRESS_COLUMN, POSTAL_CODE_COLUMN + + +class ExcelHandler: + """엑셀 파일에서 주소를 읽고 우편번호를 추가""" + + def __init__(self, file_path): + """ + Args: + file_path (str): 엑셀 파일 경로 + """ + self.file_path = file_path + self.workbook = None + self.worksheet = None + + def open_file(self): + """엑셀 파일을 엽니다""" + try: + self.workbook = load_workbook(self.file_path) + self.worksheet = self.workbook.active + print(f"✓ 파일 열기 성공: {self.file_path}") + return True + except FileNotFoundError: + print(f"✗ 파일을 찾을 수 없습니다: {self.file_path}") + return False + except Exception as e: + print(f"✗ 파일 열기 실패: {str(e)}") + return False + + def get_column_index(self, column_name): + """컬럼 이름으로부터 컬럼 인덱스를 찾습니다""" + for cell in self.worksheet[1]: + if cell.value == column_name: + return cell.column + return None + + def get_addresses(self): + """엑셀에서 주소 컬럼을 읽어서 주소 리스트를 반환합니다""" + address_col = self.get_column_index(ADDRESS_COLUMN) + + if address_col is None: + print(f"✗ '{ADDRESS_COLUMN}' 컬럼을 찾을 수 없습니다") + return None + + addresses = [] + for row in range(2, self.worksheet.max_row + 1): + cell_value = self.worksheet.cell(row, address_col).value + if cell_value: + addresses.append(str(cell_value).strip()) + + print(f"✓ {len(addresses)}개의 주소를 읽었습니다") + return addresses + + def add_postal_codes(self, postal_code_dict): + """ + 우편번호 딕셔너리를 엑셀에 추가합니다. + + Args: + postal_code_dict (dict): {주소: 우편번호} 형태의 딕셔너리 + """ + # "우편번호" 컬럼이 없으면 추가 + postal_col = self.get_column_index(POSTAL_CODE_COLUMN) + if postal_col is None: + postal_col = self.worksheet.max_column + 1 + self.worksheet.cell(1, postal_col).value = POSTAL_CODE_COLUMN + print(f"✓ '{POSTAL_CODE_COLUMN}' 컬럼 추가됨 (컬럼 {postal_col})") + + address_col = self.get_column_index(ADDRESS_COLUMN) + if address_col is None: + print(f"✗ '{ADDRESS_COLUMN}' 컬럼을 찾을 수 없습니다") + return False + + # 주소와 매칭하여 우편번호 입력 + updated_count = 0 + for row in range(2, self.worksheet.max_row + 1): + address = self.worksheet.cell(row, address_col).value + if address: + address_str = str(address).strip() + if address_str in postal_code_dict: + postal_code = postal_code_dict[address_str] + self.worksheet.cell(row, postal_col).value = postal_code + updated_count += 1 + + print(f"✓ {updated_count}개 행의 우편번호가 추가되었습니다") + return True + + def save_file(self, output_path=None): + """ + 엑셀 파일을 저장합니다. + + Args: + output_path (str): 저장할 파일 경로 (None이면 원본 파일 덮어쓰기) + """ + try: + save_path = output_path or self.file_path + self.workbook.save(save_path) + print(f"✓ 파일 저장 완료: {save_path}") + return True + except Exception as e: + print(f"✗ 파일 저장 실패: {str(e)}") + return False + + def close_file(self): + """파일을 닫습니다""" + if self.workbook: + self.workbook.close() diff --git a/main.py b/main.py new file mode 100644 index 0000000..c3e6dc2 --- /dev/null +++ b/main.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python3 +""" +엑셀 파일의 주소에서 우편번호를 조회하여 추가하는 애플리케이션 +""" + +import sys +import os +from pathlib import Path +from address_handler import AddressLookup +from excel_handler import ExcelHandler + + +def main(): + """메인 함수""" + + print("=" * 60) + print("엑셀 주소 → 우편번호 자동 변환 앱") + print("=" * 60) + print() + + # 파일 경로 입력 + if len(sys.argv) > 1: + file_path = sys.argv[1] + else: + file_path = input("처리할 엑셀 파일 경로를 입력하세요: ").strip() + + # 파일 확인 + if not os.path.exists(file_path): + print(f"✗ 파일을 찾을 수 없습니다: {file_path}") + return False + + if not file_path.endswith(('.xlsx', '.xls')): + print("✗ 엑셀 파일(.xlsx 또는 .xls)만 지원합니다") + return False + + print() + + # 1. 엑셀 파일 열기 + excel = ExcelHandler(file_path) + if not excel.open_file(): + return False + + print() + + # 2. 주소 읽기 + addresses = excel.get_addresses() + if addresses is None or len(addresses) == 0: + print("✗ 처리할 주소가 없습니다") + excel.close_file() + return False + + print() + + # 3. 네이버 API 초기화 + try: + lookup = AddressLookup() + except ValueError as e: + print(f"✗ {str(e)}") + excel.close_file() + return False + + print() + print("주소에서 우편번호 조회 중...") + print("-" * 60) + + # 4. 우편번호 조회 + postal_codes = lookup.get_postal_codes_batch(addresses) + + print() + print("-" * 60) + print() + + # 5. 우편번호 추가 + excel.add_postal_codes(postal_codes) + + # 조회 결과 통계 + successful = sum(1 for v in postal_codes.values() if v) + failed = len(postal_codes) - successful + + print() + print("-" * 60) + print(f"조회 결과: 성공 {successful}개, 실패 {failed}개") + print("-" * 60) + print() + + # 6. 파일 저장 + if excel.save_file(): + print() + print("=" * 60) + print("✓ 처리 완료!") + print("=" * 60) + excel.close_file() + return True + else: + excel.close_file() + return False + + +if __name__ == '__main__': + try: + success = main() + sys.exit(0 if success else 1) + except KeyboardInterrupt: + print("\n\n중단되었습니다.") + sys.exit(1) + except Exception as e: + print(f"\n✗ 예상치 못한 오류: {str(e)}") + sys.exit(1) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..23e9720 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +openpyxl==3.1.5 +requests==2.31.0 +python-dotenv==1.0.0