373 lines
16 KiB
Markdown
373 lines
16 KiB
Markdown
# web-ui — CLAUDE.md
|
|
|
|
개인 웹페이지 프론트엔드 프로젝트에 대한 컨텍스트 문서입니다.
|
|
|
|
## 프로젝트 개요
|
|
|
|
- **스택**: React 18 + Vite + react-router-dom v6
|
|
- **목적**: 개인 블로그, 로또 실험, 주식 뉴스/트레이딩, 여행 기록을 한 곳에 모은 개인 웹 UI
|
|
- **배포 대상**: Synology NAS (`gahusb.synology.me`) Docker 컨테이너 내 nginx
|
|
|
|
## 페이지 구조
|
|
|
|
| 경로 | 컴포넌트 | 설명 |
|
|
|------|----------|------|
|
|
| `/` | `Home` | 메인 허브 |
|
|
| `/blog` | `Blog` | 마크다운 기반 블로그 |
|
|
| `/lotto` | `Lotto` | 로또 추천/통계 |
|
|
| `/stock` | `Stock` | 주식 뉴스/지수 |
|
|
| `/stock/trade` | `StockTrade` | 주식 트레이딩 |
|
|
| `/stock/screener` | `Screener` | 노드 기반 강세주 스크리너 (폼 ↔ n8n 스타일 캔버스 모드 토글, 점수 노드 7 + 위생 게이트 + ATR 포지션 사이저) |
|
|
| `/realestate` | `Subscription` | 청약 자격·일정 관리<br>• **프로필 탭**: 자치구 5티어 분류(드래그&드롭, PC 전용 / 모바일 read-only), 매칭 임계값 슬라이더, 텔레그램 알림 토글<br>• **카드/매칭 결과**: district 뱃지 + 5티어(S/A/B/C/D) 뱃지 표시<br>• **상세 모달**: 매칭 분석 섹션 (점수 + 사유 + 신청 자격) |
|
|
| `/realestate/property` | `RealEstate` | 관심 단지 정보 |
|
|
| `/travel` | `Travel` | 여행 사진 갤러리 (Dark Room 테마) |
|
|
| `/lab` | `EffectLab` | UI/UX 실험 허브 |
|
|
| `/lab/sword-stream` | `SwordStream` | Three.js 파티클 인터랙션 |
|
|
| `/lab/day-calc` | `DayCalc` | 날짜 계산기 |
|
|
| `/lab/music` | `MusicStudio` | AI 음악 생성 스튜디오 (Sonic Forge) |
|
|
| `/todo` | `Todo` | 태스크 보드 |
|
|
| `/blog-lab` | `BlogMarketing` | 블로그 마케팅 수익화 대시보드 |
|
|
| `/agent-office` | `AgentOffice` | AI 에이전트 가상 오피스 (WebSocket + 채팅) |
|
|
| `/portfolio` | `Portfolio` | 개인 포트폴리오 (프로필·경력·프로젝트·자기소개) |
|
|
|
|
라우트 정의: `src/routes.jsx` / 라우터 설정: `src/Router.jsx`
|
|
|
|
---
|
|
|
|
## API 설정
|
|
|
|
### 핵심 원칙
|
|
|
|
- **항상 상대 경로 사용**: 프로덕션에서 프론트와 백엔드는 nginx 리버스 프록시로 동일 도메인에서 서비스됨
|
|
- **절대 URL 사용 금지**: `https://` 절대 URL을 fetch에 직접 사용하면 Mixed Content 오류 발생
|
|
- `VITE_API_BASE` 환경변수는 사용하지 않음
|
|
|
|
### API 헬퍼 (`src/api.js`)
|
|
|
|
```js
|
|
// 모든 API 호출은 이 헬퍼를 통해 사용
|
|
import { apiGet, apiPost, apiPut, apiDelete } from './api';
|
|
|
|
// 예시
|
|
apiGet('/api/lotto/latest')
|
|
apiPost('/api/portfolio', { ... })
|
|
```
|
|
|
|
제공 함수: `apiGet`, `apiPost`, `apiPut`, `apiDelete`
|
|
|
|
### 개발 서버 프록시 (`vite.config.js`)
|
|
|
|
```js
|
|
proxy: {
|
|
'/api': { target: 'https://gahusb.synology.me', changeOrigin: true, secure: true },
|
|
'/media': { target: 'https://gahusb.synology.me', changeOrigin: true, secure: true },
|
|
// /ext/* — Yahoo Finance, CNN Fear&Greed 등 외부 API 프록시
|
|
}
|
|
```
|
|
|
|
- `/api/*` → NAS 백엔드 (nginx가 서비스별 라우팅: lotto, personal, stock, music-lab 등)
|
|
- `/media/*` → NAS 미디어 파일 (여행 사진 `/media/travel/`, 음악 `/media/music/`)
|
|
- 개발 서버 포트: **3007**
|
|
|
|
### API 엔드포인트 목록
|
|
|
|
| 분류 | 메서드 | 경로 |
|
|
|------|--------|------|
|
|
| 로또 기본 | GET | `/api/lotto/latest`, `/api/lotto/stats`, `/api/lotto/recommend` |
|
|
| 로또 기본 | GET | `/api/lotto/best`, `/api/lotto/analysis` |
|
|
| 로또 기본 | POST | `/api/admin/simulate` |
|
|
| 로또 고도화 | GET | `/api/lotto/stats/performance` |
|
|
| 로또 고도화 | GET | `/api/lotto/report/latest`, `/api/lotto/report/:drw_no`, `/api/lotto/report/history?limit=N` |
|
|
| 로또 고도화 | GET | `/api/lotto/analysis/personal` |
|
|
| 로또 구매 | GET | `/api/lotto/purchase?draw_no=N&days=N`, `/api/lotto/purchase/stats` |
|
|
| 로또 구매 | POST/PUT/DELETE | `/api/lotto/purchase`, `/api/lotto/purchase/:id` |
|
|
| 히스토리 | GET | `/api/history` |
|
|
| 히스토리 | DELETE | `/api/history/:id` |
|
|
| 주식 | GET | `/api/stock/news`, `/api/stock/indices` |
|
|
| 트레이딩 | GET | `/api/trade/balance` |
|
|
| 트레이딩 | POST | `/api/trade/order` |
|
|
| 스크리너 | GET | `/api/stock/screener/nodes` |
|
|
| 스크리너 | GET/PUT | `/api/stock/screener/settings` |
|
|
| 스크리너 | POST | `/api/stock/screener/run` — body: `{ mode, asof?, weights?, ... }` |
|
|
| 스크리너 | POST | `/api/stock/screener/snapshot/refresh` |
|
|
| 스크리너 | GET | `/api/stock/screener/runs?limit=N` |
|
|
| 스크리너 | GET | `/api/stock/screener/runs/:id` |
|
|
| 포트폴리오 | GET/POST | `/api/portfolio` |
|
|
| 포트폴리오 | PUT/DELETE | `/api/portfolio/:id` |
|
|
| 예수금 | PUT | `/api/portfolio/cash` — body: `{ broker, cash }` |
|
|
| 예수금 | DELETE | `/api/portfolio/cash/:broker` |
|
|
| 자산 스냅샷 | POST | `/api/portfolio/snapshot` — body: `{ total_assets }` 또는 body 없이 서버 계산 |
|
|
| 자산 스냅샷 | GET | `/api/portfolio/snapshot/history?days=N` — response: `{ history: [{date, total_assets}] }` |
|
|
| 실현손익 | GET | `/api/portfolio/sell-history?broker=X&days=N` — response: `{ records: [...] }` |
|
|
| 실현손익 | POST/PUT | `/api/portfolio/sell-history`, `/api/portfolio/sell-history/:id` |
|
|
| 실현손익 | DELETE | `/api/portfolio/sell-history/:id` |
|
|
| TODO | GET/POST | `/api/todos` — personal 서비스 |
|
|
| TODO | PUT/DELETE | `/api/todos/:id`, `/api/todos/done` — personal 서비스 |
|
|
| 블로그 | GET/POST | `/api/blog/posts` — personal 서비스 |
|
|
| 블로그 | PUT/DELETE | `/api/blog/posts/:id` — personal 서비스 |
|
|
| AI 음악 | POST | `/api/music/generate` — body: `{ title, genre, moods, instruments, duration_sec, bpm, key, scale, prompt }` → `{ task_id }` |
|
|
| AI 음악 | GET | `/api/music/status/:task_id` → `{ status, progress, message, audio_url?, error?, track? }` |
|
|
| AI 음악 라이브러리 | GET/POST | `/api/music/library` — response: `{ tracks: [...] }` |
|
|
| AI 음악 라이브러리 | DELETE | `/api/music/library/:id` |
|
|
| 여행 | GET | `/api/travel/regions`, `/api/travel/albums`, `/api/travel/photos` |
|
|
| 여행 | POST | `/api/travel/sync` |
|
|
| 여행 | PUT | `/api/travel/albums/:album/cover`, `/api/travel/albums/:album/region` |
|
|
| 여행 | PUT | `/api/travel/regions/:id` |
|
|
| 블로그마케팅 | POST | `/api/blog-marketing/research`, `/api/blog-marketing/generate` |
|
|
| 블로그마케팅 | GET | `/api/blog-marketing/posts`, `/api/blog-marketing/dashboard` |
|
|
| 블로그마케팅 | POST | `/api/blog-marketing/market/:id`, `/api/blog-marketing/review/:id` |
|
|
| 에이전트 | GET | `/api/agent-office/agents`, `/api/agent-office/states` |
|
|
| 에이전트 | POST | `/api/agent-office/command`, `/api/agent-office/approve` |
|
|
| 에이전트 | WS | `/api/agent-office/ws` |
|
|
| 부동산 | GET | `/api/realestate/announcements`, `/api/realestate/matches` |
|
|
| 부동산 | GET | `/api/realestate/profile` — 프로필 조회 |
|
|
| 부동산 | PUT | `/api/realestate/profile` — body: `{ preferred_districts: { "S": [...], "A": [...], "B": [...], "C": [...], "D": [...] }, min_match_score: int, notify_enabled: bool, ... }` |
|
|
| AI 큐레이터 | GET | `/api/lotto/briefing/latest`, `/api/lotto/curator/usage` |
|
|
| 포트폴리오 | GET | `/api/profile/public` — personal 서비스 |
|
|
| 포트폴리오 | POST | `/api/profile/auth` — personal 서비스 |
|
|
| 포트폴리오 | CRUD | `/api/profile/careers`, `/api/profile/projects`, `/api/profile/skills`, `/api/profile/introductions` — personal 서비스 |
|
|
|
|
---
|
|
|
|
## NAS 배포 설정
|
|
|
|
### 배포 경로
|
|
|
|
| 환경 | 경로 |
|
|
|------|------|
|
|
| Windows | `Z:\docker\webpage\frontend\` (NAS 네트워크 드라이브 마운트) |
|
|
| macOS (SMB) | `/Volumes/gahusb.synology.me/docker/webpage/frontend/` |
|
|
| macOS (SSH) | `/volume1/docker/webpage/frontend/` |
|
|
|
|
### 배포 명령어
|
|
|
|
```bash
|
|
# 빌드 + 배포 (권장)
|
|
npm run release:nas
|
|
|
|
# 빌드만
|
|
npm run build
|
|
|
|
# 배포만 (dist 폴더가 이미 있을 때)
|
|
npm run deploy:nas
|
|
```
|
|
|
|
### Windows 배포
|
|
|
|
NAS가 `Z:` 드라이브로 마운트되어 있어야 함. `robocopy`로 `/MIR` 동기화하며 로그는 `robocopy.log`에 저장됨.
|
|
|
|
### macOS 배포 — SSH 방식 (권장)
|
|
|
|
```bash
|
|
# 환경변수 설정 후 배포
|
|
NAS_SSH_TARGET=user@gahusb.synology.me NAS_SSH_PORT=22 npm run release:nas
|
|
```
|
|
|
|
`NAS_SSH_TARGET`이 설정되면 `rsync`로 SSH 배포. SMB 마운트 방식보다 안정적.
|
|
|
|
### macOS 배포 — SMB 마운트 방식
|
|
|
|
SMB 마운트 후 `ditto`로 복사. `NAS_CLEAN=1` 설정 시 배포 전 기존 파일 전체 삭제.
|
|
|
|
```bash
|
|
NAS_CLEAN=1 npm run release:nas
|
|
```
|
|
|
|
### 배포 스크립트 파일
|
|
|
|
`scripts/deploy-nas.cjs` — Node.js CJS 모듈, 플랫폼 자동 감지
|
|
|
|
---
|
|
|
|
## 개발 환경
|
|
|
|
```bash
|
|
npm install
|
|
npm run dev # localhost:3007 에서 개발 서버 실행
|
|
npm run build # dist/ 로 프로덕션 빌드
|
|
npm run lint # ESLint 검사
|
|
npm run preview # 빌드 결과물 미리보기
|
|
```
|
|
|
|
## 주요 파일 위치
|
|
|
|
| 파일 | 역할 |
|
|
|------|------|
|
|
| `src/api.js` | API 헬퍼 함수 모음 |
|
|
| `src/routes.jsx` | 라우트 및 네비게이션 링크 정의 |
|
|
| `src/Router.jsx` | BrowserRouter 설정 |
|
|
| `vite.config.js` | 개발 서버 및 프록시 설정 |
|
|
| `scripts/deploy-nas.cjs` | NAS 배포 스크립트 |
|
|
| `src/content/blog/` | 블로그 마크다운 파일 |
|
|
| `public/` | 정적 파일 (로고, API 스펙 등) |
|
|
|
|
---
|
|
|
|
## Sonic Forge — AI 음악 생성 스튜디오 (`/lab/music`)
|
|
|
|
### 파일 구조
|
|
|
|
| 파일 | 역할 |
|
|
|------|------|
|
|
| `src/pages/music/MusicStudio.jsx` | 메인 컴포넌트 |
|
|
| `src/pages/music/MusicStudio.css` | 스타일 (Bebas Neue · Syne · Courier Prime) |
|
|
|
|
### 주요 컴포넌트
|
|
|
|
- **SonicRadar** — 헤더 우측 비주얼. SVG 링·크로스헤어·스윕 라인 + 48개 CSS 방사형 바. `isGenerating` / `accentColor` prop으로 상태 전환
|
|
- **WaveformCanvas** — 스테이지 우측 캔버스 오실로스코프 (헤더와 별도)
|
|
- **AudioPlayer** — 실제 `<audio>` 엘리먼트 기반. `audio_url` 없으면 타이머 폴백
|
|
- **Library** — 저장된 트랙 카드 그리드 + 삭제/재생
|
|
- **GenerationProgress** — 진행률 바 + 단계 메시지
|
|
|
|
### 생성 플로우
|
|
|
|
```
|
|
handleGenerate()
|
|
→ POST /api/music/generate (payload에 title 포함)
|
|
→ task_id 반환 시: setInterval 3초 폴링 (getMusicStatus)
|
|
succeeded → setTrack(status.track 우선, 없으면 로컬 조립) + loadLibrary()
|
|
failed → genError 표시
|
|
→ API 실패 시: 6단계 시뮬레이션 폴백 (오프라인 모드)
|
|
```
|
|
|
|
### 백엔드 연동 규칙
|
|
|
|
- `audio_url`은 반드시 **상대경로** `/media/music/{task_id}.mp3` 형식 (절대 URL 금지)
|
|
- `status` 응답 shape: `{ status, progress, message, audio_url?, error?, track? }`
|
|
- `track` 객체: `{ id, title, genre, moods[], instruments[], duration_sec, bpm, key, scale, audio_url, created_at }`
|
|
- 백엔드가 `succeeded` 시 library 자동 등록 → 프론트는 "Save" 버튼 없음, `loadLibrary()` 자동 호출
|
|
- generate payload에 `title` 포함 → 백엔드에서 payload title 우선 사용 권장
|
|
|
|
### CSS 설계 특이사항
|
|
|
|
- 설명 토글: `.ms-desc-wrap` + `grid-template-rows: 0fr → 1fr` 트랜지션으로 높이 애니메이션
|
|
- 완전히 닫힐 때 노출 방지: `.ms-desc-wrap { overflow: hidden }` + `.ms-desc-wrap > * { min-height: 0 }`
|
|
- 장르 선택 시 `--ms-accent` / `--radar-accent` / `--g-color` CSS 변수로 전체 컬러 테마 동기화
|
|
|
|
---
|
|
|
|
## Lotto 고도화 (`/lotto`)
|
|
|
|
`src/pages/lotto/Functions.jsx`는 3탭 구조 (`브리핑 / 분석·통계 / 구매·성과`)로 리팩토링되었습니다.
|
|
|
|
| 탭 | 파일 | 설명 |
|
|
|----|------|------|
|
|
| 이번 주 브리핑 | `tabs/BriefingTab.jsx` | AI 큐레이터 브리핑 표시 (`components/briefing/` 하위 컴포넌트) |
|
|
| 분석·통계 | `tabs/AnalysisTab.jsx` | 시뮬레이션 추천·통계·ReportPanel·수동 추천 |
|
|
| 구매·성과 | `tabs/PurchaseTab.jsx` | 구매 내역 CRUD + 성과 통계 |
|
|
|
|
### 브리핑 전용 컴포넌트 (`components/briefing/`)
|
|
|
|
| 컴포넌트 | 설명 |
|
|
|----------|------|
|
|
| `BriefingTab.jsx` | 탭 루트, 브리핑 로드 + 트리거 |
|
|
| `BriefingHeader.jsx` | 회차·생성일시 헤더 |
|
|
| `BriefingSummary.jsx` | 내러티브 요약 표시 |
|
|
| `PickSetCard.jsx` | 번호 세트 1장 카드 |
|
|
| `BriefingEmpty.jsx` | 브리핑 없을 때 빈 상태 |
|
|
| `CuratorUsageFooter.jsx` | 토큰·비용 집계 푸터 |
|
|
|
|
### 신규 api.js 헬퍼
|
|
|
|
- `getLatestBriefing()` — `GET /api/lotto/briefing/latest`
|
|
- `getCuratorUsage(days)` — `GET /api/lotto/curator/usage?days=N`
|
|
- `triggerLottoCurate()` — `POST /api/agent-office/command` (lotto_agent curate 명령)
|
|
|
|
### 기존 섹션 (AnalysisTab 내)
|
|
|
|
| 섹션 | API | 설명 |
|
|
|------|-----|------|
|
|
| PerformanceBanner | `/api/lotto/stats/performance` | 수익률·당첨 통계 상단 띠 |
|
|
| ReportPanel | `/api/lotto/report/*` | 주간 리포트 + 전략 카드 + ConfidenceRing |
|
|
| PersonalAnalysisPanel | `/api/lotto/analysis/personal` | 개인 번호 성향 분석 |
|
|
| PurchasePanel | `/api/lotto/purchase/*` | 구매 내역 CRUD |
|
|
|
|
---
|
|
|
|
## Travel 갤러리 (`/travel`)
|
|
|
|
테마: "Dark Room" (배경 `#0f0c09`, 서체 Cormorant Garamond + Space Mono)
|
|
|
|
### 파일 구조
|
|
|
|
| 파일 | 역할 |
|
|
|------|------|
|
|
| `src/pages/travel/Travel.jsx` | 메인 페이지 — 앨범 카드 목록 + MiniMap |
|
|
| `src/pages/travel/AlbumCard.jsx` | 앨범 썸네일 카드 (커버 이미지 + 사진 수) |
|
|
| `src/pages/travel/AlbumDetail.jsx` | 앨범 상세 오버레이 — 사진/영상 탭 + 지역 편집 |
|
|
| `src/pages/travel/MasonryGrid.jsx` | CSS columns 기반 Masonry 레이아웃 + 무한 스크롤 |
|
|
| `src/pages/travel/HeroLightbox.jsx` | 전체화면 사진 뷰어 — 스와이프/키보드 네비게이션 |
|
|
| `src/pages/travel/MiniMap.jsx` | 접이식 Leaflet 지도 — GeoJSON 지역 + 핀 마커 |
|
|
| `src/pages/travel/RegionPinPicker.jsx` | 지도 핀 위치 지정 모달 (Leaflet 클릭 → 좌표 저장) |
|
|
| `src/pages/travel/VideoTab.jsx` | 영상 탭 (준비 중) |
|
|
|
|
### 핵심 기능
|
|
|
|
- **지역 관리**: GeoJSON 기반 지역 선택 → 앨범 필터링 + 지역 변경 + 핀 좌표 지정
|
|
- **앨범 카드**: 커버 사진, 지역 라벨, 사진 수 표시, 접근성 accent 색상
|
|
- **Masonry 그리드**: 40장 단위 청크 로딩, IntersectionObserver 기반 무한 스크롤
|
|
- **Lightbox**: 앨범 커버 지정, 스와이프/키보드 네비게이션, 추가 로딩 지원
|
|
- **MiniMap**: Polygon(기존 지역) + CircleMarker(커스텀 핀) 이중 렌더링
|
|
- **지역 편집**: AlbumDetail에서 인라인 편집 + 자동완성 + "위치 지정" 버튼
|
|
|
|
### API 연동
|
|
|
|
| 메서드 | 경로 | 설명 |
|
|
|--------|------|------|
|
|
| GET | `/api/travel/regions` | GeoJSON (커스텀 지역 포함) |
|
|
| GET | `/api/travel/photos?region=X&page=N&size=40` | 사진 페이지네이션 |
|
|
| GET | `/api/travel/albums` | 앨범 목록 + cover + region |
|
|
| POST | `/api/travel/sync` | 폴더 동기화 |
|
|
| PUT | `/api/travel/albums/{album}/cover` | 커버 지정 |
|
|
| PUT | `/api/travel/albums/{album}/region` | 지역 변경 |
|
|
| PUT | `/api/travel/regions/{id}` | 핀 좌표 저장 |
|
|
|
|
### 미디어 URL
|
|
- 사진: `/media/travel/{album}/{filename}`
|
|
- 썸네일: `/media/travel/.thumb/{album}/{filename}`
|
|
- `vite.config.js` `/media` 프록시로 처리, 프로덕션 nginx에서 직접 서빙
|
|
|
|
---
|
|
|
|
## Windows AI 서버 (`C:\Users\jaeoh\Desktop\workspace\music_ai`)
|
|
|
|
NAS의 music-lab 컨테이너 대신 Windows PC(RTX 5070 Ti)에서 MusicGen을 로컬 추론하는 별도 서버.
|
|
|
|
### 구성 파일
|
|
|
|
| 파일 | 역할 |
|
|
|------|------|
|
|
| `server.py` | FastAPI 서버 (generate/status/audio 엔드포인트) |
|
|
| `model.py` | MusicGen 래퍼 + 프롬프트 빌더 (genre/mood/instruments→텍스트) |
|
|
| `.env` | MODEL_NAME, OUTPUT_DIR, SERVER_PORT 등 |
|
|
| `setup.bat` | venv 생성 + PyTorch CUDA 12.4 + audiocraft 설치 |
|
|
| `start.bat` | 서버 시작 |
|
|
|
|
### 엔드포인트
|
|
|
|
| 메서드 | 경로 | 설명 |
|
|
|--------|------|------|
|
|
| GET | `/health` | 서버·GPU 상태 확인 |
|
|
| POST | `/generate` | 음악 생성 → `task_id` 즉시 반환 |
|
|
| GET | `/status/{task_id}` | 생성 진행 폴링 |
|
|
| GET | `/audio/{task_id}.mp3` | 완성 오디오 파일 |
|
|
|
|
### 모델
|
|
|
|
- 기본: `facebook/musicgen-stereo-large` (16GB VRAM, 스테레오 고품질)
|
|
- RTX 5070 Ti(16GB)로 실행 가능
|
|
|
|
### NAS 연동 흐름
|
|
|
|
```
|
|
web-ui → POST /api/music/generate (NAS music-lab)
|
|
→ music-lab이 Windows PC :8765/generate 호출
|
|
→ Windows PC가 MusicGen 추론 → WAV → MP3 변환
|
|
→ music-lab이 /status 폴링 → audio_url 다운로드
|
|
→ /media/music/{task_id}.mp3 저장 → DB 등록
|
|
→ 프론트 폴링 성공 → Library 자동 갱신
|
|
```
|
|
|
|
Windows 방화벽에서 포트 8765 인바운드 허용 필요.
|