diff --git a/app/api/contact/route.ts b/app/api/contact/route.ts index 52e8b97..70a9fd6 100644 --- a/app/api/contact/route.ts +++ b/app/api/contact/route.ts @@ -8,6 +8,8 @@ import { getClientIp, INPUT_LIMITS, } from '@/lib/security'; +import { createAdminClient } from '@/lib/supabase/admin'; +import { createClient } from '@/lib/supabase/server'; const resend = new Resend(process.env.RESEND_API_KEY); @@ -58,27 +60,67 @@ export async function POST(request: Request) { // message는 pre-wrap으로 렌더링되므로 반드시 이스케이프 const safeMessage = escapeHtml(message); - await resend.emails.send({ - from: 'onboarding@resend.dev', - to: ['bgg8988@gmail.com'], - replyTo: email, - subject: `[쟁승메이드] 새로운 문의: ${safeSubject}`, - html: ` -
이름: ${safeName}
-연락처: ${safePhone}
-이메일: ${safeEmail}
-서비스: ${safeService}
-${safeMessage}
-- 이 메일은 jaengseung-made.com의 문의 폼에서 발송되었습니다. -
- `, - }); + // ── 로그인 사용자 확인 (optional) ───────────────────────── + let userId: string | null = null; + try { + const supabase = await createClient(); + const { data } = await supabase.auth.getUser(); + userId = data?.user?.id ?? null; + } catch { + // 비로그인 상태 — 무시 + } + + // ── 이메일 전송 ────────────────────────────────────────── + let emailSent = true; + try { + await resend.emails.send({ + from: 'onboarding@resend.dev', + to: ['bgg8988@gmail.com'], + replyTo: email, + subject: `[쟁승메이드] 새로운 문의: ${safeSubject}`, + html: ` +이름: ${safeName}
+연락처: ${safePhone}
+이메일: ${safeEmail}
+서비스: ${safeService}
+${safeMessage}
++ 이 메일은 jaengseung-made.com의 문의 폼에서 발송되었습니다. +
+ `, + }); + } catch (emailError) { + console.error('[Contact] Email send error:', emailError); + emailSent = false; + } + + // ── DB 저장 (이메일 성공/실패 무관) ────────────────────── + try { + const admin = createAdminClient(); + await admin.from('contact_requests').insert({ + name, + email, + phone: phone || null, + service: service || null, + message, + user_id: userId, + created_at: new Date().toISOString(), + }); + } catch (dbError) { + console.error('[Contact] DB insert error:', dbError); + } + + if (!emailSent) { + return NextResponse.json( + { error: '메일 전송에 실패했습니다. 다시 시도해주세요.' }, + { status: 500 } + ); + } return NextResponse.json( { success: true, message: '문의가 성공적으로 전송되었습니다!' }, @@ -86,9 +128,9 @@ export async function POST(request: Request) { ); } catch (error) { // 클라이언트에 내부 오류 상세 노출 금지 - console.error('[Contact] Email send error:', error); + console.error('[Contact] Unexpected error:', error); return NextResponse.json( - { error: '메일 전송에 실패했습니다. 다시 시도해주세요.' }, + { error: '문의 처리 중 오류가 발생했습니다. 다시 시도해주세요.' }, { status: 500 } ); } diff --git a/app/api/projects/link/route.ts b/app/api/projects/link/route.ts index 71c7cb0..81e0151 100644 --- a/app/api/projects/link/route.ts +++ b/app/api/projects/link/route.ts @@ -1,29 +1,12 @@ import { NextResponse } from 'next/server'; import { createAdminClient } from '@/lib/supabase/admin'; import { createClient } from '@/lib/supabase/server'; -import { createClient as createSupabaseClient } from '@supabase/supabase-js'; export const runtime = 'nodejs'; export async function POST(request: Request) { - // Cookie 기반 또는 Bearer 토큰 기반 인증 모두 지원 - const authHeader = request.headers.get('authorization'); - let user = null; - - if (authHeader?.startsWith('Bearer ')) { - const token = authHeader.slice(7); - const client = createSupabaseClient( - process.env.NEXT_PUBLIC_SUPABASE_URL!, - process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, - ); - const { data } = await client.auth.getUser(token); - user = data?.user ?? null; - } else { - const supabase = await createClient(); - const { data } = await supabase.auth.getUser(); - user = data?.user ?? null; - } - + const supabase = await createClient(); + const { data: { user } } = await supabase.auth.getUser(); if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); const body = await request.json(); @@ -56,7 +39,10 @@ export async function POST(request: Request) { .update({ user_id: user.id, updated_at: new Date().toISOString() }) .eq('id', quote.id); - if (updateErr) return NextResponse.json({ error: updateErr.message }, { status: 500 }); + if (updateErr) { + console.error('[Projects/Link] DB update error:', updateErr.message); + return NextResponse.json({ error: '견적서 연결에 실패했습니다. 다시 시도해주세요.' }, { status: 500 }); + } return NextResponse.json({ success: true, quoteId: quote.id }); } diff --git a/app/api/projects/route.ts b/app/api/projects/route.ts index 7e4900f..bcb83a3 100644 --- a/app/api/projects/route.ts +++ b/app/api/projects/route.ts @@ -1,28 +1,12 @@ import { NextResponse } from 'next/server'; import { createAdminClient } from '@/lib/supabase/admin'; import { createClient } from '@/lib/supabase/server'; -import { createClient as createSupabaseClient } from '@supabase/supabase-js'; export const runtime = 'nodejs'; -export async function GET(request: Request) { - // Cookie 기반 또는 Bearer 토큰 기반 인증 모두 지원 - const authHeader = request.headers.get('authorization'); - let user = null; - - if (authHeader?.startsWith('Bearer ')) { - const token = authHeader.slice(7); - const client = createSupabaseClient( - process.env.NEXT_PUBLIC_SUPABASE_URL!, - process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, - ); - const { data } = await client.auth.getUser(token); - user = data?.user ?? null; - } else { - const supabase = await createClient(); - const { data } = await supabase.auth.getUser(); - user = data?.user ?? null; - } +export async function GET() { + const supabase = await createClient(); + const { data: { user } } = await supabase.auth.getUser(); if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); const admin = createAdminClient(); @@ -34,7 +18,10 @@ export async function GET(request: Request) { .in('status', ['sent', 'accepted', 'in_progress', 'completed', 'delivered']) .order('created_at', { ascending: false }); - if (qErr) return NextResponse.json({ error: qErr.message }, { status: 500 }); + if (qErr) { + console.error('[Projects] DB query error:', qErr.message); + return NextResponse.json({ error: '프로젝트 정보를 불러올 수 없습니다.' }, { status: 500 }); + } if (!quotes?.length) return NextResponse.json({ projects: [] }); const quoteIds = quotes.map((q) => q.id); diff --git a/app/api/quote/[token]/route.ts b/app/api/quote/[token]/route.ts index 10b919a..4987d69 100644 --- a/app/api/quote/[token]/route.ts +++ b/app/api/quote/[token]/route.ts @@ -15,7 +15,13 @@ export async function GET(_req: Request, { params }: { params: Promise<{ token: .single(); if (error || !data) return NextResponse.json({ error: 'Not found' }, { status: 404 }); - return NextResponse.json({ quote: data }); + + // 만료 검증: valid_until이 현재 시간보다 과거이면 expired 플래그 추가 + const expired = data.valid_until + ? new Date(data.valid_until).getTime() < Date.now() + : false; + + return NextResponse.json({ quote: data, expired }); } // 고객이 견적 수락 diff --git a/app/freelance/page.tsx b/app/freelance/page.tsx index a43c915..f1cb61e 100644 --- a/app/freelance/page.tsx +++ b/app/freelance/page.tsx @@ -13,6 +13,7 @@ const portfolio = [ tags: ['Next.js', 'Tailwind CSS', 'Vercel', 'SEO'], status: '납품 완료', statusType: 'done', + priceRange: '50~200만원', accentColor: 'text-indigo-400', accentBg: 'bg-[#0d0a2e]', borderAccent: 'border-indigo-400/20', @@ -25,6 +26,7 @@ const portfolio = [ tags: ['Python', 'Gmail API', 'Google Apps Script'], status: '납품 완료', statusType: 'done', + priceRange: '30~150만원', accentColor: 'text-red-400', accentBg: 'bg-[#200a0a]', borderAccent: 'border-red-400/20', @@ -37,6 +39,7 @@ const portfolio = [ tags: ['Python', 'Selenium', 'Telegram Bot'], status: '납품 완료', statusType: 'done', + priceRange: '30~150만원', accentColor: 'text-violet-400', accentBg: 'bg-[#0d0a2e]', borderAccent: 'border-violet-400/20', @@ -49,6 +52,7 @@ const portfolio = [ tags: ['Python', 'OpenPyXL', 'ReportLab'], status: '납품 완료', statusType: 'done', + priceRange: '30~150만원', accentColor: 'text-cyan-400', accentBg: 'bg-[#012030]', borderAccent: 'border-cyan-400/20', @@ -61,6 +65,7 @@ const portfolio = [ tags: ['Python', '공공데이터 API', 'PostgreSQL', 'Telegram'], status: '납품 완료', statusType: 'done', + priceRange: '30~150만원', accentColor: 'text-blue-400', accentBg: 'bg-[#04102b]', borderAccent: 'border-blue-400/20', @@ -341,6 +346,12 @@ export default function FreelancePage() { ))} +* 의뢰인 동의 하에 게시된 후기입니다. 전체 대화 내역 공개 요청 시 제공 가능합니다.
+ +24시간 내 답변 · 상담은 무료입니다
+CONTACT
diff --git a/app/mypage/page.tsx b/app/mypage/page.tsx index afc856e..1afc990 100644 --- a/app/mypage/page.tsx +++ b/app/mypage/page.tsx @@ -48,14 +48,6 @@ interface Order { status: string; } -interface LottoHistoryItem { - id: number; - numbers: number[]; - source: string; - plan_id: string; - created_at: string; -} - interface ProjectMilestone { id: string; step_number: number; @@ -85,22 +77,15 @@ interface ActiveSubscription { cancelled_at: string | null; } -const PLAN_LABELS: Record