Commit Graph

69 Commits

Author SHA1 Message Date
claude-owner
ecf8b9112b feat(views): anonymous daily view counter with social-proof badge
- New table symbol_views_daily(code, view_date, views) via migration 004.
- POST /api/views/{code} UPSERTs and returns today_views; GET returns
  today + last_7d + trend (last 7 days).
- Client dedupes via localStorage (views:logged map keyed by KST date) so a
  given browser counts once per day per code; refresh/extra tabs don't inflate.
- PriceHero shows "오늘 N명이 봤어요" pill when viewsToday > 0.

Read-only social proof — Toss 의 종목 상세에서 익명 카운트로 진입 신호를 주는 패턴.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 23:56:36 +09:00
claude-owner
ef127ae2e8 feat(chart): 메인 → RSI/MACD crosshair 동기화
토스 차트 인상 — 메인 캔들에 hover 하면 RSI/MACD 보조 패널도 같은 시점을
가리키며 가격 라벨이 뜬다. lightweight-charts setCrosshairPosition /
clearCrosshairPosition API 활용.

- rsiByTsRef / macdSignalByTsRef: 보조 시계열의 ts→value O(1) lookup.
  RSI/MACD 데이터 push 할 때 동시에 채움.
- crosshair handler: hover 시 보조 차트도 setCrosshairPosition,
  마우스가 차트 밖이면 clearCrosshairPosition 으로 양쪽 다 해제.
- MACD 는 signal 라인 기준 — 가격 라벨이 안정적으로 보이도록.
- 보조 차트 toggle off / 데이터 부족 시점은 ref guard 와 lookup miss 로 자연스럽게 noop.
2026-05-28 23:49:29 +09:00
claude-owner
759ea20daa fix(pwa)+feat(investor): SW dev 등록 + 투자자별 누적 순매수
PwaRegister:
- production 게이트 제거. sw.js 는 fetch handler 가 없어 HMR/네트워크 응답을
  가로채지 않음 → dev 에서 등록해도 안전. Docker 가 next dev 로 도는 실제
  배포에서도 install 자격이 잡힘.
- 등록 실패는 그대로 silently 무시 (secure context 아님 등).

InvestorCumulative:
- chart.trading_value 를 재사용해 외국인/기관/개인 N일 누적 순매수를 미니
  SVG 라인으로 표시. 백엔드 호출 0.
- 윈도우 토글: 20일/60일/120일. 데이터 부족하면 가용분으로 자동 축소.
- 라인 색은 주체별 고정 (외국인=amber/기관=cyan/개인=violet) — 비교 가능.
- 끝값 라벨은 한국 컬러 (양수=rose-300, 음수=sky-300), 억원 단위.
- 단위는 원 → 1e8 환산. 시작 0 기준 누적 + 점선 기준선.
- TradingValuePanel (최근 5일 표) 옆 그리드 자리에 배치, MetricsPanel 은
  아래줄로 이동.
2026-05-28 23:44:24 +09:00
claude-owner
89f3a865af feat(pwa): installable PWA (manifest + 빈 SW + SVG 아이콘)
설치 가능한 PWA 자격을 갖추는 최소 셋:
- web/public/manifest.webmanifest: 한국어 name/short_name, 다크 테마 컬러,
  standalone display, /icon.svg (any maskable, sizes any)
