Files
saju-web/lib/saju-calculator.ts

725 lines
27 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 === 0) === isDayYang),
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 === 0) === isDayYang),
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 === 0) === isDayYang),
fortune: getTwelveFortune(dayStem, hourGanzi.branch)
};
}
return result;
}
// ============================================================
// 지장간 (藏干) - 각 지지에 숨어있는 천간
// ============================================================
export const HIDDEN_STEMS: { [key: string]: string[] } = {
'子': ['癸'],
'丑': ['己', '癸', '辛'],
'寅': ['甲', '丙', '戊'],
'卯': ['乙'],
'辰': ['戊', '乙', '癸'],
'巳': ['丙', '庚', '戊'],
'午': ['丁', '己'],
'未': ['己', '丁', '乙'],
'申': ['庚', '壬', '戊'],
'酉': ['辛'],
'戌': ['戊', '辛', '丁'],
'亥': ['壬', '甲'],
};
/**
* 지지의 지장간(숨은 천간) 반환
*/
export function getHiddenStems(branch: string): string[] {
return HIDDEN_STEMS[branch] || [];
}
/**
* 4주 전체의 지장간 정보 반환
*/
export function getAllHiddenStems(saju: SajuData): { pillar: string; branch: string; branchKr: string; stems: { stem: string; stemKr: string; element: string; role: string }[] }[] {
const pillars = [
{ pillar: '년주', branch: saju.year.branch, branchKr: saju.year.branchKr },
{ pillar: '월주', branch: saju.month.branch, branchKr: saju.month.branchKr },
{ pillar: '일주', branch: saju.day.branch, branchKr: saju.day.branchKr },
];
if (saju.hour) {
pillars.push({ pillar: '시주', branch: saju.hour.branch, branchKr: saju.hour.branchKr });
}
return pillars.map(p => {
const hidden = getHiddenStems(p.branch);
return {
...p,
stems: hidden.map((stem, idx) => {
const stemIndex = HEAVENLY_STEMS.indexOf(stem as any);
const role = idx === 0 ? '정기(본기)' : idx === 1 ? '중기' : '여기';
return {
stem,
stemKr: HEAVENLY_STEMS_KR[stemIndex],
element: FIVE_ELEMENTS[stem as keyof typeof FIVE_ELEMENTS],
role,
};
}),
};
});
}
// ============================================================
// 지지 상호작용 (合/沖/刑/破/害)
// ============================================================
export interface BranchInteraction {
type: string; // 육합, 삼합, 방합, 충, 형, 파, 해
branches: string[]; // 관련 지지 (한자)
branchesKr: string[]; // 관련 지지 (한글)
pillars: string[]; // 관련 기둥 (년주, 월주 등)
description: string;
resultElement?: string; // 합의 결과 오행 (해당 시)
}
const YUKAP_PAIRS: [string, string, string][] = [
['子', '丑', '土'], ['寅', '亥', '木'], ['卯', '戌', '火'],
['辰', '酉', '金'], ['巳', '申', '水'], ['午', '未', '火'],
];
const SAMHAP_GROUPS: [string, string, string, string][] = [
['申', '子', '辰', '水'], ['亥', '卯', '未', '木'],
['寅', '午', '戌', '火'], ['巳', '酉', '丑', '金'],
];
const BANGHAP_GROUPS: [string, string, string, string][] = [
['寅', '卯', '辰', '木'], ['巳', '午', '未', '火'],
['申', '酉', '戌', '金'], ['亥', '子', '丑', '水'],
];
const CHUNG_PAIRS: [string, string][] = [
['子', '午'], ['丑', '未'], ['寅', '申'],
['卯', '酉'], ['辰', '戌'], ['巳', '亥'],
];
const HYUNG_GROUPS: { branches: string[]; name: string }[] = [
{ branches: ['寅', '巳', '申'], name: '무은지형(無恩之刑)' },
{ branches: ['丑', '戌', '未'], name: '지세지형(恃勢之刑)' },
{ branches: ['子', '卯'], name: '무례지형(無禮之刑)' },
];
const JAHYUNG_BRANCHES = ['辰', '午', '酉', '亥'];
const PA_PAIRS: [string, string][] = [
['子', '酉'], ['丑', '辰'], ['寅', '亥'],
['卯', '午'], ['巳', '申'], ['未', '戌'],
];
const HAE_PAIRS: [string, string][] = [
['子', '未'], ['丑', '午'], ['寅', '巳'],
['卯', '辰'], ['申', '亥'], ['酉', '戌'],
];
const ELEMENT_NAMES_KR: { [key: string]: string } = { '木': '목', '火': '화', '土': '토', '金': '금', '水': '수' };
/**
* 지지 상호작용 분석
*/
export function analyzeBranchInteractions(saju: SajuData): BranchInteraction[] {
const interactions: BranchInteraction[] = [];
// 기둥별 지지 수집
const pillarBranches: { branch: string; pillar: string; branchKr: string }[] = [
{ branch: saju.year.branch, pillar: '년주', branchKr: saju.year.branchKr },
{ branch: saju.month.branch, pillar: '월주', branchKr: saju.month.branchKr },
{ branch: saju.day.branch, pillar: '일주', branchKr: saju.day.branchKr },
];
if (saju.hour) {
pillarBranches.push({ branch: saju.hour.branch, pillar: '시주', branchKr: saju.hour.branchKr });
}
const branches = pillarBranches.map(p => p.branch);
// 육합 (六合) 검사
for (const [a, b, elem] of YUKAP_PAIRS) {
const idxA = branches.indexOf(a);
const idxB = branches.indexOf(b);
if (idxA !== -1 && idxB !== -1) {
interactions.push({
type: '육합(六合)',
branches: [a, b],
branchesKr: [pillarBranches[idxA].branchKr, pillarBranches[idxB].branchKr],
pillars: [pillarBranches[idxA].pillar, pillarBranches[idxB].pillar],
description: `${pillarBranches[idxA].branchKr}${pillarBranches[idxB].branchKr} 육합 → ${ELEMENT_NAMES_KR[elem]}(${elem}) 기운 생성. 조화와 화합의 관계.`,
resultElement: elem,
});
}
}
// 삼합 (三合) 검사
for (const [a, b, c, elem] of SAMHAP_GROUPS) {
const found = [a, b, c].filter(x => branches.includes(x));
if (found.length >= 2) {
const foundPillars = found.map(x => {
const idx = branches.indexOf(x);
return pillarBranches[idx];
});
const isComplete = found.length === 3;
interactions.push({
type: isComplete ? '삼합(三合)' : '반삼합(半三合)',
branches: found,
branchesKr: foundPillars.map(p => p.branchKr),
pillars: foundPillars.map(p => p.pillar),
description: `${foundPillars.map(p => p.branchKr).join('')} ${isComplete ? '삼합' : '반삼합'}${ELEMENT_NAMES_KR[elem]}(${elem})국. ${isComplete ? '강력한 합의 기운.' : '삼합의 기운이 부분적으로 작용.'}`,
resultElement: elem,
});
}
}
// 방합 (方合) 검사
for (const [a, b, c, elem] of BANGHAP_GROUPS) {
const found = [a, b, c].filter(x => branches.includes(x));
if (found.length === 3) {
const foundPillars = found.map(x => {
const idx = branches.indexOf(x);
return pillarBranches[idx];
});
interactions.push({
type: '방합(方合)',
branches: found,
branchesKr: foundPillars.map(p => p.branchKr),
pillars: foundPillars.map(p => p.pillar),
description: `${foundPillars.map(p => p.branchKr).join('')} 방합 → ${ELEMENT_NAMES_KR[elem]}(${elem}) 방국. 매우 강한 오행 기운.`,
resultElement: elem,
});
}
}
// 충 (沖) 검사
for (const [a, b] of CHUNG_PAIRS) {
const idxA = branches.indexOf(a);
const idxB = branches.indexOf(b);
if (idxA !== -1 && idxB !== -1) {
interactions.push({
type: '충(沖)',
branches: [a, b],
branchesKr: [pillarBranches[idxA].branchKr, pillarBranches[idxB].branchKr],
pillars: [pillarBranches[idxA].pillar, pillarBranches[idxB].pillar],
description: `${pillarBranches[idxA].branchKr}${pillarBranches[idxB].branchKr} 충 → 변동, 갈등, 변화의 에너지. ${pillarBranches[idxA].pillar}${pillarBranches[idxB].pillar} 사이의 긴장 관계.`,
});
}
}
// 형 (刑) 검사
for (const group of HYUNG_GROUPS) {
const found = group.branches.filter(x => branches.includes(x));
if (found.length >= 2) {
const foundPillars = found.map(x => {
const idx = branches.indexOf(x);
return pillarBranches[idx];
});
interactions.push({
type: '형(刑)',
branches: found,
branchesKr: foundPillars.map(p => p.branchKr),
pillars: foundPillars.map(p => p.pillar),
description: `${foundPillars.map(p => p.branchKr).join('')} ${group.name} → 시련과 갈등의 기운. 주의가 필요한 관계.`,
});
}
}
// 자형 (自刑) 검사
for (const jb of JAHYUNG_BRANCHES) {
const count = branches.filter(x => x === jb).length;
if (count >= 2) {
const brKr = EARTHLY_BRANCHES_KR[EARTHLY_BRANCHES.indexOf(jb as any)];
interactions.push({
type: '자형(自刑)',
branches: [jb, jb],
branchesKr: [brKr, brKr],
pillars: pillarBranches.filter(p => p.branch === jb).map(p => p.pillar),
description: `${brKr}${brKr} 자형 → 자기 자신과의 갈등, 내면의 갈등 기운.`,
});
}
}
// 파 (破) 검사
for (const [a, b] of PA_PAIRS) {
const idxA = branches.indexOf(a);
const idxB = branches.indexOf(b);
if (idxA !== -1 && idxB !== -1) {
interactions.push({
type: '파(破)',
branches: [a, b],
branchesKr: [pillarBranches[idxA].branchKr, pillarBranches[idxB].branchKr],
pillars: [pillarBranches[idxA].pillar, pillarBranches[idxB].pillar],
description: `${pillarBranches[idxA].branchKr}${pillarBranches[idxB].branchKr} 파 → 관계의 균열, 계획의 차질 가능성.`,
});
}
}
// 해 (害) 검사
for (const [a, b] of HAE_PAIRS) {
const idxA = branches.indexOf(a);
const idxB = branches.indexOf(b);
if (idxA !== -1 && idxB !== -1) {
interactions.push({
type: '해(害)',
branches: [a, b],
branchesKr: [pillarBranches[idxA].branchKr, pillarBranches[idxB].branchKr],
pillars: [pillarBranches[idxA].pillar, pillarBranches[idxB].pillar],
description: `${pillarBranches[idxA].branchKr}${pillarBranches[idxB].branchKr} 해 → 은근한 방해, 원망의 기운.`,
});
}
}
return interactions;
}
// ============================================================
// 신살 (神煞) 계산
// ============================================================
export interface Shinsal {
name: string;
nameHanja: string;
branch: string;
branchKr: string;
pillar: string;
description: string;
}
// 일지 삼합국 기준 신살 매핑
const SAMHAP_GROUP_MAP: { [key: string]: string } = {
'申': '申子辰', '子': '申子辰', '辰': '申子辰',
'寅': '寅午戌', '午': '寅午戌', '戌': '寅午戌',
'巳': '巳酉丑', '酉': '巳酉丑', '丑': '巳酉丑',
'亥': '亥卯未', '卯': '亥卯未', '未': '亥卯未',
};
const YEOKMA_MAP: { [key: string]: string } = {
'申子辰': '寅', '寅午戌': '申', '巳酉丑': '亥', '亥卯未': '巳',
};
const DOHWA_MAP: { [key: string]: string } = {
'申子辰': '酉', '寅午戌': '卯', '巳酉丑': '午', '亥卯未': '子',
};
const HWAGAE_MAP: { [key: string]: string } = {
'申子辰': '辰', '寅午戌': '戌', '巳酉丑': '丑', '亥卯未': '未',
};
// 천을귀인 (天乙貴人) - 일간 기준
const CHEONUL_MAP: { [key: string]: string[] } = {
'甲': ['丑', '未'], '乙': ['子', '申'], '丙': ['亥', '酉'], '丁': ['亥', '酉'],
'戊': ['丑', '未'], '己': ['子', '申'], '庚': ['丑', '未'], '辛': ['寅', '午'],
'壬': ['卯', '巳'], '癸': ['卯', '巳'],
};
// 문창귀인 (文昌貴人) - 일간 기준
const MUNCHANG_MAP: { [key: string]: string } = {
'甲': '巳', '乙': '午', '丙': '申', '丁': '酉',
'戊': '申', '己': '酉', '庚': '亥', '辛': '子',
'壬': '寅', '癸': '卯',
};
// 천덕귀인 (天德貴人) - 월지 기준
const CHEONDUK_MAP: { [key: string]: string } = {
'寅': '丁', '卯': '申', '辰': '壬', '巳': '辛',
'午': '亥', '未': '甲', '申': '癸', '酉': '寅',
'戌': '丙', '亥': '乙', '子': '巳', '丑': '庚',
};
/**
* 신살 계산
*/
export function calculateShinsal(saju: SajuData): Shinsal[] {
const result: Shinsal[] = [];
const dayBranch = saju.day.branch;
const dayStem = saju.dayStem;
const monthBranch = saju.month.branch;
// 4주의 지지 수집
const pillarBranches: { branch: string; branchKr: string; pillar: string }[] = [
{ branch: saju.year.branch, branchKr: saju.year.branchKr, pillar: '년주' },
{ branch: saju.month.branch, branchKr: saju.month.branchKr, pillar: '월주' },
{ branch: saju.day.branch, branchKr: saju.day.branchKr, pillar: '일주' },
];
if (saju.hour) {
pillarBranches.push({ branch: saju.hour.branch, branchKr: saju.hour.branchKr, pillar: '시주' });
}
const group = SAMHAP_GROUP_MAP[dayBranch];
// 역마살
if (group) {
const yeokma = YEOKMA_MAP[group];
for (const pb of pillarBranches) {
if (pb.branch === yeokma && pb.pillar !== '일주') {
result.push({
name: '역마살', nameHanja: '驛馬殺', branch: yeokma,
branchKr: pb.branchKr, pillar: pb.pillar,
description: '이동, 변동, 해외, 출장이 많은 기운. 활동적이고 한 곳에 머물지 못하는 성향.',
});
}
}
// 도화살
const dohwa = DOHWA_MAP[group];
for (const pb of pillarBranches) {
if (pb.branch === dohwa && pb.pillar !== '일주') {
result.push({
name: '도화살', nameHanja: '桃花殺', branch: dohwa,
branchKr: pb.branchKr, pillar: pb.pillar,
description: '매력, 인기, 예술적 감각. 이성에게 끌리는 기운이 강하며 대인관계가 화려함.',
});
}
}
// 화개살
const hwagae = HWAGAE_MAP[group];
for (const pb of pillarBranches) {
if (pb.branch === hwagae && pb.pillar !== '일주') {
result.push({
name: '화개살', nameHanja: '華蓋殺', branch: hwagae,
branchKr: pb.branchKr, pillar: pb.pillar,
description: '학문, 종교, 예술에 심취하는 기운. 고독을 즐기며 정신적 세계에 몰두하는 성향.',
});
}
}
}
// 천을귀인
const cheonulBranches = CHEONUL_MAP[dayStem] || [];
for (const pb of pillarBranches) {
if (cheonulBranches.includes(pb.branch) && pb.pillar !== '일주') {
result.push({
name: '천을귀인', nameHanja: '天乙貴人', branch: pb.branch,
branchKr: pb.branchKr, pillar: pb.pillar,
description: '위기에서 귀인의 도움을 받는 길한 기운. 어려울 때 도움을 주는 사람이 나타남.',
});
}
}
// 문창귀인
const munchangBranch = MUNCHANG_MAP[dayStem];
if (munchangBranch) {
for (const pb of pillarBranches) {
if (pb.branch === munchangBranch && pb.pillar !== '일주') {
result.push({
name: '문창귀인', nameHanja: '文昌貴人', branch: pb.branch,
branchKr: pb.branchKr, pillar: pb.pillar,
description: '학문, 시험, 문서에 유리한 기운. 공부를 잘하며 시험운이 좋음.',
});
}
}
}
// 천덕귀인 (월지 기준, 천간에서 확인)
const cheondukStem = CHEONDUK_MAP[monthBranch];
if (cheondukStem) {
const allStems = [
{ stem: saju.year.stem, pillar: '년주' },
{ stem: saju.day.stem, pillar: '일주' },
];
if (saju.hour) allStems.push({ stem: saju.hour.stem, pillar: '시주' });
for (const ps of allStems) {
if (ps.stem === cheondukStem) {
const stemIdx = HEAVENLY_STEMS.indexOf(ps.stem as any);
result.push({
name: '천덕귀인', nameHanja: '天德貴人', branch: monthBranch,
branchKr: EARTHLY_BRANCHES_KR[EARTHLY_BRANCHES.indexOf(monthBranch as any)],
pillar: ps.pillar,
description: '하늘의 덕을 받는 기운. 재난을 피하고 복을 받는 길신 중의 길신.',
});
}
}
}
return result;
}
// ============================================================
// 공망 (空亡) 계산
// ============================================================
/**
* 60갑자에서 일주의 순(旬)을 찾아 공망 지지 2개를 반환
*/
export function calculateGongmang(dayStem: string, dayBranch: string): { branches: string[]; branchesKr: string[]; description: string } {
const stemIdx = HEAVENLY_STEMS.indexOf(dayStem as any);
const branchIdx = EARTHLY_BRANCHES.indexOf(dayBranch as any);
// 60갑자에서 해당 순(旬)의 시작점 = 천간이 甲인 지점
// 순의 시작 지지 인덱스 = (branchIdx - stemIdx + 12) % 12
const startBranchIdx = (branchIdx - stemIdx + 120) % 12;
// 공망 = 순에 포함되지 않는 2개의 지지
// 순은 10개의 간지 → 10개의 지지 사용, 2개가 남음
const gongmang1Idx = (startBranchIdx + 10) % 12;
const gongmang2Idx = (startBranchIdx + 11) % 12;
const branch1 = EARTHLY_BRANCHES[gongmang1Idx];
const branch2 = EARTHLY_BRANCHES[gongmang2Idx];
const branchKr1 = EARTHLY_BRANCHES_KR[gongmang1Idx];
const branchKr2 = EARTHLY_BRANCHES_KR[gongmang2Idx];
return {
branches: [branch1, branch2],
branchesKr: [branchKr1, branchKr2],
description: `${branchKr1}(${branch1}${branchKr2}(${branch2}) 공망 → 해당 지지의 기운이 비어있어 허무하거나 집착이 없는 영역. 오히려 초월적 능력이 될 수 있음.`,
};
}