크몽/숨고 등록용 썸네일 및 배너 — SVG 파일 다운로드 가능
+SVG → PNG 변환 방법
+브라우저에서 파일 열기 → 우클릭 → "이미지 다른 이름으로 저장" (PNG), 또는 Figma에 SVG 드래그 후 PNG Export를 추천합니다. 크몽은 JPG/PNG만 허용합니다.
+{asset.desc}
+{asset.size}px
+ +{preview.size}px · {preview.desc}
+{form.client_name || '고객 미지정'} · 합계 {totalPrice.toLocaleString()}원
+작업 분류 체계(WBS)를 단계별로 작성합니다
+ +작업 없음 — 위 버튼으로 추가하세요
+ )} +선택 항목(optional)은 고객이 직접 선택/해제할 수 있습니다
+ +납품 후 유지보수 플랜을 구성합니다 (고객이 하나를 선택)
+ +{msg}
+([]); + const [loading, setLoading] = useState(true); + const [creating, setCreating] = useState(false); + const [deleting, setDeleting] = useState(null); + const [copied, setCopied] = useState (null); + + useEffect(() => { + fetch('/api/admin/quotes') + .then((r) => r.json()) + .then((d) => setQuotes(d.quotes ?? [])) + .finally(() => setLoading(false)); + }, []); + + async function handleCreate() { + setCreating(true); + const res = await fetch('/api/admin/quotes', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ title: '새 견적서' }), + }); + const d = await res.json(); + if (d.quote?.id) router.push(`/admin/quotes/${d.quote.id}`); + else setCreating(false); + } + + async function handleDelete(id: string) { + if (!confirm('이 견적서를 삭제할까요?')) return; + setDeleting(id); + await fetch(`/api/admin/quotes/${id}`, { method: 'DELETE' }); + setQuotes((prev) => prev.filter((q) => q.id !== id)); + setDeleting(null); + } + + function copyLink(token: string, id: string) { + const url = `${window.location.origin}/quote/${token}`; + navigator.clipboard.writeText(url); + setCopied(id); + setTimeout(() => setCopied(null), 2000); + } + + return ( + + {/* 헤더 */} ++ ); +} diff --git a/app/api/admin/quotes/[id]/route.ts b/app/api/admin/quotes/[id]/route.ts new file mode 100644 index 0000000..974759f --- /dev/null +++ b/app/api/admin/quotes/[id]/route.ts @@ -0,0 +1,47 @@ +import { NextResponse } from 'next/server'; +import { createAdminClient } from '@/lib/supabase/admin'; +import { verifyAdminTokenNode } from '@/lib/admin-auth'; +import { cookies } from 'next/headers'; + +export const runtime = 'nodejs'; + +async function checkAuth() { + const cookieStore = await cookies(); + const token = cookieStore.get('admin_token')?.value; + return token && verifyAdminTokenNode(token); +} + +export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) { + if (!(await checkAuth())) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + const { id } = await params; + const supabase = createAdminClient(); + const { data, error } = await supabase.from('quotes').select('*').eq('id', id).single(); + if (error) return NextResponse.json({ error: error.message }, { status: 404 }); + return NextResponse.json({ quote: data }); +} + +export async function PUT(request: Request, { params }: { params: Promise<{ id: string }> }) { + if (!(await checkAuth())) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + const { id } = await params; + const body = await request.json(); + const supabase = createAdminClient(); + + const { data, error } = await supabase + .from('quotes') + .update({ ...body, updated_at: new Date().toISOString() }) + .eq('id', id) + .select() + .single(); + + if (error) return NextResponse.json({ error: error.message }, { status: 500 }); + return NextResponse.json({ quote: data }); +} + +export async function DELETE(_req: Request, { params }: { params: Promise<{ id: string }> }) { + if (!(await checkAuth())) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + const { id } = await params; + const supabase = createAdminClient(); + const { error } = await supabase.from('quotes').delete().eq('id', id); + if (error) return NextResponse.json({ error: error.message }, { status: 500 }); + return NextResponse.json({ success: true }); +} diff --git a/app/api/admin/quotes/route.ts b/app/api/admin/quotes/route.ts new file mode 100644 index 0000000..188ed32 --- /dev/null +++ b/app/api/admin/quotes/route.ts @@ -0,0 +1,55 @@ +import { NextResponse } from 'next/server'; +import { createAdminClient } from '@/lib/supabase/admin'; +import { verifyAdminTokenNode } from '@/lib/admin-auth'; +import { cookies } from 'next/headers'; + +export const runtime = 'nodejs'; + +async function checkAuth() { + const cookieStore = await cookies(); + const token = cookieStore.get('admin_token')?.value; + return token && verifyAdminTokenNode(token); +} + +export async function GET() { + if (!(await checkAuth())) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const supabase = createAdminClient(); + const { data, error } = await supabase + .from('quotes') + .select('id, title, client_name, client_email, status, valid_until, public_token, items, created_at') + .order('created_at', { ascending: false }); + + if (error) return NextResponse.json({ error: error.message }, { status: 500 }); + return NextResponse.json({ quotes: data ?? [] }); +} + +export async function POST(request: Request) { + if (!(await checkAuth())) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const body = await request.json(); + const supabase = createAdminClient(); + + const { data, error } = await supabase + .from('quotes') + .insert({ + title: body.title || '새 견적서', + client_name: body.client_name || '', + client_email: body.client_email || '', + valid_until: body.valid_until || null, + wbs: body.wbs || [], + items: body.items || [], + maintenance: body.maintenance || [], + notes: body.notes || '', + status: 'draft', + }) + .select() + .single(); + + if (error) return NextResponse.json({ error: error.message }, { status: 500 }); + return NextResponse.json({ quote: data }, { status: 201 }); +} diff --git a/app/api/quote/[token]/route.ts b/app/api/quote/[token]/route.ts new file mode 100644 index 0000000..10b919a --- /dev/null +++ b/app/api/quote/[token]/route.ts @@ -0,0 +1,48 @@ +import { NextResponse } from 'next/server'; +import { createAdminClient } from '@/lib/supabase/admin'; + +export const runtime = 'nodejs'; + +// 고객용 공개 견적서 조회 (토큰 기반) +export async function GET(_req: Request, { params }: { params: Promise<{ token: string }> }) { + const { token } = await params; + const supabase = createAdminClient(); + + const { data, error } = await supabase + .from('quotes') + .select('id, title, client_name, valid_until, status, wbs, items, maintenance, notes, created_at') + .eq('public_token', token) + .single(); + + if (error || !data) return NextResponse.json({ error: 'Not found' }, { status: 404 }); + return NextResponse.json({ quote: data }); +} + +// 고객이 견적 수락 +export async function POST(request: Request, { params }: { params: Promise<{ token: string }> }) { + const { token } = await params; + const body = await request.json(); // { selectedItems, selectedMaintenance } + const supabase = createAdminClient(); + + const { data: quote, error: findErr } = await supabase + .from('quotes') + .select('id, title, client_name, client_email') + .eq('public_token', token) + .single(); + + if (findErr || !quote) return NextResponse.json({ error: 'Not found' }, { status: 404 }); + + // 상태를 accepted로 변경 + await supabase + .from('quotes') + .update({ + status: 'accepted', + accepted_items: body.selectedItems, + accepted_maintenance: body.selectedMaintenance, + accepted_total: body.total, + updated_at: new Date().toISOString(), + }) + .eq('id', quote.id); + + return NextResponse.json({ success: true }); +} diff --git a/app/freelance/page.tsx b/app/freelance/page.tsx index 411d29b..4d1a3b1 100644 --- a/app/freelance/page.tsx +++ b/app/freelance/page.tsx @@ -231,14 +231,14 @@ export default function FreelancePage() { 현재 프로젝트 접수 가능++ + {/* 목록 */} + {loading ? ( +++ +견적서 관리
+고객에게 제시할 견적서를 작성하고 관리합니다
+불러오는 중...+ ) : quotes.length === 0 ? ( +++ ) : ( +📄+아직 견적서가 없습니다
+위 버튼을 눌러 첫 번째 견적서를 작성해보세요
+++ )} ++ +
++ + + + {quotes.map((q) => { + const st = STATUS[q.status] ?? STATUS.draft; + const total = calcTotal(q.items ?? []); + return ( +견적서명 +고객 +합계 +상태 +유효기간 +작성일 ++ + + ); + })} + ++ + {q.title} + + ++ +{q.client_name || '—'}+{q.client_email || ''}++ {total > 0 ? `${total.toLocaleString()}원` : '—'} + ++ + {st.label} + + ++ {q.valid_until ? q.valid_until.slice(0, 10) : '—'} + ++ {new Date(q.created_at).toLocaleDateString('ko-KR')} + ++ ++ {/* 공개 링크 복사 */} + + {/* 편집 */} + + + + {/* 삭제 */} + ++- 맞춤 개발,
+ 연락 두절? 그런 거 없습니다.
- 처음부터 직접 만들어드립니다 + 직접 만들고, 직접 책임집니다- 검증된 코드 품질을 합리적인 가격에 경험하세요. - 아이디어만 있어도 충분합니다. + 개발자에게 맡겼다가 연락 두절된 경험 있으신가요?
@@ -437,11 +437,11 @@ export default function FreelancePage() {
+ 계약서 작성, 중간 보고, 소스코드 인도까지 — 단계마다 증거를 남깁니다.{[ - { icon: '🏢', title: '대기업 백엔드 경력', desc: '대기업 수준의 코드 품질과 개발 프로세스 적용' }, - { icon: '🖥️', title: '운영 중인 실제 서비스', desc: 'NAS 서버에서 로또·주식 시스템 직접 운영' }, - { icon: '📄', title: '계약서 + 소스코드 제공', desc: '계약서 포함, 완성 후 소스코드 전체 인도' }, - { icon: '🔒', title: '1개월 무상 AS 보장', desc: '납품 후 버그·문제 발생 시 무료 수정' }, - { icon: '⚡', title: '24시간 내 답변', desc: '문의 후 하루 이내 답변 보장' }, + { icon: '🌐', title: '지금 URL로 직접 확인', desc: 'jaengseung-made.com — 로또 분석, 주식 자동매매 지금도 운영 중' }, + { icon: '📋', title: '계약서 먼저, 개발 나중', desc: '구두 약속 없음 — 견적서·계약서 발송 후 착수' }, + { icon: '🔒', title: '납품 전 전액 환불 보장', desc: '마음에 안 드시면 이유 불문 전액 환불' }, + { icon: '📦', title: '소스코드 100% 인도', desc: '완성 후 전체 소스코드 + 배포 가이드 제공' }, + { icon: '⚡', title: '납기 지연 시 패널티', desc: '하루 늦을 때마다 10만원 감면 — 그래서 안 늦습니다' }, ].map((item) => (
- {item.icon} @@ -462,7 +462,7 @@ export default function FreelancePage() {
CONTACT
프로젝트 문의
-아이디어만 있어도 충분합니다. 24시간 이내 답변드립니다.
+개발사 연락 두절로 손해 본 경험 있으신가요? 여기선 계약서부터 시작합니다.
diff --git a/app/page.tsx b/app/page.tsx index 633b647..925b027 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -5,10 +5,10 @@ import Link from 'next/link'; import ContactModal from './components/ContactModal'; const stats = [ - { value: '7년+', label: '개발 경력' }, - { value: '100+', label: '완료 프로젝트' }, - { value: '24h', label: '평균 응답' }, - { value: '98%', label: '고객 만족도' }, + { value: '3개', label: '지금 운영 중인 서비스' }, + { value: '24h', label: '이내 견적 발송 보장' }, + { value: '100%', label: '소스코드 전달' }, + { value: '1개월', label: '무상 AS 보장' }, ]; const techStack = [ @@ -83,18 +83,18 @@ export default function Home() { diff --git a/app/services/stock/page.tsx b/app/services/stock/page.tsx index a3d6bf2..5213c07 100644 --- a/app/services/stock/page.tsx +++ b/app/services/stock/page.tsx @@ -262,12 +262,12 @@ export default function StockPage() {diff --git a/app/services/website/page.tsx b/app/services/website/page.tsx index f2170e6..a761bc4 100644 --- a/app/services/website/page.tsx +++ b/app/services/website/page.tsx @@ -148,14 +148,14 @@ export default function WebsiteServicePage() { background: 'linear-gradient(135deg, #ffffff 0%, #c7d2fe 50%, #818cf8 100%)', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent', }}> - 비즈니스를 빛내는 홈페이지,START TRADING
지금 도입 상담 받아보세요
-무료 상담 후 정확한 견적을 드립니다
+계약서 먼저, 개발 나중 — 구두 약속 없음
직접 만들어드립니다 +홈페이지 맡겼다가 연락 끊긴 경험,
여기선 없습니다- 7년차 대기업 개발자가 기획·디자인·개발·배포까지 원스톱으로.
- 단순한 외주가 아닌, 비즈니스 성과를 만드는 홈페이지를 제작합니다. +계약서 작성 → 중간 보고 → 소스코드 인도까지 단계마다 증거를 남깁니다.
+ 납기 지연 시 하루당 10만원 감면 — 그래서 안 늦습니다.{[ - { num: '3~5일', label: '최단 납품' }, + { num: '3~5일', label: '최단 납품 (스타터)' }, { num: '50만원~', label: '시작 가격' }, - { num: '100%', label: '반응형 지원' }, + { num: '전액환불', label: '납품 전 환불 보장' }, ].map((s) => ({s.num}diff --git a/public/marketing/banner-homepage.svg b/public/marketing/banner-homepage.svg new file mode 100644 index 0000000..e0766a2 --- /dev/null +++ b/public/marketing/banner-homepage.svg @@ -0,0 +1,125 @@ + diff --git a/public/marketing/thumb-automation.svg b/public/marketing/thumb-automation.svg new file mode 100644 index 0000000..c6727d5 --- /dev/null +++ b/public/marketing/thumb-automation.svg @@ -0,0 +1,158 @@ + diff --git a/public/marketing/thumb-homepage-A.svg b/public/marketing/thumb-homepage-A.svg new file mode 100644 index 0000000..f1fd7ac --- /dev/null +++ b/public/marketing/thumb-homepage-A.svg @@ -0,0 +1,186 @@ + diff --git a/public/marketing/thumb-homepage-B.svg b/public/marketing/thumb-homepage-B.svg new file mode 100644 index 0000000..1e7720d --- /dev/null +++ b/public/marketing/thumb-homepage-B.svg @@ -0,0 +1,88 @@ + diff --git a/public/marketing/thumb-prompt.svg b/public/marketing/thumb-prompt.svg new file mode 100644 index 0000000..62351c3 --- /dev/null +++ b/public/marketing/thumb-prompt.svg @@ -0,0 +1,124 @@ + diff --git a/public/marketing/thumb-stock.svg b/public/marketing/thumb-stock.svg new file mode 100644 index 0000000..909e24e --- /dev/null +++ b/public/marketing/thumb-stock.svg @@ -0,0 +1,151 @@ +