feat: 절기 정밀화, AI 해석 추가, 공유 기능 개선

절기 날짜 정밀화:
- solarlunar 라이브러리 추가
- 천문학적 계산 기반 정확한 절기 날짜 계산
- 각 절기별 정확한 날짜 범위 검색
- 폴백 메커니즘으로 안정성 확보

AI 상세 해석 시스템:
- ai-interpretation.ts 라이브러리 생성
- 일간 기반 성격 분석 (10개 천간별 상세 해석)
- 오행 균형 분석 및 점수 계산
- 십성 기반 다차원 분석
  - 직업 운세 (오행 + 십성 조합)
  - 대인 관계 (십성 기반)
  - 재물 운세 (정재/편재 분석)
  - 건강 운세 (오행 균형)
- 맞춤형 조언 생성

결과 페이지 AI 해석 섹션:
- 오행 균형 시각화 (막대 그래프)
- 장점/주의할 점 구분 표시
- 4가지 운세 카드 (직업/대인/재물/건강)
- AI 조언 그리드 레이아웃
- 전문가 상담 권장 안내

공유 기능 개선:
- localhost 감지 로직 추가
- localhost인 경우:
  - 카카오톡: 텍스트 형식으로 사주 정보 공유
  - 링크 복사: 사주 정보 텍스트 복사
  - 사용자에게 개발 환경임을 안내
- 배포 환경: 기존대로 URL 공유
- 더 나은 사용자 경험 제공

기술 개선:
- solarlunar 라이브러리 (정밀 절기 계산)
- 타입 안전성 강화
- 모듈화된 해석 로직
- 성능 최적화

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-12 00:26:12 +09:00
parent e233e18a55
commit d513c063cf
6 changed files with 638 additions and 53 deletions

View File

