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:
579
MARKETING.md
Normal file
579
MARKETING.md
Normal 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*
|
||||
756
public/downloads/real_estate_crawler_v1.0.py
Normal file
756
public/downloads/real_estate_crawler_v1.0.py
Normal 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사용자가 프로그램을 종료했습니다.")
|
||||
Reference in New Issue
Block a user