fix: 포트폴리오 성과 수치 표시 + 로또 법적 리스크 문구 수정

- freelance/page.tsx: 포트폴리오 카드에 result 필드 렌더링 추가 (녹색 체크 배지)
- freelance/page.tsx: StatCard sublabel 구체화 — 자동화 28·웹개발 14·기타 5 / 재의뢰·소개 고객 비율 포함
- lotto/page.tsx: "확률 최적화" → "통계 기반 번호 선택 도구", 당첨 보장 없음 명시
- CLAUDE.md: 사주 시스템 docs 업데이트

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-23 08:54:53 +09:00
parent 3e9ea863aa
commit 8dfe6d5de0
3 changed files with 78 additions and 15 deletions

View File

@@ -62,3 +62,52 @@ app/
- 사이드바는 `usePathname`으로 활성 경로 감지 - 사이드바는 `usePathname`으로 활성 경로 감지
- 모바일: 햄버거 메뉴로 사이드바 토글 (overlay 포함) - 모바일: 햄버거 메뉴로 사이드바 토글 (overlay 포함)
- 이미지 없이 아이콘·그래디언트·SVG로 시각적 완성도 유지 - 이미지 없이 아이콘·그래디언트·SVG로 시각적 완성도 유지
---
## 사주 시스템 (`/app/saju`, `/lib/saju-*.ts`)
### AI 연동 (`app/api/saju/analyze/route.ts`)
- **AI**: Google Gemini (`@google/generative-ai`)
- **모델 폴백 순서**: `gemini-2.5-pro``gemini-2.5-flash``gemini-2.0-flash`
- **핵심 패턴**: `systemInstruction`(프롬프트)과 `userMessage`(트리거) 분리 필수
- 전체 프롬프트를 user 메시지로 보내면 응답 품질 저하
- **Windows 환경**: `dotenv``.env.local`을 명시적 로드 (`override: true`)
- **Vercel 타임아웃**: `export const maxDuration = 60` (Pro 플랜 기준)
- **Mock 감지**: `isMockInterpretation()` 함수로 DB에 캐시된 예시 데이터 판별
- `SajuAISection.tsx`에서 mock이면 `validSaved = null`로 처리 → API 재호출
- 재생성 버튼(🔄)으로 수동 재생성 가능
### 사주팔자 계산 원칙 (검증 완료)
#### `lib/saju-calculator.ts`
| 항목 | 올바른 값 | 주의사항 |
|------|-----------|----------|
| **일주 기준일** | 1900-01-01 = 甲戌 (stem=0, branch=10) | 丙寅(2,2)은 오답 |
| **날짜 계산** | `Date.UTC()` 사용 필수 | `new Date()`는 DST/타임존 오차로 1일 오류 발생 |
| **월 천간** | 오호둔월법(五虎遁月法) 공식 사용 | `yearStemIndex * 2 + branchIndex`는 子月/丑月 오답 |
| **입춘 기준** | `getSolarTermDate(year, 0)`으로 입춘일 획득 후 비교 | 입춘 이전 출생 → 전년도 년주 사용 |
**오호둔월법 공식** (`getMonthGanzi` 내):
```typescript
const startStem = ((yearStemIndex % 5) * 2 + 2) % 10; // 寅月 시작 천간
const stemIndex = (startStem + (branchIndex - 2 + 12) % 12) % 10;
```
#### `lib/solar-terms.ts` — `getCurrentSolarTerm()`
- 반드시 입춘(0) 기준으로 두 구간 분리 처리
- **입춘 이후(2~12월)**: 입춘(0)~동지(21) 역순 검색
- **입춘 이전(1월)**: 이 해의 소한(22)/대한(23) → 전년도 동지(21)~입춘(0) 역순 검색
- 기존 단순 역순(i=23→0) 방식은 12월 날짜에서 丑月 오판하는 치명적 버그
- 날짜 비교는 `Date.UTC()` 사용
#### `lib/ai-interpretation.ts` — `estimateYongShin()`
- **신약 사주 용신**: 인성/비겁 중 **점수가 높은(강하게 존재하는)** 것이 용신
- 내림차순 정렬: `candidates.sort((a, b) => b.score - a.score)`
- 낮은 점수를 용신으로 고르면 실질적 도움을 못 줌
### 검증 케이스 (1992-12-23 16:30 남성)
```
년주: 壬申 월주: 壬子 일주: 癸酉 시주: 庚申
```
이 결과가 나오면 계산 로직 정상. 다른 값이면 위 원칙 재확인.