- web/public/icon.svg: 다크 배경 + 한국 컬러 캔들(상승=#ef4444 / 하락=#3b82f6)
  + 우상향 추세선. maskable safe-zone (중심 80%) 보존
- web/public/sw.js: install/activate 만 처리 (fetch handler 없음).
  install 자격만 충족하고 네트워크는 그대로 통과 → 캐시 stale 위험 0
- web/components/PwaRegister.tsx: production 빌드에서만 SW 등록,
  실패는 silently 무시
- web/app/layout.tsx: Metadata.manifest/icons/appleWebApp,
  viewport.themeColor/colorScheme 분리 (Next 14 권장)

dev (next dev) 에서는 SW 등록을 우회 — HMR 응답 간섭 회피.
2026-05-28 23:39:27 +09:00
claude-owner
38db4b8c24 feat(chart): crosshair 한국어 OHLC 오버레이 (토스 스타일)
차트 위에 hover 위치 봉의 날짜(+요일)/OHLC/등락률/거래량을 띄우는 좌상단
정보 박스 추가. lightweight-charts subscribeCrosshairMove 로 hover 좌표를
잡고, ohlcv 를 timestamp 기준 Map 으로 O(1) 조회. hover 가 차트 밖일 때는
마지막 봉을 기본 표시해 페이지 진입 직후에도 정보가 보임.

- 거래량 fmt: 억/만 (사이드바와 동일)
- 등락률: (close-open)/open, 한국 컬러 (상승=#ef4444 / 하락=#3b82f6)
- intraday(10분봉) 면 시:분 까지 포함
2026-05-28 19:31:35 +09:00
claude-owner
52733df235 fix(fundamentals): logger.exception → logger.error %r (UnicodeDecodeError 회귀 방지)
pykrx/KRX 비정상 응답에서 traceback 포매팅 중 UnicodeDecodeError 가 발생해
API 500 을 만든 회귀 사례가 있었음. 새 pykrx fundamentals 경로에 동일 패턴이
다시 들어가지 않도록 %r 안전 직렬화로 통일.
2026-05-28 19:29:49 +09:00
claude-owner
a79e784550 feat(fundamentals): 시가총액·PER·PBR·EPS·BPS·배당 — pykrx 스냅샷 + SymbolSidebar
backend:
- fetch/fundamentals.py: get_fundamentals(code) — pykrx 의 get_market_cap +
  get_market_fundamental 을 같은 영업일로 정렬해서 호출. 토/일/공휴일이면 KRX 가 빈
  응답을 줘서 최대 7일까지 역순 backoff. (code, today) 인메모리 daily 캐시로 중복 호출 방지.
- api/fundamentals.py: GET /api/fundamentals/{code} — 없는 코드 404, KRX 가 데이터를
  안 주는 케이스는 status="no_data" + null 들. 200 응답으로 단순화 (호출자 분기 편의).
- main.py: fundamentals_router 등록.

frontend:
- lib/api.ts: FundamentalsResponse 타입 + api.fundamentals(code).
- components/SymbolSidebar.tsx: 1Y 차트와 별도로 fundamentals 호출. ok 일 때만
  '기업 지표' 섹션 노출 — 시가총액(조/억) / 상장주식수 / PER / PBR / EPS / BPS /
  배당수익률(%) / 주당배당금. PER·PBR 음수면 '적자' 표기 (이익 음수 → 멀티플 무의미).
- 기존 시세/52주 게이지 코멘트에 fundamentals 추가됨을 반영.

pykrx 종목당 호출 1~2초 → 첫 페이지 로딩이 살짝 느려질 수 있으나 daily 캐시로
재방문은 즉시. 펀더멘털 실패는 silent — 시세 표시는 영향 없음.
2026-05-28 19:25:53 +09:00
claude-owner
59dc8c4e7a fix(nav): Cmd/Ctrl+K 단축키에도 isEditableTarget 가드 적용
리뷰어 지적: 주석엔 '다른 인풋/편집 영역에서 무시'라고 적어놨지만 실제 코드는
'/' 키에만 가드를 걸어둬서 Cmd/Ctrl+K 는 모든 input/textarea/contenteditable
에서 브라우저/편집기 단축키를 가로채는 상태였음.

수정: '/' 와 동일하게 !isEditableTarget(e.target) 조건 추가. 검색 인풋 자체에
포커스 중이면 어차피 focus()/setOpen(true) 가 noop 이라 UX 손해 없음.
2026-05-28 19:21:19 +09:00
claude-owner
7cd865c450 feat(nav): TopNav 검색 키보드 네비게이션 + 글로벌 단축키
키보드 조작:
- "/"           : 검색 인풋 포커스 (다른 인풋 포커스 중이면 무시)
- Cmd/Ctrl+K   : 동일 (OS 공통)
- ↑/↓         : 결과 목록 wrap-around 이동
- Enter        : 현재 강조 항목으로 이동 (검색 결과 또는 최근 본 종목)
- Esc          : popover 닫기 + 인풋 blur

기존 mouse hover 흐름과 충돌 없도록 onMouseEnter 에서 hi 동기화. navCodes 길이
변하면 hi 를 자동 clamp. placeholder 에 "( / )" 힌트 추가.

isEditableTarget() 가드로 다른 입력 필드 (PredictionPanel, 검색칩 등) 에 포커스
중일 때 "/" 가로채지 않음.
2026-05-28 19:19:01 +09:00
claude-owner
d83ac137a9 feat(chart): 메인 차트 하단에 거래량 막대 overlay 추가
- 캔들 priceScale 을 위 78%(top=0.05, bottom=0.22)로 좁힘
- 거래량 Histogram 시리즈를 overlay priceScale("volume")로 같은 차트에 부착,
  scaleMargins top=0.82 → 차트 영역 아래 18% 만 사용
- 상승봉=빨강(0x40 알파), 하락봉=파랑 — 한국 거래소 관행
- 가격축 라벨/마지막값/priceLine 숨김 → 캔들 시야 방해 최소화
- 시계열은 메인 timeScale 그대로 따라가서 별도 sync 불필요

cleanup 에서 volumeRef.current=null 도 같이 정리. intraday 포함 항상 ON
(거래량은 일/분봉 공통으로 의미 있음).
2026-05-28 19:17:13 +09:00
claude-owner
fed8ff287e feat(orderbook): 10단계 호가 패널 (KIS asking-price 스냅샷)
backend:
- fetch/kis.py: fetch_orderbook(code) — KIS FHKST01010200 (inquire-asking-price-exp-ccn)
  호출. output1 의 askp1~10/bidp1~10 + 잔량 + 합계, output2 의 현재가/전일대비 파싱.
- api/orderbook.py: GET /api/orderbook/{code}. symbols 매칭 안 되면 404, KIS 키 없으면
  skipped_missing_key 상태로 빈 호가 반환 (502 와 구분).
- main.py: orderbook_router 등록.

frontend:
- lib/api.ts: OrderbookResponse 타입 + api.orderbook(code).
- components/OrderbookPanel.tsx: 매도 10단계(위→아래 가격 내림차순) / 현재가(굵게, 전일대비)
  / 매수 10단계. 잔량 막대는 사이드별 max 정규화로 셀 안에서 자라남. ask=파랑, bid=빨강
  (한국 거래소 관행). 10초 폴링.
- app/[code]/page.tsx: 사이드바에 SymbolSidebar 다음으로 OrderbookPanel 마운트.

실시간 ws 가 아니라 HTTP 폴링이라 KIS rate-limit 친화적. 표시 전용 — 주문/체결 endpoint 는
일절 import 하지 않음 (kis.py 문서 정책).
2026-05-28 19:13:35 +09:00
claude-owner
1a6d953d17 feat(chart): MACD(12/26/9) 보조지표 sub-pane + 토글 추가
- EMA + MACD(line/signal/hist) 계산 함수 추가
- MACD 라인(파랑) + Signal 라인(주황) + Histogram (양수=빨강 / 음수=파랑 옅게)
- 별도 lightweight-charts 인스턴스 (RSI 와 동일 패턴, handleScroll/Scale off)
- MA chip 줄에 MACD 토글 chip 추가, 기본 OFF, intraday 에선 숨김
- timeScale 동기화 effect 를 단일 follower 리스트로 일반화 (RSI + MACD 동시 가능)

EMA 헬퍼는 null-tolerant — 결측 시점은 직전 ema 유지. Wilder 가 아닌
표준 EMA (alpha=2/(n+1)) 채택, 트레이딩뷰/Toss 와 동일.
2026-05-28 18:38:18 +09:00
claude-owner
9e43663b23 feat(symbol): 종목 페이지 헤더에 '⇄ 비교' 버튼 — /compare?codes={code} 로 비교 시작 2026-05-28 18:35:57 +09:00
claude-owner
5aeea1a559 feat(chart): RSI(14) 보조지표 sub-pane + 토글 추가
- Wilder's smoothing 기반 RSI 14일 계산 (워밍업 구간 null)
- 메인 차트 아래 110px 별도 lightweight-charts 인스턴스로 sub-pane
- 30/70 가이드 라인 (axisLabel 표시)
- 메인 ↔ RSI timeScale 단방향 동기화 (RSI scroll/scale off)
- MA chip 옆 RSI 토글 chip — 기본 OFF, intraday 에선 숨김

별도 인스턴스 방식 채택 이유: 같은 chart 에 priceScaleId/scaleMargins 로 영역 분할 시
캔들 priceScale 까지 영향받아 메인 줌이 망가짐. sub-chart 패턴이 lightweight-charts
공식 권장 + react cleanup 도 깔끔.
2026-05-28 18:35:19 +09:00
claude-owner
d5acf79e05 fix(alerts): /alerts 안내문 — 탭이 열려 있어야 동작함을 명시
리뷰어 지적: 기존 문구가 '탭이 닫혀 있어도 알람'으로 오해되어
실제 AlertsToaster 의 60초 폴링 동작(탭 열려 있을 때만)과 불일치.

- 메인 설명: '웹앱이 열려 있는 동안 60초마다 체크' + '탭 닫히면 체크 X' 명시
- NotificationHint: '다른 창을 보고 있을 때도 OS 알림 (탭 열려 있어야)'로 정정

Service Worker + Push API 미구현 — 로컬 localStorage 기반 앱이라 VAPID/푸시
백엔드는 과한 인프라. 문구로 한계를 정확히 표시하는 쪽으로 정정.
2026-05-28 18:33:07 +09:00
claude-owner
0f89b2c042 feat(compare): 종목 비교 페이지 (/compare)
- 최대 5종목 누적수익률(%) 한 차트 비교, 기간 1M/3M/6M/1Y
- URL ?codes=A,B,C 양방향 동기화 (Suspense + useSearchParams)
- 검색 칩으로 종목 추가, ✕ 로 제거. 종목별 최종 수익률 칩 표시
- TopNav 우측에 "비교" 링크
2026-05-28 18:27:27 +09:00
claude-owner
139403c133 feat(alerts): /alerts 통합 관리 페이지 + TopNav 🔔 링크
- 종목별 그룹핑 (Map by code), 활성·발사됨 분리 카운트
- 행 단위 재무장/삭제 + 전체 삭제 (confirm)
- Notification 권한이 default 일 때만 권한 요청 배너 노출
- TopNav 우측 ★ 옆에 🔔 알람 페이지 링크 추가
2026-05-28 18:25:27 +09:00
claude-owner
fd7026ad09 feat(alerts): 가격 알람 (localStorage + 60s 폴러 + 토스트)
- lib/alerts.ts: id/code/op(gte|lte)/target/triggered 저장소, watchlist/recent 와 동일 pub/sub
- AlertsPanel: 종목 사이드바에 ≥/≤ 토글 + 목표가 입력 + 활성 알람 리스트(재무장/삭제)
- AlertsToaster: layout 전역 마운트, 미발사 알람 코드만 폴링, 조건 충족시 markTriggered + 우측 하단 토스트(6s), Notification 권한 허용 시 브라우저 알림 병행
2026-05-28 15:26:10 +09:00
claude-owner
01e5eba3c1 feat(chart): SMA(5/20/60) 토글 오버레이
- lightweight-charts addLineSeries 로 종가 기반 단순이동평균 라인 추가
- 칩 토글 (기본 MA20 ON), intraday(10분봉)에선 UI/시리즈 모두 숨김
- fitContent 는 호출 안 함 — 사용자 줌 유지
2026-05-28 15:23:34 +09:00
claude-owner
645e93e7bf feat(market): 거래대금 상위 + 52주 위치 게이지
- /api/markets/movers: by_trading_value (close*volume DESC) 카테고리 추가
- MarketsOverview: 4 컬럼 그리드 (등락 상위/하위/거래대금/거래량), 거래대금은 억·조 표기
- SymbolSidebar: 52주 범위 내 현재가 위치 게이지 (그라데이션 바 + 화이트 점)
2026-05-28 15:21:27 +09:00
claude-owner
aa58761176 feat(ui): 최근 본 종목 (localStorage + 홈 카드 + TopNav popover)
- web/lib/recent.ts: stockchart.recent KEY, push/get/clear/subscribe (storage + custom event)
- [code]/page.tsx: 페이지 진입 시 recent.push(code) — 자동 트래킹
- web/components/RecentTiles.tsx: 홈 카드 그리드 (최대 10개, 2~5컬럼)
- web/app/page.tsx: SearchBox 아래에 RecentTiles 배치 (빈 리스트면 자동 숨김)
- TopNav: 검색어 비면서 포커스 시 최근 본 종목 popover 표시 + "지우기" 버튼

토스 "최근 본 종목" 톤 — 검색 빈 상태에서도 popover 가 비어 있지 않게.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 15:14:40 +09:00
claude-owner
300f7beef0 feat(nav): 글로벌 sticky 헤더 + 컴팩트 검색
- web/components/TopNav.tsx: sticky top header
  · 좌: Stock Chart 로고 (→ /)
  · 중: 컴팩트 검색 input + popover (debounced 200ms, with_sparkline=false 빠른 응답)
  · 우: ★ 관심 (→ /watchlist)
- 바깥 클릭/Esc 로 popover 닫힘
- 결과 클릭 시 자동 닫힘 + 검색어 클리어
- web/app/layout.tsx: <body> 최상단에 <TopNav /> 부착

토스 톱바 톤. 모든 페이지에서 검색 가능 — 홈으로 돌아갈 필요 없음.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 15:10:14 +09:00
claude-owner
85da979fbe feat(peers): 동종업종(테마) 비교 패널
- backend/app/seed/themes.py: themes_for_code(code) 헬퍼 추가 (코드 → 소속 테마들)
- backend/app/api/themes.py: GET /api/themes/peer/{code} 신규
  · 종목이 속한 모든 테마의 코드 합집합을 peer 풀로
  · 단일 SQL (ROW_NUMBER OVER PARTITION BY code) 로 21영업일 누적수익률 일괄 계산
  · 본인 / peer 평균 / 상·하위 5종목 반환
- web/lib/api.ts: PeerCompare 타입 + api.peerCompare()
- web/components/PeerComparePanel.tsx: 테마 태그 + 3-칸 요약 (본인/평균/격차) + 상·하위 5
- [code]/page.tsx 에 PeriodReturns 아래 부착. 테마 무소속 종목은 패널 자동 숨김.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 15:07:09 +09:00
claude-owner
0b4795b03f feat(ui): AI 종합 점수 카드 (모멘텀·수급·감성·추세)
토스 "투자자 분석" 톤의 단일 카드:
- 종합 점수 반원 게이지 (conic-gradient + radial mask)
- 4개 서브 점수 (0~100, 50=중립)
  · 모멘텀: SMA5 vs SMA20 격차
  · 수급:   외국인+기관 5일 누적 net 정규화
  · 감성:   뉴스 weighted_score 7일 평균
  · 추세:   20일 종가 선형회귀 기울기
- 백엔드 호출 없음 — chart payload (ohlcv+sentiment+trading_value) 클라이언트 합성

색조: 65↑ 강세=빨강 / 35↓ 약세=파랑 / 그 사이 중립=주황.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 15:02:04 +09:00
claude-owner
9b25bc6970 feat(ui): 기간 수익률 + 거래주체 순매수 패널
- PeriodReturns: chart.ohlcv 종가로 1주/1개월/3개월/6개월/1년/전체 수익률 그리드
- TradingValuePanel: chart.trading_value 외국인/기관/개인 최근 5일 표 (억원 단위)
- [code]/page.tsx 레이아웃 재배치: 수익률 → 거래주체+메트릭 → 뉴스+공시

백엔드 변경 없음 — 기존 ChartPayload 의 미사용 시리즈를 재활용.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 14:57:40 +09:00
claude-owner
da36195cf3 feat(ui): 관심종목(localStorage) + DART 공시 패널
- web/lib/watchlist.ts: KEY=stockchart.watchlist, storage+custom event 구독
- StarButton: 종목 페이지 헤더에서 ☆/★ 토글
- /watchlist 페이지: 카드 그리드 + 현재가/전일대비 + ✕ 제거
- DisclosuresPanel: 기존 /api/news?source=dart 재사용 (백엔드 신설 없음)
- 종목 페이지 하단에 공시 패널 부착, 홈에 ★ 관심종목 링크 추가

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 14:54:43 +09:00
claude-owner
ae4f07d675 feat(home+search): KOSPI/KOSDAQ 인덱스 카드 + 검색결과 미니 sparkline
- Sparkline 컴포넌트를 PriceHero 내부에서 web/components/Sparkline.tsx 로 분리·재사용
- /api/markets/indices: macro_daily 의 kospi/kosdaq N일 시계열 + 최신값/전일대비
- 홈 IndicesPanel: 두 인덱스 카드(현재값/등락/우측 sparkline)
- /api/symbols/search?with_sparkline=true: 결과 한 번에 최근 30 종가 batch 조회
- SearchBox 결과 행에 mini sparkline + 현재가/등락률 인라인 표시

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 14:46:13 +09:00
claude-owner
66a28cc7ca feat(themes): 테마 인덱스/상세 (11 카테고리 · 111 종목)
- seed/themes.py: 반도체/2차전지/바이오/게임/엔터/자동차/금융/조선·방산/플랫폼·AI/유통·소비재/에너지 정적 매핑
- /api/themes (인덱스) + /api/themes/{slug} (종목 카드 + 최신 종가/등락)
- 홈에 ThemeTiles 그리드, /themes/{slug} 상세 페이지

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 14:29:16 +09:00
claude
21db09f65f feat(markets): /api/markets/movers + /related/{code} + 홈/종목 페이지 연동
backend:
  - 신규 라우터 app/api/markets.py
  - GET /api/markets/movers?market=ALL|KOSPI|KOSDAQ&limit=10
    → gainers / losers / by_volume 3 카테고리. ohlcv_daily 최신 2 거래일 비교.
  - GET /api/markets/related/{code}?limit=8
    → 같은 market 내 거래량 상위 (sector 컬럼은 시드 단계 NULL 다수라 제외).
  - main.py 에 라우터 include.

frontend:
  - lib/api.ts 에 Mover/MoversResponse/RelatedResponse 타입 + .movers/.related 추가.
  - components/MarketsOverview.tsx: 등락 상위 / 등락 하위 / 거래량 상위 3 컬럼.
  - components/RelatedStocks.tsx: 같은 시장 내 관련 종목 8 개.
  - 홈 페이지에 MarketsOverview 배치 (SeedTiles 아래).
  - 종목 페이지 우측 사이드바 컬럼에 RelatedStocks 추가 (SymbolSidebar 아래).

verify: py_compile clean, tsc --noEmit clean, next lint clean.
2026-05-28 01:36:13 +09:00
claude
04358469d1 feat(ui): 홈 SEED-10 타일 + 가격 헤로 sparkline
홈 페이지:
  - 텍스트 카드를 SeedTiles 컴포넌트로 교체. 10개 병렬로 /api/chart 2일치 받아
    현재가 + 전일대비 (절대값+%) 카드 그리드 (2/3/5 컬럼).
  - max-w 를 3xl → 5xl 로 확장하여 타일 5열 그리드 수용.

종목 페이지:
  - PriceHero 에 series prop 추가, 우측에 SVG sparkline (최근 30 종가) 표시.
  - 라인 색상은 전일대비 부호로 결정 (상승 빨강 / 하락 파랑).
  - lightweight-charts 인스턴스 추가 없이 인라인 SVG 만 사용 — 가벼움.

verify: tsc/next lint clean.
2026-05-28 01:27:49 +09:00
claude
832e73b5ee feat(ui): 뉴스 리스트 카드 리디자인 (감성 칩 + 출처 배지 + 상대시간)
토스 뉴스 카드 톤. 변경:
  - 감성 라벨을 chip 형식으로 (긍정=빨강, 부정=파랑, KR 관행 유지)
  - 출처를 작은 배지로 분리, 본문과 시각 구분
  - published_at 을 'N분 전 / N시간 전 / N일 전' 상대시간으로
  - 카드 hover 시 border-zinc-700 강조

썸네일은 백엔드가 og_image 미제공이라 텍스트 카드로 한정. 추후 백엔드에
컬럼 추가되면 SourceBadge 위에 16:9 썸네일 슬롯 추가.

verify: tsc/next lint clean.
2026-05-28 01:22:43 +09:00
claude
16aeba1a20 feat(ui): 우측 시세 정보 사이드바 추가 (시가/고가/저가/거래량/52주범위)
토스 종목 페이지 우측 카드와 동일 위치/구성. ChartPayload(1Y, 1d) 한 번 받아
오늘 OHLC + 1년 high/low 만 계산. 백엔드가 PER/EPS/시총 미제공이라 제외.

lg 브레이크포인트에서 chart(1fr) + sidebar(280px) 그리드.
mobile 에서는 사이드바가 chart 아래로 스택.

verify: tsc --noEmit clean, next lint clean.
2026-05-28 01:21:43 +09:00
Claude
6ab3679331 feat(seed): KRX live 실패 시 정적 주요종목 fallback (156개)
배경: 토요일/봇차단 등으로 KRX 가 LOGOUT/JSONDecodeError 를 주면
pykrx 가 빈 리스트만 돌려줘서 SEED 10 종목 외엔 검색이 안 되는
회귀가 있었다 (기아/네이버/엔비디아 같은 비-seed 검색 실패).

해결: 시총/거래대금 상위 + 친숙도 기준으로 KOSPI 104 / KOSDAQ 52
= 156 개를 정적 스냅샷 (app/seed/krx_static.py) 으로 묶었다.
seed_symbols() 가 KRX 라이브 fetch 결과가 0건이면 자동으로 이
스냅샷에서 upsert. KRX 가 정상 돌아오면 다음 호출이 라이브 데이터로
자연 덮어쓰여진다. 응답에 static_fallback_used 필드 노출.

수록 항목은 KRX 공시 종목코드 + 현재 등록명 (공개 사실).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 01:15:58 +09:00
Claude
b16f2b578d feat(ui): 예측 오버레이 캔들화 + 7탭 기간 셀렉터 + 가격 헤로
- 예측 오버레이: 라인 시리즈 3개 (median/q10/q90) 를 단일 캔들 시리즈
  하나로 합쳤다. 합성 OHLC: open=직전 close, close=point_close,
  high=ci_high, low=ci_low. 옅은 색 (up #fda4af / down #93c5fd) 으로
  실 캔들과 시각 톤은 같고 명도만 낮춰 "어우러지되 미래"임이 보이게.
- 예측 프리셋 단순화: 단기/중기/장기 + 직접입력 다 떼고 15일/30일/1년
  3개로. 백엔드 horizon cap 30 → 252 로 확장 (1년 = 240 거래일).
- PriceHero: 종목명/현재가/등락 큰 글씨 헤더. KR 관례대로 상승=빨강,
  하락=파랑.
- PeriodTabs: 1일/1주/1달/3달/1년/5년/최대 7탭. 기존 interval+days
  이중 컨트롤 제거. 5년·최대 는 자동으로 주봉/월봉으로 폴백.
- 차트 본체 캔들 색조도 KR 관례 (적/청) 로 통일.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 01:05:23 +09:00
Claude
bf898d78be chore(gitignore): backend/data/ 추가 (LGBM 학습 체크포인트)
리뷰어가 호스트에서 backend/data/ 가 untracked 로 잡힌 걸 발견. LGBM
체크포인트는 런타임 산출물이라 커밋되면 안 됨.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 17:01:41 +09:00
Claude
73593adb5c fix(seed): /api/refresh/seed/symbols 500 → 200 (logger.exception 우회)
pykrx 가 주말/장마감 시 KRX 에서 비-JSON 응답을 받아 JSONDecodeError 를
던지는데, except 블록의 logger.exception() 이 traceback 을 포매팅하다
Python 3.11 의 _byte_offset_to_character_offset 버그(pykrx 소스의 한글
주석 'df = 가...' 바이트 처리) 로 UnicodeDecodeError 가 떠서 try 밖으로
escape — 500 의 진짜 원인이었다.

- symbols_seed.py: logger.exception → logger.error(repr(e)) 로 교체.
  traceback 포매팅을 피한다.
- refresh.py: 라우트 핸들러를 try/except 로 감싸 만일 다른 경로로
  예외가 새도 200 + ok:false 로 응답. SEED 10 종목은 별도 트랜잭션이라
  KRX fetch 실패와 무관하게 보장됨.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 16:54:55 +09:00
claude-owner
323061df02 fix(chronos): inference RuntimeError 시 자동 CPU 폴백
ea88597 의 fp32 변경만으로는 sm_86 커널 누락 케이스를 100% 회피한다는
보장이 없음 (에러 메시지가 dtype 과 무관한 dispatch 단계에서 날 수 있어
fp32 도 같은 증상 가능). 그래서 forecast() 안에서 RuntimeError 잡아
'no kernel image' / 'CUDA error' / 'CUBLAS' 신호면 pipeline 을 CPU 로
재로드하고 한 번 더 추론. 폴백 후엔 그 세션 동안 계속 CPU 사용 (재시도
비용 회피).

이로써 사용자는 환경변수 수동 변경 없이도 GPU 비호환 시 자동으로 차트가
뜸. GPU 가 잘 도는 경우는 영향 없음.
2026-05-23 16:09:09 +09:00
claude-owner
ea885973c7 fix(chronos): cuda 도 fp32 기본 — sm_86 bf16 커널 누락 우회
torch 2.3.1+cu121 사전빌드 wheel 이 RTX 3070 Ti(sm_86) 의 일부 T5 bf16
커널을 빠뜨려서 inference 첫 호출에 'no kernel image is available for
execution on the device' 가 났음. ping/_load 까지는 통과해 진단이 까다로움.

chronos-t5-small 은 46M params 라 fp32 로도 8GB VRAM 여유 충분. cuda 도
fp32 default 로 바꾸고, CHRONOS_DTYPE=bf16|fp16 env 로 강제 가능하게 둠.

005930 h=1 예측 409 의 실제 원인이 이거였음 (LAN 으로 직접 확인).
2026-05-23 16:03:31 +09:00
claude-owner
e0edc8f1e3 feat: 예측 실패 원인 노출 + /health/models 진단 + restart-ci.bat
사금향님이 만난 409 'both chronos & lgbm failed' 에러가 원인을 안 보여줘서
디버깅 어려웠음. 세 군데 보강:

1. ensemble.py: 두 모델 다 실패 시 chronos/lgbm 각각의 실제 에러 원문
   (type:message) 을 RuntimeError 메시지에 포함. predict.py 가 409 detail
   로 그대로 노출하므로 브라우저에서 바로 원인 확인 가능. LGBM 가 None
   반환 (체크포인트 없음) 인 경우도 'model checkpoint not found' 로 명시.

2. /health/models 엔드포인트 추가:
   - chronos.ping() — lazy load 시도 + 디바이스/모델명 반환
   - LGBM_MODEL_DIR 의 *.pkl 개수와 샘플 8개 파일명 반환. cold start
     (체크포인트 0개) 면 'no_checkpoints' 상태로 알림.

3. restart-ci.bat 추가 — restart.bat 에서 pause 빼고 종료 코드로만 알리는
   SSH 비대화형 친화 버전. 일반 사용은 그대로 restart.bat.
2026-05-23 15:42:44 +09:00
claude-owner
44873ddb39 fix(chart): "오늘" 표시를 차트 본체 마커 → 시간축 아래 캡션으로 이동
lightweight-charts 의 timeScale tick 라벨에 직접 텍스트를 끼우는 공식 API 가
없어서, 차트 컨테이너 바로 아래에 작은 캡션 (• 오늘 · YYYY-MM-DD (요일)) 으로
표시. 사용자 요청 ("차트에 표시 말고 아래 날짜 표시하는 곳에").
2026-05-23 02:49:36 +09:00
claude-owner
e610599879 fix(chart): 10분봉 페이지네이션, 장외 캐시, 예측 horizon 캡 일치
reviewer 지적사항 반영:

1. KIS 분봉이 한 번에 30개만 와서 10분봉이 최대 3개만 나오던 문제 →
   fetch_minute_range() 추가. FID_INPUT_HOUR_1 을 30분씩 후퇴시키며
   페이지네이션, 중복 ts 자연 dedupe, max_pages=20 으로 무한루프 방지.
   _ensure_intraday_fresh 는 last_ts+1m ~ now 빈 구간만 채우므로 평소엔
   1~2 페이지로 끝남.

2. 장외/주말에 매번 KIS 를 때리던 문제 →
   - 주말: 'weekend' 반환, KIS 안 부름 (분봉 endpoint 는 당일만 지원)
   - 평일 장외 + 오늘 데이터 있음: 'cached_closed' 반환
   - 장중 + 10분 이내: 'fresh' 반환
   토큰/조회 제한 다시 밟지 않음.

3. 프론트 horizon 입력 60 vs 백엔드 30 불일치 →
   PredictionPanel 의 cap 을 30 으로 맞춤. 백엔드 predict.py 의 학습/검증
   범위와 일치. placeholder 도 '1~30' 으로 명시.
2026-05-23 01:41:43 +09:00
claude-owner
0a5c634680 feat(chart): 10m 실시간 / 일·주·월 토글 / 오늘 마커 / 예측 거래일 선택
- backend/app/api/chart.py: interval=10m|1d|1w|1mo. 10m 은 ohlcv_1m 을
  time_bucket(10min) 으로 집계, stale(>10분) 이면 KIS 분봉 fetch 후 재조회.
  1w/1mo 는 ohlcv_daily 를 date_trunc 로 집계. today 필드 추가.
- backend/app/fetch/kis.py: fetch_minute_price() 추가 (tr_id FHKST03010200).
  KIS 응답 KST 시각을 tz-aware datetime 으로 변환, 오름차순 정렬.
- web/lib/api.ts: ChartInterval 타입, getChart(interval), predict(horizons[]).
- web/components/StockChart.tsx: 10m 이면 timeVisible. 일·주·월에서 오늘
  화살표 마커 표시. ISO datetime 도 파싱.
- web/components/PredictionPanel.tsx: 단기/중기/장기 프리셋 + 사용자 직접
  지정 (예: 1,2,3,7). API 에 horizons 배열 전달.
- web/app/[code]/page.tsx: interval 칩 (10분/일/주/월). 10m 일 때 60초마다
  폴링. interval 별 기본 lookback (10m=1, 1d=180, 1w=730, 1mo=1825).
2026-05-23 01:34:29 +09:00
claude-owner
928c2160f9 feat(kis): 토큰 디스크 캐시 + restart.bat env 재로드
토큰 발급 1분/1일 제한 (EGW00133 등 403) 회피:
- _load_disk_cache / _save_disk_cache 로 /app/.cache/kis_token.json 영속화
  · ./backend:/app 바인드 마운트로 호스트 backend/.cache/ 에 저장
  · backend/.cache/ 는 .gitignore 에 이미 포함됨 (secret 비커밋)
  · app_key prefix 캐시 무효화 키 (.env 갱신 시 자동 폐기)
  · atomic write (tmp → rename) + 0600 권한
- get_token: 메모리 → 디스크 → 신규 발급 순으로 fallback
- 컨테이너 재기동해도 24시간 유효 토큰 재사용 → 발급 폭주 방지

restart.bat:
- restart → up -d --force-recreate --no-deps backend web
  · restart 는 env_file 재로드 안 함 (.env 의 KIS_APP_KEY 변경이 무시됨)
  · up -d 는 새 인스턴스 생성하며 env_file 다시 읽음
  · --no-deps 로 db 는 절대 건드리지 않음 (postgres_data 영속)
2026-05-23 01:22:32 +09:00
tkrmagid
78388d347e fix(web): API base 를 페이지 host 로 동적 해석 (LAN 접속 대응)
리뷰어 지적: docker-compose 가 NEXT_PUBLIC_API_BASE=http://localhost:8000 을
주입해서 클라이언트 번들에 localhost 가 inline 됨. 사금향 게임컴 (192.168.10.13)
브라우저에선 동작하지만, 같은 내부망의 다른 PC 또는 외부 검증자가
http://192.168.10.13:3000 에 접속하면 fetch 가 그 PC 의 localhost:8000 으로 가서
연결 실패. 백엔드는 정상인데도 '검색 결과 없음' 으로 보임.

해석 우선순위:
  1) NEXT_PUBLIC_API_BASE 가 비 localhost 값 → 그대로 (프로덕션 도메인 케이스)
  2) 브라우저 → window.location.hostname:8000 (LAN/localhost 자동 대응)
  3) SSR 폴백 → localhost:8000

