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

316
lib/ai-interpretation.ts Normal file
View File

@@ -0,0 +1,316 @@
import { SajuData } from './saju-calculator';
/**
* AI 기반 사주 해석
* 사주 데이터를 분석하여 상세한 해석 제공
*/
interface Interpretation {
personality: string[];
strengths: string[];
weaknesses: string[];
career: string[];
relationships: string[];
health: string[];
wealth: string[];
advice: string[];
}
/**
* 오행 균형 분석
*/
function analyzeElementBalance(saju: SajuData): { [key: string]: number } {
const elements = { : 0, : 0, : 0, : 0, : 0 };
// 사주팔자의 각 기둥에서 오행 카운트
elements[saju.year.element]++;
elements[saju.month.element]++;
elements[saju.day.element]++;
if (saju.hour) elements[saju.hour.element]++;
return elements;
}
/**
* 십성 분석
*/
function analyzeTenGods(saju: SajuData): { [key: string]: number } {
const tenGods: { [key: string]: number } = {};
[saju.year.tenGod, saju.month.tenGod, saju.hour?.tenGod].forEach(god => {
if (god && god !== '일간') {
tenGods[god] = (tenGods[god] || 0) + 1;
}
});
return tenGods;
}
/**
* 일간 기반 성격 해석
*/
function interpretDayStem(stem: string, element: string): string[] {
const interpretations: { [key: string]: string[] } = {
'甲': [
'큰 나무처럼 곧고 꿋꿋한 성격입니다.',
'리더십이 강하고 개척 정신이 뛰어납니다.',
'정의감이 강하고 원칙을 중요시합니다.',
'때로는 융통성이 부족할 수 있습니다.'
],
'乙': [
'부드러운 풀처럼 유연하고 적응력이 뛰어납니다.',
'섬세하고 예술적 감각이 있습니다.',
'주변 환경에 잘 적응하며 협력을 중시합니다.',
'때로는 우유부단할 수 있습니다.'
],
'丙': [
'태양처럼 밝고 활발한 성격입니다.',
'사교성이 뛰어나고 열정적입니다.',
'창의적이고 표현력이 풍부합니다.',
'때로는 충동적일 수 있습니다.'
],
'丁': [
'촛불처럼 따뜻하고 섬세한 성격입니다.',
'예민하고 감수성이 풍부합니다.',
'예의 바르고 배려심이 깊습니다.',
'때로는 너무 예민할 수 있습니다.'
],
'戊': [
'산처럼 묵직하고 안정적인 성격입니다.',
'책임감이 강하고 신뢰할 수 있습니다.',
'현실적이고 실용적입니다.',
'때로는 고집이 셀 수 있습니다.'
],
'己': [
'밭처럼 포용력 있고 온화한 성격입니다.',
'배려심이 깊고 참을성이 강합니다.',
'현실적이며 실속을 챙깁니다.',
'때로는 소극적일 수 있습니다.'
],
'庚': [
'금속처럼 단단하고 강인한 성격입니다.',
'결단력이 있고 추진력이 강합니다.',
'정직하고 의리를 중시합니다.',
'때로는 융통성이 부족할 수 있습니다.'
],
'辛': [
'보석처럼 고귀하고 섬세한 성격입니다.',
'예리하고 통찰력이 뛰어납니다.',
'품위 있고 우아함을 추구합니다.',
'때로는 까다로울 수 있습니다.'
],
'壬': [
'큰 바다처럼 넓고 깊은 성격입니다.',
'지혜롭고 포용력이 있습니다.',
'융통성이 있고 적응력이 뛰어납니다.',
'때로는 변덕스러울 수 있습니다.'
],
'癸': [
'이슬처럼 섬세하고 조용한 성격입니다.',
'지적이고 사려 깊습니다.',
'인내심이 강하고 끈기가 있습니다.',
'때로는 소심할 수 있습니다.'
]
};
return interpretations[stem] || ['독특한 개성을 가진 사람입니다.'];
}
/**
* 직업 운세 분석
*/
function interpretCareer(saju: SajuData, tenGods: { [key: string]: number }): string[] {
const career: string[] = [];
const element = saju.day.element;
// 오행 기반 직업 추천
const careerByElement: { [key: string]: string[] } = {
'木': ['교육', '출판', '디자인', '패션', '임업', '환경'],
'火': ['예술', '광고', '방송', '요식업', 'IT', '전자'],
'土': ['부동산', '건설', '농업', '유통', '중개', '컨설팅'],
'金': ['금융', '법조', '의료', '기계', '자동차', '보석'],
'水': ['무역', '물류', '여행', '수산', '음료', '화학']
};
career.push(...careerByElement[element].slice(0, 3).map(c => `${c} 분야에 적성이 있습니다.`));
// 십성 기반 직업 성향
if (tenGods['정관'] || tenGods['편관']) {
career.push('조직 생활이나 공직에 적합합니다.');
}
if (tenGods['정재'] || tenGods['편재']) {
career.push('재물 관리나 사업에 능력이 있습니다.');
}
if (tenGods['식신'] || tenGods['상관']) {
career.push('창의적인 일이나 표현하는 직업이 좋습니다.');
}
if (tenGods['정인'] || tenGods['편인']) {
career.push('학문, 연구, 교육 분야가 적합합니다.');
}
return career;
}
/**
* 대인 관계 분석
*/
function interpretRelationships(saju: SajuData, tenGods: { [key: string]: number }): string[] {
const relationships: string[] = [];
if (tenGods['비견'] || tenGods['겁재']) {
relationships.push('친구나 동료와의 관계가 중요합니다.');
relationships.push('경쟁심이 있지만 협력도 잘합니다.');
}
if (tenGods['정관'] || tenGods['편관']) {
relationships.push('윗사람의 인정을 받기 쉽습니다.');
relationships.push('사회적 명예를 중시합니다.');
}
if (tenGods['정재'] || tenGods['편재']) {
if (saju.gender === 'male') {
relationships.push('이성과의 인연이 좋습니다.');
} else {
relationships.push('재물 운이 좋습니다.');
}
}
if (tenGods['정인'] || tenGods['편인']) {
if (saju.gender === 'female') {
relationships.push('가족과의 유대가 깊습니다.');
} else {
relationships.push('멘토를 만나기 쉽습니다.');
}
}
return relationships;
}
/**
* 건강 운세 분석
*/
function interpretHealth(saju: SajuData, elements: { [key: string]: number }): string[] {
const health: string[] = [];
const element = saju.day.element;
// 오행별 건강 주의사항
const healthByElement: { [key: string]: string } = {
'木': '간, 담낭, 눈 건강에 주의하세요.',
'火': '심장, 혈압, 소장 건강에 주의하세요.',
'土': '위장, 소화기, 비장 건강에 주의하세요.',
'金': '폐, 대장, 피부 건강에 주의하세요.',
'水': '신장, 방광, 생식기 건강에 주의하세요.'
};
health.push(healthByElement[element]);
// 오행 불균형 체크
const maxElement = Object.keys(elements).reduce((a, b) =>
elements[a] > elements[b] ? a : b
);
const minElement = Object.keys(elements).reduce((a, b) =>
elements[a] < elements[b] ? a : b
);
if (elements[maxElement] - elements[minElement] >= 2) {
health.push('오행 균형을 맞추기 위한 식습관 관리가 필요합니다.');
}
health.push('규칙적인 생활과 적절한 운동이 중요합니다.');
return health;
}
/**
* 재물 운세 분석
*/
function interpretWealth(saju: SajuData, tenGods: { [key: string]: number }): string[] {
const wealth: string[] = [];
if (tenGods['정재']) {
wealth.push('정직한 노력으로 재물을 모을 수 있습니다.');
wealth.push('월급이나 안정적인 수입이 좋습니다.');
}
if (tenGods['편재']) {
wealth.push('사업이나 투자로 재물을 얻을 수 있습니다.');
wealth.push('재테크에 관심을 가지면 좋습니다.');
}
if (tenGods['식신'] || tenGods['상관']) {
wealth.push('재능을 활용한 수입원이 있습니다.');
wealth.push('창의적인 일로 돈을 벌 수 있습니다.');
}
if (!tenGods['정재'] && !tenGods['편재']) {
wealth.push('재물보다는 명예나 학문을 추구합니다.');
wealth.push('꾸준한 저축이 중요합니다.');
}
return wealth;
}
/**
* 종합 조언
*/
function generateAdvice(saju: SajuData, elements: { [key: string]: number }): string[] {
const advice: string[] = [];
const element = saju.day.element;
// 오행별 조언
const adviceByElement: { [key: string]: string[] } = {
'木': ['아침 산책으로 하루를 시작하세요.', '녹색 식물을 가까이 하세요.', '독서로 마음을 충전하세요.'],
'火': ['밝은 색상의 옷을 입으세요.', '사람들과 적극적으로 소통하세요.', '예술 활동을 즐기세요.'],
'土': ['규칙적인 식사를 하세요.', '안정적인 계획을 세우세요.', '자연과 가까운 곳에 가세요.'],
'金': ['명확한 목표를 설정하세요.', '금속 액세서리를 착용하세요.', '원칙을 지키되 융통성도 발휘하세요.'],
'水': ['충분한 수분 섭취를 하세요.', '유연한 사고를 유지하세요.', '명상이나 요가로 마음을 다스리세요.']
};
advice.push(...adviceByElement[element]);
// 일반적인 조언
advice.push('자신의 장점을 살리고 단점을 보완하세요.');
advice.push('긍정적인 마인드로 하루를 시작하세요.');
return advice;
}
/**
* 전체 사주 해석 생성
*/
export function generateInterpretation(saju: SajuData): Interpretation {
const elements = analyzeElementBalance(saju);
const tenGods = analyzeTenGods(saju);
const personality = interpretDayStem(saju.day.stem, saju.day.element);
// 장점과 단점 분리
const strengths = personality.filter((_, i) => i < 3);
const weaknesses = [personality[3] || '균형 잡힌 성격입니다.'];
return {
personality,
strengths,
weaknesses,
career: interpretCareer(saju, tenGods),
relationships: interpretRelationships(saju, tenGods),
health: interpretHealth(saju, elements),
wealth: interpretWealth(saju, tenGods),
advice: generateAdvice(saju, elements)
};
}
/**
* 오행 균형 점수 계산
*/
export function calculateElementScore(saju: SajuData): { [key: string]: number } {
const elements = analyzeElementBalance(saju);
const total = Object.values(elements).reduce((a, b) => a + b, 0);
const scores: { [key: string]: number } = {};
for (const [element, count] of Object.entries(elements)) {
scores[element] = Math.round((count / total) * 100);
}
return scores;
}

