카카오 앱 키 설정: - 환경 변수(.env.local)를 통한 안전한 키 관리 - NEXT_PUBLIC_KAKAO_APP_KEY 환경 변수 사용 - layout.tsx에서 환경 변수 읽어서 Kakao SDK 초기화 - .env.local 템플릿 파일 생성 (키 발급 가이드 포함) 음력 변환 정확도 개선: - 24절기 계산 라이브러리 구현 (solar-terms.ts) - 절기 기준 월주 계산으로 정확도 향상 - 입춘, 경칩, 청명 등 12개 월 절기 지원 - getSolarTermMonthBranch() - 절기 기준 월 지지 계산 - getCurrentSolarTerm() - 현재 절기 확인 - 사주 결과 페이지에 절기 정보 표시 대운 시작 나이 정밀 계산: - 절기 기준 대운수 계산 구현 - 양남음녀(순행), 음남양녀(역행) 정확한 일수 계산 - 3일 = 1세 공식 적용 - calculateDaeunStartAge() 함수로 정밀 계산 - 이전 평균 8세 → 실제 계산값 (1~10세 범위) - 대운 섹션에 시작 나이 계산 근거 표시 문서화: - SETUP.md 생성 - 카카오 앱 키 발급 및 설정 가이드 - 절기 기준 사주 계산 설명 - 대운 계산 원리 설명 - 음력 변환 사용법 - 기술 스택 및 개발 환경 사주 결과 페이지 개선: - 절기 정보 표시 (녹색 박스) - 대운 시작 나이 설명 추가 - 사용자에게 계산 원리 투명하게 공개 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
270 lines
11 KiB
TypeScript
270 lines
11 KiB
TypeScript
// 천간 (天干) - 10개
|
|
export const HEAVENLY_STEMS = ['甲', '乙', '丙', '丁', '戊', '己', '庚', '辛', '壬', '癸'] as const;
|
|
export const HEAVENLY_STEMS_KR = ['갑', '을', '병', '정', '무', '기', '경', '신', '임', '계'] as const;
|
|
|
|
// 지지 (地支) - 12개
|
|
export const EARTHLY_BRANCHES = ['子', '丑', '寅', '卯', '辰', '巳', '午', '未', '申', '酉', '戌', '亥'] as const;
|
|
export const EARTHLY_BRANCHES_KR = ['자', '축', '인', '묘', '진', '사', '오', '미', '신', '유', '술', '해'] as const;
|
|
|
|
// 오행 (五行)
|
|
export const FIVE_ELEMENTS = {
|
|
'甲': '木', '乙': '木',
|
|
'丙': '火', '丁': '火',
|
|
'戊': '土', '己': '土',
|
|
'庚': '金', '辛': '金',
|
|
'壬': '水', '癸': '水',
|
|
'寅': '木', '卯': '木',
|
|
'巳': '火', '午': '火',
|
|
'辰': '土', '戌': '土', '丑': '土', '未': '土',
|
|
'申': '金', '酉': '金',
|
|
'子': '水', '亥': '水',
|
|
} as const;
|
|
|
|
export const FIVE_ELEMENTS_KR = {
|
|
'木': '목', '火': '화', '土': '토', '金': '금', '水': '수'
|
|
} as const;
|
|
|
|
// 십성 (十星)
|
|
export const TEN_GODS = {
|
|
same: { yang: '비견', yin: '겁재' }, // 같은 오행
|
|
produce: { yang: '식신', yin: '상관' }, // 내가 생하는 오행
|
|
overcome: { yang: '편재', yin: '정재' }, // 내가 극하는 오행
|
|
overcome_me: { yang: '편관', yin: '정관' }, // 나를 극하는 오행
|
|
produce_me: { yang: '편인', yin: '정인' } // 나를 생하는 오행
|
|
} as const;
|
|
|
|
// 십이운성 (十二運星)
|
|
export const TWELVE_FORTUNES = [
|
|
'장생', '목욕', '관대', '건록', '제왕', '쇠', '병', '사', '묘', '절', '태', '양'
|
|
] as const;
|
|
|
|
// 간지 계산을 위한 기준일 (1900년 1월 1일 = 경자년 정축월 병인일)
|
|
const BASE_YEAR = 1900;
|
|
const BASE_YEAR_STEM = 6; // 庚
|
|
const BASE_YEAR_BRANCH = 0; // 子
|
|
|
|
/**
|
|
* 년도의 간지를 계산
|
|
*/
|
|
export function getYearGanzi(year: number): { stem: string; branch: string; stemKr: string; branchKr: string } {
|
|
const yearDiff = year - BASE_YEAR;
|
|
const stemIndex = (BASE_YEAR_STEM + yearDiff) % 10;
|
|
const branchIndex = (BASE_YEAR_BRANCH + yearDiff) % 12;
|
|
|
|
return {
|
|
stem: HEAVENLY_STEMS[stemIndex],
|
|
branch: EARTHLY_BRANCHES[branchIndex],
|
|
stemKr: HEAVENLY_STEMS_KR[stemIndex],
|
|
branchKr: EARTHLY_BRANCHES_KR[branchIndex]
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 월의 간지를 계산 (절기 기준)
|
|
*/
|
|
export function getMonthGanzi(year: number, month: number, day: number): { stem: string; branch: string; stemKr: string; branchKr: string } {
|
|
// 절기 기준으로 월 지지 계산
|
|
const { getSolarTermMonthBranch } = require('./solar-terms');
|
|
const branchIndex = getSolarTermMonthBranch(year, month, day);
|
|
|
|
// 월 천간 계산 (년간에 따라 달라짐)
|
|
const yearStem = getYearGanzi(year).stem;
|
|
const yearStemIndex = HEAVENLY_STEMS.indexOf(yearStem as any);
|
|
|
|
// 월 천간 공식: (년간 * 2 + 월지지) % 10
|
|
const stemIndex = (yearStemIndex * 2 + branchIndex) % 10;
|
|
|
|
return {
|
|
stem: HEAVENLY_STEMS[stemIndex],
|
|
branch: EARTHLY_BRANCHES[branchIndex],
|
|
stemKr: HEAVENLY_STEMS_KR[stemIndex],
|
|
branchKr: EARTHLY_BRANCHES_KR[branchIndex]
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 일의 간지를 계산 (만세력 기준)
|
|
*/
|
|
export function getDayGanzi(year: number, month: number, day: number): { stem: string; branch: string; stemKr: string; branchKr: string } {
|
|
// 기준일 (1900-01-01) 부터의 일수 계산
|
|
const baseDate = new Date(1900, 0, 1);
|
|
const targetDate = new Date(year, month - 1, day);
|
|
const daysDiff = Math.floor((targetDate.getTime() - baseDate.getTime()) / (1000 * 60 * 60 * 24));
|
|
|
|
// 1900-01-01 = 丙寅일
|
|
const baseDayStem = 2; // 丙
|
|
const baseDayBranch = 2; // 寅
|
|
|
|
const stemIndex = (baseDayStem + daysDiff) % 10;
|
|
const branchIndex = (baseDayBranch + daysDiff) % 12;
|
|
|
|
return {
|
|
stem: HEAVENLY_STEMS[stemIndex < 0 ? stemIndex + 10 : stemIndex],
|
|
branch: EARTHLY_BRANCHES[branchIndex < 0 ? branchIndex + 12 : branchIndex],
|
|
stemKr: HEAVENLY_STEMS_KR[stemIndex < 0 ? stemIndex + 10 : stemIndex],
|
|
branchKr: EARTHLY_BRANCHES_KR[branchIndex < 0 ? branchIndex + 12 : branchIndex]
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 시의 간지를 계산
|
|
*/
|
|
export function getHourGanzi(dayGanzi: { stem: string }, hour: number): { stem: string; branch: string; stemKr: string; branchKr: string } {
|
|
// 시 지지: 자시(23-01)=0, 축시(01-03)=1, ...
|
|
let branchIndex: number;
|
|
|
|
if (hour >= 23 || hour < 1) branchIndex = 0; // 子
|
|
else if (hour >= 1 && hour < 3) branchIndex = 1; // 丑
|
|
else if (hour >= 3 && hour < 5) branchIndex = 2; // 寅
|
|
else if (hour >= 5 && hour < 7) branchIndex = 3; // 卯
|
|
else if (hour >= 7 && hour < 9) branchIndex = 4; // 辰
|
|
else if (hour >= 9 && hour < 11) branchIndex = 5; // 巳
|
|
else if (hour >= 11 && hour < 13) branchIndex = 6; // 午
|
|
else if (hour >= 13 && hour < 15) branchIndex = 7; // 未
|
|
else if (hour >= 15 && hour < 17) branchIndex = 8; // 申
|
|
else if (hour >= 17 && hour < 19) branchIndex = 9; // 酉
|
|
else if (hour >= 19 && hour < 21) branchIndex = 10; // 戌
|
|
else branchIndex = 11; // 亥
|
|
|
|
// 시 천간 계산 (일간에 따라 달라짐)
|
|
const dayStemIndex = HEAVENLY_STEMS.indexOf(dayGanzi.stem as any);
|
|
const stemIndex = (dayStemIndex * 2 + branchIndex) % 10;
|
|
|
|
return {
|
|
stem: HEAVENLY_STEMS[stemIndex],
|
|
branch: EARTHLY_BRANCHES[branchIndex],
|
|
stemKr: HEAVENLY_STEMS_KR[stemIndex],
|
|
branchKr: EARTHLY_BRANCHES_KR[branchIndex]
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 십성 계산
|
|
*/
|
|
export function getTenGod(dayStem: string, targetStem: string, isYang: boolean): string {
|
|
const dayElement = FIVE_ELEMENTS[dayStem as keyof typeof FIVE_ELEMENTS];
|
|
const targetElement = FIVE_ELEMENTS[targetStem as keyof typeof FIVE_ELEMENTS];
|
|
|
|
// 같은 오행
|
|
if (dayElement === targetElement) {
|
|
return isYang ? '비견' : '겁재';
|
|
}
|
|
|
|
// 오행 상생/상극 관계 확인
|
|
const produceMap: { [key: string]: string } = {
|
|
'木': '火', '火': '土', '土': '金', '金': '水', '水': '木'
|
|
};
|
|
const overcomeMap: { [key: string]: string } = {
|
|
'木': '土', '火': '金', '土': '水', '金': '木', '水': '火'
|
|
};
|
|
|
|
// 내가 생하는 오행
|
|
if (produceMap[dayElement] === targetElement) {
|
|
return isYang ? '식신' : '상관';
|
|
}
|
|
|
|
// 내가 극하는 오행
|
|
if (overcomeMap[dayElement] === targetElement) {
|
|
return isYang ? '편재' : '정재';
|
|
}
|
|
|
|
// 나를 극하는 오행
|
|
if (overcomeMap[targetElement] === dayElement) {
|
|
return isYang ? '편관' : '정관';
|
|
}
|
|
|
|
// 나를 생하는 오행
|
|
if (produceMap[targetElement] === dayElement) {
|
|
return isYang ? '편인' : '정인';
|
|
}
|
|
|
|
return '비견';
|
|
}
|
|
|
|
/**
|
|
* 십이운성 계산
|
|
*/
|
|
export function getTwelveFortune(dayStem: string, branch: string): string {
|
|
// 간단한 십이운성 계산 (실제로는 더 복잡함)
|
|
const fortuneMap: { [key: string]: { [key: string]: number } } = {
|
|
'甲': { '亥': 11, '子': 0, '丑': 1, '寅': 2, '卯': 3, '辰': 4, '巳': 5, '午': 6, '未': 7, '申': 8, '酉': 9, '戌': 10 },
|
|
'乙': { '午': 11, '未': 0, '申': 1, '酉': 2, '戌': 3, '亥': 4, '子': 5, '丑': 6, '寅': 7, '卯': 8, '辰': 9, '巳': 10 },
|
|
'丙': { '寅': 11, '卯': 0, '辰': 1, '巳': 2, '午': 3, '未': 4, '申': 5, '酉': 6, '戌': 7, '亥': 8, '子': 9, '丑': 10 },
|
|
'丁': { '酉': 11, '戌': 0, '亥': 1, '子': 2, '丑': 3, '寅': 4, '卯': 5, '辰': 6, '巳': 7, '午': 8, '未': 9, '申': 10 },
|
|
'戊': { '寅': 11, '卯': 0, '辰': 1, '巳': 2, '午': 3, '未': 4, '申': 5, '酉': 6, '戌': 7, '亥': 8, '子': 9, '丑': 10 },
|
|
'己': { '酉': 11, '戌': 0, '亥': 1, '子': 2, '丑': 3, '寅': 4, '卯': 5, '辰': 6, '巳': 7, '午': 8, '未': 9, '申': 10 },
|
|
'庚': { '巳': 11, '午': 0, '未': 1, '申': 2, '酉': 3, '戌': 4, '亥': 5, '子': 6, '丑': 7, '寅': 8, '卯': 9, '辰': 10 },
|
|
'辛': { '子': 11, '丑': 0, '寅': 1, '卯': 2, '辰': 3, '巳': 4, '午': 5, '未': 6, '申': 7, '酉': 8, '戌': 9, '亥': 10 },
|
|
'壬': { '申': 11, '酉': 0, '戌': 1, '亥': 2, '子': 3, '丑': 4, '寅': 5, '卯': 6, '辰': 7, '巳': 8, '午': 9, '未': 10 },
|
|
'癸': { '卯': 11, '辰': 0, '巳': 1, '午': 2, '未': 3, '申': 4, '酉': 5, '戌': 6, '亥': 7, '子': 8, '丑': 9, '寅': 10 }
|
|
};
|
|
|
|
const index = fortuneMap[dayStem as keyof typeof fortuneMap]?.[branch as keyof typeof fortuneMap['甲']] ?? 0;
|
|
return TWELVE_FORTUNES[index];
|
|
}
|
|
|
|
/**
|
|
* 사주팔자 전체 계산
|
|
*/
|
|
export interface SajuData {
|
|
year: { stem: string; branch: string; stemKr: string; branchKr: string; element: string; tenGod: string; fortune: string };
|
|
month: { stem: string; branch: string; stemKr: string; branchKr: string; element: string; tenGod: string; fortune: string };
|
|
day: { stem: string; branch: string; stemKr: string; branchKr: string; element: string; tenGod: string; fortune: string };
|
|
hour?: { stem: string; branch: string; stemKr: string; branchKr: string; element: string; tenGod: string; fortune: string };
|
|
dayStem: string;
|
|
birthDate: { year: number; month: number; day: number; hour?: number };
|
|
gender: 'male' | 'female';
|
|
}
|
|
|
|
export function calculateSaju(
|
|
year: number,
|
|
month: number,
|
|
day: number,
|
|
hour: number | null,
|
|
gender: 'male' | 'female'
|
|
): SajuData {
|
|
const yearGanzi = getYearGanzi(year);
|
|
const monthGanzi = getMonthGanzi(year, month, day);
|
|
const dayGanzi = getDayGanzi(year, month, day);
|
|
const hourGanzi = hour !== null ? getHourGanzi(dayGanzi, hour) : null;
|
|
|
|
const dayStem = dayGanzi.stem;
|
|
const dayStemIndex = HEAVENLY_STEMS.indexOf(dayStem as any);
|
|
const isDayYang = dayStemIndex % 2 === 0;
|
|
|
|
const result: SajuData = {
|
|
year: {
|
|
...yearGanzi,
|
|
element: FIVE_ELEMENTS[yearGanzi.stem as keyof typeof FIVE_ELEMENTS],
|
|
tenGod: getTenGod(dayStem, yearGanzi.stem, HEAVENLY_STEMS.indexOf(yearGanzi.stem as any) % 2 === isDayYang ? true : false),
|
|
fortune: getTwelveFortune(dayStem, yearGanzi.branch)
|
|
},
|
|
month: {
|
|
...monthGanzi,
|
|
element: FIVE_ELEMENTS[monthGanzi.stem as keyof typeof FIVE_ELEMENTS],
|
|
tenGod: getTenGod(dayStem, monthGanzi.stem, HEAVENLY_STEMS.indexOf(monthGanzi.stem as any) % 2 === isDayYang ? true : false),
|
|
fortune: getTwelveFortune(dayStem, monthGanzi.branch)
|
|
},
|
|
day: {
|
|
...dayGanzi,
|
|
element: FIVE_ELEMENTS[dayGanzi.stem as keyof typeof FIVE_ELEMENTS],
|
|
tenGod: '일간',
|
|
fortune: getTwelveFortune(dayStem, dayGanzi.branch)
|
|
},
|
|
dayStem,
|
|
birthDate: { year, month, day, hour: hour ?? undefined },
|
|
gender
|
|
};
|
|
|
|
if (hourGanzi) {
|
|
result.hour = {
|
|
...hourGanzi,
|
|
element: FIVE_ELEMENTS[hourGanzi.stem as keyof typeof FIVE_ELEMENTS],
|
|
tenGod: getTenGod(dayStem, hourGanzi.stem, HEAVENLY_STEMS.indexOf(hourGanzi.stem as any) % 2 === isDayYang ? true : false),
|
|
fortune: getTwelveFortune(dayStem, hourGanzi.branch)
|
|
};
|
|
}
|
|
|
|
return result;
|
|
}
|