feat: 카카오 오픈채팅 플로팅 버튼 + 견적서 PDF 저장 기능 추가
- DashboardShell: 카카오 오픈채팅 플로팅 버튼 (우하단 고정, 스프링 hover) - 링크: https://open.kakao.com/o/s9stoNvb - admin/quote 페이지 제외, 일반 사용자 페이지 전체 노출 - quote/[token]: PDF 저장 버튼 (window.print) + @media print 스타일 - quote/[token]: ?print=1 파라미터로 접속 시 자동 인쇄 다이얼로그 - admin/quotes/[id]: PDF 저장 버튼 추가 (?print=1 링크로 새탭 열기) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -200,6 +200,17 @@ export default function QuoteEditorPage() {
|
||||
미리보기
|
||||
</a>
|
||||
)}
|
||||
{/* PDF 저장 */}
|
||||
{publicToken && (
|
||||
<a href={`/quote/${publicToken}?print=1`} target="_blank" rel="noreferrer"
|
||||
className="flex items-center gap-2 px-3 py-2 rounded-lg text-sm font-medium border border-violet-700 text-violet-400 hover:text-violet-300 hover:border-violet-500 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="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 11v6m-3-3l3 3 3-3" />
|
||||
</svg>
|
||||
PDF 저장
|
||||
</a>
|
||||
)}
|
||||
{/* 저장 */}
|
||||
<button onClick={() => save()} disabled={saving}
|
||||
className={`flex items-center gap-2 px-5 py-2 rounded-xl text-sm font-semibold transition-all ${saved ? 'bg-green-600 text-white' : 'bg-blue-600 hover:bg-blue-500 text-white'} disabled:opacity-60`}>
|
||||
|
||||
@@ -46,6 +46,58 @@ export default function DashboardShell({ children }: { children: React.ReactNode
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
{/* 카카오 오픈채팅 플로팅 버튼 */}
|
||||
<a
|
||||
href="https://open.kakao.com/o/s9stoNvb"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="kakao-float-btn"
|
||||
aria-label="카카오 오픈채팅 상담"
|
||||
title="카카오 오픈채팅으로 1:1 상담"
|
||||
>
|
||||
<svg width="28" height="28" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12 3C6.477 3 2 6.589 2 11c0 2.713 1.574 5.117 4 6.663V21l3.5-2.1A11.5 11.5 0 0 0 12 19c5.523 0 10-3.589 10-8s-4.477-8-10-8z"/>
|
||||
</svg>
|
||||
<span className="kakao-float-label">1:1 상담</span>
|
||||
</a>
|
||||
|
||||
<style>{`
|
||||
.kakao-float-btn {
|
||||
position: fixed;
|
||||
bottom: 28px;
|
||||
right: 28px;
|
||||
z-index: 50;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
background: #FEE500;
|
||||
color: #3A1D1D;
|
||||
padding: 12px 18px;
|
||||
border-radius: 100px;
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
text-decoration: none;
|
||||
box-shadow: 0 4px 20px rgba(254,229,0,0.4), 0 2px 8px rgba(0,0,0,0.15);
|
||||
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.kakao-float-btn:hover {
|
||||
transform: translateY(-3px) scale(1.04);
|
||||
box-shadow: 0 8px 28px rgba(254,229,0,0.5), 0 4px 12px rgba(0,0,0,0.15);
|
||||
}
|
||||
.kakao-float-btn:active {
|
||||
transform: translateY(-1px) scale(0.98);
|
||||
}
|
||||
@media (max-width: 640px) {
|
||||
.kakao-float-btn {
|
||||
bottom: 20px;
|
||||
right: 16px;
|
||||
padding: 10px 14px;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useParams } from 'next/navigation';
|
||||
import { useParams, useSearchParams } from 'next/navigation';
|
||||
|
||||
interface WBSTask { id: string; name: string; duration: string; description: string; }
|
||||
interface WBSPhase { id: string; phase: string; tasks: WBSTask[]; }
|
||||
@@ -26,6 +26,7 @@ const CATEGORY_COLORS: Record<string, string> = {
|
||||
|
||||
export default function QuotePage() {
|
||||
const { token } = useParams<{ token: string }>();
|
||||
const searchParams = useSearchParams();
|
||||
const [quote, setQuote] = useState<Quote | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [notFound, setNotFound] = useState(false);
|
||||
@@ -50,6 +51,10 @@ export default function QuotePage() {
|
||||
const rec = (d.quote.maintenance as MaintenancePlan[]).find((p) => p.recommended);
|
||||
if (rec) setSelectedMaintenance(rec.id);
|
||||
else if (d.quote.maintenance.length > 0) setSelectedMaintenance(d.quote.maintenance[0].id);
|
||||
// ?print=1 파라미터 시 자동 인쇄 다이얼로그
|
||||
if (searchParams.get('print') === '1') {
|
||||
setTimeout(() => window.print(), 800);
|
||||
}
|
||||
})
|
||||
.catch(() => setNotFound(true))
|
||||
.finally(() => setLoading(false));
|
||||
@@ -137,6 +142,15 @@ export default function QuotePage() {
|
||||
* { box-sizing: border-box; }
|
||||
input[type=checkbox] { accent-color: #6366f1; width: 18px; height: 18px; cursor: pointer; }
|
||||
input[type=radio] { accent-color: #6366f1; width: 18px; height: 18px; cursor: pointer; }
|
||||
@media print {
|
||||
body { background: white !important; color: #1e293b !important; }
|
||||
.no-print { display: none !important; }
|
||||
.print-break { page-break-before: always; }
|
||||
[style*="background: #0a0f1e"], [style*="background: #0f172a"] {
|
||||
background: white !important;
|
||||
color: #1e293b !important;
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
|
||||
{/* 헤더 */}
|
||||
@@ -149,10 +163,28 @@ export default function QuotePage() {
|
||||
<div style={{ color: 'white', fontWeight: 700, fontSize: 14 }}>쟁승메이드</div>
|
||||
<div style={{ color: '#475569', fontSize: 11 }}>jaengseung-made.com</div>
|
||||
</div>
|
||||
<div style={{ marginLeft: 'auto' }}>
|
||||
<div style={{ marginLeft: 'auto', display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||
<span style={{ background: 'rgba(99,102,241,0.15)', border: '1px solid rgba(99,102,241,0.3)', color: '#818cf8', fontSize: 11, fontWeight: 600, padding: '4px 12px', borderRadius: 100 }}>
|
||||
공식 견적서
|
||||
</span>
|
||||
<button
|
||||
className="no-print"
|
||||
onClick={() => window.print()}
|
||||
style={{
|
||||
display: 'flex', alignItems: 'center', gap: 6,
|
||||
background: 'rgba(255,255,255,0.08)', border: '1px solid rgba(255,255,255,0.12)',
|
||||
color: '#cbd5e1', fontSize: 13, fontWeight: 600,
|
||||
padding: '6px 14px', borderRadius: 8, cursor: 'pointer',
|
||||
transition: 'all 0.2s',
|
||||
}}
|
||||
onMouseEnter={(e) => { e.currentTarget.style.background = 'rgba(255,255,255,0.14)'; e.currentTarget.style.color = 'white'; }}
|
||||
onMouseLeave={(e) => { e.currentTarget.style.background = 'rgba(255,255,255,0.08)'; e.currentTarget.style.color = '#cbd5e1'; }}
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/>
|
||||
</svg>
|
||||
PDF 저장
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user