Files
jaengseung-made/app/services/automation/tools/scraper/page.tsx
gahusb df22691d50 feat: 보안 강화 + 자동화 도구 3종 추가 (웹 크롤러·PPT·엑셀)
- lib/security.ts: escapeHtml, isValidEmail, sanitizeStr, checkRateLimit 유틸 추가
- next.config.ts: 보안 헤더 적용 (X-Frame-Options, HSTS, Permissions-Policy 등)
- api/contact: XSS 방어, Rate Limit(5/min), 입력 길이 제한
- api/payment/confirm: 사용자 인증·소유권 검증, 타입 체크, 에러 메시지 정제
- api/admin/quotes: PUT 허용 필드 화이트리스트 적용
- api/saju/analyze: 로그인·결제 검증, 입력 크기 제한, gender 값 검증
- public/downloads/web_scraper_v1.0.py: requests+BS4+openpyxl 웹 크롤러
- public/downloads/ppt_automation_v1.0.py: python-pptx+openpyxl PPT 자동화
- app/services/automation/tools/scraper: 크롤러 상세 페이지 추가
- app/services/automation/tools/ppt: PPT 도구 상세 페이지 추가
- app/services/automation/page.tsx: scraper ready=true, email→PPT 교체

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 07:25:46 +09:00

285 lines
17 KiB
TypeScript

