Initial commit: Excel Address to Postal Code Converter
This commit is contained in:
4
.env.example
Normal file
4
.env.example
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# 네이버 API 인증 정보
|
||||||
|
# https://console.ncloud.com/ 에서 발급받을 수 있습니다
|
||||||
|
NAVER_CLIENT_ID=your_client_id
|
||||||
|
NAVER_CLIENT_SECRET=your_client_secret
|
||||||
42
.gitignore
vendored
Normal file
42
.gitignore
vendored
Normal file
@@ -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
|
||||||
170
README.md
Normal file
170
README.md
Normal file
@@ -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
|
||||||
|
|
||||||
|
## 지원
|
||||||
|
|
||||||
|
문제가 발생하거나 기능 요청이 있으면 이슈를 등록해주세요.
|
||||||
92
address_handler.py
Normal file
92
address_handler.py
Normal file
@@ -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
|
||||||
16
config.py
Normal file
16
config.py
Normal file
@@ -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 = '우편번호'
|
||||||
107
excel_handler.py
Normal file
107
excel_handler.py
Normal file
@@ -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()
|
||||||
108
main.py
Normal file
108
main.py
Normal file
@@ -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)
|
||||||
3
requirements.txt
Normal file
3
requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
openpyxl==3.1.5
|
||||||
|
requests==2.31.0
|
||||||
|
python-dotenv==1.0.0
|
||||||
Reference in New Issue
Block a user