사주 기능 이식 & 로그인, 유저 페이지 Supabase 연동 & 토스 페이먼츠 결제 연동 & 사주 심층 분석을 위한 기능 분리
This commit is contained in:
386
lib/ai-interpretation.ts
Normal file
386
lib/ai-interpretation.ts
Normal file
@@ -0,0 +1,386 @@
|
||||
|
||||
import {
|
||||
SajuData, FIVE_ELEMENTS, HEAVENLY_STEMS,
|
||||
getHiddenStems, getAllHiddenStems,
|
||||
analyzeBranchInteractions, calculateShinsal, calculateGongmang,
|
||||
getYearGanzi, FIVE_ELEMENTS_KR, EARTHLY_BRANCHES_KR, EARTHLY_BRANCHES,
|
||||
BranchInteraction, Shinsal,
|
||||
} from './saju-calculator';
|
||||
import { DaeunPillar } from './daeun-calculator';
|
||||
|
||||
// ============================================================
|
||||
// 오행 밸런스 정밀 분석 (가중치 적용)
|
||||
// ============================================================
|
||||
|
||||
export interface ElementBalance {
|
||||
木: number;
|
||||
火: number;
|
||||
土: number;
|
||||
金: number;
|
||||
水: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 가중치 적용 오행 점수 계산
|
||||
* - 천간: 1.0
|
||||
* - 지지 본기(정기): 1.0
|
||||
* - 지장간 중기: 0.5
|
||||
* - 지장간 여기: 0.3
|
||||
*/
|
||||
export function calculateDetailedElementBalance(saju: SajuData): ElementBalance {
|
||||
const balance: ElementBalance = { 木: 0, 火: 0, 土: 0, 金: 0, 水: 0 };
|
||||
|
||||
// 천간 오행 (각 1.0)
|
||||
const stems = [saju.year.stem, saju.month.stem, saju.day.stem];
|
||||
if (saju.hour) stems.push(saju.hour.stem);
|
||||
|
||||
for (const stem of stems) {
|
||||
const elem = FIVE_ELEMENTS[stem as keyof typeof FIVE_ELEMENTS] as keyof ElementBalance;
|
||||
if (elem) balance[elem] += 1.0;
|
||||
}
|
||||
|
||||
// 지지 지장간 (본기 1.0, 중기 0.5, 여기 0.3)
|
||||
const branches = [saju.year.branch, saju.month.branch, saju.day.branch];
|
||||
if (saju.hour) branches.push(saju.hour.branch);
|
||||
|
||||
const weights = [1.0, 0.5, 0.3];
|
||||
for (const branch of branches) {
|
||||
const hidden = getHiddenStems(branch);
|
||||
for (let i = 0; i < hidden.length; i++) {
|
||||
const elem = FIVE_ELEMENTS[hidden[i] as keyof typeof FIVE_ELEMENTS] as keyof ElementBalance;
|
||||
if (elem) balance[elem] += weights[i] || 0.3;
|
||||
}
|
||||
}
|
||||
|
||||
// 소수점 둘째 자리로 반올림
|
||||
for (const key of Object.keys(balance) as (keyof ElementBalance)[]) {
|
||||
balance[key] = Math.round(balance[key] * 100) / 100;
|
||||
}
|
||||
|
||||
return balance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 오행 비율(%) 계산
|
||||
*/
|
||||
export function calculateElementScore(saju: SajuData): { [key: string]: number } {
|
||||
const balance = calculateDetailedElementBalance(saju);
|
||||
const total = Object.values(balance).reduce((a, b) => a + b, 0);
|
||||
|
||||
const scores: { [key: string]: number } = {};
|
||||
for (const [element, value] of Object.entries(balance)) {
|
||||
scores[element] = total > 0 ? Math.round((value / total) * 100) : 0;
|
||||
}
|
||||
|
||||
return scores;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 신강/신약 자동 판단
|
||||
// ============================================================
|
||||
|
||||
export interface DayMasterStrength {
|
||||
result: '신강' | '신약' | '중화';
|
||||
score: number;
|
||||
reasons: string[];
|
||||
}
|
||||
|
||||
const PRODUCE_MAP: { [key: string]: string } = {
|
||||
'木': '火', '火': '土', '土': '金', '金': '水', '水': '木',
|
||||
};
|
||||
|
||||
function getProducingElement(elem: string): string {
|
||||
for (const [k, v] of Object.entries(PRODUCE_MAP)) {
|
||||
if (v === elem) return k;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 신강/신약 판단
|
||||
*/
|
||||
export function analyzeDayMasterStrength(saju: SajuData): DayMasterStrength {
|
||||
const dayStem = saju.dayStem;
|
||||
const dayElement = FIVE_ELEMENTS[dayStem as keyof typeof FIVE_ELEMENTS];
|
||||
const producingElement = getProducingElement(dayElement);
|
||||
const reasons: string[] = [];
|
||||
let score = 0;
|
||||
|
||||
// 1. 월령 득령 확인
|
||||
const monthBranch = saju.month.branch;
|
||||
const monthHidden = getHiddenStems(monthBranch);
|
||||
const monthMainElement = FIVE_ELEMENTS[monthHidden[0] as keyof typeof FIVE_ELEMENTS];
|
||||
|
||||
if (monthMainElement === dayElement) {
|
||||
score += 3;
|
||||
reasons.push(`월령 득령: 월지 ${saju.month.branchKr}(${monthBranch})의 본기가 일간과 같은 ${FIVE_ELEMENTS_KR[dayElement as keyof typeof FIVE_ELEMENTS_KR]}으로 강한 힘을 받음`);
|
||||
} else if (monthMainElement === producingElement) {
|
||||
score += 2;
|
||||
reasons.push(`월령 득령: 월지 ${saju.month.branchKr}(${monthBranch})의 본기가 일간을 생하는 ${FIVE_ELEMENTS_KR[producingElement as keyof typeof FIVE_ELEMENTS_KR]}으로 힘을 받음`);
|
||||
} else {
|
||||
score -= 2;
|
||||
reasons.push(`월령 실령: 월지 ${saju.month.branchKr}(${monthBranch})의 본기가 일간을 돕지 않음`);
|
||||
}
|
||||
|
||||
// 2. 통근 확인
|
||||
const allBranches = [saju.year.branch, saju.month.branch, saju.day.branch];
|
||||
if (saju.hour) allBranches.push(saju.hour.branch);
|
||||
|
||||
let rootCount = 0;
|
||||
for (const branch of allBranches) {
|
||||
const hidden = getHiddenStems(branch);
|
||||
for (const h of hidden) {
|
||||
const hElem = FIVE_ELEMENTS[h as keyof typeof FIVE_ELEMENTS];
|
||||
if (hElem === dayElement || hElem === producingElement) {
|
||||
rootCount++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (rootCount >= 3) {
|
||||
score += 2;
|
||||
reasons.push(`통근 강함: ${rootCount}개 지지에서 일간의 뿌리를 찾음`);
|
||||
} else if (rootCount >= 2) {
|
||||
score += 1;
|
||||
reasons.push(`통근 보통: ${rootCount}개 지지에서 일간의 뿌리를 찾음`);
|
||||
} else {
|
||||
score -= 1;
|
||||
reasons.push(`통근 약함: ${rootCount}개 지지에서만 일간의 뿌리를 찾음`);
|
||||
}
|
||||
|
||||
// 3. 투출 확인
|
||||
const allStems = [saju.year.stem, saju.month.stem];
|
||||
if (saju.hour) allStems.push(saju.hour.stem);
|
||||
|
||||
let helpingStemCount = 0;
|
||||
for (const stem of allStems) {
|
||||
const stemElem = FIVE_ELEMENTS[stem as keyof typeof FIVE_ELEMENTS];
|
||||
if (stemElem === dayElement || stemElem === producingElement) {
|
||||
helpingStemCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (helpingStemCount >= 2) {
|
||||
score += 2;
|
||||
reasons.push(`투출 강함: 천간에 비겁/인성이 ${helpingStemCount}개 있어 일간을 도움`);
|
||||
} else if (helpingStemCount === 1) {
|
||||
score += 1;
|
||||
reasons.push(`투출 보통: 천간에 비겁/인성이 1개 있음`);
|
||||
} else {
|
||||
score -= 1;
|
||||
reasons.push(`투출 없음: 천간에 일간을 돕는 비겁/인성이 없음`);
|
||||
}
|
||||
|
||||
// 4. 오행 비율 기반 조력 분석
|
||||
const balance = calculateDetailedElementBalance(saju);
|
||||
const helpingScore = balance[dayElement as keyof ElementBalance] + balance[producingElement as keyof ElementBalance];
|
||||
const drainingScore = Object.entries(balance)
|
||||
.filter(([k]) => k !== dayElement && k !== producingElement)
|
||||
.reduce((sum, [, v]) => sum + v, 0);
|
||||
|
||||
if (helpingScore > drainingScore * 1.3) {
|
||||
score += 1;
|
||||
reasons.push(`오행 비율: 비겁+인성(${helpingScore.toFixed(1)}) > 식상+재관(${drainingScore.toFixed(1)}) → 일간 세력 우세`);
|
||||
} else if (drainingScore > helpingScore * 1.3) {
|
||||
score -= 1;
|
||||
reasons.push(`오행 비율: 식상+재관(${drainingScore.toFixed(1)}) > 비겁+인성(${helpingScore.toFixed(1)}) → 일간 세력 열세`);
|
||||
}
|
||||
|
||||
let result: '신강' | '신약' | '중화';
|
||||
if (score >= 3) result = '신강';
|
||||
else if (score <= -2) result = '신약';
|
||||
else result = '중화';
|
||||
|
||||
return { result, score, reasons };
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 용신 (用神) 추정
|
||||
// ============================================================
|
||||
|
||||
export interface YongShinResult {
|
||||
yongShin: string;
|
||||
yongShinKr: string;
|
||||
heeShin: string;
|
||||
heeShinKr: string;
|
||||
giShin: string;
|
||||
giShinKr: string;
|
||||
explanation: string;
|
||||
}
|
||||
|
||||
const OVERCOME_MAP: { [key: string]: string } = {
|
||||
'木': '土', '火': '金', '土': '水', '金': '木', '水': '火',
|
||||
};
|
||||
|
||||
function getOvercomingMe(elem: string): string {
|
||||
for (const [k, v] of Object.entries(OVERCOME_MAP)) {
|
||||
if (v === elem) return k;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
export function estimateYongShin(saju: SajuData, strength: DayMasterStrength): YongShinResult {
|
||||
const dayElement = FIVE_ELEMENTS[saju.dayStem as keyof typeof FIVE_ELEMENTS];
|
||||
const balance = calculateDetailedElementBalance(saju);
|
||||
|
||||
const producingMe = getProducingElement(dayElement); // 인성
|
||||
const myProduct = PRODUCE_MAP[dayElement]; // 식상
|
||||
const myOvercome = OVERCOME_MAP[dayElement]; // 재성
|
||||
const overcomeMe = getOvercomingMe(dayElement); // 관살
|
||||
|
||||
const kr = (e: string) => FIVE_ELEMENTS_KR[e as keyof typeof FIVE_ELEMENTS_KR] || e;
|
||||
|
||||
if (strength.result === '신강') {
|
||||
const candidates = [
|
||||
{ elem: myProduct, score: balance[myProduct as keyof ElementBalance], name: '식상' },
|
||||
{ elem: myOvercome, score: balance[myOvercome as keyof ElementBalance], name: '재성' },
|
||||
{ elem: overcomeMe, score: balance[overcomeMe as keyof ElementBalance], name: '관살' },
|
||||
];
|
||||
candidates.sort((a, b) => a.score - b.score);
|
||||
const yong = candidates[0];
|
||||
const hee = candidates[1];
|
||||
|
||||
return {
|
||||
yongShin: yong.elem, yongShinKr: kr(yong.elem),
|
||||
heeShin: hee.elem, heeShinKr: kr(hee.elem),
|
||||
giShin: dayElement, giShinKr: kr(dayElement),
|
||||
explanation: `신강한 사주로 일간의 힘이 넘치므로 ${yong.name}(${kr(yong.elem)}) 기운을 용신으로 삼아 기운을 설기(泄氣)하거나 제어해야 합니다. ${hee.name}(${kr(hee.elem)})이 희신으로 보조합니다.`,
|
||||
};
|
||||
} else if (strength.result === '신약') {
|
||||
const candidates = [
|
||||
{ elem: producingMe, score: balance[producingMe as keyof ElementBalance], name: '인성' },
|
||||
{ elem: dayElement, score: balance[dayElement as keyof ElementBalance], name: '비겁' },
|
||||
];
|
||||
candidates.sort((a, b) => a.score - b.score);
|
||||
const yong = candidates[0];
|
||||
const hee = candidates[1];
|
||||
|
||||
return {
|
||||
yongShin: yong.elem, yongShinKr: kr(yong.elem),
|
||||
heeShin: hee.elem, heeShinKr: kr(hee.elem),
|
||||
giShin: overcomeMe, giShinKr: kr(overcomeMe),
|
||||
explanation: `신약한 사주로 일간의 힘이 부족하므로 ${yong.name}(${kr(yong.elem)}) 기운을 용신으로 삼아 일간을 돕고 힘을 보충해야 합니다. ${hee.name}(${kr(hee.elem)})이 희신으로 보조합니다.`,
|
||||
};
|
||||
} else {
|
||||
const entries = Object.entries(balance) as [string, number][];
|
||||
entries.sort((a, b) => a[1] - b[1]);
|
||||
const yong = entries[0];
|
||||
const hee = entries[1];
|
||||
const gi = entries[entries.length - 1];
|
||||
|
||||
return {
|
||||
yongShin: yong[0], yongShinKr: kr(yong[0]),
|
||||
heeShin: hee[0], heeShinKr: kr(hee[0]),
|
||||
giShin: gi[0], giShinKr: kr(gi[0]),
|
||||
explanation: `중화에 가까운 사주로 오행이 비교적 균형을 이루고 있습니다. 가장 부족한 ${kr(yong[0])}(${yong[0]}) 기운을 보충하면 더욱 좋아집니다.`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 세운 (歲運) 계산
|
||||
// ============================================================
|
||||
|
||||
export interface SeunInfo {
|
||||
stem: string;
|
||||
branch: string;
|
||||
stemKr: string;
|
||||
branchKr: string;
|
||||
element: string;
|
||||
elementKr: string;
|
||||
year: number;
|
||||
interactions: BranchInteraction[];
|
||||
}
|
||||
|
||||
export function calculateSeun(year: number, saju: SajuData): SeunInfo {
|
||||
const ganzi = getYearGanzi(year);
|
||||
const element = FIVE_ELEMENTS[ganzi.stem as keyof typeof FIVE_ELEMENTS];
|
||||
|
||||
const seunBranch = ganzi.branch;
|
||||
const seunBranchKr = ganzi.branchKr;
|
||||
|
||||
const interactions: BranchInteraction[] = [];
|
||||
const pillarBranches = [
|
||||
{ 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 CHUNG: [string, string][] = [
|
||||
['子', '午'], ['丑', '未'], ['寅', '申'], ['卯', '酉'], ['辰', '戌'], ['巳', '亥'],
|
||||
];
|
||||
for (const [a, b] of CHUNG) {
|
||||
for (const pb of pillarBranches) {
|
||||
if ((seunBranch === a && pb.branch === b) || (seunBranch === b && pb.branch === a)) {
|
||||
interactions.push({
|
||||
type: '충(沖)', branches: [seunBranch, pb.branch],
|
||||
branchesKr: [seunBranchKr, pb.branchKr],
|
||||
pillars: ['세운', pb.pillar],
|
||||
description: `세운 ${seunBranchKr}와 ${pb.pillar} ${pb.branchKr}가 충 → 해당 영역에 변동과 변화가 예상됨.`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const YUKAP: [string, string, string][] = [
|
||||
['子', '丑', '土'], ['寅', '亥', '木'], ['卯', '戌', '火'],
|
||||
['辰', '酉', '金'], ['巳', '申', '水'], ['午', '未', '火'],
|
||||
];
|
||||
for (const [a, b, elem] of YUKAP) {
|
||||
for (const pb of pillarBranches) {
|
||||
if ((seunBranch === a && pb.branch === b) || (seunBranch === b && pb.branch === a)) {
|
||||
interactions.push({
|
||||
type: '합(合)', branches: [seunBranch, pb.branch],
|
||||
branchesKr: [seunBranchKr, pb.branchKr],
|
||||
pillars: ['세운', pb.pillar],
|
||||
description: `세운 ${seunBranchKr}와 ${pb.pillar} ${pb.branchKr}가 합 → 해당 영역에 조화와 좋은 인연이 기대됨.`,
|
||||
resultElement: elem,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
stem: ganzi.stem, branch: ganzi.branch,
|
||||
stemKr: ganzi.stemKr, branchKr: ganzi.branchKr,
|
||||
element, elementKr: FIVE_ELEMENTS_KR[element as keyof typeof FIVE_ELEMENTS_KR],
|
||||
year, interactions,
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 종합 분석 데이터 구조체
|
||||
// ============================================================
|
||||
|
||||
export interface SajuAnalysis {
|
||||
elementBalance: ElementBalance;
|
||||
elementScores: { [key: string]: number };
|
||||
dayMasterStrength: DayMasterStrength;
|
||||
yongShin: YongShinResult;
|
||||
branchInteractions: BranchInteraction[];
|
||||
shinsal: Shinsal[];
|
||||
gongmang: { branches: string[]; branchesKr: string[]; description: string };
|
||||
seun: SeunInfo;
|
||||
hiddenStems: { pillar: string; branch: string; branchKr: string; stems: { stem: string; stemKr: string; element: string; role: string }[] }[];
|
||||
}
|
||||
|
||||
export function performFullAnalysis(saju: SajuData, currentYear: number = new Date().getFullYear()): SajuAnalysis {
|
||||
const elementBalance = calculateDetailedElementBalance(saju);
|
||||
const elementScores = calculateElementScore(saju);
|
||||
const dayMasterStrength = analyzeDayMasterStrength(saju);
|
||||
const yongShin = estimateYongShin(saju, dayMasterStrength);
|
||||
const branchInteractions = analyzeBranchInteractions(saju);
|
||||
const shinsal = calculateShinsal(saju);
|
||||
const gongmang = calculateGongmang(saju.dayStem, saju.day.branch);
|
||||
const seun = calculateSeun(currentYear, saju);
|
||||
const hiddenStems = getAllHiddenStems(saju);
|
||||
|
||||
return {
|
||||
elementBalance, elementScores, dayMasterStrength, yongShin,
|
||||
branchInteractions, shinsal, gongmang, seun, hiddenStems,
|
||||
};
|
||||
}
|
||||
188
lib/daeun-calculator.ts
Normal file
188
lib/daeun-calculator.ts
Normal file
@@ -0,0 +1,188 @@
|
||||
import { HEAVENLY_STEMS, EARTHLY_BRANCHES, HEAVENLY_STEMS_KR, EARTHLY_BRANCHES_KR } from './saju-calculator';
|
||||
|
||||
/**
|
||||
* 대운 (大運) 정보
|
||||
*/
|
||||
export interface DaeunPillar {
|
||||
age: number; // 시작 나이
|
||||
startYear: number; // 시작 년도
|
||||
endYear: number; // 끝 년도
|
||||
stem: string; // 천간
|
||||
branch: string; // 지지
|
||||
stemKr: string; // 천간 한글
|
||||
branchKr: string; // 지지 한글
|
||||
}
|
||||
|
||||
/**
|
||||
* 대운 시작 나이 정밀 계산
|
||||
* @param birthYear 생년
|
||||
* @param birthMonth 생월
|
||||
* @param birthDay 생일
|
||||
* @param gender 성별
|
||||
* @param isYangYear 양년 여부
|
||||
* @returns 대운 시작 나이
|
||||
*/
|
||||
function calculateDaeunStartAge(
|
||||
birthYear: number,
|
||||
birthMonth: number,
|
||||
birthDay: number,
|
||||
gender: 'male' | 'female',
|
||||
isYangYear: boolean
|
||||
): number {
|
||||
const { getDaysToNextSolarTerm, getCurrentSolarTerm, getSolarTermDate } = require('./solar-terms');
|
||||
|
||||
// 양남음녀는 순행 (다음 절기까지), 음남양녀는 역행 (이전 절기부터)
|
||||
let days: number;
|
||||
|
||||
if ((gender === 'male' && isYangYear) || (gender === 'female' && !isYangYear)) {
|
||||
// 순행: 생일부터 다음 절기까지의 일수
|
||||
days = getDaysToNextSolarTerm(birthYear, birthMonth, birthDay);
|
||||
} else {
|
||||
// 역행: 이전 절기부터 생일까지의 일수
|
||||
const currentTerm = getCurrentSolarTerm(birthYear, birthMonth, birthDay);
|
||||
const termDate = getSolarTermDate(birthYear, currentTerm);
|
||||
|
||||
let termYear = termDate.year;
|
||||
let termMonth = termDate.month;
|
||||
|
||||
// 대한, 소한 처리
|
||||
if (currentTerm >= 22 && birthMonth >= 2) {
|
||||
termYear = birthYear;
|
||||
} else if (currentTerm >= 22) {
|
||||
termYear = birthYear - 1;
|
||||
}
|
||||
|
||||
const termDateObj = new Date(termYear, termMonth - 1, termDate.day);
|
||||
const birthDateObj = new Date(birthYear, birthMonth - 1, birthDay);
|
||||
|
||||
const diffTime = birthDateObj.getTime() - termDateObj.getTime();
|
||||
days = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||
}
|
||||
|
||||
// 3일 = 1세 (대운수)
|
||||
// 정확히는 3일당 1세이지만, 일수를 3으로 나눈 몫
|
||||
const startAge = Math.floor(days / 3);
|
||||
|
||||
// 최소 1세, 최대 10세로 제한
|
||||
return Math.max(1, Math.min(10, startAge));
|
||||
}
|
||||
|
||||
/**
|
||||
* 대운 계산
|
||||
* @param birthYear 생년
|
||||
* @param birthMonth 생월
|
||||
* @param birthDay 생일
|
||||
* @param gender 성별
|
||||
* @param monthStem 월주 천간 인덱스
|
||||
* @param monthBranch 월주 지지 인덱스
|
||||
* @returns 대운 배열 (10년 단위)
|
||||
*/
|
||||
export function calculateDaeun(
|
||||
birthYear: number,
|
||||
birthMonth: number,
|
||||
birthDay: number,
|
||||
gender: 'male' | 'female',
|
||||
monthStem: string,
|
||||
monthBranch: string
|
||||
): DaeunPillar[] {
|
||||
const monthStemIndex = HEAVENLY_STEMS.indexOf(monthStem as any);
|
||||
const monthBranchIndex = EARTHLY_BRANCHES.indexOf(monthBranch as any);
|
||||
|
||||
if (monthStemIndex === -1 || monthBranchIndex === -1) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// 양남음녀(陽男陰女)는 순행, 음남양녀(陰男陽女)는 역행
|
||||
const yearStemIndex = (birthYear - 1900 + 6) % 10;
|
||||
const isYangYear = yearStemIndex % 2 === 0; // 양년
|
||||
|
||||
let isForward: boolean;
|
||||
if (gender === 'male') {
|
||||
isForward = isYangYear; // 양남: 순행, 음남: 역행
|
||||
} else {
|
||||
isForward = !isYangYear; // 양녀: 역행, 음녀: 순행
|
||||
}
|
||||
|
||||
// 대운 시작 나이 정밀 계산 (절기 기준)
|
||||
const startAge = calculateDaeunStartAge(birthYear, birthMonth, birthDay, gender, isYangYear);
|
||||
|
||||
const daeunList: DaeunPillar[] = [];
|
||||
|
||||
for (let i = 0; i < 8; i++) {
|
||||
const age = startAge + (i * 10);
|
||||
const startYear = birthYear + age;
|
||||
const endYear = startYear + 9;
|
||||
|
||||
let stemIndex: number;
|
||||
let branchIndex: number;
|
||||
|
||||
if (isForward) {
|
||||
// 순행: 월주에서 증가
|
||||
stemIndex = (monthStemIndex + i + 1) % 10;
|
||||
branchIndex = (monthBranchIndex + i + 1) % 12;
|
||||
} else {
|
||||
// 역행: 월주에서 감소
|
||||
stemIndex = (monthStemIndex - i - 1 + 100) % 10;
|
||||
branchIndex = (monthBranchIndex - i - 1 + 120) % 12;
|
||||
}
|
||||
|
||||
daeunList.push({
|
||||
age,
|
||||
startYear,
|
||||
endYear,
|
||||
stem: HEAVENLY_STEMS[stemIndex],
|
||||
branch: EARTHLY_BRANCHES[branchIndex],
|
||||
stemKr: HEAVENLY_STEMS_KR[stemIndex],
|
||||
branchKr: EARTHLY_BRANCHES_KR[branchIndex]
|
||||
});
|
||||
}
|
||||
|
||||
return daeunList;
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 대운 찾기
|
||||
* @param daeunList 대운 목록
|
||||
* @param currentYear 현재 년도
|
||||
*/
|
||||
export function getCurrentDaeun(daeunList: DaeunPillar[], currentYear: number): DaeunPillar | null {
|
||||
for (const daeun of daeunList) {
|
||||
if (currentYear >= daeun.startYear && currentYear <= daeun.endYear) {
|
||||
return daeun;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 대운 해석
|
||||
* @param daeun 대운 정보
|
||||
* @param dayStem 일간
|
||||
*/
|
||||
export function getDaeunDescription(daeun: DaeunPillar, dayStem: string): string {
|
||||
const age = daeun.age;
|
||||
const ganzi = `${daeun.stem}${daeun.branch}`;
|
||||
|
||||
let description = `${age}세부터 ${age + 9}세까지의 10년은 ${daeun.stemKr}${daeun.branchKr}(${ganzi}) 대운입니다. `;
|
||||
|
||||
// 대운 천간과 일간의 관계에 따른 기본 해석
|
||||
const stemIndex = HEAVENLY_STEMS.indexOf(daeun.stem as any);
|
||||
|
||||
if (age < 20) {
|
||||
description += '청소년기로 학업과 기초를 다지는 시기입니다. ';
|
||||
} else if (age < 40) {
|
||||
description += '성장과 발전의 시기로 사회활동이 왕성한 때입니다. ';
|
||||
} else if (age < 60) {
|
||||
description += '안정과 성숙의 시기로 경험이 쌓이는 때입니다. ';
|
||||
} else {
|
||||
description += '원숙한 시기로 인생의 지혜를 나누는 때입니다. ';
|
||||
}
|
||||
|
||||
if (stemIndex % 2 === 0) {
|
||||
description += '적극적이고 외향적인 활동이 유리합니다.';
|
||||
} else {
|
||||
description += '차분하고 내실을 다지는 것이 좋습니다.';
|
||||
}
|
||||
|
||||
return description;
|
||||
}
|
||||
97
lib/lunar-utils.ts
Normal file
97
lib/lunar-utils.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* 음력-양력 변환 유틸리티
|
||||
*/
|
||||
|
||||
interface LunarDate {
|
||||
year: number;
|
||||
month: number;
|
||||
day: number;
|
||||
isLeap: boolean;
|
||||
}
|
||||
|
||||
interface SolarDate {
|
||||
year: number;
|
||||
month: number;
|
||||
day: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 음력을 양력으로 변환
|
||||
* @param lunarYear 음력 년
|
||||
* @param lunarMonth 음력 월
|
||||
* @param lunarDay 음력 일
|
||||
* @param isLeapMonth 윤달 여부
|
||||
*/
|
||||
export function lunarToSolar(
|
||||
lunarYear: number,
|
||||
lunarMonth: number,
|
||||
lunarDay: number,
|
||||
isLeapMonth: boolean = false
|
||||
): SolarDate {
|
||||
try {
|
||||
const lunar = require('lunar-calendar');
|
||||
const result = lunar.lunarToSolar(lunarYear, lunarMonth, lunarDay, isLeapMonth);
|
||||
|
||||
return {
|
||||
year: result.year,
|
||||
month: result.month,
|
||||
day: result.day
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('음력 변환 오류:', error);
|
||||
// 변환 실패시 입력값 그대로 반환
|
||||
return {
|
||||
year: lunarYear,
|
||||
month: lunarMonth,
|
||||
day: lunarDay
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 양력을 음력으로 변환
|
||||
* @param solarYear 양력 년
|
||||
* @param solarMonth 양력 월
|
||||
* @param solarDay 양력 일
|
||||
*/
|
||||
export function solarToLunar(
|
||||
solarYear: number,
|
||||
solarMonth: number,
|
||||
solarDay: number
|
||||
): LunarDate {
|
||||
try {
|
||||
const lunar = require('lunar-calendar');
|
||||
const result = lunar.solarToLunar(solarYear, solarMonth, solarDay);
|
||||
|
||||
return {
|
||||
year: result.year,
|
||||
month: result.month,
|
||||
day: result.day,
|
||||
isLeap: result.isLeap || false
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('양력 변환 오류:', error);
|
||||
// 변환 실패시 입력값 그대로 반환
|
||||
return {
|
||||
year: solarYear,
|
||||
month: solarMonth,
|
||||
day: solarDay,
|
||||
isLeap: false
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 음력 날짜를 문자열로 변환
|
||||
*/
|
||||
export function formatLunarDate(lunar: LunarDate): string {
|
||||
const leapText = lunar.isLeap ? '윤' : '';
|
||||
return `음력 ${lunar.year}년 ${leapText}${lunar.month}월 ${lunar.day}일`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 양력 날짜를 문자열로 변환
|
||||
*/
|
||||
export function formatSolarDate(solar: SolarDate): string {
|
||||
return `양력 ${solar.year}년 ${solar.month}월 ${solar.day}일`;
|
||||
}
|
||||
66
lib/products.ts
Normal file
66
lib/products.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
export interface Product {
|
||||
id: string;
|
||||
name: string;
|
||||
price: number;
|
||||
type: 'one_time' | 'monthly' | 'annual';
|
||||
description: string;
|
||||
}
|
||||
|
||||
export const PRODUCTS: Record<string, Product> = {
|
||||
lotto_basic: {
|
||||
id: 'lotto_basic',
|
||||
name: '로또 기본 플랜',
|
||||
price: 4900,
|
||||
type: 'monthly',
|
||||
description: '매주 5개 번호 조합 이메일 제공',
|
||||
},
|
||||
lotto_premium: {
|
||||
id: 'lotto_premium',
|
||||
name: '로또 프리미엄 플랜',
|
||||
price: 9900,
|
||||
type: 'monthly',
|
||||
description: '매주 3회 번호 + 텔레그램 알림',
|
||||
},
|
||||
lotto_annual: {
|
||||
id: 'lotto_annual',
|
||||
name: '로또 연간 플랜',
|
||||
price: 89900,
|
||||
type: 'annual',
|
||||
description: '프리미엄 12개월 (2개월 무료)',
|
||||
},
|
||||
stock_starter_install: {
|
||||
id: 'stock_starter_install',
|
||||
name: '주식 스타터 설치',
|
||||
price: 99000,
|
||||
type: 'one_time',
|
||||
description: '1개 종목 자동 매매 설치',
|
||||
},
|
||||
stock_pro_install: {
|
||||
id: 'stock_pro_install',
|
||||
name: '주식 프로 설치',
|
||||
price: 199000,
|
||||
type: 'one_time',
|
||||
description: '5개 종목 + 전략 커스터마이징 설치',
|
||||
},
|
||||
stock_starter_monthly: {
|
||||
id: 'stock_starter_monthly',
|
||||
name: '주식 스타터 월 유지비',
|
||||
price: 29000,
|
||||
type: 'monthly',
|
||||
description: '스타터 월 유지보수 비용',
|
||||
},
|
||||
stock_pro_monthly: {
|
||||
id: 'stock_pro_monthly',
|
||||
name: '주식 프로 월 유지비',
|
||||
price: 49000,
|
||||
type: 'monthly',
|
||||
description: '프로 월 유지보수 비용',
|
||||
},
|
||||
saju_detail: {
|
||||
id: 'saju_detail',
|
||||
name: 'AI 사주 상세 리포트',
|
||||
price: 4900,
|
||||
type: 'one_time',
|
||||
description: 'AI 12가지 항목 상세 해석',
|
||||
},
|
||||
};
|
||||
223
lib/saju-ai-prompt.ts
Normal file
223
lib/saju-ai-prompt.ts
Normal file
@@ -0,0 +1,223 @@
|
||||
|
||||
import { SajuData, FIVE_ELEMENTS, FIVE_ELEMENTS_KR, HEAVENLY_STEMS_KR } from './saju-calculator';
|
||||
import { DaeunPillar } from './daeun-calculator';
|
||||
import { SajuAnalysis } from './ai-interpretation';
|
||||
|
||||
export function createSajuPrompt(
|
||||
saju: SajuData,
|
||||
currentDaeun: DaeunPillar | null,
|
||||
gender: 'male' | 'female',
|
||||
analysis: SajuAnalysis,
|
||||
daeunList: DaeunPillar[] = []
|
||||
): string {
|
||||
const genderStr = gender === 'male' ? '남성' : '여성';
|
||||
const birthDate = `${saju.birthDate.year}년 ${saju.birthDate.month}월 ${saju.birthDate.day}일 ${saju.birthDate.hour ? saju.birthDate.hour + '시' : '시간 모름'}`;
|
||||
const dayStemKr = saju.day.stemKr;
|
||||
const dayElement = FIVE_ELEMENTS[saju.dayStem as keyof typeof FIVE_ELEMENTS];
|
||||
const dayElementKr = FIVE_ELEMENTS_KR[dayElement as keyof typeof FIVE_ELEMENTS_KR];
|
||||
|
||||
// ── 사주 원국 ──
|
||||
const pillars = [
|
||||
`년주: ${saju.year.stem}${saju.year.branch} (${saju.year.stemKr}${saju.year.branchKr}) | 천간십성: ${saju.year.tenGod} | 십이운성: ${saju.year.fortune}`,
|
||||
`월주: ${saju.month.stem}${saju.month.branch} (${saju.month.stemKr}${saju.month.branchKr}) | 천간십성: ${saju.month.tenGod} | 십이운성: ${saju.month.fortune}`,
|
||||
`일주: ${saju.day.stem}${saju.day.branch} (${saju.day.stemKr}${saju.day.branchKr}) | 일간(日干) | 십이운성: ${saju.day.fortune}`,
|
||||
saju.hour
|
||||
? `시주: ${saju.hour.stem}${saju.hour.branch} (${saju.hour.stemKr}${saju.hour.branchKr}) | 천간십성: ${saju.hour.tenGod} | 십이운성: ${saju.hour.fortune}`
|
||||
: '시주: 정보 없음',
|
||||
].join('\n');
|
||||
|
||||
// ── 지장간 ──
|
||||
const hiddenStemsStr = analysis.hiddenStems.map(h => {
|
||||
const stemsDetail = h.stems.map(s => `${s.stemKr}(${s.stem}, ${FIVE_ELEMENTS_KR[s.element as keyof typeof FIVE_ELEMENTS_KR]}, ${s.role})`).join(', ');
|
||||
return `${h.pillar} ${h.branchKr}(${h.branch}): [${stemsDetail}]`;
|
||||
}).join('\n');
|
||||
|
||||
// ── 오행 분석 ──
|
||||
const eb = analysis.elementBalance;
|
||||
const es = analysis.elementScores;
|
||||
const elementStr = Object.entries(eb).map(([k, v]) => {
|
||||
return `${FIVE_ELEMENTS_KR[k as keyof typeof FIVE_ELEMENTS_KR]}(${k}): ${v}점 (${es[k]}%)`;
|
||||
}).join(' | ');
|
||||
|
||||
// ── 신강/신약 ──
|
||||
const strength = analysis.dayMasterStrength;
|
||||
const strengthStr = `판정: ${strength.result} (점수: ${strength.score})\n근거:\n${strength.reasons.map(r => `- ${r}`).join('\n')}`;
|
||||
|
||||
// ── 용신/희신/기신 ──
|
||||
const ys = analysis.yongShin;
|
||||
const yongShinStr = `용신: ${ys.yongShinKr}(${ys.yongShin}) | 희신: ${ys.heeShinKr}(${ys.heeShin}) | 기신: ${ys.giShinKr}(${ys.giShin})\n설명: ${ys.explanation}`;
|
||||
|
||||
// ── 지지 상호작용 ──
|
||||
const interactionsStr = analysis.branchInteractions.length > 0
|
||||
? analysis.branchInteractions.map(i => `- ${i.type}: ${i.branchesKr.join('')} (${i.pillars.join('↔')}) → ${i.description}`).join('\n')
|
||||
: '- 특별한 합/충/형/파/해 없음';
|
||||
|
||||
// ── 신살 ──
|
||||
const shinsalStr = analysis.shinsal.length > 0
|
||||
? analysis.shinsal.map(s => `- ${s.name}(${s.nameHanja}): ${s.pillar} ${s.branchKr}(${s.branch}) → ${s.description}`).join('\n')
|
||||
: '- 특별한 신살 없음';
|
||||
|
||||
// ── 공망 ──
|
||||
const gongmangStr = analysis.gongmang.description;
|
||||
|
||||
// ── 세운 ──
|
||||
const seun = analysis.seun;
|
||||
const seunStr = `${seun.year}년 ${seun.stemKr}${seun.branchKr}(${seun.stem}${seun.branch})년 | 오행: ${seun.elementKr}(${seun.element})`;
|
||||
const seunInteractions = seun.interactions.length > 0
|
||||
? seun.interactions.map(i => `- ${i.type}: ${i.description}`).join('\n')
|
||||
: '- 세운과 원국 사이에 특별한 충/합 없음';
|
||||
|
||||
// ── 대운 ──
|
||||
const daeunInfo = currentDaeun
|
||||
? `현재 대운: ${currentDaeun.stemKr}${currentDaeun.branchKr}(${currentDaeun.stem}${currentDaeun.branch}) 대운 | ${currentDaeun.age}세~${currentDaeun.age + 9}세 (${currentDaeun.startYear}~${currentDaeun.endYear}년)`
|
||||
: '현재 대운 정보 없음';
|
||||
|
||||
const allDaeunStr = daeunList.length > 0
|
||||
? daeunList.map(d => `${d.stemKr}${d.branchKr}(${d.age}세~${d.age + 9}세, ${d.startYear}~${d.endYear}년)`).join(' → ')
|
||||
: '';
|
||||
|
||||
const systemPrompt = `당신은 따뜻하고 유머러스한 사주 상담사예요. 마치 오랜 친구처럼 편하게, 하지만 놀라울 정도로 정확하게 사주를 읽어주는 사람이에요. 딱딱한 전문 용어 대신 비유와 이야기로 풀어내는 게 당신의 스타일이에요.
|
||||
|
||||
[핵심 원칙 - 반드시 지켜주세요]
|
||||
- 아래 제공된 계산 데이터를 바탕으로 해석하되, 전문 용어는 최소화하고 비유와 스토리텔링으로 풀어주세요.
|
||||
- "~요" 체의 친근한 말투를 사용하세요. (예: "~이에요", "~거든요", "~잖아요", "~인 거죠")
|
||||
- 각 섹션 제목은 창의적인 비유나 은유를 사용한 감성적 제목으로 만드세요. (예: "얼음 속에 숨겨진 불꽃", "당신 안의 숨은 보석")
|
||||
- 사주 데이터에 근거하되, "당신은 마치 ~같은 사람이에요"처럼 생생한 비유로 설명하세요.
|
||||
- 때로는 따끔한 조언도 섞어주세요. 친구가 해주는 솔직한 충고처럼요. (예: "솔직히 말하면... 그거 완벽주의 아니고 그냥 겁이 많은 거예요 😅")
|
||||
- 각 항목 최소 5~8문장으로 깊이 있게, 하지만 술술 읽히게 작성하세요.
|
||||
- 이 사람만을 위한 개인화된 분석이어야 해요. 일반론 절대 금지!
|
||||
- 중간중간 공감 포인트를 넣어주세요. (예: "혹시 이런 경험 있지 않나요?", "맞죠?")
|
||||
- 마지막에 진심 어린 응원 한마디를 꼭 넣어주세요.
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
[사용자 정보]
|
||||
- 성별: ${genderStr}
|
||||
- 생년월일시: ${birthDate}
|
||||
- 일간: ${dayStemKr}(${saju.dayStem}) → ${dayElementKr}(${dayElement})
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
[사주 원국]
|
||||
${pillars}
|
||||
|
||||
[지장간]
|
||||
${hiddenStemsStr}
|
||||
|
||||
[오행 점수 (가중치 적용)]
|
||||
${elementStr}
|
||||
총점: ${Object.values(eb).reduce((a, b) => a + b, 0).toFixed(1)}점
|
||||
|
||||
[신강/신약]
|
||||
${strengthStr}
|
||||
|
||||
[용신/희신/기신]
|
||||
${yongShinStr}
|
||||
|
||||
[지지 상호작용]
|
||||
${interactionsStr}
|
||||
|
||||
[신살]
|
||||
${shinsalStr}
|
||||
|
||||
[공망]
|
||||
${gongmangStr}
|
||||
|
||||
[대운]
|
||||
${daeunInfo}
|
||||
전체 흐름: ${allDaeunStr}
|
||||
|
||||
[세운 - 올해]
|
||||
${seunStr}
|
||||
${seunInteractions}
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
[분석 요구사항 - 12개 항목]
|
||||
|
||||
위 데이터를 바탕으로 아래 12개 항목을 작성하세요.
|
||||
각 항목은 반드시 "## " 로 시작하는 헤더를 사용하세요.
|
||||
헤더 제목은 번호 + 창의적인 비유/은유 제목으로 만드세요. (아래는 예시일 뿐, 사주 내용에 맞게 자유롭게 창작하세요)
|
||||
|
||||
## 1. [타고난 기질 - 창의적 제목]
|
||||
예시 제목: "차가운 호수 아래 숨겨진 용의 심장" / "봄바람처럼 자유로운 영혼"
|
||||
- ${dayStemKr}${saju.day.branchKr}일주의 핵심 성격을 비유로 풀어주세요
|
||||
- "당신은 마치 ~같은 사람이에요" 패턴 활용
|
||||
- 겉으로 보이는 모습 vs 진짜 내면을 대비시켜 흥미롭게
|
||||
- 강점은 확 칭찬하고, 약점은 "솔직히 말하면..." 패턴으로 따끔하지만 사랑스럽게
|
||||
|
||||
## 2. [오행 밸런스 & 개운법 - 창의적 제목]
|
||||
예시 제목: "당신에게 부족한 한 조각, 그걸 채우는 법" / "운을 끌어당기는 나만의 비밀 무기"
|
||||
- 오행 데이터를 인용하되, "당신의 에너지 밸런스를 보면..." 식으로 쉽게
|
||||
- 용신(${ys.yongShinKr}) 기운을 강화하는 실생활 팁: 색상, 방향, 숫자, 음식, 행동
|
||||
- 기신(${ys.giShinKr}) 기운 피하는 법도 구체적으로
|
||||
- "오늘부터 당장 ~해보세요!" 같은 실천 가능한 조언
|
||||
|
||||
## 3. [지지 상호작용 - 창의적 제목]
|
||||
예시 제목: "당신 안에서 벌어지는 보이지 않는 전쟁" / "운명이 엮어준 특별한 인연의 실타래"
|
||||
- 합/충/형 데이터를 바탕으로 실생활 영향을 이야기로 풀어주세요
|
||||
- 어려운 용어 대신 "쉽게 말하면..." 패턴 활용
|
||||
|
||||
## 4. [신살의 영향 - 창의적 제목]
|
||||
예시 제목: "당신이 타고난 숨겨진 초능력" / "조심해야 할 함정, 그리고 날개"
|
||||
- 각 신살을 흥미로운 비유로 설명 (역마살 → "여행자의 별", 도화살 → "매력의 별" 등)
|
||||
- 긍정 신살은 신나게, 주의 신살은 걱정 말라는 톤으로
|
||||
|
||||
## 5. [재물운 - 창의적 제목]
|
||||
예시 제목: "돈이 당신을 찾아오는 방식" / "통장이 웃는 시기, 우는 시기"
|
||||
- 편재/정재 위치와 강도를 쉬운 비유로
|
||||
- 돈 버는 스타일 (한방 vs 꾸준히 vs 투자형 등)
|
||||
- 주의할 시기와 기회의 시기를 구체적으로
|
||||
|
||||
## 6. [직업 적성 - 창의적 제목]
|
||||
예시 제목: "당신이 빛나는 무대는 따로 있어요" / "타고난 프로의 DNA"
|
||||
- 적합한 분야를 구체적으로 추천 (추상적 말고 직업명까지)
|
||||
- 조직형 vs 프리랜서/사업형 판단
|
||||
- ${genderStr}의 특성 고려
|
||||
|
||||
## 7. [애정운 - 창의적 제목]
|
||||
예시 제목: "사랑이 찾아오는 계절" / "당신의 이상형, 사주가 말해주는 진짜 궁합"
|
||||
- ${genderStr === '남성' ? '재성' : '관성'} 기반 배우자 복 분석을 로맨틱하게
|
||||
- 연애 스타일, 배우자 상을 재미있게 묘사
|
||||
- 결혼 적령기를 부드럽게 안내
|
||||
|
||||
## 8. [건강운 - 창의적 제목]
|
||||
예시 제목: "몸이 보내는 작은 신호들" / "100세까지 건강한 나를 위한 처방전"
|
||||
- 오행 과부족 → 주의할 건강 포인트를 걱정 안 되게 부드럽게
|
||||
- 구체적인 생활 습관 조언 (음식, 운동, 스트레스 관리)
|
||||
|
||||
## 9. [현재 대운 - 창의적 제목]
|
||||
예시 제목: "지금 당신 앞에 펼쳐진 10년의 지도" / "인생의 봄이 오고 있어요"
|
||||
- ${daeunInfo}를 바탕으로 현재 10년의 의미를 이야기로
|
||||
- 지금 집중해야 할 것, 조심할 것을 친구처럼 조언
|
||||
|
||||
## 10. [올해의 운세 - 창의적 제목] (${seun.year}년)
|
||||
예시 제목: "올해, 당신에게 찾아올 세 가지 기회" / "${seun.year}년은 당신의 해예요"
|
||||
- 세운 데이터 바탕으로 올해 키워드를 뽑아 설명
|
||||
- 상반기 vs 하반기 흐름
|
||||
- "이것만은 꼭!" 하는 핵심 조언
|
||||
|
||||
## 11. [인생의 황금기 - 창의적 제목]
|
||||
예시 제목: "인생에서 가장 빛나는 순간이 다가오고 있어요" / "대박 터지는 그 시기"
|
||||
- 전체 대운 흐름에서 최고의 시기를 콕 집어서
|
||||
- 그 시기에 어떤 기회가 오는지 구체적이고 설레게
|
||||
- "그때를 위해 지금 준비할 것" 조언
|
||||
|
||||
## 12. [종합 조언 - 창의적 제목]
|
||||
예시 제목: "당신이라는 별에게 보내는 편지" / "마지막으로 꼭 전하고 싶은 말"
|
||||
- 이 사주의 핵심 강점과 약점을 한 문장으로 요약
|
||||
- 용신(${ys.yongShinKr}) 활용 일상 팁
|
||||
- 진심 어린 응원과 철학적 메시지로 마무리
|
||||
- 마지막 문장은 감동적인 한 줄로 끝내주세요
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
[톤앤매너 - 가장 중요!!]
|
||||
- "~요" 체 친근한 말투 (절대 "~이다/한다" 체 사용 금지)
|
||||
- 전문 용어 최소화. 꼭 필요하면 비유로 풀어서 설명
|
||||
- 비유와 은유를 적극 활용 ("마치 ~처럼", "당신은 ~같은 사람이에요")
|
||||
- 중간중간 이모지를 자연스럽게 사용 (과하지 않게, 섹션당 1~2개)
|
||||
- 따끔한 조언 + 따뜻한 응원의 밸런스
|
||||
- "혹시 ~한 적 있지 않나요?" 같은 공감형 질문으로 몰입감 유도
|
||||
- Markdown 형식: ## 헤더, **볼드**, 리스트 활용
|
||||
- 각 섹션 제목은 반드시 번호 포함 (## 1. ~ ## 12.)
|
||||
- 읽는 사람이 "와, 이거 진짜 내 얘기다!" 하고 느끼게 만들어주세요`;
|
||||
|
||||
return systemPrompt;
|
||||
}
|
||||
723
lib/saju-calculator.ts
Normal file
723
lib/saju-calculator.ts
Normal file
@@ -0,0 +1,723 @@
|
||||
// 천간 (天干) - 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) {
|
||||
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}) 공망 → 해당 지지의 기운이 비어있어 허무하거나 집착이 없는 영역. 오히려 초월적 능력이 될 수 있음.`,
|
||||
};
|
||||
}
|
||||
249
lib/solar-terms.ts
Normal file
249
lib/solar-terms.ts
Normal file
@@ -0,0 +1,249 @@
|
||||
/**
|
||||
* 24절기 계산
|
||||
* 사주 계산에서 월주는 절기를 기준으로 합니다.
|
||||
*/
|
||||
|
||||
// 24절기 (입춘부터 시작)
|
||||
export const SOLAR_TERMS = [
|
||||
'입춘', '우수', '경칩', '춘분', '청명', '곡우',
|
||||
'입하', '소만', '망종', '하지', '소서', '대서',
|
||||
'입추', '처서', '백로', '추분', '한로', '상강',
|
||||
'입동', '소설', '대설', '동지', '소한', '대한'
|
||||
] as const;
|
||||
|
||||
// 월 절기 (홀수 인덱스: 입X, 짝수 인덱스: X분/X지)
|
||||
export const MONTH_SOLAR_TERMS = [
|
||||
'입춘', // 1월 (인월)
|
||||
'경칩', // 2월 (묘월)
|
||||
'청명', // 3월 (진월)
|
||||
'입하', // 4월 (사월)
|
||||
'망종', // 5월 (오월)
|
||||
'소서', // 6월 (미월)
|
||||
'입추', // 7월 (신월)
|
||||
'백로', // 8월 (유월)
|
||||
'한로', // 9월 (술월)
|
||||
'입동', // 10월 (해월)
|
||||
'대설', // 11월 (자월)
|
||||
'소한', // 12월 (축월)
|
||||
] as const;
|
||||
|
||||
interface SolarTermDate {
|
||||
year: number;
|
||||
month: number;
|
||||
day: number;
|
||||
hour: number;
|
||||
minute: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 정밀한 절기 계산 (천문학적 계산 기반)
|
||||
* solarlunar 라이브러리 사용
|
||||
*/
|
||||
export function getSolarTermDate(year: number, termIndex: number): SolarTermDate {
|
||||
try {
|
||||
const solarLunar = require('solarlunar');
|
||||
|
||||
// solarlunar의 절기 데이터 가져오기
|
||||
// 각 년도의 절기 정보를 계산
|
||||
const termNames = [
|
||||
'立春', '雨水', '驚蟄', '春分', '清明', '穀雨',
|
||||
'立夏', '小滿', '芒種', '夏至', '小暑', '大暑',
|
||||
'立秋', '處暑', '白露', '秋分', '寒露', '霜降',
|
||||
'立冬', '小雪', '大雪', '冬至', '小寒', '大寒'
|
||||
];
|
||||
|
||||
// 해당 년도의 절기 찾기
|
||||
// solarlunar는 양력 날짜로 절기 확인 가능
|
||||
// 각 절기의 대략적인 날짜 범위에서 검색
|
||||
|
||||
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 }, // 대한
|
||||
];
|
||||
|
||||
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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 주어진 날짜가 어느 절기 이후인지 확인
|
||||
* @param year 년
|
||||
* @param month 월
|
||||
* @param day 일
|
||||
* @returns 절기 인덱스 (0~23)
|
||||
*/
|
||||
export function getCurrentSolarTerm(year: number, month: number, day: number): number {
|
||||
const date = new Date(year, month - 1, day);
|
||||
const dateValue = date.getTime();
|
||||
|
||||
// 각 절기 날짜 확인
|
||||
for (let i = 23; i >= 0; i--) {
|
||||
const termDate = getSolarTermDate(year, i);
|
||||
let termYear = termDate.year;
|
||||
let termMonth = termDate.month;
|
||||
|
||||
// 대한, 소한은 이전 해 처리
|
||||
if (i >= 22 && month >= 2) {
|
||||
termYear = year;
|
||||
} else if (i >= 22) {
|
||||
termYear = year - 1;
|
||||
}
|
||||
|
||||
const term = new Date(termYear, termMonth - 1, termDate.day);
|
||||
|
||||
if (dateValue >= term.getTime()) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
// 입춘 이전이면 전년도 대한 이후
|
||||
return 23;
|
||||
}
|
||||
|
||||
/**
|
||||
* 절기 기준 월주 지지 인덱스 계산
|
||||
* @param year 년
|
||||
* @param month 월
|
||||
* @param day 일
|
||||
* @returns 지지 인덱스 (0: 자, 1: 축, 2: 인, ...)
|
||||
*/
|
||||
export function getSolarTermMonthBranch(year: number, month: number, day: number): number {
|
||||
const termIndex = getCurrentSolarTerm(year, month, day);
|
||||
|
||||
// 절기 인덱스를 월로 변환
|
||||
// 입춘(0) -> 인월(2)
|
||||
// 경칩(2) -> 묘월(3)
|
||||
// 청명(4) -> 진월(4)
|
||||
// ...
|
||||
|
||||
const monthBranches = [
|
||||
2, // 입춘 -> 인월
|
||||
2, // 우수 -> 인월
|
||||
3, // 경칩 -> 묘월
|
||||
3, // 춘분 -> 묘월
|
||||
4, // 청명 -> 진월
|
||||
4, // 곡우 -> 진월
|
||||
5, // 입하 -> 사월
|
||||
5, // 소만 -> 사월
|
||||
6, // 망종 -> 오월
|
||||
6, // 하지 -> 오월
|
||||
7, // 소서 -> 미월
|
||||
7, // 대서 -> 미월
|
||||
8, // 입추 -> 신월
|
||||
8, // 처서 -> 신월
|
||||
9, // 백로 -> 유월
|
||||
9, // 추분 -> 유월
|
||||
10, // 한로 -> 술월
|
||||
10, // 상강 -> 술월
|
||||
11, // 입동 -> 해월
|
||||
11, // 소설 -> 해월
|
||||
0, // 대설 -> 자월
|
||||
0, // 동지 -> 자월
|
||||
1, // 소한 -> 축월
|
||||
1, // 대한 -> 축월
|
||||
];
|
||||
|
||||
return monthBranches[termIndex];
|
||||
}
|
||||
|
||||
/**
|
||||
* 절기명 가져오기
|
||||
*/
|
||||
export function getSolarTermName(termIndex: number): string {
|
||||
return SOLAR_TERMS[termIndex];
|
||||
}
|
||||
|
||||
/**
|
||||
* 다음 절기까지 남은 일수 계산
|
||||
*/
|
||||
export function getDaysToNextSolarTerm(year: number, month: number, day: number): number {
|
||||
const currentDate = new Date(year, month - 1, day);
|
||||
const currentTerm = getCurrentSolarTerm(year, month, day);
|
||||
const nextTermIndex = (currentTerm + 1) % 24;
|
||||
|
||||
let nextYear = year;
|
||||
if (currentTerm === 23) {
|
||||
nextYear = year + 1;
|
||||
}
|
||||
|
||||
const nextTerm = getSolarTermDate(nextYear, nextTermIndex);
|
||||
const nextDate = new Date(nextTerm.year, nextTerm.month - 1, nextTerm.day);
|
||||
|
||||
const diffTime = nextDate.getTime() - currentDate.getTime();
|
||||
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||
|
||||
return diffDays;
|
||||
}
|
||||
7
lib/supabase/client.ts
Normal file
7
lib/supabase/client.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { createBrowserClient } from '@supabase/ssr';
|
||||
|
||||
export function createClient() {
|
||||
const url = process.env.NEXT_PUBLIC_SUPABASE_URL ?? 'https://placeholder.supabase.co';
|
||||
const key = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY ?? 'placeholder-key';
|
||||
return createBrowserClient(url, key);
|
||||
}
|
||||
27
lib/supabase/server.ts
Normal file
27
lib/supabase/server.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { createServerClient, type CookieMethodsServer } from '@supabase/ssr';
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
export async function createClient() {
|
||||
const cookieStore = await cookies();
|
||||
|
||||
const cookieMethods: CookieMethodsServer = {
|
||||
getAll() {
|
||||
return cookieStore.getAll();
|
||||
},
|
||||
setAll(cookiesToSet) {
|
||||
try {
|
||||
cookiesToSet.forEach(({ name, value, options }) =>
|
||||
cookieStore.set(name, value, options)
|
||||
);
|
||||
} catch {
|
||||
// Server Component에서 호출된 경우 무시 (미들웨어가 세션 갱신)
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
return createServerClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
||||
{ cookies: cookieMethods }
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user