'use client';
import Link from 'next/link';
import { useState } from 'react';
const features = [
{
icon: (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.5} className="w-5 h-5">
<path strokeLinecap="round" strokeLinejoin="round" d="M12 21a9.004 9.004 0 008.716-6.747M12 21a9.004 9.004 0 01-8.716-6.747M12 21c2.485 0 4.5-4.03 4.5-9S14.485 3 12 3m0 18c-2.485 0-4.5-4.03-4.5-9S9.515 3 12 3m0 0a8.997 8.997 0 017.843 4.582M12 3a8.997 8.997 0 00-7.843 4.582m15.686 0A11.953 11.953 0 0112 10.5c-2.998 0-5.74-1.1-7.843-2.918m15.686 0A8.959 8.959 0 0121 12c0 .778-.099 1.533-.284 2.253m0 0A17.919 17.919 0 0112 16.5c-3.162 0-6.133-.815-8.716-2.247m0 0A9.015 9.015 0 013 12c0-1.605.42-3.113 1.157-4.418" />
</svg>
),
title: '웹 페이지 데이터 자동 수집',
desc: '공공데이터, 쇼핑몰 가격, 뉴스 기사 등 원하는 페이지의 데이터를 자동으로 수집합니다.',
color: 'text-blue-600', bg: 'bg-blue-50', border: 'border-blue-200',
},
{
icon: (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.5} className="w-5 h-5">
<path strokeLinecap="round" strokeLinejoin="round" d="M9 12h3.75M9 15h3.75M9 18h3.75m3 .75H18a2.25 2.25 0 002.25-2.25V6.108c0-1.135-.845-2.098-1.976-2.192a48.424 48.424 0 00-1.123-.08m-5.801 0c-.065.21-.1.433-.1.664 0 .414.336.75.75.75h4.5a.75.75 0 00.75-.75 2.25 2.25 0 00-.1-.664m-5.8 0A2.251 2.251 0 0113.5 2.25H15c1.012 0 1.867.668 2.15 1.586m-5.8 0c-.376.023-.75.05-1.124.08C9.095 4.01 8.25 4.973 8.25 6.108V8.25m0 0H4.875c-.621 0-1.125.504-1.125 1.125v11.25c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V9.375c0-.621-.504-1.125-1.125-1.125H8.25zM6.75 12h.008v.008H6.75V12zm0 3h.008v.008H6.75V15zm0 3h.008v.008H6.75V18z" />
</svg>
),
title: '엑셀 자동 저장',
desc: '수집한 데이터를 열 서식, 헤더 스타일이 적용된 엑셀 파일로 자동 저장합니다.',
color: 'text-emerald-600', bg: 'bg-emerald-50', border: 'border-emerald-200',
},
{
icon: (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.5} className="w-5 h-5">
<path strokeLinecap="round" strokeLinejoin="round" d="M19 7.5v3m0 0v3m0-3h3m-3 0h-3m-2.25-4.125a3.375 3.375 0 11-6.75 0 3.375 3.375 0 016.75 0zM4 19.235v-.11a6.375 6.375 0 0112.75 0v.109A12.318 12.318 0 0110.374 21c-2.331 0-4.512-.645-6.374-1.766z" />
</svg>
),
title: '페이지네이션 자동 탐색',
desc: '다음 페이지 링크를 자동으로 찾아 여러 페이지의 데이터를 연속으로 수집합니다.',
color: 'text-violet-600', bg: 'bg-violet-50', border: 'border-violet-200',
},
{
icon: (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.5} className="w-5 h-5">
<path strokeLinecap="round" strokeLinejoin="round" d="M9 12.75L11.25 15 15 9.75m-3-7.036A11.959 11.959 0 013.598 6 11.99 11.99 0 003 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285z" />
</svg>
),
title: '재시도 로직 내장',
desc: '네트워크 오류나 일시적 접속 실패 시 자동으로 재시도합니다. 수집 실패 최소화.',
color: 'text-orange-600', bg: 'bg-orange-50', border: 'border-orange-200',
},
{
icon: (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.5} className="w-5 h-5">
<path strokeLinecap="round" strokeLinejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
),
title: '요청 간격 자동 조절',
desc: '서버에 부하를 주지 않도록 요청 간격을 자동으로 조절합니다. 차단 위험 최소화.',
color: 'text-cyan-600', bg: 'bg-cyan-50', border: 'border-cyan-200',
},
{
icon: (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.5} className="w-5 h-5">
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" />
</svg>
),
title: '로그 파일 자동 저장',
desc: '수집 과정 전체를 로그로 남겨 나중에 어떤 URL에서 몇 건을 수집했는지 확인 가능합니다.',
color: 'text-rose-600', bg: 'bg-rose-50', border: 'border-rose-200',
},
];
const howToUse = [
{ step: '01', title: 'Python 설치', desc: 'python.org에서 Python 3.10 이상을 설치하세요. "Add to PATH" 체크 필수.' },
{ step: '02', title: '패키지 설치', desc: '터미널에서 pip install requests beautifulsoup4 openpyxl lxml 실행.' },
{ step: '03', title: 'URL 설정', desc: '파일 상단 TARGET_URL에 크롤링할 주소를 입력하세요.' },
{ step: '04', title: '실행', desc: 'python web_scraper_v1.0.py 실행 → 같은 폴더에 엑셀 파일이 생성됩니다.' },
];
const faqs = [
{
q: '크롤링이 법적으로 문제없나요?',
a: '공개된 정보 수집 자체는 일반적으로 허용되지만, 사이트의 robots.txt와 이용약관을 반드시 확인하세요. 로그인이 필요한 페이지, 개인정보, 저작권 데이터 수집은 법적 문제가 생길 수 있습니다.',
},
{
q: '자바스크립트로 렌더링되는 사이트도 되나요?',
a: 'requests + BeautifulSoup은 정적 HTML만 수집합니다. JS 렌더링 사이트(React, Vue 등)는 Selenium/Playwright가 필요하며, 맞춤 개발 서비스로 문의 주시면 구현해 드립니다.',
},
{
q: '원하는 항목만 골라서 수집할 수 있나요?',
a: '파일 내 extract_data 함수를 수정하면 됩니다. HTML 선택자(CSS Selector)로 원하는 요소만 지정할 수 있으며, 코드 내 주석에 예시가 포함되어 있습니다.',
},
];
export default function ScraperToolPage() {
const [openFaq, setOpenFaq] = useState<number | null>(null);
return (
<div className="min-h-full bg-[#f0f5ff]">
{/* Hero */}
<div className="bg-gradient-to-br from-[#1e3a8a] via-[#1d4ed8] to-[#1e3a8a] px-6 py-12 lg:px-12">
<div className="max-w-4xl mx-auto">
<Link href="/services/automation"
className="inline-flex items-center gap-1.5 text-blue-300/60 hover:text-blue-300 text-sm mb-6 transition">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
</Link>
<div className="flex flex-col sm:flex-row sm:items-center gap-6">
<div className="w-20 h-20 rounded-2xl bg-blue-400/15 border border-blue-400/30 flex items-center justify-center flex-shrink-0">
<svg className="w-10 h-10 text-blue-300" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M12 21a9.004 9.004 0 008.716-6.747M12 21a9.004 9.004 0 01-8.716-6.747M12 21c2.485 0 4.5-4.03 4.5-9S14.485 3 12 3m0 18c-2.485 0-4.5-4.03-4.5-9S9.515 3 12 3m0 0a8.997 8.997 0 017.843 4.582M12 3a8.997 8.997 0 00-7.843 4.582m15.686 0A11.953 11.953 0 0112 10.5c-2.998 0-5.74-1.1-7.843-2.918m15.686 0A8.959 8.959 0 0121 12c0 .778-.099 1.533-.284 2.253m0 0A17.919 17.919 0 0112 16.5c-3.162 0-6.133-.815-8.716-2.247m0 0A9.015 9.015 0 013 12c0-1.605.42-3.113 1.157-4.418" />
</svg>
</div>
<div>
<div className="flex items-center gap-2 mb-2">
<span className="text-blue-400 text-xs font-bold uppercase tracking-widest">FREE TOOL</span>
<span className="bg-blue-400/20 border border-blue-400/40 text-blue-300 text-[10px] font-bold px-2 py-0.5 rounded-full">v1.0</span>
<span className="bg-white/10 text-white/50 text-[10px] font-bold px-2 py-0.5 rounded-full">Python · BeautifulSoup</span>
</div>
<h1 className="text-3xl md:text-4xl font-extrabold text-white mb-2 leading-tight">
<br />
<span className="text-transparent bg-clip-text bg-gradient-to-r from-blue-300 to-cyan-300">
Web Scraper
</span>
</h1>
<p className="text-blue-100/50 text-sm leading-relaxed">
, , <br />
. Python .
</p>
</div>
</div>
<div className="mt-8 inline-grid grid-cols-3 gap-px bg-blue-400/10 border border-blue-400/20 rounded-2xl overflow-hidden">
{[
{ v: '6가지', l: '핵심 기능' },
{ v: '무료', l: '완전 무료' },
{ v: 'Python 3.10+', l: '지원 버전' },
].map((s) => (
<div key={s.l} className="bg-[#1e3a8a]/60 px-5 py-3 text-center">
<div className="text-white font-extrabold text-base">{s.v}</div>
<div className="text-blue-400/50 text-xs mt-0.5">{s.l}</div>
</div>
))}
</div>
</div>
</div>
<div className="px-6 py-10 lg:px-12">
<div className="max-w-4xl mx-auto space-y-10">
{/* 다운로드 카드 */}
<div className="bg-white rounded-2xl border-2 border-blue-200 p-6 flex flex-col sm:flex-row items-center gap-6">
<div className="flex-1">
<div className="text-blue-700 text-xs font-bold uppercase tracking-widest mb-1">DOWNLOAD</div>
<div className="font-extrabold text-[#04102b] text-lg mb-1">web_scraper_v1.0.py</div>
<div className="text-slate-500 text-xs mb-3">크기: 8KB · Python · </div>
<div className="flex flex-wrap gap-2">
{['Python 3.10+', '페이지네이션', '재시도 로직', '엑셀 자동 저장', '로그 저장'].map((t) => (
<span key={t} className="text-[10px] font-bold px-2 py-0.5 rounded-full border border-blue-200 text-blue-700 bg-blue-50">{t}</span>
))}
</div>
</div>
<div className="flex flex-col gap-2 w-full sm:w-auto">
<a
href="/downloads/web_scraper_v1.0.py"
download
className="flex items-center justify-center gap-2 bg-blue-600 hover:bg-blue-500 text-white px-6 py-3 rounded-xl font-extrabold text-sm transition-all shadow-lg shadow-blue-900/20 w-full sm:w-48"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" />
</svg>
</a>
<p className="text-[10px] text-slate-400 text-center"> </p>
</div>
</div>
{/* 기능 목록 */}
<div>
<h2 className="text-xl font-extrabold text-[#04102b] mb-5"> </h2>
<div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-4">
{features.map((f) => (
<div key={f.title} className={`rounded-xl border p-4 ${f.bg} ${f.border}`}>
<div className={`${f.color} mb-3`}>{f.icon}</div>
<div className={`text-xs font-bold mb-1 ${f.color}`}>{f.title}</div>
<p className="text-slate-600 text-xs leading-relaxed">{f.desc}</p>
</div>
))}
</div>
</div>
{/* 사용 방법 */}
<div>
<h2 className="text-xl font-extrabold text-[#04102b] mb-5"> </h2>
<div className="grid sm:grid-cols-2 gap-4">
{howToUse.map((h) => (
<div key={h.step} className="bg-white rounded-xl border border-[#dbe8ff] p-5 flex gap-4">
<div className="text-blue-600 text-2xl font-black leading-none flex-shrink-0">{h.step}</div>
<div>
<div className="font-bold text-[#04102b] text-sm mb-1">{h.title}</div>
<p className="text-slate-500 text-xs leading-relaxed">{h.desc}</p>
</div>
</div>
))}
</div>
</div>
{/* 코드 예시 */}
<div className="bg-[#0f172a] rounded-2xl p-6 overflow-x-auto">
<div className="flex items-center gap-2 mb-4">
<span className="text-xs font-bold text-blue-400 uppercase tracking-widest">CODE PREVIEW</span>
<span className="text-slate-600 text-xs">extract_data </span>
</div>
<pre className="text-sm text-slate-300 leading-relaxed font-mono whitespace-pre">{`def extract_data(soup, page_url):
items = []
# 상품 목록 수집 예시
for item in soup.select(".product-item"):
name = item.select_one(".name")
price = item.select_one(".price")
items.append({
"상품명": name.get_text(strip=True),
"가격": price.get_text(strip=True),
"URL": page_url,
})
return items`}</pre>
</div>
{/* FAQ */}
<div>
<h2 className="text-xl font-extrabold text-[#04102b] mb-5"> </h2>
<div className="space-y-3">
{faqs.map((faq, i) => (
<div key={i} className="bg-white rounded-xl border border-[#dbe8ff] overflow-hidden">
<button
onClick={() => setOpenFaq(openFaq === i ? null : i)}
className="w-full flex items-center justify-between px-5 py-4 text-left"
>
<span className="font-bold text-[#04102b] text-sm">{faq.q}</span>
<svg className={`w-4 h-4 text-slate-400 transition-transform ${openFaq === i ? 'rotate-180' : ''}`}
fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
{openFaq === i && (
<div className="px-5 pb-4 text-slate-500 text-sm leading-relaxed border-t border-[#dbe8ff] pt-3">
{faq.a}
</div>
)}
</div>
))}
</div>
</div>
{/* CTA */}
<div className="bg-gradient-to-r from-[#1e3a8a] to-[#1d4ed8] rounded-2xl p-8 text-center">
<p className="text-blue-300 text-xs font-bold uppercase tracking-widest mb-2">CUSTOM DEVELOPMENT</p>
<h3 className="text-white text-xl font-extrabold mb-2"> ?</h3>
<p className="text-blue-100/50 text-sm mb-6 leading-relaxed">
JS , , , <br />
.
</p>
<div className="flex flex-col sm:flex-row gap-3 justify-center">
<a
href="/downloads/web_scraper_v1.0.py"
download
className="inline-flex items-center justify-center gap-2 bg-blue-400 hover:bg-blue-300 text-[#1e3a8a] px-6 py-3 rounded-xl font-extrabold text-sm transition-all"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" />
</svg>
</a>
<Link href="/freelance?service=업무+자동화"
className="inline-flex items-center justify-center gap-2 bg-white/10 hover:bg-white/20 text-white border border-white/20 px-6 py-3 rounded-xl font-extrabold text-sm transition-all">
</Link>
</div>
</div>
</div>
</div>
</div>
);
}