View File

@@ -36,44 +36,106 @@ interface SolarTermDate {
}
/**
* 간단한 절기 계산 (근사치)
* 실제로는 천문 계산이 필요하지만, 여기서는 근사값 사용
*
* 절기는 매년 비슷한 시기에 오지만 정확한 시간은 천문학적 계산 필요
* 정밀한 절기 계산 (천문학적 계산 기반)
* solarlunar 라이브러리 사용
*/
export function getSolarTermDate(year: number, termIndex: number): SolarTermDate {
// 절기 기준일 (대략적인 날짜)
// 입춘은 대략 2월 4일경, 각 절기는 약 15일 간격
try {
const solarLunar = require('solarlunar');
const baseMonth = [
2, 2, 3, 3, 4, 4, // 입춘~곡우
5, 5, 6, 6, 7, 7, // 입하~대서
8, 8, 9, 9, 10, 10, // 입추~상강
11, 11, 12, 12, 1, 1 // 입동~대한
];
// solarlunar의 절기 데이터 가져오기
// 각 년도의 절기 정보를 계산
const termNames = [
'立春', '雨水', '驚蟄', '春分', '清明', '穀雨',
'立夏', '小滿', '芒種', '夏至', '小暑', '大暑',
'立秋', '處暑', '白露', '秋分', '寒露', '霜降',
'立冬', '小雪', '大雪', '冬至', '小寒', '大寒'
];
const baseDay = [
4, 19, 5, 20, 4, 20, // 입춘~곡우 (2월 4일, 2월 19일...)
5, 21, 6, 21, 7, 23, // 입하~대서
7, 23, 8, 23, 8, 23, // 입추~상강
7, 22, 7, 22, 5, 20 // 입동~대한
];
// 해당 년도의 절기 찾기
// solarlunar는 양력 날짜로 절기 확인 가능
// 각 절기의 대략적인 날짜 범위에서 검색
let month = baseMonth[termIndex];
let day = baseDay[termIndex];
const searchRanges = [
{ month: 2, startDay: 3, endDay: 5 }, // 입춘
{ month: 2, startDay: 18, endDay: 20 }, // 우수
{ month: 3, startDay: 5, endDay: 7 }, // 경칩
{ month: 3, startDay: 20, endDay: 22 }, // 춘분
{ month: 4, startDay: 4, endDay: 6 }, // 청명
{ month: 4, startDay: 19, endDay: 21 }, // 곡우
{ month: 5, startDay: 5, endDay: 7 }, // 입하
{ month: 5, startDay: 20, endDay: 22 }, // 소만
{ month: 6, startDay: 5, endDay: 7 }, // 망종
{ month: 6, startDay: 20, endDay: 22 }, // 하지
{ month: 7, startDay: 6, endDay: 8 }, // 소서
{ month: 7, startDay: 22, endDay: 24 }, // 대서
{ month: 8, startDay: 7, endDay: 9 }, // 입추
{ month: 8, startDay: 22, endDay: 24 }, // 처서
{ month: 9, startDay: 7, endDay: 9 }, // 백로
{ month: 9, startDay: 22, endDay: 24 }, // 추분
{ month: 10, startDay: 7, endDay: 9 }, // 한로
{ month: 10, startDay: 23, endDay: 24 },// 상강
{ month: 11, startDay: 7, endDay: 8 }, // 입동
{ month: 11, startDay: 21, endDay: 23 },// 소설
{ month: 12, startDay: 6, endDay: 8 }, // 대설
{ month: 12, startDay: 21, endDay: 23 },// 동지
{ month: 1, startDay: 5, endDay: 7 }, // 소한
{ month: 1, startDay: 19, endDay: 21 }, // 대한
];
// 대한과 소한은 다음 해 1월이므로 조정
if (termIndex >= 22) {
// 이미 1월로 설정되어 있음
const range = searchRanges[termIndex];
const termName = termNames[termIndex];
// 해당 범위 내에서 절기 찾기
for (let day = range.startDay; day <= range.endDay; day++) {
const lunar = solarLunar.solar2lunar(year, range.month, day);
if (lunar && lunar.term === termName) {
return {
year,
month: range.month,
day,
hour: 0,
minute: 0
};
}
}
// 찾지 못한 경우 중간값 사용
const midDay = Math.floor((range.startDay + range.endDay) / 2);
return {
year,
month: range.month,
day: midDay,
hour: 0,
minute: 0
};
} catch (error) {
console.error('절기 계산 오류:', error);
// 폴백: 기존 근사값 사용
const baseMonth = [
2, 2, 3, 3, 4, 4,
5, 5, 6, 6, 7, 7,
8, 8, 9, 9, 10, 10,
11, 11, 12, 12, 1, 1
];
const baseDay = [
4, 19, 5, 20, 4, 20,
5, 21, 6, 21, 7, 23,
7, 23, 8, 23, 8, 23,
7, 22, 7, 22, 5, 20
];
return {
year,
month: baseMonth[termIndex],
day: baseDay[termIndex],
hour: 0,
minute: 0
};
}
return {
year,
month,
day,
hour: 0,
minute: 0
};
}
/**