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>
This commit is contained in:
2026-03-23 07:25:46 +09:00
parent 273da6b7b3
commit df22691d50
11 changed files with 1626 additions and 66 deletions

View File

@@ -25,9 +25,9 @@ const tools = [
{
id: 'scraper',
title: '웹 스크래핑 도구',
subtitle: 'Web Scraper v0.9 (베타)',
subtitle: 'Web Scraper v1.0',
desc: '공공데이터·쇼핑몰 가격·뉴스를 자동 수집해 엑셀로 저장하는 Python 기반 수집 도구.',
tags: ['Python', 'BeautifulSoup', 'Excel 출력'],
tags: ['Python', 'BeautifulSoup', 'Excel 출력', '무료'],
color: '#2563eb',
bgColor: '#eff6ff',
borderColor: '#bfdbfe',
@@ -37,24 +37,24 @@ const tools = [
</svg>
),
href: '/services/automation/tools/scraper',
ready: false,
ready: true,
},
{
id: 'email',
title: '이메일 자동 발송 도구',
subtitle: 'Email Scheduler (준비중)',
desc: '조건 설정 후 일정 시간에 자동으로 이메일을 발송. 엑셀 수신자 목록 연동 지원.',
tags: ['Python', 'SMTP', '스케줄링'],
id: 'ppt',
title: 'PPT 제작 자동화 도구',
subtitle: 'PPT Automation v1.0',
desc: '엑셀 데이터를 읽어 표지·내용·마무리 슬라이드를 자동 생성하는 Python 기반 PPT 도구.',
tags: ['Python', 'python-pptx', 'openpyxl', '무료'],
color: '#7c3aed',
bgColor: '#f5f3ff',
borderColor: '#ddd6fe',
icon: (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.5} className="w-7 h-7">
<path strokeLinecap="round" strokeLinejoin="round" d="M21.75 6.75v10.5a2.25 2.25 0 01-2.25 2.25h-15a2.25 2.25 0 01-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25m19.5 0v.243a2.25 2.25 0 01-1.07 1.916l-7.5 4.615a2.25 2.25 0 01-2.36 0L3.32 8.91a2.25 2.25 0 01-1.07-1.916V6.75" />
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 3v11.25A2.25 2.25 0 006 16.5h2.25M3.75 3h-1.5m1.5 0h16.5m0 0h1.5m-1.5 0v11.25A2.25 2.25 0 0118 16.5h-2.25m-7.5 0h7.5m-7.5 0l-1 3m8.5-3l1 3m0 0l.5 1.5m-.5-1.5h-9.5m0 0l-.5 1.5M9 11.25v1.5M12 9v3.75m3-6v6" />
</svg>
),
href: '/services/automation/tools/email',
ready: false,
href: '/services/automation/tools/ppt',
ready: true,
},
];

View File

