From 05d80a792682ebe2b2504cea6ff2e08d121169fc Mon Sep 17 00:00:00 2001 From: gahusb Date: Mon, 23 Mar 2026 11:42:03 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=9C=84=EC=8B=9C=EC=BC=93=C2=B7?= =?UTF-8?q?=EC=B9=B4=EC=B9=B4=EC=98=A4=20=EB=A7=88=EC=BC=80=ED=8C=85=20?= =?UTF-8?q?=EA=B0=80=EC=9D=B4=EB=93=9C=20=EC=B6=94=EA=B0=80=20+=20?= =?UTF-8?q?=EB=B6=80=EB=8F=99=EC=82=B0=20=ED=81=AC=EB=A1=A4=EB=A7=81=20?= =?UTF-8?q?=ED=94=84=EB=A1=9C=EA=B7=B8=EB=9E=A8=20v1.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - MARKETING.md: 섹션 5 위시켓 프로필 (한 줄소개·자기소개·체크리스트·플랫폼 비교) - MARKETING.md: 섹션 6 카카오 오픈채팅방 운영 가이드 (공지·입장메시지·파일탭·운영루틴) - public/downloads/real_estate_crawler_v1.0.py: 부동산 매물 크롤링 프로그램 지원: 직방·다방·피터팬·네이버부동산 출력: 플랫폼별 시트 + 중복제거 + 스타일 Excel Co-Authored-By: Claude Sonnet 4.6 --- MARKETING.md | 579 ++++++++++++++ public/downloads/real_estate_crawler_v1.0.py | 756 +++++++++++++++++++ 2 files changed, 1335 insertions(+) create mode 100644 MARKETING.md create mode 100644 public/downloads/real_estate_crawler_v1.0.py diff --git a/MARKETING.md b/MARKETING.md new file mode 100644 index 0000000..d64b5c5 --- /dev/null +++ b/MARKETING.md @@ -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* diff --git a/public/downloads/real_estate_crawler_v1.0.py b/public/downloads/real_estate_crawler_v1.0.py new file mode 100644 index 0000000..ffe236b --- /dev/null +++ b/public/downloads/real_estate_crawler_v1.0.py @@ -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사용자가 프로그램을 종료했습니다.")