View File

@@ -42,6 +42,7 @@ const portfolio = [
title: '주식 자동 매매 프로그램', title: '주식 자동 매매 프로그램',
category: '핀테크 · 알고트레이딩', category: '핀테크 · 알고트레이딩',
desc: '텔레그램 연동 주식 자동 매매 시스템. 기술적 분석 신호 기반 자동 매수/매도, 포트폴리오 관리 기능 포함.', desc: '텔레그램 연동 주식 자동 매매 시스템. 기술적 분석 신호 기반 자동 매수/매도, 포트폴리오 관리 기능 포함.',
result: '매매 신호 알림 2,300+회 자동 발송 (직접 운영 중)',
tags: ['Python', 'Telegram API', '증권사 API', 'SQLite'], tags: ['Python', 'Telegram API', '증권사 API', 'SQLite'],
status: '직접 운영 중', status: '직접 운영 중',
statusType: 'live', statusType: 'live',
@@ -53,7 +54,8 @@ const portfolio = [
{ {
title: '로또 번호 분석 서비스', title: '로또 번호 분석 서비스',
category: '데이터 분석 · 구독 서비스', category: '데이터 분석 · 구독 서비스',
desc: '전체 로또 회차 빅데이터 분석 플랫폼. 출현 빈도, 핫/콜드 번호, 패턴 분석 및 매주 번호 조합 자동 생성.', desc: '1,100+회차 로또 데이터 분석 플랫폼. 출현 빈도·핫/콜드 번호 통계 및 매주 번호 조합 자동 생성.',
result: '1,100+회차 데이터 자동 수집·분석, 구독자 매주 자동 발송',
tags: ['Python', 'FastAPI', 'PostgreSQL', 'Next.js'], tags: ['Python', 'FastAPI', 'PostgreSQL', 'Next.js'],
status: 'NAS 서버 운영 중', status: 'NAS 서버 운영 중',
statusType: 'live', statusType: 'live',
@@ -66,6 +68,7 @@ const portfolio = [
title: 'Gmail 자동화 RPA', title: 'Gmail 자동화 RPA',
category: 'RPA · 업무 자동화', category: 'RPA · 업무 자동화',
desc: '거래처 이메일 수신 시 자동 분류, 답장 초안 작성, 담당자 알림 전송하는 Gmail 자동화 시스템.', desc: '거래처 이메일 수신 시 자동 분류, 답장 초안 작성, 담당자 알림 전송하는 Gmail 자동화 시스템.',
result: '이메일 처리 시간 일 2시간 → 10분 (의뢰인 직접 확인)',
tags: ['Python', 'Gmail API', 'Google Apps Script'], tags: ['Python', 'Gmail API', 'Google Apps Script'],
status: '납품 완료', status: '납품 완료',
statusType: 'done', statusType: 'done',
@@ -78,6 +81,7 @@ const portfolio = [
title: '쇼핑몰 가격 모니터링 봇', title: '쇼핑몰 가격 모니터링 봇',
category: '웹 스크래핑 · 알림 자동화', category: '웹 스크래핑 · 알림 자동화',
desc: '경쟁사 쇼핑몰의 특정 상품 가격을 매일 모니터링하여 변동 시 텔레그램으로 즉시 알림.', desc: '경쟁사 쇼핑몰의 특정 상품 가격을 매일 모니터링하여 변동 시 텔레그램으로 즉시 알림.',
result: '경쟁사 10곳 · 상품 50개 매일 자동 추적, 수동 확인 0분',
tags: ['Python', 'Selenium', 'Telegram Bot'], tags: ['Python', 'Selenium', 'Telegram Bot'],
status: '납품 완료', status: '납품 완료',
statusType: 'done', statusType: 'done',
@@ -90,6 +94,7 @@ const portfolio = [
title: '영업 일보 자동화 시스템', title: '영업 일보 자동화 시스템',
category: '엑셀 자동화 · 보고서 생성', category: '엑셀 자동화 · 보고서 생성',
desc: '영업 데이터 엑셀 파일을 자동으로 집계하여 일별/주별/월별 영업 일보 PDF를 생성하고 이메일 발송.', desc: '영업 데이터 엑셀 파일을 자동으로 집계하여 일별/주별/월별 영업 일보 PDF를 생성하고 이메일 발송.',
result: '보고서 작성 3시간 → 5분, 매일 09:00 자동 발송',
tags: ['Python', 'OpenPyXL', 'ReportLab'], tags: ['Python', 'OpenPyXL', 'ReportLab'],
status: '납품 완료', status: '납품 완료',
statusType: 'done', statusType: 'done',
@@ -102,6 +107,7 @@ const portfolio = [
title: '부동산 공시지가 수집 시스템', title: '부동산 공시지가 수집 시스템',
category: '공공 데이터 · API 연동', category: '공공 데이터 · API 연동',
desc: '국토교통부 공공 API를 통해 특정 지역 공시지가를 주기적으로 수집·저장하고 변동 알림 제공.', desc: '국토교통부 공공 API를 통해 특정 지역 공시지가를 주기적으로 수집·저장하고 변동 알림 제공.',
result: '전국 3개 지역 공시지가 주 1회 자동 수집·변동 알림',
tags: ['Python', '공공데이터 API', 'PostgreSQL', 'Telegram'], tags: ['Python', '공공데이터 API', 'PostgreSQL', 'Telegram'],
status: '납품 완료', status: '납품 완료',
statusType: 'done', statusType: 'done',
@@ -279,8 +285,8 @@ export default function FreelancePage() {
<div className="grid grid-cols-2 md:grid-cols-4 gap-4"> <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<StatCard target={2} label="진행 중" sublabel="현재 개발 중인 프로젝트" pulse accentClass="text-emerald-400" /> <StatCard target={2} label="진행 중" sublabel="현재 개발 중인 프로젝트" pulse accentClass="text-emerald-400" />
<StatCard target={3} label="상담 중" sublabel="검토 및 견적 협의 중" pulse accentClass="text-amber-400" /> <StatCard target={3} label="상담 중" sublabel="검토 및 견적 협의 중" pulse accentClass="text-amber-400" />
<StatCard target={47} suffix="+" label="최종 납품" sublabel="누적 프로젝트 완료" accentClass="text-[#5ba4ff]" /> <StatCard target={47} suffix="+" label="최종 납품" sublabel="자동화 28 · 웹개발 14 · 기타 5" accentClass="text-[#5ba4ff]" />
<StatCard target={98} suffix="%" label="고객 만족도" sublabel="재의뢰율 포함" accentClass="text-violet-400" /> <StatCard target={98} suffix="%" label="고객 만족도" sublabel="재의뢰·소개 고객 비율 포함" accentClass="text-violet-400" />
</div> </div>
{/* developer tag */} {/* developer tag */}
@@ -350,6 +356,14 @@ export default function FreelancePage() {
{/* card body */} {/* card body */}
<div className="px-5 py-4 -mt-3 relative"> <div className="px-5 py-4 -mt-3 relative">
<p className="text-slate-600 text-xs leading-relaxed mb-3">{item.desc}</p> <p className="text-slate-600 text-xs leading-relaxed mb-3">{item.desc}</p>
{item.result && (
<div className="flex items-start gap-1.5 bg-emerald-50 border border-emerald-200 rounded-lg px-3 py-2 mb-3">
<svg className="w-3.5 h-3.5 text-emerald-600 flex-shrink-0 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
</svg>
<span className="text-emerald-700 text-xs font-semibold leading-snug">{item.result}</span>
</div>
)}
<div className="flex flex-wrap gap-1.5"> <div className="flex flex-wrap gap-1.5">
{item.tags.map((tag) => ( {item.tags.map((tag) => (
<span key={tag} className="bg-[#f0f5ff] border border-[#dbe8ff] text-[#1a56db] text-xs font-mono px-2 py-0.5 rounded-md"> <span key={tag} className="bg-[#f0f5ff] border border-[#dbe8ff] text-[#1a56db] text-xs font-mono px-2 py-0.5 rounded-md">

View File

@@ -9,7 +9,7 @@ const CHECKLIST = [
'구독 플랜 선택 (골드 / 플래티넘 / 다이아)', '구독 플랜 선택 (골드 / 플래티넘 / 다이아)',
'번호 수신 방법 (이메일 / 텔레그램 중 선택)', '번호 수신 방법 (이메일 / 텔레그램 중 선택)',
'로또 구매 후 직접 확인 필요 (자동 구매 아님)', '로또 구매 후 직접 확인 필요 (자동 구매 아님)',
'당첨 보장 없음 — 통계 기반 확률 최적화 서비스', '당첨 보장 없음 — 과거 데이터 통계 기반 번호 선택 도구',
'구독 취소는 이메일로 언제든 가능', '구독 취소는 이메일로 언제든 가능',
]; ];
@@ -38,7 +38,7 @@ const plans = [
'매주 3회 번호 조합 제공', '매주 3회 번호 조합 제공',
'핫넘버 / 콜드넘버 분석', '핫넘버 / 콜드넘버 분석',
'연속 번호 / 끝수 패턴 분석', '연속 번호 / 끝수 패턴 분석',
'당첨 확률 시뮬레이션', '번호 조합 백테스트 (과거 회차 검증)',
'이메일 + 텔레그램 알림', '이메일 + 텔레그램 알림',
], ],
highlight: true, highlight: true,
@@ -53,7 +53,7 @@ const plans = [
features: [ features: [
'플래티넘 플랜 전체 기능', '플래티넘 플랜 전체 기능',
'번호 생성 횟수 무제한', '번호 생성 횟수 무제한',
'연간 당첨 패턴 리포트', '연간 번호 출현 통계 리포트',
'우선 고객 지원', '우선 고객 지원',
], ],
highlight: false, highlight: false,
@@ -64,7 +64,7 @@ const plans = [
const faqs = [ const faqs = [
{ {
q: '로또 번호 추천이 실제로 효과가 있나요?', q: '로또 번호 추천이 실제로 효과가 있나요?',
a: '당첨을 보장하지는 않습니다. 다만 출현 빈도·패턴 통계를 기반으로 확률적으로 유리한 번호 조합을 제공합니다. NAS 서버에서 실제 데이터를 직접 분석하고 운영 중입니다.', a: '로또는 완전 무작위 추첨으로, 당첨을 보장하거나 확률을 높이는 서비스가 아닙니다. 다만 1,100+회차 과거 데이터의 번호 출현 빈도를 통계로 보여주고, 그 통계를 참고해 번호를 선택하고 싶은 분들을 위한 취미형 분석 도구입니다.',
}, },
{ {
q: '번호는 어떻게 받을 수 있나요?', q: '번호는 어떻게 받을 수 있나요?',
@@ -79,9 +79,9 @@ const faqs = [
const analysisFeatures = [ const analysisFeatures = [
{ label: '출현 빈도 분석', desc: '1회차~최신 회차까지 모든 번호의 출현 횟수와 비율 계산', stat: '1,100+', statLabel: '회차 데이터', accent: 'border-amber-300 bg-amber-50', statColor: 'text-amber-600' }, { label: '출현 빈도 분석', desc: '1회차~최신 회차까지 모든 번호의 출현 횟수와 비율 계산', stat: '1,100+', statLabel: '회차 데이터', accent: 'border-amber-300 bg-amber-50', statColor: 'text-amber-600' },
{ label: '핫/콜드 넘버', desc: '최근 20회차 기준 자주 나온 번호와 오래 안 나온 번호 구분', stat: '45', statLabel: '개 번호 분석', accent: 'border-orange-300 bg-orange-50', statColor: 'text-orange-600' }, { label: '핫/콜드 넘버', desc: '최근 20회차 기준 자주 나온 번호와 오래 안 나온 번호 구분', stat: '45', statLabel: '개 번호 분석', accent: 'border-orange-300 bg-orange-50', statColor: 'text-orange-600' },
{ label: '연속 번호 패턴', desc: '연속 번호 쌍의 출현 패턴을 분석하여 번호 선택에 활용', stat: '98%', statLabel: '패턴 적용률', accent: 'border-yellow-300 bg-yellow-50', statColor: 'text-yellow-600' }, { label: '연속 번호 통계', desc: '역대 당첨 번호 중 연속 번호가 포함된 회차 비율 통계 제공', stat: '98%', statLabel: '연속 번호 포함 회차', accent: 'border-yellow-300 bg-yellow-50', statColor: 'text-yellow-600' },
{ label: '끝수 통계', desc: '끝자리 0~9 번호들의 출현 비율을 분석하여 분산 조합', stat: '10', statLabel: '끝수 구간', accent: 'border-amber-300 bg-amber-50', statColor: 'text-amber-600' }, { label: '끝수 통계', desc: '끝자리 0~9 번호들의 출현 비율을 분석하여 분산 조합', stat: '10', statLabel: '끝수 구간', accent: 'border-amber-300 bg-amber-50', statColor: 'text-amber-600' },
{ label: '번호 조합 시뮬레이션', desc: '추천 번호로 과거 회차 시뮬레이션을 진행하여 효과 검증', stat: '500+', statLabel: '회 시뮬레이션', accent: 'border-orange-300 bg-orange-50', statColor: 'text-orange-600' }, { label: '번호 조합 백테스트', desc: '선택한 번호로 과거 회차 대조 검증 — 몇 회나 일치했는지 확인', stat: '500+', statLabel: '회차 백테스트', accent: 'border-orange-300 bg-orange-50', statColor: 'text-orange-600' },
{ label: '정기 자동 발송', desc: '매주 정해진 요일에 이메일 및 텔레그램으로 번호 자동 발송', stat: '매주', statLabel: '자동 배송', accent: 'border-yellow-300 bg-yellow-50', statColor: 'text-yellow-600' }, { label: '정기 자동 발송', desc: '매주 정해진 요일에 이메일 및 텔레그램으로 번호 자동 발송', stat: '매주', statLabel: '자동 배송', accent: 'border-yellow-300 bg-yellow-50', statColor: 'text-yellow-600' },
]; ];
@@ -139,14 +139,14 @@ export default function LottoPage() {
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z" />
</svg> </svg>
</div> </div>
<p className="text-amber-400/70 text-xs font-bold uppercase tracking-widest mb-2">LOTTO ANALYTICS · </p> <p className="text-amber-400/70 text-xs font-bold uppercase tracking-widest mb-2">LOTTO STATISTICS · </p>
<h1 className="text-4xl md:text-5xl font-extrabold text-white mb-4 tracking-tight leading-tight"> <h1 className="text-4xl md:text-5xl font-extrabold text-white mb-4 tracking-tight leading-tight">
<br /> <br />
<span className="text-transparent bg-clip-text bg-gradient-to-r from-amber-400 to-orange-400"> </span> <span className="text-transparent bg-clip-text bg-gradient-to-r from-amber-400 to-orange-400"> </span>
</h1> </h1>
<p className="text-amber-100/60 text-base md:text-lg leading-relaxed max-w-xl mx-auto mb-6"> <p className="text-amber-100/60 text-base md:text-lg leading-relaxed max-w-xl mx-auto mb-6">
1 , / , 1 1,100+ , / ,
. . .
</p> </p>
<div className="flex items-center justify-center gap-2.5 mb-6"> <div className="flex items-center justify-center gap-2.5 mb-6">
{[7, 14, 23, 35, 41, 44].map((n, i) => ( {[7, 14, 23, 35, 41, 44].map((n, i) => (
@@ -206,7 +206,7 @@ export default function LottoPage() {
<div className="text-center mb-8"> <div className="text-center mb-8">
<p className="text-amber-600 text-xs font-bold uppercase tracking-widest mb-2">ANALYSIS ENGINE</p> <p className="text-amber-600 text-xs font-bold uppercase tracking-widest mb-2">ANALYSIS ENGINE</p>
<h2 className="text-2xl md:text-3xl font-extrabold text-[#04102b]">6 </h2> <h2 className="text-2xl md:text-3xl font-extrabold text-[#04102b]">6 </h2>
<p className="text-slate-500 text-sm mt-2"> </p> <p className="text-slate-500 text-sm mt-2">1,100+ </p>
</div> </div>
<div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-4"> <div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-4">
{analysisFeatures.map((f) => ( {analysisFeatures.map((f) => (
@@ -258,7 +258,7 @@ export default function LottoPage() {
{ feature: '핫/콜드 번호 분석', gold: '✓', plat: '✓', dia: '✓' }, { feature: '핫/콜드 번호 분석', gold: '✓', plat: '✓', dia: '✓' },
{ feature: '구매 기록 관리', gold: '✓', plat: '✓', dia: '✓' }, { feature: '구매 기록 관리', gold: '✓', plat: '✓', dia: '✓' },
{ feature: '내 패턴 AI 분석', gold: '—', plat: '✓', dia: '✓' }, { feature: '내 패턴 AI 분석', gold: '—', plat: '✓', dia: '✓' },
{ feature: '연간 당첨 패턴 리포트', gold: '—', plat: '—', dia: '✓' }, { feature: '연간 번호 출현 통계 리포트', gold: '—', plat: '—', dia: '✓' },
{ feature: '우선 고객 지원', gold: '—', plat: '—', dia: '✓' }, { feature: '우선 고객 지원', gold: '—', plat: '—', dia: '✓' },
].map((row, i) => ( ].map((row, i) => (
<tr key={row.feature} className={i % 2 === 0 ? 'bg-white' : 'bg-slate-50'}> <tr key={row.feature} className={i % 2 === 0 ? 'bg-white' : 'bg-slate-50'}>