@@ -0,0 +1,365 @@
'use client';
import Link from 'next/link';
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="M3.75 3v11.25A2.25 2.25 0 006 16.5h2.25M3.75 3h-1.5m1.5 0h16.5m0 0h1.5m-1.5 0v11.25A2.25 2.25 0 0118 16.5h-2.25m-7.5 0h7.5m-7.5 0l-1 3m8.5-3l1 3m0 0l.5 1.5m-.5-1.5h-9.5m0 0l-.5 1.5M9 11.25v1.5M12 9v3.75m3-6v6" />
</svg>
),
title: '표지 · 내용 · 마무리 자동 생성',
desc: '표지(제목/날짜), 내용 슬라이드(불릿 포인트), 마무리 슬라이드까지 3가지 레이아웃을 자동으로 구성합니다.',
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="M3.375 19.5h17.25m-17.25 0a1.125 1.125 0 01-1.125-1.125M3.375 19.5h1.5C5.496 19.5 6 18.996 6 18.375m-3.75.125v-1.125c0-.621.504-1.125 1.125-1.125h1.5m0 0v1.25m0-1.25c0-.621.504-1.125 1.125-1.125h1.5m0 0V7.875m0 0c0-.621.504-1.125 1.125-1.125h8.25c.621 0 1.125.504 1.125 1.125v8.25m0 0v1.125m0-1.125c0 .621-.504 1.125-1.125 1.125H6m12-8.25v8.25" />
</svg>
),
title: '엑셀에서 데이터 일괄 생성',
desc: 'data.xlsx 파일의 A열(제목), B~열(불릿 내용)을 읽어 슬라이드를 자동 생성합니다. 수십 장도 한 번에 처리.',
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="M4.098 19.902a3.75 3.75 0 005.304 0l6.401-6.402M6.75 21A3.75 3.75 0 013 17.25V4.125C3 3.504 3.504 3 4.125 3h5.25c.621 0 1.125.504 1.125 1.125v4.072M6.75 21a3.75 3.75 0 003.75-3.75V8.197M6.75 21h13.125c.621 0 1.125-.504 1.125-1.125v-5.25c0-.621-.504-1.125-1.125-1.125h-4.072M10.5 8.197l2.88-2.88c.438-.439 1.15-.439 1.59 0l3.712 3.713c.44.44.44 1.152 0 1.59l-2.879 2.88" />
</svg>
),
title: '색상 테마 커스터마이징',
desc: '상단 설정 영역에서 PRIMARY, SECONDARY, ACCENT 색상을 RGB로 변경하면 전체 슬라이드에 즉시 반영됩니다.',
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="M7.5 8.25h9m-9 3H12m-9.75 1.51c0 1.6 1.123 2.994 2.707 3.227 1.129.166 2.27.293 3.423.379.35.026.67.21.865.501L12 21l2.755-4.133a1.14 1.14 0 01.865-.501 48.172 48.172 0 003.423-.379c1.584-.233 2.707-1.626 2.707-3.228V6.741c0-1.602-1.123-2.995-2.707-3.228A48.394 48.394 0 0012 3c-2.392 0-4.744.175-7.043.513C3.373 3.746 2.25 5.14 2.25 6.741v6.018z" />
</svg>
),
title: '슬라이드 번호 자동 추가',
desc: '각 내용 슬라이드 우측 상단에 슬라이드 번호(01, 02...)가 자동으로 표시됩니다. 따로 설정할 필요 없음.',
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 17.25v1.007a3 3 0 01-.879 2.122L7.5 21h9l-.621-.621A3 3 0 0115 18.257V17.25m6-12V15a2.25 2.25 0 01-2.25 2.25H5.25A2.25 2.25 0 013 15V5.25m18 0A2.25 2.25 0 0018.75 3H5.25A2.25 2.25 0 003 5.25m18 0V12a2.25 2.25 0 01-2.25 2.25H5.25A2.25 2.25 0 013 12V5.25" />
</svg>
),
title: '16:9 비율 · 맑은 고딕 폰트',
desc: '발표 표준 비율인 16:9(13.33×7.5인치)로 설정되며, 한글 가독성이 좋은 맑은 고딕 폰트를 기본 적용합니다.',
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="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z" />
</svg>
),
title: '예시 데이터 자동 실행',
desc: 'data.xlsx 파일이 없어도 내장 예시 데이터로 바로 실행됩니다. 처음 사용할 때 결과를 즉시 확인 가능.',
color: 'text-rose-600',
bg: 'bg-rose-50',
border: 'border-rose-200',
},
];
const howToUse = [
{
step: '01',
title: '패키지 설치',
desc: '터미널에서 필요한 Python 패키지를 설치합니다.',
code: 'pip install python-pptx openpyxl',
color: 'bg-orange-500',
},
{
step: '02',
title: '설정 수정',
desc: '스크립트 상단 설정 영역에서 제목, 날짜, 색상 테마를 수정합니다.',
code: 'TITLE_TEXT = "발표 제목"\nCOLOR_PRIMARY = RGBColor(0x1D, 0x4E, 0xD8)',
color: 'bg-emerald-500',
},
{
step: '03',
title: '엑셀 데이터 준비',
desc: 'data.xlsx를 만들어 A열=슬라이드 제목, B~열=불릿 내용을 입력합니다. (없으면 예시 데이터로 실행)',
code: 'A열: 슬라이드 제목\nB~열: 불릿 포인트 내용',
color: 'bg-violet-500',
},
{
step: '04',
title: '실행 후 확인',
desc: '터미널에서 스크립트를 실행하면 같은 폴더에 PPT 파일이 자동 저장됩니다.',
code: 'python ppt_automation_v1.0.py',
color: 'bg-blue-500',
},
];
const faqs = [
{
q: '엑셀 파일이 없어도 실행되나요?',
a: 'data.xlsx 파일이 없으면 내장 예시 데이터로 자동 실행됩니다. 먼저 결과를 확인한 뒤 자신의 데이터로 교체하면 됩니다.',
},
{
q: '슬라이드 수에 제한이 있나요?',
a: '제한 없습니다. 엑셀에 입력한 행 수만큼 슬라이드가 생성됩니다. 단, 슬라이드당 불릿 포인트는 최대 8개입니다.',
},
{
q: '챕터 구분 슬라이드도 넣을 수 있나요?',
a: 'create_divider_slide() 함수가 포함되어 있습니다. main() 함수에서 원하는 위치에 호출하면 챕터 구분 슬라이드를 추가할 수 있습니다.',
},
{
q: '맥(Mac)에서도 사용할 수 있나요?',
a: '맥에서도 동일하게 사용 가능합니다. 단, 맥에는 맑은 고딕 폰트가 없으므로 FONT_NAME을 "AppleGothic" 또는 "Nanum Gothic"으로 변경하세요.',
},
];
export default function PptToolPage() {
return (
<div className="min-h-full bg-[#f0f5ff]">
{/* ─── Hero ─── */}
<div className="relative overflow-hidden bg-gradient-to-br from-[#1a0a3d] via-[#2d1560] to-[#1a0a3d] px-6 py-14 lg:px-12">
<div className="absolute inset-0 opacity-[0.05]">
<svg width="100%" height="100%" viewBox="0 0 800 300" preserveAspectRatio="xMidYMid slice">
<rect x="50" y="40" width="200" height="130" rx="6" fill="none" stroke="#c084fc" strokeWidth="1.5"/>
<line x1="70" y1="70" x2="230" y2="70" stroke="#c084fc" strokeWidth="1"/>
<line x1="70" y1="90" x2="200" y2="90" stroke="#c084fc" strokeWidth="1"/>
<line x1="70" y1="110" x2="210" y2="110" stroke="#c084fc" strokeWidth="1"/>
<rect x="320" y="60" width="200" height="130" rx="6" fill="none" stroke="#c084fc" strokeWidth="1.5"/>
<rect x="340" y="75" width="160" height="20" rx="3" fill="#c084fc" fillOpacity="0.2"/>
<line x1="340" y1="110" x2="500" y2="110" stroke="#c084fc" strokeWidth="1"/>
<line x1="340" y1="130" x2="480" y2="130" stroke="#c084fc" strokeWidth="1"/>
<rect x="590" y="40" width="160" height="130" rx="6" fill="none" stroke="#c084fc" strokeWidth="1.5"/>
<line x1="610" y1="100" x2="730" y2="100" stroke="#c084fc" strokeWidth="1.5"/>
<line x1="660" y1="110" x2="660" y2="160" stroke="#c084fc" strokeWidth="1"/>
<path d="M610 160 L660 110 L710 145 L730 120" fill="none" stroke="#c084fc" strokeWidth="1.5"/>
</svg>
</div>
<div className="relative max-w-3xl mx-auto text-center">
<Link href="/services/automation" className="inline-flex items-center gap-1.5 text-purple-300/60 hover:text-purple-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="w-16 h-16 mx-auto rounded-2xl bg-purple-400/15 border border-purple-400/25 flex items-center justify-center mb-5">
<svg className="w-9 h-9 text-purple-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 3v11.25A2.25 2.25 0 006 16.5h2.25M3.75 3h-1.5m1.5 0h16.5m0 0h1.5m-1.5 0v11.25A2.25 2.25 0 0118 16.5h-2.25m-7.5 0h7.5m-7.5 0l-1 3m8.5-3l1 3m0 0l.5 1.5m-.5-1.5h-9.5m0 0l-.5 1.5M9 11.25v1.5M12 9v3.75m3-6v6" />
</svg>
</div>
<p className="text-purple-400/70 text-xs font-bold uppercase tracking-widest mb-2">PPT AUTOMATION · </p>
<h1 className="text-4xl md:text-5xl font-extrabold text-white mb-4 tracking-tight leading-tight">
PPT <br />
<span className="text-transparent bg-clip-text bg-gradient-to-r from-purple-400 to-pink-400"> </span>
</h1>
<p className="text-purple-100/50 text-base md:text-lg leading-relaxed max-w-xl mx-auto mb-6">
·· .<br />
python-pptx .
</p>
<div className="flex flex-col sm:flex-row items-center justify-center gap-3">
<a
href="/downloads/ppt_automation_v1.0.py"
download
className="inline-flex items-center gap-2 bg-purple-400 hover:bg-purple-300 text-[#1a0a3d] px-8 py-3.5 rounded-xl font-extrabold text-sm transition-all shadow-lg shadow-purple-900/30"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" /></svg>
(Python .py)
</a>
<Link href="/freelance?service=PPT+자동화+맞춤+개발"
className="inline-flex items-center gap-2 bg-white/10 hover:bg-white/20 text-white border border-white/20 px-6 py-3.5 rounded-xl font-bold text-sm transition-all">
</Link>
</div>
</div>
</div>
{/* ─── 다운로드 카드 ─── */}
<div className="px-6 py-10 lg:px-12">
<div className="max-w-5xl mx-auto">
<div className="bg-white rounded-2xl border-2 border-purple-200 p-6 flex flex-col md:flex-row items-start md:items-center gap-6">
<div className="w-14 h-14 rounded-2xl bg-purple-50 border border-purple-200 flex items-center justify-center flex-shrink-0">
<svg className="w-7 h-7 text-purple-600" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 3v11.25A2.25 2.25 0 006 16.5h2.25M3.75 3h-1.5m1.5 0h16.5m0 0h1.5m-1.5 0v11.25A2.25 2.25 0 0118 16.5h-2.25m-7.5 0h7.5m-7.5 0l-1 3m8.5-3l1 3m0 0l.5 1.5m-.5-1.5h-9.5m0 0l-.5 1.5M9 11.25v1.5M12 9v3.75m3-6v6" />
</svg>
</div>
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<span className="text-purple-600 text-xs font-bold">PPT AUTOMATION v1.0</span>
<span className="bg-purple-100 text-purple-700 text-[10px] font-bold px-2 py-0.5 rounded-full border border-purple-200"></span>
</div>
<div className="font-extrabold text-[#04102b] text-lg mb-1">PPT </div>
<p className="text-slate-500 text-sm">python-pptx · · // · </p>
<div className="flex flex-wrap gap-2 mt-3">
{['Python 3.8+', 'python-pptx', 'openpyxl', '한글 지원', '엑셀 연동'].map((tag) => (
<span key={tag} className="text-[10px] font-bold px-2 py-0.5 rounded-full border border-purple-200 text-purple-600 bg-purple-50">{tag}</span>
))}
</div>
</div>
<a
href="/downloads/ppt_automation_v1.0.py"
download
className="flex-shrink-0 inline-flex items-center gap-2 bg-purple-600 hover:bg-purple-700 text-white px-6 py-3 rounded-xl font-bold text-sm transition"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" /></svg>
</a>
</div>
</div>
</div>
{/* ─── 기능 ─── */}
<div className="px-6 pb-12 lg:px-12">
<div className="max-w-5xl mx-auto">
<div className="text-center mb-8">
<p className="text-purple-600 text-xs font-bold uppercase tracking-widest mb-2">FEATURES</p>
<h2 className="text-2xl md:text-3xl font-extrabold text-[#04102b]"> </h2>
</div>
<div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-4">
{features.map((f) => (
<div key={f.title} className={`bg-white rounded-2xl border-2 ${f.border} p-5`}>
<div className={`w-9 h-9 rounded-xl ${f.bg} flex items-center justify-center mb-3 ${f.color}`}>
{f.icon}
</div>
<h3 className="font-bold text-[#04102b] text-sm mb-2">{f.title}</h3>
<p className="text-slate-500 text-xs leading-relaxed">{f.desc}</p>
</div>
))}
</div>
</div>
</div>
{/* ─── 사용법 ─── */}
<div className="px-6 pb-12 lg:px-12">
<div className="max-w-5xl mx-auto">
<div className="text-center mb-8">
<p className="text-purple-600 text-xs font-bold uppercase tracking-widest mb-2">HOW TO USE</p>
<h2 className="text-2xl md:text-3xl font-extrabold text-[#04102b]"> </h2>
</div>
<div className="grid sm:grid-cols-2 gap-4">
{howToUse.map((h) => (
<div key={h.step} className="bg-white rounded-2xl border border-slate-200 p-6">
<div className="flex items-center gap-3 mb-3">
<div className={`w-8 h-8 rounded-lg ${h.color} flex items-center justify-center text-white text-xs font-extrabold flex-shrink-0`}>
{h.step}
</div>
<h3 className="font-bold text-[#04102b] text-sm">{h.title}</h3>
</div>
<p className="text-slate-500 text-xs leading-relaxed mb-3">{h.desc}</p>
<div className="bg-[#1a0a3d] rounded-xl px-4 py-3 font-mono text-[11px] text-purple-200 whitespace-pre leading-relaxed">
{h.code}
</div>
</div>
))}
</div>
</div>
</div>
{/* ─── 코드 미리보기 ─── */}
<div className="px-6 pb-12 lg:px-12">
<div className="max-w-5xl mx-auto">
<div className="text-center mb-6">
<p className="text-purple-600 text-xs font-bold uppercase tracking-widest mb-2">PREVIEW</p>
<h2 className="text-2xl font-extrabold text-[#04102b]"> </h2>
<p className="text-slate-500 text-sm mt-1"> PPT가 </p>
</div>
<div className="bg-[#1a0a3d] rounded-2xl overflow-hidden border border-purple-900/40">
<div className="flex items-center gap-2 px-5 py-3 border-b border-purple-900/40">
<div className="w-3 h-3 rounded-full bg-rose-400/70" />
<div className="w-3 h-3 rounded-full bg-amber-400/70" />
<div className="w-3 h-3 rounded-full bg-emerald-400/70" />
<span className="ml-2 text-purple-300/50 text-xs font-mono">ppt_automation_v1.0.py</span>
</div>
<pre className="px-5 py-5 text-xs font-mono text-purple-100 leading-6 overflow-x-auto">{`<span class="text-purple-400"># ── 설정 (이 부분을 수정하세요) ──────────────</span>
DATA_FILE = <span class="text-amber-300">"data.xlsx"</span> <span class="text-slate-400"># 입력 엑셀 파일</span>
OUTPUT_FILE = f<span class="text-amber-300">"발표자료_{datetime}.pptx"</span>
<span class="text-slate-400"># 표지 정보</span>
TITLE_TEXT = <span class="text-amber-300">"발표 제목을 입력하세요"</span>
SUBTITLE_TEXT = <span class="text-amber-300">"부제목 또는 발표자 이름"</span>
DATE_TEXT = <span class="text-amber-300">"2025년 01월 01일"</span>
<span class="text-slate-400"># 색상 테마 (RGB 값으로 변경)</span>
COLOR_PRIMARY = RGBColor(<span class="text-emerald-400">0x1D, 0x4E, 0xD8</span>) <span class="text-slate-400"># 파란색</span>
COLOR_SECONDARY = RGBColor(<span class="text-emerald-400">0x0F, 0x17, 0x2A</span>) <span class="text-slate-400"># 다크 네이비</span>
COLOR_ACCENT = RGBColor(<span class="text-emerald-400">0x60, 0xA5, 0xFA</span>) <span class="text-slate-400"># 라이트 블루</span>
FONT_NAME = <span class="text-amber-300">"맑은 고딕"</span> <span class="text-slate-400"># 한글 폰트</span>`}
</pre>
</div>
<p className="text-center text-slate-400 text-xs mt-3">
* . .
</p>
</div>
</div>
{/* ─── FAQ ─── */}
<div className="px-6 pb-12 lg:px-12">
<div className="max-w-3xl mx-auto">
<div className="text-center mb-8">
<p className="text-purple-600 text-xs font-bold uppercase tracking-widest mb-2">FAQ</p>
<h2 className="text-2xl md:text-3xl font-extrabold text-[#04102b]"> </h2>
</div>
<div className="space-y-3">
{faqs.map((faq) => (
<div key={faq.q} className="bg-white rounded-2xl border border-slate-200 p-5">
<div className="flex items-start gap-3">
<span className="text-purple-600 font-extrabold text-sm flex-shrink-0 mt-0.5">Q.</span>
<div>
<div className="font-bold text-[#04102b] text-sm mb-1.5">{faq.q}</div>
<div className="text-slate-500 text-xs leading-relaxed">{faq.a}</div>
</div>
</div>
</div>
))}
</div>
</div>
</div>
{/* ─── CTA ─── */}
<div className="px-6 pb-12 lg:px-12">
<div className="max-w-3xl mx-auto">
<div className="bg-gradient-to-r from-[#1a0a3d] to-[#2d1560] rounded-2xl border border-purple-400/20 p-8 text-center">
<p className="text-purple-400 text-xs font-bold uppercase tracking-widest mb-2">CUSTOM DEVELOPMENT</p>
<h3 className="text-white text-2xl font-extrabold mb-2"> PPT ?</h3>
<p className="text-purple-100/40 text-sm mb-6">
, , 릿 <br />
PPT .
</p>
<div className="flex flex-col sm:flex-row items-center justify-center gap-3">
<a
href="/downloads/ppt_automation_v1.0.py"
download
className="inline-flex items-center gap-2 bg-purple-400 hover:bg-purple-300 text-[#1a0a3d] px-8 py-3 rounded-xl font-extrabold text-sm transition-all shadow-lg shadow-purple-900/30"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" /></svg>
</a>
<Link href="/freelance?service=PPT+자동화+맞춤+개발"
className="inline-flex items-center gap-2 bg-white/10 hover:bg-white/20 text-white border border-white/20 px-6 py-3 rounded-xl font-bold text-sm transition-all">
</Link>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,284 @@
'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>
);
}