localhost/127.0.0.1 판별은 //(localhost|127.0.0.1)(?::|$) 정규식 — 'localhost.evil.com'
같은 서브도메인 우회는 매치 안 됨. node 로 7케이스 검증 완료.

web/app 코드만이라 사금향 PC 는 restart.bat 으로 반영 (next dev hot-reload).
2026-05-21 21:52:09 +09:00
tkrmagid
659871118f fix(compose-gpu): MODEL_DEVICE 를 .env 로 덮어쓸 수 있게 변경
이전: MODEL_DEVICE: cuda (하드코딩) → .env 에 MODEL_DEVICE=cpu 두어도
compose environment 가 env_file 보다 우선이라 무시됨.

수정: MODEL_DEVICE: ${MODEL_DEVICE:-cuda} — 셸 env 또는 .env 의 값이
있으면 그 값, 없으면 기본 cuda.

용도: FinBERT/Chronos 가 GPU 에서 'no kernel image is available for
execution on the device' (PyTorch cu121 / RTX 3070 Ti 호환 이슈) 가 날 때
.env 에 MODEL_DEVICE=cpu 두고 backend 만 recreate 하면 CPU 폴백.

  echo MODEL_DEVICE=cpu>> .env
  docker compose -f docker-compose.yml -f docker-compose.gpu.yml up -d backend
