feat: 위시켓·카카오 마케팅 가이드 추가 + 부동산 크롤링 프로그램 v1.0

- MARKETING.md: 섹션 5 위시켓 프로필 (한 줄소개·자기소개·체크리스트·플랫폼 비교)
- MARKETING.md: 섹션 6 카카오 오픈채팅방 운영 가이드 (공지·입장메시지·파일탭·운영루틴)
- public/downloads/real_estate_crawler_v1.0.py: 부동산 매물 크롤링 프로그램
  지원: 직방·다방·피터팬·네이버부동산
  출력: 플랫폼별 시트 + 중복제거 + 스타일 Excel

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-23 11:42:03 +09:00
parent 1bf916cbcb
commit 05d80a7926
2 changed files with 1335 additions and 0 deletions

579
MARKETING.md Normal file
View File

@@ -0,0 +1,579 @@
# 쟁승메이드 마케팅 플레이북
> 7년차 대기업 백엔드 개발자 박재오 · bgg8988@gmail.com · 010-3907-1392
> **핵심 포지셔닝**: 계약서 먼저 · 납기 패널티 명시 · 소스코드 100% 인도 · 1개월 AS · 연락 두절 없음
---
## 목차
1. [크몽 서비스 카피](#1-크몽-서비스-카피)
2. [숨고 서비스 카피](#2-숨고-서비스-카피)
3. [공통 운영 가이드](#3-공통-운영-가이드)
4. [추가 홍보 채널 전략](#4-추가-홍보-채널-전략)
5. [위시켓 프리랜서 프로필](#5-위시켓-프리랜서-프로필)
6. [카카오 오픈채팅방 운영 가이드](#6-카카오-오픈채팅방-운영-가이드)
---
## 1. 크몽 서비스 카피
> 크몽 전략: **키워드 검색 → 포트폴리오 클릭 → 구매** 흐름.
> 제목 키워드 앞배치, 소개문 구조화, 태그 최적화가 핵심.
---
### 1-1. 외주 개발
**제목**
```
[7년차 대기업 개발자] 맞춤형 소프트웨어 외주개발 · 계약서 작성 · 소스코드 전달
```
**소개문**
```
안녕하세요, 7년차 대기업 백엔드 개발자 박재오입니다.
프리랜서 개발자를 찾다가 중간에 연락이 끊기거나,
완성물을 받지 못한 경험이 있으신가요?
저는 다릅니다.
✅ 계약서 먼저 작성합니다
✅ 납기일 지키고, 못 지키면 패널티 명시
✅ 완료 후 소스코드 100% 인도
✅ 1개월 무상 AS 기본 포함
✅ 주 1회 진행 상황 보고
─────────────────────────────
📌 주요 개발 분야
─────────────────────────────
• 업무 자동화 (Python RPA, 엑셀/이메일/보고서 자동화)
• 웹 서비스 개발 (Next.js, React, FastAPI)
• 데이터 수집·분석 시스템 (크롤링, 공공API 연동)
• 텔레그램 봇 / 알림 자동화
• 관리자 대시보드 / 사내 툴 개발
─────────────────────────────
📌 실제 납품 사례
─────────────────────────────
• 쇼핑몰 경쟁사 가격 모니터링 봇 → 수동 확인 0분/일
• Gmail 자동화 RPA → 이메일 처리 2시간 → 10분
• 영업 일보 자동화 → 보고서 작성 3시간 → 5분
• 주식 자동 매매 시스템 (직접 운영 중)
─────────────────────────────
📌 진행 방식
─────────────────────────────
1단계. 무료 상담 (요구사항 정리)
2단계. 견적서 + 계약서 작성
3단계. 개발 착수 (주 1회 보고)
4단계. 검수 + 소스코드 인도
5단계. 1개월 무상 AS
처음 외주를 맡기시는 분도 걱정 없이 진행할 수 있도록 단계마다 안내드립니다.
```
**패키지**
| 구분 | 가격 | 내용 | 기간 | AS |
|------|------|------|------|----|
| 베이직 | 30만원~ | 단순 스크립트·봇 | 1~2주 | 1개월 |
| 스탠다드 | 80만원~ | 자동화 시스템·API 연동 | 2~4주 | 1개월 |
| 프리미엄 | 200만원~ | 풀스택 웹서비스 | 4~8주 | 3개월 |
**태그**
```
외주개발, 프리랜서개발자, 파이썬개발, 업무자동화, 웹개발, RPA, 소프트웨어개발, 맞춤개발, 백엔드개발, 자동화프로그램
```
---
### 1-2. 업무 자동화
**제목**
```
[7년차 개발자] 엑셀·이메일·보고서 업무 자동화 개발 · Python RPA · 반복업무 제거
```
**소개문**
```
안녕하세요, 박재오입니다.
매일 반복하는 업무, 자동화하면 하루 몇 시간을 돌려받을 수 있습니다.
─────────────────────────────
📌 이런 분께 딱 맞습니다
─────────────────────────────
☑ 매일 같은 엑셀 파일을 수작업으로 정리하고 있다면
☑ 이메일 분류·답장 초안을 매번 손으로 작성한다면
☑ 보고서를 만드는 데 매주 2~3시간씩 쏟고 있다면
☑ 여러 사이트에서 데이터를 직접 긁어 모으고 있다면
─────────────────────────────
📌 자동화 가능한 업무
─────────────────────────────
• 엑셀 데이터 집계 → 보고서 자동 생성 (PDF/이메일 발송)
• 이메일 자동 분류 · 답변 초안 작성
• 웹사이트 데이터 자동 수집 (크롤링)
• 경쟁사 가격 모니터링 + 텔레그램 알림
• 공공데이터 API 연동 자동 수집
• PPT 자동 생성 (데이터 기반)
• 카카오톡·텔레그램·슬랙 알림 봇
─────────────────────────────
📌 실제 납품 결과
─────────────────────────────
"보고서 작성 3시간 → 5분, 매일 09:00 자동 발송" — 영업팀 고객
"이메일 처리 일 2시간 → 10분" — 무역업 고객
"경쟁사 50개 상품 매일 자동 추적, 수동 확인 0분" — 쇼핑몰 고객
─────────────────────────────
📌 진행 방식
─────────────────────────────
① 무료 상담 → 자동화 가능 여부 판단
② 견적 + 계약서 작성
③ 개발 + 테스트
④ 소스코드 인도 + 사용법 가이드 문서
⑤ 1개월 무상 AS
자동화가 가능한지 확인만 해도 됩니다. 부담 없이 먼저 문의해 주세요.
```
**패키지**
| 구분 | 가격 | 내용 | 기간 |
|------|------|------|------|
| 베이직 | 15만원~ | 단일 반복 업무 자동화 | 3~7일 |
| 스탠다드 | 40만원~ | 복합 자동화 + 알림 연동 | 1~2주 |
| 프리미엄 | 100만원~ | 다부서 통합 자동화 시스템 | 2~4주 |
**태그**
```
업무자동화, 엑셀자동화, Python자동화, RPA, 보고서자동화, 크롤링, 이메일자동화, 텔레그램봇, 반복업무, 자동화프로그램
```
---
### 1-3. 홈페이지 제작
**제목**
```
[7년차 개발자] 반응형 홈페이지 · 랜딩페이지 · 소개페이지 제작 · 직접 개발 · 템플릿 X
```
**소개문**
```
안녕하세요, 박재오입니다.
템플릿 없이, 처음부터 직접 코딩합니다.
─────────────────────────────
📌 이런 분께 추천합니다
─────────────────────────────
☑ 업체 소개 / 서비스 소개 페이지가 필요한 소상공인
☑ 포트폴리오·이력서 사이트가 필요한 프리랜서
☑ 신규 서비스 런칭 랜딩페이지가 필요한 스타트업
☑ 워드프레스·카페24 없이 직접 관리하고 싶은 분
─────────────────────────────
📌 제작 방식
─────────────────────────────
• 템플릿 X — 디자인부터 퍼블리싱까지 직접 제작
• 모바일 완벽 대응 (반응형)
• 빠른 로딩 속도 (Next.js / React 기반)
• Vercel 무료 배포 포함 (도메인 연결 안내)
• 소스코드 100% 인도
─────────────────────────────
📌 포함 항목
─────────────────────────────
✅ 기획 상담 1회
✅ 화면 설계 (와이어프레임)
✅ 반응형 개발
✅ 문의 폼 연동 (이메일 수신)
✅ 배포 + 도메인 연결 안내
✅ 1개월 무상 수정
─────────────────────────────
📌 기간 및 비용
─────────────────────────────
• 단일 페이지 (랜딩): 2~5일 / 50만원~
• 5페이지 이하 소개 사이트: 1~2주 / 100만원~
• 관리자 기능 포함: 2~4주 / 200만원~
```
**태그**
```
홈페이지제작, 랜딩페이지, 반응형웹, 소개페이지, 웹개발, Next.js, React, 소상공인홈페이지, 포트폴리오사이트, 직접개발
```
---
## 2. 숨고 서비스 카피
> 숨고 전략: **고객이 요청 → 전문가가 제안** 흐름.
> 제안서 첫 줄이 클릭률 결정. 간결함 + 신뢰 + 인간미가 핵심.
---
### 2-1. 외주 개발
**프로필 한 줄 소개**
```
7년차 대기업 백엔드 개발자 · 계약서 먼저 쓰고, 납기 지키고, 소스코드 드립니다
```
**제안서 본문**
```
안녕하세요, 박재오입니다.
요청 내용 잘 읽었습니다.
[고객 요청 핵심 한 줄 요약] 작업이 필요하시군요.
비슷한 프로젝트 경험이 있어 충분히 도와드릴 수 있습니다.
─────────────────
저는 이렇게 합니다
─────────────────
✅ 진행 전에 계약서부터 씁니다 (구두 약속 X)
✅ 납기일은 반드시 지킵니다 — 못 지키면 패널티 명시
✅ 개발 중 주 1회 진행 상황 보고
✅ 완료 후 소스코드 100% 드립니다
✅ 1개월은 무상으로 수정·보완해드립니다
개발자 찾다가 연락이 끊기거나 결과물을 못 받으신 분들이
많으셔서, 저는 처음부터 이 부분을 확실히 약속드립니다.
먼저 30분 정도 무료로 상담해드리겠습니다.
어떤 기능이 필요하신지 편하게 말씀해 주세요.
```
---
### 2-2. 업무 자동화
**프로필 한 줄 소개**
```
반복 업무 자동화 전문 · 엑셀·이메일·보고서·크롤링 · 실제 운영 중인 자동화 시스템 다수
```
**제안서 본문**
```
안녕하세요, 박재오입니다.
[고객 요청 업무] 자동화, 충분히 가능합니다.
직접 운영 중인 자동화 시스템이 여러 개 있고,
비슷한 의뢰를 여럿 납품해드렸습니다.
─── 최근 비슷한 사례 ───
• 영업팀 일보 자동화 → 보고서 작성 3시간 → 5분
• 쇼핑몰 가격 모니터링 → 수동 확인 완전 제거
• Gmail 자동화 → 이메일 처리 2시간 → 10분
자동화가 가능한지 모르겠다고 하셔도 괜찮습니다.
먼저 무료로 확인해드리겠습니다.
계약서 작성 후 착수, 소스코드 전달, 1개월 AS까지 기본입니다.
편하게 연락 주세요.
```
---
### 2-3. 홈페이지 제작
**프로필 한 줄 소개**
```
홈페이지 직접 개발 · 템플릿 X · 반응형 · 소스코드 전달 · 배포까지
```
**제안서 본문**
```
안녕하세요, 박재오입니다.
[고객 업종/목적] 홈페이지 제작, 도와드리겠습니다.
템플릿이나 워드프레스 없이 처음부터 직접 코딩합니다.
그래서 원하시는 대로 만들어드릴 수 있습니다.
─── 기본 포함 사항 ───
✅ 모바일 완벽 대응
✅ 빠른 로딩 (Next.js 기반)
✅ 문의 폼 연동 (이메일 수신)
✅ 배포 + 도메인 연결
✅ 소스코드 전달
✅ 1개월 무상 수정
계약서 먼저 쓰고, 납기 지키고, 중간 보고도 드립니다.
개발자 연락 두절 걱정 없이 맡기실 수 있습니다.
먼저 어떤 페이지가 필요하신지 말씀해 주세요.
같이 정리해드리겠습니다.
```
---
## 3. 공통 운영 가이드
| 항목 | 크몽 | 숨고 |
|------|------|------|
| **제목 전략** | 키워드 앞배치 + 대괄호 경력 표시 | 프로필 한 줄로 차별점 압축 |
| **가격 노출** | 패키지 3단계 명시 | 최저가 노출 후 상담 유도 |
| **응답 속도** | 24시간 이내 응답 뱃지 목표 | 요청 후 1시간 이내 제안 발송 |
| **후기 전략** | 초반 3건 지인 의뢰로 확보 | 5점 후기 누적 → 노출 순위 상승 |
| **CTA** | "무료 상담 문의" 버튼 | 제안서 발송 → 카카오 연결 |
**어디서든 반복할 핵심 5문장**
```
계약서 먼저 작성합니다.
납기일을 지킵니다. 못 지키면 패널티를 명시합니다.
완료 후 소스코드 100% 드립니다.
1개월 무상 AS가 기본입니다.
연락 두절 없습니다.
```
---
## 4. 추가 홍보 채널 전략
### 4-1. 콘텐츠 마케팅 (무료 · 장기)
#### 네이버 블로그
가장 빠르게 검색 유입을 만들 수 있는 채널. "외주 개발" 관련 정보성 글이 강점.
| 주제 예시 | 검색 의도 |
|-----------|-----------|
| `프리랜서 개발자 고르는 법 — 연락 두절 피하는 5가지 체크리스트` | 외주 개발 의뢰 예정자 |
| `엑셀 업무 자동화, 직접 해보기 vs 개발자 의뢰 — 비용 비교` | 자동화 관심자 |
| `소상공인 홈페이지 제작 비용 현실적으로 알아보기` | 홈페이지 필요 소상공인 |
| `파이썬으로 내 업무 자동화하기 — 실제 사례 3가지` | 자동화 입문자 |
| `크몽 외주 개발 의뢰 전에 꼭 확인해야 할 것들` | 크몽 잠재 고객 |
> **운영 팁**: 글 말미에 "무료 상담 링크(쟁승메이드)" 자연스럽게 삽입. 월 4~8편 꾸준히.
#### 유튜브 / 쇼츠
보여주기 콘텐츠가 신뢰도를 폭발적으로 높임.
| 영상 아이디어 | 형식 |
|---------------|------|
| `엑셀 3시간 업무, 자동화하면 5분 됩니다 (실제 시연)` | 쇼츠 60초 |
| `개발자 외주 맡기다 돈 날린 실제 사례 — 계약서 없이 진행하면 생기는 일` | 롱폼 7~10분 |
| `텔레그램 봇 만들어서 가격 모니터링 자동화 — 실제 코드 공개` | 롱폼 15분 |
| `내가 직접 만든 주식 자동매매 프로그램 — 2년째 운영 중` | 롱폼 10분 |
---
### 4-2. 커뮤니티 마케팅 (무료 · 즉효)
직접 링크 홍보보다 **도움 주는 댓글 → 자연스러운 유입** 방식이 효과적.
| 커뮤니티 | 공략 방법 |
|----------|-----------|
| **클리앙 · 루리웹** | "자동화 가능한가요?" 류 질문 글에 실제 사례 답변 + 프로필 링크 |
| **네이버 카페 (스타트업, 소상공인)** | "개발자 구해요" 글에 제안, 정보성 글 기고 |
| **오픈카카오 (사업자/스타트업 채널)** | 자동화·개발 관련 질문에 전문 답변 |
| **링크드인** | 프로젝트 케이스 스터디 포스팅 (Before → After 수치 공개) |
| **X (트위터)** | 자동화 팁 쓰레드 → 사이트 링크 |
---
### 4-3. 포트폴리오 플랫폼 등록 (무료)
| 플랫폼 | 특징 | 등록 방법 |
|--------|------|-----------|
| **위시켓** | B2B 프로젝트 중심, 단가 높음 | 프리랜서 프로필 + 포트폴리오 등록 |
| **라우드소싱** | 디자인·개발 공모전·의뢰 혼합 | 프리랜서 등록, 프로젝트 입찰 |
| **탈잉** | 재능 판매 + 강의 | 자동화 강의 or 1:1 컨설팅 |
| **오투잡** | 소규모 의뢰 다수 | 단순 업무 자동화·스크립트 판매 |
| **GitHub 프로필** | 개발자 신뢰도 핵심 | README에 포트폴리오·연락처 정리 |
---
### 4-4. 네트워킹 (오프라인·온라인)
| 활동 | 기대 효과 |
|------|-----------|
| **IT 밋업·해커톤 참가** | 잠재 고객 직접 만남, 명함 배포 |
| **소상공인 협회·상공회의소** | 디지털 전환 수요 높은 실사용자층 접근 |
| **스타트업 스쿨 / 엑셀러레이터 행사** | MVP 개발 의뢰 연결 |
| **지인 추천 인센티브** | 소개 성사 시 다음 의뢰 10% 할인 제공 |
---
### 4-5. 유료 광고 (예산 있을 때)
| 채널 | 예산 | 타겟 키워드 |
|------|------|-------------|
| **네이버 검색광고** | 월 10~30만원 | `외주 개발`, `업무 자동화 개발`, `홈페이지 제작` |
| **카카오 비즈보드** | 월 10~20만원 | 소상공인·자영업자 타겟팅 |
| **구글 검색광고** | 월 10만원~ | `python 자동화 외주`, `프리랜서 개발자` |
> **우선순위**: 콘텐츠 마케팅(블로그) → 커뮤니티 → 크몽/숨고 → 유료 광고 순서로 단계적 진행 권장.
---
### 4-6. 신뢰도 빌드업 로드맵
```
1개월차 크몽/숨고 등록 + 블로그 4편 작성 + 지인 후기 2~3건 확보
2개월차 쇼츠 영상 4개 + 커뮤니티 답변 활동 시작
3개월차 블로그 검색 유입 확인 + 크몽 리뷰 5개 달성 → 노출 순위 상승
6개월차 위시켓 등록 + 링크드인 케이스 스터디 포스팅
12개월차 재의뢰·소개 고객으로 신규 유입 없이도 수주 안정화
```
---
---
## 5. 위시켓 프리랜서 프로필
> 위시켓 전략: **경력·기술 스택 중심** 플랫폼. 클라이언트가 검색하거나 먼저 제안을 보내는 구조.
> 크몽 대비 단가 높고 B2B 프로젝트 비중이 큼. 프로필 완성도 100%가 노출의 전제 조건.
### 한 줄 소개 (검색 키워드 포함)
```
7년차 대기업 백엔드 개발자 · Python 업무 자동화 · 웹 서비스 개발 · 납기 보장
```
### 자기소개 본문
```
안녕하세요, 7년차 대기업 백엔드 개발자 박재오입니다.
본업과 병행하며 업무 자동화·웹 개발 프리랜서 프로젝트를 진행하고 있습니다.
직접 운영 중인 서비스(주식 자동매매 시스템, 로또 분석 플랫폼)가 있어
설계부터 운영까지 전 과정을 직접 경험했습니다.
──────────────────────────
주요 기술
──────────────────────────
• Backend: Python, FastAPI, Node.js, Next.js
• 자동화: RPA, Selenium, Gmail API, Google Apps Script, OpenPyXL
• 데이터: PostgreSQL, SQLite, 공공데이터 API, 웹 크롤링
• 인프라: Vercel, NAS 자체 서버 운영, Supabase
──────────────────────────
진행 방식 (차별점)
──────────────────────────
✅ 계약서 먼저 작성 (구두 약속 없음)
✅ 납기일 명시 + 지연 시 패널티 조항
✅ 개발 중 주 1회 진행 보고
✅ 완료 후 소스코드 100% 인도
✅ 1개월 무상 AS
──────────────────────────
납품 사례
──────────────────────────
• 영업팀 일보 자동화 → 보고서 작성 3시간 → 5분
• 쇼핑몰 경쟁사 가격 모니터링 봇 → 수동 확인 0분/일
• Gmail 자동화 RPA → 이메일 처리 2시간 → 10분
포트폴리오: jaengseung-made.vercel.app
```
### 프로필 등록 체크리스트
```
□ 프로필 사진 (전문적인 사진 or 깔끔한 단색 배경)
□ 기술 스택 태그 최대한 추가 (Python, Next.js, RPA, FastAPI 등)
□ 포트폴리오 URL 입력 (jaengseung-made.vercel.app)
□ 희망 단가: 시간당 5~7만원 (초반, 경력 쌓이면 상향)
□ 가능 프로젝트 유형: 단기·중기 모두 체크
□ 프로필 완성도 100% (미완성 시 노출 차단됨)
□ 포트폴리오 파일 첨부 (PDF 1~2페이지)
```
### 위시켓 vs 크몽/숨고 차이
| 항목 | 위시켓 | 크몽/숨고 |
|------|--------|-----------|
| **주 사용자** | 스타트업, 중소기업 | 개인, 소상공인 |
| **평균 단가** | 높음 (프로젝트 단위) | 낮음~중간 |
| **프로젝트 규모** | 중대형 | 소~중형 |
| **수수료** | 10~15% | 20% 내외 |
| **경쟁 방식** | 제안서 입찰 | 검색 노출 |
| **핵심 무기** | 경력·기술력 | 후기·가격 |
---
## 6. 카카오 오픈채팅방 운영 가이드
> 오픈채팅 링크: https://open.kakao.com/o/s9stoNvb
### 채팅방 기본 설정
```
채팅방 이름: 쟁승메이드 · 개발 무료 상담
프로필 사진: 쟁승메이드 로고 이미지
채팅방 설명: 7년차 개발자의 무료 상담 채널
외주 개발 · 업무 자동화 · 홈페이지 제작
부담 없이 질문하세요 :)
```
### 공지 (상단 고정) — 입장 즉시 보이는 텍스트
```
📌 쟁승메이드 무료 상담 채널입니다
안녕하세요, 7년차 대기업 개발자 박재오입니다.
개발 관련 고민이라면 무엇이든 편하게 물어보세요.
──────────────────
💬 상담 가능 분야
──────────────────
• 엑셀·이메일·보고서 업무 자동화
• 웹사이트·홈페이지 제작
• 맞춤형 소프트웨어 개발
• "이런 것도 되나요?" 가능 여부 확인
──────────────────
📋 상담 시작하는 법
──────────────────
아래 형식으로 남겨주시면 빠르게 답변드립니다.
[원하는 것]
[예산 범위 (대략적으로)]
[연락 가능 시간]
🌐 포트폴리오: jaengseung-made.vercel.app
```
### 입장 인사 메시지 (설정 위치: 관리 → 입장 메시지)
```
반갑습니다! 쟁승메이드 상담 채널에 오신 걸 환영합니다 😊
궁금하신 것 편하게 남겨주세요.
"이런 것도 자동화 되나요?" 같은 가벼운 질문도 좋습니다.
보통 1~2시간 내에 답변드립니다.
```
### 파일 탭에 올려둘 문서
| 파일명 | 내용 | 목적 |
|--------|------|------|
| `쟁승메이드_서비스소개.pdf` | 서비스 목록 + 가격 요약 1페이지 | 신뢰 + 가격 가이드 |
| `업무자동화_체크리스트.pdf` | 자동화 가능 여부 자가진단 10문항 | 리드 필터링 |
| `외주개발_진행절차.pdf` | 계약~납품 5단계 플로우 | 프로세스 신뢰 확보 |
### 부재 시 공지 템플릿 (복사 사용)
```
⏰ 현재 업무 중입니다.
퇴근 후 19시 이후에 확인하겠습니다.
급하신 분은 아래 문의 폼을 이용해 주세요.
👉 jaengseung-made.vercel.app/freelance
```
### 운영 루틴
```
출근 전 (08:30) 전날 밤 문의 확인 + 답변
점심 (12:30) 빠른 확인 + 간단 답변
퇴근 후 (19:00) 상세 답변 + 견적 안내
```
---
*최종 수정: 2026-03-23*

View File

@@ -0,0 +1,756 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
===========================================================
부동산 매물 크롤링 프로그램 v1.0
쟁승메이드 | jaengseung-made.vercel.app
문의: bgg8988@gmail.com | 010-3907-1392
===========================================================
지원 플랫폼:
- 직방 (Zigbang)
- 다방 (Dabang)
- 피터팬 (Peterpanz)
- 네이버 부동산 (Naver Land)
수집 결과: Excel 파일 (.xlsx) — 플랫폼별 시트 + 전체 시트
필요 패키지 설치:
pip install requests beautifulsoup4 pandas openpyxl tqdm
===========================================================
"""
import sys
import time
import json
import re
import random
from datetime import datetime
from pathlib import Path
# ── 패키지 확인 ────────────────────────────────────────────
try:
import requests
from bs4 import BeautifulSoup
import pandas as pd
from openpyxl.styles import PatternFill, Font, Alignment, Border, Side
from openpyxl.utils import get_column_letter
except ImportError as e:
print(f"\n[오류] 필요한 패키지가 없습니다: {e}")
print("아래 명령어로 설치 후 다시 실행하세요:")
print(" pip install requests beautifulsoup4 pandas openpyxl tqdm\n")
sys.exit(1)
# ══════════════════════════════════════════════════════════
# 공통 설정
# ══════════════════════════════════════════════════════════
HEADERS = {
"User-Agent": (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/122.0.0.0 Safari/537.36"
),
"Accept": "application/json, text/plain, */*",
"Accept-Language": "ko-KR,ko;q=0.9,en-US;q=0.8",
"Accept-Encoding": "gzip, deflate, br",
"Connection": "keep-alive",
}
DELAY_MIN = 1.2
DELAY_MAX = 2.8
def _delay():
"""요청 간 랜덤 딜레이 — 서버 부하 방지 및 차단 회피"""
time.sleep(random.uniform(DELAY_MIN, DELAY_MAX))
# ══════════════════════════════════════════════════════════
# 데이터 스키마
# ══════════════════════════════════════════════════════════
COLUMNS = [
"플랫폼", "매물유형", "거래유형",
"보증금(만원)", "월세(만원)", "가격표시",
"면적(㎡)", "층수", "주소", "건물명",
"설명", "등록일", "링크",
]
def _item(**kwargs) -> dict:
base = {c: "" for c in COLUMNS}
base.update(kwargs)
return base
def _fmt(amount) -> str:
"""정수(만원) → '3억 2,000만' or '1,500만' 문자열"""
try:
amount = int(amount)
except (TypeError, ValueError):
return str(amount)
if amount <= 0:
return "0"
if amount >= 10000:
eok = amount // 10000
man = amount % 10000
return f"{eok}" if man == 0 else f"{eok}{man:,}"
return f"{amount:,}"
def _price_str(deposit, rent, trade_type: str) -> str:
if trade_type == "월세":
return f"{_fmt(deposit)} / {_fmt(rent)}"
if trade_type == "전세":
return f"전세 {_fmt(deposit)}"
return _fmt(deposit)
# ══════════════════════════════════════════════════════════
# 위경도 → Geohash 변환 (직방 API용)
# ══════════════════════════════════════════════════════════
def _to_geohash(lat: float, lng: float, precision: int = 5) -> str:
BASE32 = "0123456789bcdefghjkmnpqrstuvwxyz"
lat_r, lng_r = [-90.0, 90.0], [-180.0, 180.0]
gh, bits, bit, even = [], 0, 0, True
while len(gh) < precision:
if even:
mid = (lng_r[0] + lng_r[1]) / 2
if lng >= mid:
bits = (bits << 1) | 1; lng_r[0] = mid
else:
bits <<= 1; lng_r[1] = mid
else:
mid = (lat_r[0] + lat_r[1]) / 2
if lat >= mid:
bits = (bits << 1) | 1; lat_r[0] = mid
else:
bits <<= 1; lat_r[1] = mid
even = not even
bit += 1
if bit == 5:
gh.append(BASE32[bits]); bits = bit = 0
return "".join(gh)
# ══════════════════════════════════════════════════════════
# 주요 지역 위경도 DB
# ══════════════════════════════════════════════════════════
LOCATION_DB: dict[str, tuple[float, float]] = {
# 서울 자치구
"강남구": (37.5172, 127.0473), "서초구": (37.4837, 127.0324),
"송파구": (37.5145, 127.1059), "마포구": (37.5638, 126.9084),
"강서구": (37.5509, 126.8495), "영등포구": (37.5264, 126.8962),
"용산구": (37.5311, 126.9810), "성동구": (37.5634, 127.0369),
"동작구": (37.5124, 126.9393), "관악구": (37.4784, 126.9516),
"강북구": (37.6396, 127.0255), "노원구": (37.6543, 127.0568),
"은평구": (37.6026, 126.9291), "서대문구": (37.5791, 126.9368),
"종로구": (37.5735, 126.9789), "중구": (37.5641, 126.9979),
"성북구": (37.5894, 127.0167), "광진구": (37.5385, 127.0823),
"중랑구": (37.5953, 127.0843), "도봉구": (37.6688, 127.0471),
"강동구": (37.5301, 127.1238), "구로구": (37.4954, 126.8874),
"금천구": (37.4600, 126.9000), "양천구": (37.5170, 126.8666),
"동대문구": (37.5744, 127.0396),
# 주요 동/지역
"강남": (37.5172, 127.0473), "홍대": (37.5563, 126.9233),
"신촌": (37.5596, 126.9361), "이태원": (37.5346, 126.9944),
"합정": (37.5495, 126.9140), "연남동": (37.5607, 126.9224),
"성수동": (37.5443, 127.0557), "건대": (37.5402, 127.0702),
"혜화": (37.5820, 127.0019), "신림": (37.4843, 126.9292),
"사당": (37.4762, 126.9815), "잠실": (37.5133, 127.1000),
"여의도": (37.5216, 126.9245), "명동": (37.5607, 126.9862),
# 수도권
"인천": (37.4563, 126.7052), "수원": (37.2636, 127.0286),
"성남": (37.4201, 127.1267), "분당": (37.3825, 127.1175),
"판교": (37.3943, 127.1106), "일산": (37.6577, 126.7670),
"부천": (37.5035, 126.7660), "안양": (37.3943, 126.9568),
"의정부": (37.7381, 127.0337), "평택": (36.9921, 127.1130),
# 지방 광역시
"부산": (35.1796, 129.0756), "대구": (35.8714, 128.6014),
"대전": (36.3504, 127.3845), "광주": (35.1595, 126.8526),
"울산": (35.5384, 129.3114), "세종": (36.4800, 127.2890),
}
# 네이버 부동산 법정동 코드 주요 목록
CORTAR_CODES: dict[str, str] = {
"서울 강남구": "1168000000",
"서울 서초구": "1165000000",
"서울 송파구": "1171000000",
"서울 마포구": "1144000000",
"서울 용산구": "1117000000",
"서울 영등포구": "1156000000",
"서울 강서구": "1150000000",
"서울 관악구": "1162000000",
"서울 성동구": "1120000000",
"서울 동작구": "1159000000",
"서울 노원구": "1135000000",
"서울 은평구": "1138000000",
"서울 강동구": "1174000000",
"경기 성남시 분당구": "4113500000",
"경기 수원시 팔달구": "4111700000",
"경기 고양시 일산동구": "4128100000",
"부산 해운대구": "2635000000",
"부산 수영구": "2644000000",
"대구 수성구": "2723000000",
"인천 연수구": "2817000000",
}
def get_coords(address: str) -> tuple[float, float] | None:
for key, coords in LOCATION_DB.items():
if key in address:
return coords
return None
# ══════════════════════════════════════════════════════════
# 1. 직방 크롤러
# ══════════════════════════════════════════════════════════
class ZigbangCrawler:
"""직방 내부 API 기반 매물 수집"""
API = "https://apis.zigbang.com"
ROOM_CODE = {
"원룸": "원룸", "투룸": "투쓰리룸", "오피스텔": "오피스텔",
"아파트": "아파트", "빌라": "빌라",
}
TRADE_CODE = {"월세": "월세", "전세": "전세", "매매": "매매"}
def _geocode(self, address: str) -> tuple[float, float] | None:
"""직방 자체 검색 API로 위경도 조회"""
try:
r = requests.get(
f"{self.API}/v2/suggest",
params={"q": address, "serviceType": "all"},
headers=HEADERS, timeout=10,
)
for item in r.json().get("items", []):
lat = item.get("lat") or item.get("latitude")
lng = item.get("lng") or item.get("longitude")
if lat and lng:
return float(lat), float(lng)
except Exception:
pass
return None
def fetch(self, address: str, room_type: str, trade_type: str, limit: int = 100) -> list[dict]:
tag = f"[직방] {room_type}/{trade_type}"
print(f" {tag} 수집 중...", end=" ", flush=True)
coords = get_coords(address) or self._geocode(address)
if not coords:
print(f"위치 찾기 실패")
return []
lat, lng = coords
gh = _to_geohash(lat, lng, 5)
stype = self.ROOM_CODE.get(room_type, "원룸")
ttype = self.TRADE_CODE.get(trade_type, "월세")
url = (
f"{self.API}/v2/items"
f"?deposit_gteq=0&rent_gteq=0"
f"&sales_type_in={ttype}"
f"&service_type_eq={stype}"
f"&geohash={gh}&domain=zigbang&withCoords=true&per_page={limit}"
)
results = []
try:
_delay()
r = requests.get(url, headers={**HEADERS, "Referer": "https://www.zigbang.com/"}, timeout=15)
for it in r.json().get("items", []):
dep = int(it.get("deposit") or 0)
rent = int(it.get("rent") or 0)
item_id = it.get("item_id", "")
results.append(_item(
플랫폼="직방",
매물유형=room_type,
거래유형=trade_type,
**{"보증금(만원)": dep, "월세(만원)": rent},
가격표시=_price_str(dep, rent, trade_type),
**{"면적(㎡)": round(float(it.get("area") or 0), 1) or ""},
층수=str(it.get("floor") or ""),
주소=it.get("address") or it.get("local1") or "",
건물명=it.get("title") or "",
링크=f"https://www.zigbang.com/home/oneroom/items/{item_id}" if item_id else "",
))
except Exception as e:
print(f"오류({e})")
return results
print(f"{len(results)}")
return results
# ══════════════════════════════════════════════════════════
# 2. 다방 크롤러
# ══════════════════════════════════════════════════════════
class DabangCrawler:
"""다방 내부 API 기반 매물 수집"""
API = "https://www.dabangapp.com/api/3"
ROOM_CODE = {"원룸": 1, "투룸": 2, "쓰리룸": 3, "오피스텔": 10, "아파트": 11, "빌라": 21}
TRADE_CODE = {"월세": 1, "전세": 2, "매매": 3}
def fetch(self, lat: float, lng: float, room_type: str, trade_type: str, limit: int = 80) -> list[dict]:
tag = f"[다방] {room_type}/{trade_type}"
print(f" {tag} 수집 중...", end=" ", flush=True)
rc = self.ROOM_CODE.get(room_type, 1)
tc = self.TRADE_CODE.get(trade_type, 1)
delta = 0.025
params = {
"call_type": "web",
"device_type": "web",
"filters": json.dumps({
"sellingTypeList": [tc],
"roomTypeList": [rc],
"depositRange": {"min": 0, "max": 90000},
"priceRange": {"min": 0, "max": 999},
"areaRange": {"min": 0, "max": 999},
}),
"location": json.dumps([
[lat - delta, lng - delta],
[lat + delta, lng + delta],
]),
"page": 1,
"limit": limit,
"ordering": "-created_at",
}
results = []
try:
_delay()
r = requests.get(
f"{self.API}/room/list/map", params=params,
headers={**HEADERS, "Referer": "https://www.dabangapp.com/"}, timeout=15,
)
data = r.json()
rooms = data.get("rooms") or data.get("result", {}).get("rooms") or []
for room in rooms:
price = room.get("price") or [0, 0]
dep = int(price[0]) if isinstance(price, list) and price else 0
rent = int(price[1]) if isinstance(price, list) and len(price) > 1 else 0
rid = room.get("id", "")
results.append(_item(
플랫폼="다방",
매물유형=room_type,
거래유형=trade_type,
**{"보증금(만원)": dep, "월세(만원)": rent},
가격표시=_price_str(dep, rent, trade_type),
**{"면적(㎡)": round(float(room.get("area") or 0), 1) or ""},
층수=str(room.get("floor") or ""),
주소=room.get("address") or "",
건물명=room.get("name") or "",
설명=(room.get("title") or "")[:80],
링크=f"https://www.dabangapp.com/room/{rid}" if rid else "",
))
except Exception as e:
print(f"오류({e})")
return results
print(f"{len(results)}")
return results
# ══════════════════════════════════════════════════════════
# 3. 피터팬 크롤러
# ══════════════════════════════════════════════════════════
class PeterpanzCrawler:
"""피터팬의 좋은방구하기 크롤러 (API + HTML 폴백)"""
BASE = "https://www.peterpanz.com"
TRADE_CODE = {"월세": "monthly", "전세": "charter", "매매": "selling"}
def fetch(self, lat: float, lng: float, trade_type: str, max_pages: int = 3) -> list[dict]:
tag = f"[피터팬] {trade_type}"
print(f" {tag} 수집 중...", end=" ", flush=True)
tc = self.TRADE_CODE.get(trade_type, "monthly")
results = []
for page in range(1, max_pages + 1):
try:
_delay()
r = requests.get(
f"{self.BASE}/house/all",
params={"lat": lat, "lng": lng, "zoom": 13, "type": tc, "page": page},
headers={**HEADERS, "Referer": f"{self.BASE}/"},
timeout=15,
)
# JSON 응답 시도
try:
data = r.json()
houses = data.get("houses") or data.get("items") or []
if not houses:
break
for h in houses:
hid = h.get("idx") or h.get("id") or ""
dep = int(h.get("deposit") or 0)
rent = int(h.get("rent") or 0)
results.append(_item(
플랫폼="피터팬",
매물유형=h.get("type") or "",
거래유형=trade_type,
**{"보증금(만원)": dep, "월세(만원)": rent},
가격표시=_price_str(dep, rent, trade_type),
**{"면적(㎡)": h.get("area") or ""},
층수=str(h.get("floor") or ""),
주소=h.get("address") or "",
건물명=h.get("title") or "",
설명=(h.get("description") or "")[:80],
링크=f"{self.BASE}/house/detail/{hid}" if hid else "",
))
except (json.JSONDecodeError, ValueError):
# HTML 파싱 폴백
soup = BeautifulSoup(r.text, "html.parser")
cards = soup.select(".item-list li, .house-list-item, [class*='item']")
if not cards:
break
for card in cards:
title_el = card.select_one(".title, .name, h3, h4")
price_el = card.select_one(".price, .cost, [class*='price']")
addr_el = card.select_one(".address, .addr, [class*='address']")
link_el = card.select_one("a[href]")
href = link_el["href"] if link_el else ""
results.append(_item(
플랫폼="피터팬",
거래유형=trade_type,
가격표시=price_el.get_text(strip=True) if price_el else "",
주소=addr_el.get_text(strip=True) if addr_el else "",
건물명=title_el.get_text(strip=True) if title_el else "",
링크=self.BASE + href if href.startswith("/") else href,
))
except Exception as e:
print(f"페이지{page} 오류({e}) ", end="")
break
print(f"{len(results)}")
return results
# ══════════════════════════════════════════════════════════
# 4. 네이버 부동산 크롤러
# ══════════════════════════════════════════════════════════
class NaverLandCrawler:
"""네이버 부동산 내부 API 기반 매물 수집 (법정동 코드 필요)"""
API = "https://new.land.naver.com/api"
TRADE_CODE = {"매매": "A1", "전세": "B1", "월세": "B2,B3"}
TYPE_CODE = {
"아파트": "APT", "오피스텔": "OPST",
"빌라": "VL", "원룸": "OR", "상가": "SG",
}
def fetch(self, cortar_no: str, real_estate_type: str, trade_type: str, max_pages: int = 5) -> list[dict]:
tag = f"[네이버부동산] {real_estate_type}/{trade_type}"
print(f" {tag} 수집 중...", end=" ", flush=True)
tc = self.TRADE_CODE.get(trade_type, "A1")
rc = self.TYPE_CODE.get(real_estate_type, "APT")
naver_headers = {
**HEADERS,
"Referer": "https://new.land.naver.com/",
"Accept": "*/*",
}
results = []
for page in range(1, max_pages + 1):
try:
_delay()
r = requests.get(
f"{self.API}/articles",
params={
"cortarNo": cortar_no,
"order": "rank",
"realEstateType": rc,
"tradeType": tc,
"page": page,
"pageSize": 20,
},
headers=naver_headers,
timeout=15,
)
if r.status_code != 200:
if r.status_code == 403:
print(f"\n [네이버부동산] 봇 차단 감지 (403). 잠시 후 재시도하거나 VPN 사용 권장.")
break
data = r.json()
articles = data.get("articleList") or []
if not articles:
break
for art in articles:
ano = art.get("articleNo") or ""
dep_str = art.get("dealOrWarrantPrc") or art.get("warrantyPrice") or ""
rent_str = art.get("rentPrc") or ""
results.append(_item(
플랫폼="네이버부동산",
매물유형=art.get("realEstateTypeName") or real_estate_type,
거래유형=trade_type,
가격표시=dep_str + (f" / {rent_str}" if rent_str else ""),
**{"면적(㎡)": art.get("area1") or ""},
층수=art.get("floorInfo") or "",
주소=art.get("cortarAddress") or "",
건물명=art.get("articleName") or "",
설명=(art.get("articleFeatureDesc") or "")[:80],
등록일=art.get("articleConfirmYmd") or "",
링크=f"https://new.land.naver.com/houses?articleNo={ano}" if ano else "",
))
except Exception as e:
print(f"페이지{page} 오류({e}) ", end="")
break
print(f"{len(results)}")
return results
# ══════════════════════════════════════════════════════════
# Excel 내보내기
# ══════════════════════════════════════════════════════════
COL_WIDTHS = {
"플랫폼": 10, "매물유형": 10, "거래유형": 8,
"보증금(만원)": 14, "월세(만원)": 12, "가격표시": 22,
"면적(㎡)": 10, "층수": 8, "주소": 32, "건물명": 22,
"설명": 38, "등록일": 12, "링크": 55,
}
HEADER_FILL = PatternFill("solid", fgColor="1D4ED8")
HEADER_FONT = Font(color="FFFFFF", bold=True, size=10)
ALT_FILL = PatternFill("solid", fgColor="EFF6FF")
CELL_BORDER = Border(
left=Side(style="thin", color="CBD5E1"),
right=Side(style="thin", color="CBD5E1"),
top=Side(style="thin", color="CBD5E1"),
bottom=Side(style="thin", color="CBD5E1"),
)
def _style_sheet(ws) -> None:
for cell in ws[1]:
cell.fill = HEADER_FILL
cell.font = HEADER_FONT
cell.alignment = Alignment(horizontal="center", vertical="center")
cell.border = CELL_BORDER
for i, row in enumerate(ws.iter_rows(min_row=2), 2):
for cell in row:
if i % 2 == 0:
cell.fill = ALT_FILL
cell.border = CELL_BORDER
cell.alignment = Alignment(vertical="center", wrap_text=False)
for col_idx, col_name in enumerate(COLUMNS, 1):
ws.column_dimensions[get_column_letter(col_idx)].width = COL_WIDTHS.get(col_name, 15)
ws.row_dimensions[1].height = 22
ws.freeze_panes = "A2"
ws.auto_filter.ref = ws.dimensions
def export_excel(items: list[dict], filename: str) -> None:
if not items:
print("\n[경고] 수집된 데이터가 없습니다.")
return
df = pd.DataFrame(items, columns=COLUMNS)
with pd.ExcelWriter(filename, engine="openpyxl") as writer:
# 전체 시트
df.to_excel(writer, sheet_name="전체", index=False)
# 플랫폼별 시트
for platform in df["플랫폼"].unique():
sub = df[df["플랫폼"] == platform]
sub.to_excel(writer, sheet_name=platform[:31], index=False)
# 요약 시트
summary = (
df.groupby(["플랫폼", "거래유형", "매물유형"])
.size().reset_index(name="건수")
.sort_values("건수", ascending=False)
)
summary.to_excel(writer, sheet_name="요약", index=False)
# 스타일 적용
for ws in writer.book.worksheets:
_style_sheet(ws)
total = len(df)
platforms = df["플랫폼"].unique().tolist()
print(f"\n✅ 저장 완료: {filename}")
print(f"{total}건 | 플랫폼: {', '.join(platforms)}")
# ══════════════════════════════════════════════════════════
# 메인 인터페이스
# ══════════════════════════════════════════════════════════
BANNER = """
╔══════════════════════════════════════════════════╗
║ 부동산 매물 크롤링 프로그램 v1.0 ║
║ 쟁승메이드 · jaengseung-made.vercel.app ║
╚══════════════════════════════════════════════════╝
"""
def _choose(prompt: str, options: list[str]) -> str:
print(f"\n{prompt}")
for i, o in enumerate(options, 1):
print(f" {i}. {o}")
while True:
try:
n = int(input("▶ 선택: ").strip())
if 1 <= n <= len(options):
return options[n - 1]
except (ValueError, KeyboardInterrupt):
pass
print(" 올바른 번호를 입력하세요.")
def _choose_multi(prompt: str, options: list[str]) -> list[str]:
print(f"\n{prompt} (콤마 구분, 예: 1,3 / 전체: 0)")
for i, o in enumerate(options, 1):
print(f" {i}. {o}")
raw = input("▶ 선택: ").strip()
if raw == "0" or not raw:
return list(options)
chosen = []
for part in raw.split(","):
try:
n = int(part.strip())
if 1 <= n <= len(options):
chosen.append(options[n - 1])
except ValueError:
pass
return chosen if chosen else list(options)
def main() -> None:
print(BANNER)
# 1. 지역 입력
print("📍 검색할 지역을 입력하세요")
print(" 예: 강남구 / 마포구 / 홍대 / 수원 / 부산")
print(" (DB에 없는 지역은 위경도 직접 입력 가능)")
address = input("▶ 지역: ").strip() or "강남구"
coords = get_coords(address)
if not coords:
print(f"\n '{address}'이(가) 기본 DB에 없습니다.")
try:
lat = float(input(" 위도(lat) 입력 (예: 37.5172): ").strip())
lng = float(input(" 경도(lng) 입력 (예: 127.0473): ").strip())
coords = (lat, lng)
except ValueError:
print(" 좌표 입력 실패. 강남구 기본값으로 진행합니다.")
coords = (37.5172, 127.0473)
lat, lng = coords
print(f" → 위치: {lat:.4f}, {lng:.4f}")
# 2. 거래 유형
trade_type = _choose("💰 거래 유형", ["월세", "전세", "매매"])
# 3. 매물 유형
room_types = _choose_multi(
"🏠 매물 유형 (복수 선택 가능)",
["원룸", "투룸", "오피스텔", "아파트", "빌라"],
)
# 4. 플랫폼 선택
platforms = _choose_multi(
"🌐 수집 플랫폼",
["직방", "다방", "피터팬", "네이버부동산"],
)
# 네이버 부동산 법정동 코드 입력
cortar_no = ""
naver_types: list[str] = []
if "네이버부동산" in platforms:
print("\n [네이버부동산] 법정동 코드가 필요합니다.")
print(" 알려진 코드 목록:")
for name, code in list(CORTAR_CODES.items())[:8]:
print(f" {name:20s} : {code}")
cortar_no = input(" 법정동 코드 입력 (없으면 Enter 건너뜀): ").strip()
if cortar_no:
naver_types = [rt for rt in room_types if rt in NaverLandCrawler.TYPE_CODE]
if not naver_types:
print(" 선택한 매물 유형 중 네이버부동산 지원 유형 없음 — 건너뜀")
cortar_no = ""
# ── 수집 시작 ──────────────────────────────────────────
print(f"\n{''*50}")
print(f"🔍 수집 시작")
print(f" 지역: {address} | 거래: {trade_type}")
print(f" 매물: {', '.join(room_types)}")
print(f" 플랫폼: {', '.join(platforms)}")
print(f"{''*50}\n")
all_items: list[dict] = []
if "직방" in platforms:
zc = ZigbangCrawler()
for rt in room_types:
all_items.extend(zc.fetch(address, rt, trade_type))
if "다방" in platforms:
dc = DabangCrawler()
for rt in room_types:
all_items.extend(dc.fetch(lat, lng, rt, trade_type))
if "피터팬" in platforms:
pc = PeterpanzCrawler()
all_items.extend(pc.fetch(lat, lng, trade_type, max_pages=3))
if "네이버부동산" in platforms and cortar_no:
nc = NaverLandCrawler()
for rt in naver_types:
all_items.extend(nc.fetch(cortar_no, rt, trade_type))
# 중복 제거 (링크 기준)
seen: set[str] = set()
deduped: list[dict] = []
for item in all_items:
key = item.get("링크") or f"{item.get('주소')}{item.get('건물명')}{item.get('가격표시')}"
if key and key not in seen:
seen.add(key)
deduped.append(item)
print(f"\n{''*50}")
print(f"📊 수집 완료: {len(all_items)}건 → 중복 제거 후 {len(deduped)}")
if not deduped:
print(" 수집된 매물이 없습니다. 지역·플랫폼 옵션을 변경해보세요.")
return
# 저장
timestamp = datetime.now().strftime("%Y%m%d_%H%M")
safe_addr = re.sub(r'[\\/:*?"<>|]', "_", address)
filename = f"부동산매물_{safe_addr}_{trade_type}_{timestamp}.xlsx"
print(f"💾 저장 중: {filename}")
export_excel(deduped, filename)
print(f"\n{''*50}")
print(" 프로그램 완료!")
print(f" 문의: bgg8988@gmail.com | 010-3907-1392")
print(f" 쟁승메이드: jaengseung-made.vercel.app")
print(f"{''*50}\n")
if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
print("\n\n사용자가 프로그램을 종료했습니다.")