From 8da844bb408e559e96ecedae708878cadbc068fd Mon Sep 17 00:00:00 2001 From: gahusb Date: Wed, 15 Apr 2026 01:13:00 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20=EB=B0=B0=ED=8F=AC=20=EC=A0=84=20?= =?UTF-8?q?=EB=B3=B4=EA=B0=95=20=E2=80=94=20HMAC=20=ED=83=80=EC=9D=B4?= =?UTF-8?q?=EB=B0=8D=20=EC=95=88=EC=A0=84=20=EB=B9=84=EA=B5=90=20+=20?= =?UTF-8?q?=EA=B3=84=EC=A2=8C=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8=20+?= =?UTF-8?q?=20=EA=B3=A0=EC=95=84=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - lib/admin-auth: createHmac 비교를 timingSafeEqual로 교체 (타이밍 공격 방어) - PurchaseAgreementModal: 입금 계좌 케이뱅크 100-116-337157 박재오 - /legal/refund: 구독 서비스 설명에서 삭제된 로또/주식 언급 제거 - app/landing/: 삭제된 서비스 참조만 남은 고아 디렉토리 제거 Co-Authored-By: Claude Opus 4.6 --- app/components/PurchaseAgreementModal.tsx | 4 +- app/landing/layout.tsx | 4 - app/landing/page.tsx | 679 ---------------------- app/legal/refund/page.tsx | 2 +- lib/admin-auth.ts | 13 +- 5 files changed, 13 insertions(+), 689 deletions(-) delete mode 100644 app/landing/layout.tsx delete mode 100644 app/landing/page.tsx diff --git a/app/components/PurchaseAgreementModal.tsx b/app/components/PurchaseAgreementModal.tsx index c105892..77ccfc9 100644 --- a/app/components/PurchaseAgreementModal.tsx +++ b/app/components/PurchaseAgreementModal.tsx @@ -16,8 +16,8 @@ interface Props { } const DEFAULT_BANK = { - bank: '토스뱅크', - account: '1000-0000-0000', + bank: '케이뱅크', + account: '100-116-337157', holder: '박재오', }; diff --git a/app/landing/layout.tsx b/app/landing/layout.tsx deleted file mode 100644 index 5faf4a9..0000000 --- a/app/landing/layout.tsx +++ /dev/null @@ -1,4 +0,0 @@ -// DashboardShell을 우회하는 독립 레이아웃 -export default function LandingLayout({ children }: { children: React.ReactNode }) { - return <>{children}; -} diff --git a/app/landing/page.tsx b/app/landing/page.tsx deleted file mode 100644 index c12e83a..0000000 --- a/app/landing/page.tsx +++ /dev/null @@ -1,679 +0,0 @@ -'use client'; - -import { useState, useEffect, useRef } from 'react'; -import Link from 'next/link'; - -/* ─────────────────────────── DATA ─────────────────────────── */ -const services = [ - { - id: 'automation', - icon: 'automation', - title: '업무 자동화', - tag: '외주', - tagColor: '#10b981', - price: '5만원~', - desc: 'RPA·엑셀·이메일 자동화. 반복 업무를 코드로 대체해 드립니다.', - url: null, - href: '/services/automation', - color: '#10b981', - }, - { - id: 'prompt', - icon: 'prompt', - title: '프롬프트 엔지니어링', - tag: '컨설팅', - tagColor: '#f59e0b', - price: '5만원~', - desc: '업무 특화 AI 프롬프트 설계. ChatGPT·Claude를 실무에 맞게 최적화.', - url: null, - href: '/services/prompt', - color: '#f59e0b', - }, - { - id: 'freelance', - icon: 'freelance', - title: '외주 개발', - tag: '맞춤형', - tagColor: '#2563eb', - price: '견적 무료', - desc: '요구사항 분석부터 납품까지. 계약서 작성 + 소스코드 전체 인도.', - url: null, - href: '/freelance', - color: '#2563eb', - }, -]; - -const guarantees = [ - { - icon: 'contract', - title: '계약서 필수', - desc: '구두 약속 없이 모든 작업은 서면 계약서로 시작합니다. 범위·가격·납기 모두 명시.', - }, - { - icon: 'refund', - title: '전액 환불 보장', - desc: '납품 후 7일 내 심각한 하자 발생 시 전액 환불. 조건 계약서에 명기합니다.', - }, - { - icon: 'source', - title: '소스코드 전체 인도', - desc: '완성된 코드 전체를 GitHub에 이관. 이후 직접 수정·확장 가능합니다.', - }, - { - icon: 'penalty', - title: '납기 지연 패널티', - desc: '납기 초과 1일당 대금의 1% 자동 차감. 계약서에 명시된 조건입니다.', - }, -]; - -const faqs = [ - { q: '개발 경험이 없어도 의뢰할 수 있나요?', a: '네. 요구사항을 말씀해 주시면 제가 기술 스펙으로 번역하고, 검토 후 계약서를 작성합니다. 기술 용어를 몰라도 됩니다.' }, - { q: '중간에 요구사항이 바뀌면 어떻게 되나요?', a: '계약서 범위 내 소폭 변경은 무상으로 반영합니다. 범위 초과 시 추가 견적서를 별도 작성합니다.' }, - { q: '납품 후 유지보수는 어떻게 되나요?', a: '1개월 무상 A/S가 기본 포함됩니다. 이후 월 유지보수 계약도 가능합니다.' }, - { q: '구독형 서비스는 언제든 해지 가능한가요?', a: '네. 다음 결제일 전 언제든 해지 가능하며 잔여 기간 환불은 이용약관을 따릅니다.' }, -]; - -/* ─────────────────────────── ICONS ─────────────────────────── */ -function ServiceIcon({ type, color }: { type: string; color: string }) { - const c = color; - if (type === 'lotto') return ( - - - {[7, 14, 21].map((x, i) => )} - - - - ); - if (type === 'stock') return ( - - - - - - ); - if (type === 'automation') return ( - - - - - - - ); - if (type === 'prompt') return ( - - - - - - - ); - return ( - - - - - - ); -} - -function GuaranteeIcon({ type }: { type: string }) { - if (type === 'contract') return ( - - - - - - ); - if (type === 'refund') return ( - - - - - - ); - if (type === 'source') return ( - - - - ); - return ( - - - - - - ); -} - -/* ─────────────────────────── COUNTER ─────────────────────────── */ -function useCountUp(target: number, duration = 2000) { - const [count, setCount] = useState(0); - const ref = useRef(null); - const started = useRef(false); - - useEffect(() => { - const observer = new IntersectionObserver(([entry]) => { - if (entry.isIntersecting && !started.current) { - started.current = true; - const start = performance.now(); - const step = (now: number) => { - const pct = Math.min((now - start) / duration, 1); - const ease = 1 - Math.pow(1 - pct, 3); - setCount(Math.floor(ease * target)); - if (pct < 1) requestAnimationFrame(step); - }; - requestAnimationFrame(step); - } - }, { threshold: 0.5 }); - if (ref.current) observer.observe(ref.current); - return () => observer.disconnect(); - }, [target, duration]); - - return { count, ref }; -} - -function CountUp({ target, suffix = '', prefix = '' }: { target: number; suffix?: string; prefix?: string }) { - const { count, ref } = useCountUp(target); - return
{prefix}{count}{suffix}
; -} - -/* ─────────────────────────── PAGE ─────────────────────────── */ -export default function LandingPage() { - const [openFaq, setOpenFaq] = useState(null); - const [hovered, setHovered] = useState(null); - - return ( -
- - - {/* ── NAV ── */} - - - {/* ── HERO ── */} -
- {/* Grid pattern */} -
- {/* Glow orbs */} -
-
- {/* Scan line */} -
- -
- {/* Left */} -
- {/* Status badge */} -
-
- 지금 이 순간도 서비스가 돌아가고 있습니다 -
- -

- URL을 드립니다.
- 직접 확인하고
결정하세요. -

- -

- 개발자에게 맡겼다가 연락 두절된 경험 있으신가요?
- 납기 지키고 연락 끊지 않는 개발팀이 계약서부터 소스코드 인도까지
끝까지 책임집니다. -

- -
- - 무료 견적 · 24시간 내 회신 → - - - 운영 중인 서비스 보기 - -
-
- - {/* Right — URL Proof Card */} -
-
- {/* Browser chrome */} -
- {['#ef4444', '#f59e0b', '#10b981'].map((c, i) => ( -
- ))} -
- - https://lotto.jaengseung-made.com - | - -
-
- {/* Content */} -
-
-
- -
-
-
로또 번호 추천
-
● 운영 중
-
-
- {/* Mock chart lines */} - {[['최신 분석', '2024.08.14 완료'], ['이번 주 추천', '3, 7, 14, 28, 35, 42'], ['정확도', '통계 기반 분석']].map(([k, v]) => ( -
- {k} - {v} -
- ))} -
- - ✓ 이 서비스는 지금 실제로 운영되고 있습니다 - -
-
-
- - {/* Small badges below card */} -
- {['NAS 자체 서버', 'Vercel 배포', 'Supabase DB'].map(b => ( -
- {b} -
- ))} -
-
-
-
- - {/* ── TRUST BAR ── */} -
-
- {[ - { label: '실제 운영 서비스', value: 3, suffix: '개', color: '#60a5fa' }, - { label: '평균 견적 발송', value: 24, suffix: 'h 내', color: '#34d399' }, - { label: '소스코드 인도', value: 100, suffix: '%', color: '#a78bfa' }, - { label: '무상 A/S 기간', value: 1, suffix: '개월', color: '#fbbf24' }, - ].map((s, i) => ( -
-
- -
-
{s.label}
-
- ))} -
-
- - {/* ── SERVICES ── */} -
-
-
-
Services
-

- 제공 서비스 5가지 -

-

- 구독형 솔루션부터 맞춤 외주까지 — 모두 계약서와 함께 시작합니다 -

-
- -
- {services.map((svc) => ( - setHovered(svc.id)} - onMouseLeave={() => setHovered(null)} - style={{ - background: 'white', borderRadius: 16, padding: '28px 24px', - border: `1px solid ${hovered === svc.id ? svc.color + '50' : '#e2e8f0'}`, - boxShadow: '0 2px 12px rgba(0,0,0,0.04)', - textDecoration: 'none', display: 'block', position: 'relative', overflow: 'hidden', - }} - > - {/* Color top border */} -
- -
-
- -
-
- - {svc.tag} - -
-
- -

{svc.title}

-

{svc.desc}

- - {/* URL 증거 */} - {svc.url && ( -
-
- {svc.url} -
- )} - -
- {svc.price} - 자세히 보기 → -
- - ))} -
-
-
- - {/* ── PROOF ── */} -
-
-
-
Proof of Work
-

- 말 대신 URL로 증명합니다 -

-

- 실제로 운영 중인 서비스를 직접 확인해 보세요 -

-
- -
- {[ - { title: '로또 번호 추천', url: 'lotto.jaengseung-made.com', stack: ['Next.js', 'Supabase', 'NAS'], status: 'LIVE', desc: '매주 업데이트되는 빅데이터 기반 번호 분석', color: '#7c3aed' }, - { title: '주식 자동매매', url: 'stock.jaengseung-made.com', stack: ['Python', 'Telegram API', 'NAS'], status: 'LIVE', desc: '24시간 무인 운영 중인 텔레그램 봇 시스템', color: '#0ea5e9' }, - { title: 'AI 사주 분석', url: 'saju.jaengseung-made.com', stack: ['Gemini API', 'Next.js', 'DB'], status: 'LIVE', desc: 'Gemini 2.5 Pro 기반 사주 해석 서비스', color: '#f59e0b' }, - ].map((p) => ( -
- {/* Browser bar */} -
- {['#ef4444', '#f59e0b', '#10b981'].map((c, i) =>
)} -
- https://{p.url} -
-
-
-
- {p.title} -
-
- {p.status} -
-
-

{p.desc}

-
- {p.stack.map(s => ( - {s} - ))} -
-
-
- ))} -
- - {/* Career Timeline */} -
-

개발자 이력

-
- {[ - { year: '2018', title: '대기업 입사', desc: '백엔드 개발 시작', color: '#3b82f6' }, - { year: '2021', title: '시니어 개발자', desc: 'MSA 아키텍처 설계', color: '#7c3aed' }, - { year: '2023', title: '부업 서비스 시작', desc: '첫 SaaS 론칭', color: '#0ea5e9' }, - { year: '2024', title: '3개 서비스 운영', desc: '쟁승메이드 풀가동', color: '#10b981' }, - ].map((t) => ( -
-
-
{t.year}
-
{t.title}
-
{t.desc}
-
- ))} -
-
-
-
- - {/* ── GUARANTEES ── */} -
-
-
-
Guarantees
-

- 4가지 보장 -

-

- 모두 계약서에 명기됩니다 — 구두 약속이 아닙니다 -

-
- -
- {guarantees.map((g) => ( -
-
- -
-
-

{g.title}

-

{g.desc}

-
-
- ))} -
-
-
- - {/* ── PRICING ── */} -
-
-
-
Pricing
-

- 투명한 가격표 -

-

숨겨진 요금 없이 처음부터 명확하게

-
- -
- {/* Header */} -
- {['서비스', '플랜', '가격', ''].map((h, i) => ( -
{h}
- ))} -
- {[ - { svc: '홈페이지 제작', plan: '스타터', price: '20만원~', cta: '상담 신청' }, - { svc: '', plan: '비즈니스', price: '100만원~', cta: '상담 신청' }, - { svc: '업무 자동화', plan: '단순 자동화', price: '5만원~', cta: '견적 받기' }, - { svc: '', plan: '자동화 심화', price: '15만원~', cta: '견적 받기' }, - { svc: '주식 자동매매', plan: '스타터', price: '설치 49,000 + 월 9,900원', cta: '신청하기' }, - { svc: '', plan: '프로', price: '설치 99,000 + 월 29,000원', cta: '신청하기' }, - { svc: '로또 추천', plan: '기본', price: '월 9,900원', cta: '구독하기' }, - { svc: '프롬프트', plan: '단건 설계', price: '5만원~', cta: '문의하기' }, - ].map((row, i) => ( -
-
- {row.svc || '-'} -
-
{row.plan}
-
{row.price}
- - {row.cta} - -
- ))} -
- -

- * 모든 가격은 VAT 별도 · 복잡한 요구사항은 무료 상담 후 정확한 견적 제공 -

-
-
- - {/* ── FAQ ── */} -
-
-
-
FAQ
-

- 자주 묻는 질문 -

-
- -
- {faqs.map((faq, i) => ( -
- - {openFaq === i && ( -
-
-

{faq.a}

-
- )} -
- ))} -
-
-
- - {/* ── FINAL CTA ── */} -
- {/* Background dots */} - - - - - - - -
-
-
- 지금 문의하면 24시간 내 견적 발송 -
- -

- 개발사 연락 두절로
- 손해 본 경험 있으신가요? -

-

- 여기선 계약서부터 시작합니다.
무료 상담으로 지금 바로 확인해 보세요. -

- -
- - 무료 견적 신청하기 → - - - 이메일로 문의 - -
- -

- bgg8988@gmail.com · 010-3907-1392 -

-
-
- - {/* ── FOOTER ── */} -
-
-
-
쟁승메이드
-
© 2024 JaengseungMade. All rights reserved.
-
-
- {[ - { label: '서비스', href: '#services' }, - { label: '로또', href: '/services/lotto' }, - { label: '주식', href: '/services/stock' }, - { label: '자동화', href: '/services/automation' }, - { label: '외주', href: '/freelance' }, - ].map(l => ( - - {l.label} - - ))} -
-
-
-
- ); -} diff --git a/app/legal/refund/page.tsx b/app/legal/refund/page.tsx index eae8836..e3188e2 100644 --- a/app/legal/refund/page.tsx +++ b/app/legal/refund/page.tsx @@ -52,7 +52,7 @@ export default function RefundPage() {

2. 구독 서비스

-

대상: AI 자동화 키트 월 구독, 로또 월간 플랜, 주식 월 유지비 등

+

대상: 향후 도입될 월 구독형 서비스 (현재 운영 중인 구독 상품 없음)

diff --git a/lib/admin-auth.ts b/lib/admin-auth.ts index 3b313da..654c165 100644 --- a/lib/admin-auth.ts +++ b/lib/admin-auth.ts @@ -1,4 +1,11 @@ -import { createHmac } from 'crypto'; +import { createHmac, timingSafeEqual } from 'crypto'; + +function safeEqual(a: string, b: string): boolean { + const bufA = Buffer.from(a); + const bufB = Buffer.from(b); + if (bufA.length !== bufB.length) return false; + return timingSafeEqual(bufA, bufB); +} const TOKEN_TTL = 24 * 60 * 60 * 1000; // 24시간 @@ -17,7 +24,7 @@ export function verifyAdminTokenNode(token: string): boolean { const [encoded, sig] = token.split('.'); if (!encoded || !sig) return false; const expected = createHmac('sha256', secret).update(encoded).digest('base64url'); - if (sig !== expected) return false; + if (!safeEqual(sig, expected)) return false; const { exp } = JSON.parse(Buffer.from(encoded, 'base64url').toString()); return Date.now() < exp; } catch { @@ -64,7 +71,7 @@ export function verifyPortfolioTokenNode( const [encoded, sig] = token.split('.'); if (!encoded || !sig) return null; const expected = createHmac('sha256', secret).update(encoded).digest('base64url'); - if (sig !== expected) return null; + if (!safeEqual(sig, expected)) return null; const payload = JSON.parse( Buffer.from(encoded, 'base64url').toString() ) as PortfolioTokenPayload;