2026-05-21 21:32:54 +09:00
tkrmagid
bd47198088 fix(backend): setuptools<80 핀 (pykrx 가 pkg_resources 필요)
증상 — 사금향 게임컴 (192.168.10.13) 백엔드 진단으로 확정:
  POST /api/refresh/005930 → pykrx_ohlcv: failed
    error: 'No module named pkg_resources'

root cause: setuptools 80.0.0 (2025-04) 이 pkg_resources 모듈을 제거함.
Dockerfile 의 'pip install --upgrade ... setuptools ...' 가 최신 (80+) 을
설치 → pykrx 의 'import pkg_resources' 가 import 실패.

해결:
  1) bootstrap 단계에서 'pip install setuptools<80' 명시 핀
  2) reqs.txt 설치 후에도 동일 핀 반복 (transitive upgrade 차단)
  3) 빌드 단계에서 'import pkg_resources' 확인 — 깨지면 빌드 실패시켜
     런타임에서 발견되는 일 없도록 함

이 패치 후 KIS (이미 ok 였음) 와 함께 pykrx ohlcv/trading_value 가 정상
동작 → chart 자동 충전 (fa817b3) 이 의도대로 데이터 채워 넣음.

별개 이슈 (이 커밋 범위 밖): FinBERT/Chronos 가 RTX 3070 Ti 에서
'no kernel image is available for execution on the device' 발생.
임시 회피: .env 에 MODEL_DEVICE=cpu 추가.
2026-05-21 21:30:01 +09:00
tkrmagid
fa817b31e4 feat(chart): ohlcv_daily 비어있으면 자동 refresh
문제: symbols 시드 후 첫 방문 시 ohlcv_daily 가 비어있어서
  - 차트가 빈 캔버스로 렌더링됨
  - 예상차트 보기 → 409 'no ohlcv_daily for {code}; refresh first'