@@ -12,31 +12,53 @@ export default function ShareButtons({ title, description, url }: ShareButtonsPr
const [showShareMenu, setShowShareMenu] = useState(false);
const shareUrl = url || (typeof window !== 'undefined' ? window.location.href : '');
// localhost 체크
const isLocalhost = shareUrl.includes('localhost') || shareUrl.includes('127.0.0.1');
const handleKakaoShare = () => {
if (typeof window !== 'undefined' && (window as any).Kakao) {
(window as any).Kakao.Share.sendDefault({
objectType: 'feed',
content: {
title: title,
description: description,
imageUrl: 'https://developers.kakao.com/assets/img/about/logos/kakaolink/kakaolink_btn_medium.png',
if (isLocalhost) {
// localhost인 경우 텍스트로 공유
const shareText = `${title}\n\n${description}\n\n🔮 사주보기 - 쟁승메이드\n(배포 후 링크가 제공됩니다)`;
if (typeof window !== 'undefined' && (window as any).Kakao) {
(window as any).Kakao.Share.sendDefault({
objectType: 'text',
text: shareText,
link: {
mobileWebUrl: shareUrl,
webUrl: shareUrl,
mobileWebUrl: 'https://jaengseung-made.com',
webUrl: 'https://jaengseung-made.com',
},
},
buttons: [
{
title: '자세히 보기',
});
} else {
alert('카카오톡 공유 기능을 사용할 수 없습니다.');
}
} else {
// 배포된 URL인 경우 정상 공유
if (typeof window !== 'undefined' && (window as any).Kakao) {
(window as any).Kakao.Share.sendDefault({
objectType: 'feed',
content: {
title: title,
description: description,
imageUrl: 'https://developers.kakao.com/assets/img/about/logos/kakaolink/kakaolink_btn_medium.png',
link: {
mobileWebUrl: shareUrl,
webUrl: shareUrl,
},
},
],
});
} else {
alert('카카오톡 공유 기능을 사용할 수 없습니다.');
buttons: [
{
title: '자세히 보기',
link: {
mobileWebUrl: shareUrl,
webUrl: shareUrl,
},
},
],
});
} else {
alert('카카오톡 공유 기능을 사용할 수 없습니다.');
}
}
};
@@ -52,11 +74,18 @@ export default function ShareButtons({ title, description, url }: ShareButtonsPr
const handleCopyLink = async () => {
try {
await navigator.clipboard.writeText(shareUrl);
alert('링크가 복사되었습니다!');
if (isLocalhost) {
// localhost인 경우 텍스트 정보 복사
const shareText = `${title}\n\n${description}\n\n🔮 사주보기 - 쟁승메이드\n(배포 후 링크가 제공됩니다)`;
await navigator.clipboard.writeText(shareText);
alert('사주 정보가 복사되었습니다!\n(개발 환경이므로 URL 대신 텍스트 정보가 복사됩니다)');
} else {
await navigator.clipboard.writeText(shareUrl);
alert('링크가 복사되었습니다!');
}
setShowShareMenu(false);
} catch (err) {
alert('링크 복사에 실패했습니다.');
alert('복사에 실패했습니다.');
}
};

View File

@@ -5,6 +5,7 @@ import ShareButtons from '../components/ShareButtons';
import { calculateDaeun, getCurrentDaeun, getDaeunDescription } from '@/lib/daeun-calculator';
import { getCurrentSolarTerm, getSolarTermName, getSolarTermMonthBranch } from '@/lib/solar-terms';
import { EARTHLY_BRANCHES_KR } from '@/lib/saju-calculator';
import { generateInterpretation, calculateElementScore } from '@/lib/ai-interpretation';
interface PageProps {
searchParams: Promise<{
@@ -34,6 +35,10 @@ export default async function ResultPage({ searchParams }: PageProps) {
const monthBranchIndex = getSolarTermMonthBranch(yearNum, monthNum, dayNum);
const monthBranchName = EARTHLY_BRANCHES_KR[monthBranchIndex];
// AI 해석 생성
const interpretation = generateInterpretation(sajuData);
const elementScores = calculateElementScore(sajuData);
// 대운 계산
const daeunList = calculateDaeun(
yearNum,
@@ -246,6 +251,171 @@ export default async function ResultPage({ searchParams }: PageProps) {
</div>
</div>
{/* AI 상세 해석 */}
<div className="bg-gradient-to-br from-purple-50 to-indigo-50 rounded-3xl shadow-2xl p-8 md:p-12 mb-8">
<h2 className="text-3xl font-bold text-gray-900 mb-2 text-center flex items-center justify-center">
<span className="text-4xl mr-3">🤖</span>
AI
</h2>
<p className="text-center text-gray-600 mb-8"> </p>
{/* 오행 균형 */}
<div className="bg-white rounded-2xl p-6 mb-6">
<h3 className="text-xl font-bold text-gray-900 mb-4 flex items-center">
<span className="text-2xl mr-2"></span>
</h3>
<div className="grid grid-cols-5 gap-3">
{Object.entries(elementScores).map(([element, score]) => (
<div key={element} className="text-center">
<div className="text-2xl font-bold mb-1">{element}</div>
<div className="text-sm text-gray-600 mb-2">
{element === '木' && '목'}
{element === '火' && '화'}
{element === '土' && '토'}
{element === '金' && '금'}
{element === '水' && '수'}
</div>
<div className="w-full bg-gray-200 rounded-full h-2 mb-1">
<div
className={`h-2 rounded-full ${
element === sajuData.day.element
? 'bg-gradient-to-r from-indigo-500 to-purple-500'
: 'bg-gray-400'
}`}
style={{ width: `${score}%` }}
></div>
</div>
<div className="text-xs font-semibold text-gray-700">{score}%</div>
</div>
))}
</div>
</div>
{/* 장단점 */}
<div className="grid md:grid-cols-2 gap-6 mb-6">
<div className="bg-white rounded-2xl p-6">
<h3 className="text-xl font-bold text-gray-900 mb-4 flex items-center">
<span className="text-2xl mr-2">💪</span>
</h3>
<ul className="space-y-2">
{interpretation.strengths.map((strength, i) => (
<li key={i} className="flex items-start text-gray-700">
<span className="text-green-600 mr-2"></span>
<span>{strength}</span>
</li>
))}
</ul>
</div>
<div className="bg-white rounded-2xl p-6">
<h3 className="text-xl font-bold text-gray-900 mb-4 flex items-center">
<span className="text-2xl mr-2"></span>
</h3>
<ul className="space-y-2">
{interpretation.weaknesses.map((weakness, i) => (
<li key={i} className="flex items-start text-gray-700">
<span className="text-orange-600 mr-2">!</span>
<span>{weakness}</span>
</li>
))}
</ul>
</div>
</div>
{/* 직업, 대인관계, 재물, 건강 */}
<div className="grid md:grid-cols-2 gap-6">
{/* 직업 */}
<div className="bg-white rounded-2xl p-6">
<h3 className="text-xl font-bold text-gray-900 mb-4 flex items-center">
<span className="text-2xl mr-2">💼</span>
</h3>
<ul className="space-y-2">
{interpretation.career.map((item, i) => (
<li key={i} className="flex items-start text-gray-700 text-sm">
<span className="text-blue-600 mr-2"></span>
<span>{item}</span>
</li>
))}
</ul>
</div>
{/* 대인관계 */}
<div className="bg-white rounded-2xl p-6">
<h3 className="text-xl font-bold text-gray-900 mb-4 flex items-center">
<span className="text-2xl mr-2">👥</span>
</h3>
<ul className="space-y-2">
{interpretation.relationships.map((item, i) => (
<li key={i} className="flex items-start text-gray-700 text-sm">
<span className="text-pink-600 mr-2"></span>
<span>{item}</span>
</li>
))}
</ul>
</div>
{/* 재물 */}
<div className="bg-white rounded-2xl p-6">
<h3 className="text-xl font-bold text-gray-900 mb-4 flex items-center">
<span className="text-2xl mr-2">💰</span>
</h3>
<ul className="space-y-2">
{interpretation.wealth.map((item, i) => (
<li key={i} className="flex items-start text-gray-700 text-sm">
<span className="text-yellow-600 mr-2"></span>
<span>{item}</span>
</li>
))}
</ul>
</div>
{/* 건강 */}
<div className="bg-white rounded-2xl p-6">
<h3 className="text-xl font-bold text-gray-900 mb-4 flex items-center">
<span className="text-2xl mr-2">🏥</span>
</h3>
<ul className="space-y-2">
{interpretation.health.map((item, i) => (
<li key={i} className="flex items-start text-gray-700 text-sm">
<span className="text-red-600 mr-2"></span>
<span>{item}</span>
</li>
))}
</ul>
</div>
</div>
{/* 조언 */}
<div className="bg-white rounded-2xl p-6 mt-6">
<h3 className="text-xl font-bold text-gray-900 mb-4 flex items-center">
<span className="text-2xl mr-2">💡</span>
AI의
</h3>
<div className="grid md:grid-cols-2 gap-3">
{interpretation.advice.map((item, i) => (
<div key={i} className="flex items-start text-gray-700 text-sm bg-indigo-50 p-3 rounded-lg">
<span className="text-indigo-600 mr-2"></span>
<span>{item}</span>
</div>
))}
</div>
</div>
<div className="mt-6 p-4 bg-purple-100 rounded-xl">
<p className="text-xs text-gray-700 text-center">
💡 AI . ,
.
</p>
</div>
</div>
{/* 대운 (大運) */}
<div className="bg-white rounded-3xl shadow-2xl p-8 md:p-12 mb-8">
<h2 className="text-3xl font-bold text-gray-900 mb-8 text-center">