feat: 토정비결 및 PDF 저장 기능 추가
- 토정비결 페이지 구현 (연간/월별 운세) - 분야별 운세 (재물, 건강, 관운, 애정) - PDF 저장 기능 구현 (jsPDF + html2canvas) - 모든 결과 페이지에 PDF 다운로드 기능 추가 - PDFButton 재사용 가능한 컴포넌트 생성 - 홈페이지에 토정비결 링크 추가 - 페이지 간 네비게이션 링크 업데이트 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
47
app/components/PDFButton.tsx
Normal file
47
app/components/PDFButton.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { downloadPDF } from '@/lib/pdf-utils';
|
||||
|
||||
interface PDFButtonProps {
|
||||
elementId: string;
|
||||
filename: string;
|
||||
buttonText?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function PDFButton({
|
||||
elementId,
|
||||
filename,
|
||||
buttonText = 'PDF 저장',
|
||||
className = ''
|
||||
}: PDFButtonProps) {
|
||||
const [isGenerating, setIsGenerating] = useState(false);
|
||||
|
||||
const handleDownload = async () => {
|
||||
setIsGenerating(true);
|
||||
try {
|
||||
await downloadPDF(elementId, filename);
|
||||
} finally {
|
||||
setIsGenerating(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={handleDownload}
|
||||
disabled={isGenerating}
|
||||
className={className || "bg-gradient-to-r from-indigo-600 to-purple-600 text-white rounded-xl p-6 shadow-lg hover:shadow-xl transition text-center group w-full disabled:opacity-50 disabled:cursor-not-allowed"}
|
||||
>
|
||||
<div className="text-4xl mb-3">
|
||||
{isGenerating ? '⏳' : '📥'}
|
||||
</div>
|
||||
<h3 className="text-xl font-bold mb-2">
|
||||
{isGenerating ? 'PDF 생성 중...' : buttonText}
|
||||
</h3>
|
||||
<p className="text-sm opacity-90">
|
||||
{isGenerating ? '잠시만 기다려주세요' : '결과를 PDF로 저장하기'}
|
||||
</p>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
139
app/components/TojeongForm.tsx
Normal file
139
app/components/TojeongForm.tsx
Normal file
@@ -0,0 +1,139 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
export default function TojeongForm() {
|
||||
const router = useRouter();
|
||||
const [year, setYear] = useState('');
|
||||
const [month, setMonth] = useState('');
|
||||
const [day, setDay] = useState('');
|
||||
const [gender, setGender] = useState<'male' | 'female'>('male');
|
||||
const [targetYear, setTargetYear] = useState(new Date().getFullYear().toString());
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!year || !month || !day) {
|
||||
alert('생년월일을 모두 입력해주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
// URL 파라미터로 전달
|
||||
const params = new URLSearchParams({
|
||||
year,
|
||||
month,
|
||||
day,
|
||||
gender,
|
||||
targetYear
|
||||
});
|
||||
|
||||
router.push(`/tojeong/result?${params.toString()}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="bg-white rounded-3xl shadow-2xl p-8 md:p-12">
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-8 text-center">생년월일 입력</h2>
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* 생년월일 */}
|
||||
<div>
|
||||
<label className="block text-left text-sm font-semibold text-gray-700 mb-2">
|
||||
생년월일
|
||||
</label>
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<input
|
||||
type="number"
|
||||
placeholder="년 (예: 1990)"
|
||||
className="px-4 py-3 border-2 border-gray-200 rounded-xl focus:border-amber-500 focus:outline-none transition"
|
||||
min="1900"
|
||||
max="2100"
|
||||
value={year}
|
||||
onChange={(e) => setYear(e.target.value)}
|
||||
required
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
placeholder="월 (1-12)"
|
||||
className="px-4 py-3 border-2 border-gray-200 rounded-xl focus:border-amber-500 focus:outline-none transition"
|
||||
min="1"
|
||||
max="12"
|
||||
value={month}
|
||||
onChange={(e) => setMonth(e.target.value)}
|
||||
required
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
placeholder="일 (1-31)"
|
||||
className="px-4 py-3 border-2 border-gray-200 rounded-xl focus:border-amber-500 focus:outline-none transition"
|
||||
min="1"
|
||||
max="31"
|
||||
value={day}
|
||||
onChange={(e) => setDay(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 성별 선택 */}
|
||||
<div>
|
||||
<label className="block text-left text-sm font-semibold text-gray-700 mb-2">
|
||||
성별
|
||||
</label>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setGender('male')}
|
||||
className={`px-6 py-3 rounded-xl font-semibold transition ${
|
||||
gender === 'male'
|
||||
? 'bg-amber-600 text-white'
|
||||
: 'bg-white border-2 border-gray-200 text-gray-700 hover:border-amber-500 hover:text-amber-600'
|
||||
}`}
|
||||
>
|
||||
남성
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setGender('female')}
|
||||
className={`px-6 py-3 rounded-xl font-semibold transition ${
|
||||
gender === 'female'
|
||||
? 'bg-amber-600 text-white'
|
||||
: 'bg-white border-2 border-gray-200 text-gray-700 hover:border-amber-500 hover:text-amber-600'
|
||||
}`}
|
||||
>
|
||||
여성
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 확인할 년도 */}
|
||||
<div>
|
||||
<label className="block text-left text-sm font-semibold text-gray-700 mb-2">
|
||||
확인할 년도
|
||||
</label>
|
||||
<select
|
||||
className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl focus:border-amber-500 focus:outline-none transition"
|
||||
value={targetYear}
|
||||
onChange={(e) => setTargetYear(e.target.value)}
|
||||
>
|
||||
<option value={new Date().getFullYear() - 1}>{new Date().getFullYear() - 1}년</option>
|
||||
<option value={new Date().getFullYear()}>{new Date().getFullYear()}년 (올해)</option>
|
||||
<option value={new Date().getFullYear() + 1}>{new Date().getFullYear() + 1}년 (내년)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 제출 버튼 */}
|
||||
<button
|
||||
type="submit"
|
||||
className="w-full mt-8 bg-gradient-to-r from-amber-600 to-orange-600 text-white py-4 rounded-xl text-lg font-bold hover:from-amber-700 hover:to-orange-700 transition shadow-lg hover:shadow-xl"
|
||||
>
|
||||
🎋 토정비결 확인하기 →
|
||||
</button>
|
||||
|
||||
<p className="text-sm text-gray-500 text-center mt-4">
|
||||
* 토정비결은 음력 생일을 기준으로 합니다.
|
||||
</p>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user