daily_batch (16:00 KST) 또는 수동 POST /api/refresh/{code} 만이 OHLCV 를
채우는 구조였는데, 첫 방문 사용자는 둘 다 트리거 안 됨.

수정: GET /api/chart/{code} 가 ohlcv 0행 + symbol 존재 케이스에서
refresh_code 를 동기 호출 (pykrx OHLCV + trading_value + news + finbert)
후 재조회. idempotent 라 다음 방문부터는 캐시 히트.

lookback_days = max(days, 365) — 차트 요청 범위 + Chronos/LightGBM
학습용 과거 시계열 마진. 첫 호출 ~10-30s, 이후는 즉시.

KRX 가 도커망에서 막힌 환경이면 refresh 가 실패 status 로 끝나고
ohlcv 는 여전히 비어있음 — 기존 동작과 동일 (graceful).
2026-05-21 21:10:44 +09:00
tkrmagid
96b7afd443 fix(restart.bat): backend/web 만 재시작 (db 제외)
리뷰어 지적: docker compose restart 는 서비스명 생략시 db 까지 같이 재시작한다.
db 까지 같이 내려가면 backend lifespan 부팅 시드가 db healthcheck 와 무관하게
실행돼서 race 로 seed 가 다시 실패할 수 있다 (restart 는 depends_on.condition:
service_healthy 를 지키지 않음). db 는 postgres_data 볼륨에 상태가 영속이라
재시작할 이유도 없음.

추가: 실행중 컨테이너 체크도 backend/web 기준으로 좁힘 — db 만 떠있고
backend 가 down 인 경우 통과되던 버그 수정.
2026-05-21 21:05:57 +09:00
tkrmagid
89651251a4 feat: restart.bat 추가 (재빌드 없이 컨테이너만 재시작)
build.bat 은 이미지 재빌드 포함 — Dockerfile / pyproject.toml / package*.json /
compose 설정 같은 의존성/이미지 구성이 바뀌었을 때 사용.

restart.bat 은 docker compose restart 만 호출 — backend/app/, web/app/ 안의
코드만 바뀐 경우 사용. 바인드 마운트 (./backend:/app, ./web:/app) 덕에 새 코드가
즉시 컨테이너에서 보이고, 재시작으로 lifespan 부팅 시드도 재실행된다.

사용 가이드:
  git pull 후 변경 파일이
    - pyproject.toml / Dockerfile / package*.json → build.bat
    - app/ 코드만                                  → restart.bat
2026-05-21 21:03:16 +09:00
tkrmagid
296bd6dccd fix(seed): SEED 10종목 항상 보장 + KRX 실패 graceful degrade + 수동 reseed API
문제: 빌드/기동은 성공했는데 종목 검색 결과 빈 화면. 원인은 시드 트랜잭션
구조 — `_fetch_market_listing(KRX)` 가 `with engine.begin()` 블록 *밖*에서
호출되고, 컨테이너에서 KRX 서버 접근이 실패하거나 단일 ticker 처리 중
한 곳이라도 예외가 나면 전체 트랜잭션이 롤백되어 SEED_TICKERS 10개조차
들어가지 않음.

수정:
- `_upsert_seed_tickers()` 분리. SEED 10종목 전용 트랜잭션. 네트워크 무관.
- `seed_symbols()` 가 (1) SEED 먼저 → (2) KRX 시장별 fetch (try-per-market)
  → (3) 전 종목 bulk upsert (별도 트랜잭션) 순서로 동작. KRX fetch 실패해도
  SEED 는 살아남음.
- `main.py` lifespan bootstrap 이 SEED upsert 를 매 부팅마다 무조건 실행
  (10회, ms 단위, idempotent). count <= 10 일 때만 KRX 전 종목 fetch.
- 새 admin 엔드포인트 `POST /api/refresh/seed/symbols` — 컨테이너 재기동
  없이 시드 재시도 가능.

이번 사례 (사금향 PC) 의 복구 절차:
    curl -X POST http://localhost:8000/api/refresh/seed/symbols
이후 검색에 SEED 10 종목은 즉시 떠야 함. KRX 가 막힌 환경이면 markets={0,0}
가 응답에 찍히고, 그래도 SEED 만으로 핵심 동작은 가능.
2026-05-21 02:31:49 +09:00