23 Commits

Author SHA1 Message Date
claude-owner
d53ff7a745 feat(sidebar): RelatedStocks defaults collapsed + readOpen garbage-value fallback
readOpen() (reviewer f9027f5 non-blocking note):
- Previously returned v === '1' which collapsed '0' AND any garbage
  value (legacy markers, manual edits) into the same false branch,
  contradicting the 'fallback to default' comment. Split into three
  explicit cases: '1' → true, '0' → false, anything else →
  defaultOpen. Behavior unchanged for the writeOpen-only path; only
  garbage input is corrected.

RelatedStocks defaultOpen=false:
- Of the three collapsible sidebar panels (AlertsPanel,
  InvestmentNote, RelatedStocks), RelatedStocks has the lowest
  information density per row and the weakest direct tie to 'this
  symbol' (it's other symbols in the same market). Starting collapsed
  cuts initial mobile scroll height for first-time visitors; once the
  user toggles, localStorage pins their preference.
2026-06-01 10:31:48 +09:00
claude-owner
f9027f5365 feat(sidebar): collapsible AlertsPanel / InvestmentNote / RelatedStocks
Three lower-sidebar panels are now wrapped in a shared Collapsible so
mobile readers can fold away sections they're not actively using.

New shared pieces:
- lib/collapsible.ts — readOpen/writeOpen helpers backed by
  localStorage under 'panel-open:<name>'. Per-panel-name (not
  per-code) because the preference is a panel-level habit; users
  don't want different fold state for every symbol.
- components/Collapsible.tsx — button-as-header + chevron + body.
  aria-expanded for AT. Body uses 'hidden' instead of conditional
  unmount so textarea text, partial alert form input, and
  RelatedStocks fetch results all survive collapse cycles.
  SSR-safe: useState(defaultOpen) then useEffect-sync to avoid React
  hydration mismatch when localStorage diverges from default.

Panel refactors:
- RelatedStocks: outer rounded div + header replaced with Collapsible;
  loading/error/empty/data states all rendered inside body (header
  stays visible so user can still toggle/recognize the section even
  while loading).
- InvestmentNote: same swap. saved.updatedAt drives the subtitle.
- AlertsPanel: same swap. current price drives the subtitle.

Defaults are open=true everywhere; the surface area is the toggle
itself, not a forced collapse. User's first interaction sets the
sticky preference.
2026-06-01 10:26:33 +09:00
claude-owner
d6fc9d8a0b feat(symbol-page): intraday short-term returns + hline storage-failure guard
- IntradayReturns: 10분/30분/1시간/3시간/시가대비 mini cards for 1D mode
  (PeriodReturns labels lie when interval is 10m; swap them out by isIntraday)
- addLine/removeLine/clearLines now return previous state when localStorage
  write fails — UI no longer diverges from storage in locked-storage env

addresses reviewer 318eae6 non-blocking caveat about write-failure divergence
2026-05-29 00:35:21 +09:00
claude-owner
318eae67df feat(chart): horizontal line drawing tool + modal-open shortcut guards
리뷰어 093ac86 지적 처리:
- 종목 페이지 단축키 핸들러 (←/→/S) 에 modal 가드 추가.
  `document.querySelector('[role="dialog"][aria-modal="true"]')` 가 매치되면
  return — ShortcutsHelp 같은 모달이 열린 상태에서 close 버튼이 포커스를
  잡고 있어도 페이지 단축키가 뒤에서 동작하지 않도록.
- StockChart 의 F 단축키에도 동일한 modal 가드 — 동일 문제 (모달 위에서
  F 가 차트 전체화면 토글) 차단.

신규 슬라이스 — 차트 수평선 그리기 도구:
- `web/lib/lines.ts` — `hlines:<code>` 키 localStorage. id 단위 add/remove/clear,
  최대 10 개, 같은 가격 중복 추가 방지(부동소수 4 자리 비교), crypto.randomUUID
  폴백 포함.
- StockChart 툴바: `─ 수평선 (N/10)` 버튼 — hover 가격 또는 마지막 봉 종가에
  추가. 한도 도달 / 가격 없음이면 disabled.
- 추가된 라인은 chips 형태로 toolbar 아래 한 줄에 표시 — 각각 ✕ 로 제거,
  `전체 해제` 버튼 별도.
- H 키 단축키 — modifier/editable/modal 가드 일관 적용. 가격은 ref 로 들고
  있어 keydown effect 가 재구독되지 않음.
- 차트엔 `createPriceLine` 으로 zinc-400 solid 라인 (목표/손절의 점선과 톤
  분리). 종목 전환 시 자동 reload.
- ShortcutsHelp 에 H 단축키 등록.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 00:29:56 +09:00
claude-owner
1e0c917f68 feat(chart): CSV export of current chart range + fix ShareButton comment
- New lib/csv.ts: RFC 4180 escaping, UTF-8 BOM prefix (한글 Excel 호환),
  Blob + temporary anchor download, filename
  `<name>_<code>_<interval>_<from>_<to>.csv` with filesystem-illegal
  characters replaced by `_`.
- StockChart toolbar now always renders (was hidden in 10m intraday mode
  because it gated on showSma) and includes a right-aligned `⬇ CSV`
  button next to the SMA/RSI/MACD chips. Button is disabled when ohlcv
  is empty.

Also addresses the reviewer's non-blocking note on f18493f:
ShareButton.tsx header comment claimed clipboard-first, but the code
calls navigator.share() first. Comment now matches the actual order:
share → clipboard → execCommand → "복사 안됨".

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 00:11:57 +09:00
claude-owner
f18493fad4 feat(share): symbol page share button + distinguish note delete vs failure
- New ShareButton in the page header tries navigator.share() first
  (native sheet on mobile), then falls back to navigator.clipboard, then
  to a hidden textarea + execCommand("copy") for non-secure-context LAN.
  Shows a brief "링크 복사됨" / "복사 안됨" toast; native share success
  shows none (the OS sheet is the feedback).
- Mounted between ⇄ 비교 and the Star button.

Fixes reviewer concern on 442cc38: writeNote() used to return null for
both "user emptied the textarea (intentional delete)" and "localStorage
quota failure," so clearing a note showed "저장 안됨." It now returns a
discriminated { status: "saved" | "deleted" | "failed" } and the panel
shows "저장됨" / "삭제됨" / "저장 안됨" accordingly. Failure keeps the
user's text so they can retry or shorten it.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 00:08:37 +09:00
claude-owner
442cc38c32 feat(notes): per-code private investment note panel
- New lib/notes.ts: localStorage key `notes:<code>` storing { text, updatedAt },
  2000-char cap, blank/whitespace removes the key.
- New InvestmentNote sidebar panel: textarea + character counter, save button
  enabled only when dirty, "방금 전 / N분 전 / 오늘 HH:MM / YYYY-MM-DD"
  relative timestamp on the last save, short save-confirmation toast.
- Mounted in [code]/page.tsx sidebar after AlertsPanel.

Also tightens writeTargets() to require strictly positive finite values,
matching the UI's parseNum() guard so the lib is safe under direct external
calls (reviewer's non-blocking note on a4a4a7f).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 00:05:33 +09:00
claude-owner
a4a4a7f98c feat(targets): price target/stop simulator with chart overlay lines
- New lib/targets.ts: per-code localStorage (key `targets:<code>`) with
  read/write/clear; rejects non-positive/non-finite values.
- New PriceTargets sidebar panel: 목표가/손절가 inputs, live %-from-current
  preview (rose=up, sky=down), save/clear actions.
- StockChart accepts optional `targets` prop and draws dashed horizontal
  price lines on the candle series (rose=target, sky=stop) with axis labels.
- Page loads saved targets on mount/code-change and re-renders chart on save.

Also fixes view-counter dedupe caveat from reviewer: split into
wasRecordedToday() + markRecorded(); the page now marks localStorage only
after POST /api/views succeeds, so a failed POST stays retryable on the
next visit instead of being silently swallowed for the day.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 00:01:43 +09:00
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
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
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
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
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
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
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-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
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
5e6ce11491 feat(weights): shadow chronos/lgbm 예측 + ensemble_weights 자동 보정
- ensemble.predict() 가 chronos_raw / lgbm_raw 를 함께 반환
- predict_and_store() 가 매 호출마다 3종 행 적재:
    model='ensemble' (user_triggered=인자)
    model='chronos'  (user_triggered=FALSE, shadow)
    model='lgbm'     (user_triggered=FALSE, shadow)
- retrain_weekly.adjust_weights(): 최근 30일 prediction_outcomes 의
  chronos vs lgbm hit_rate 로 ensemble_weights upsert
    w_chronos = clamp(0.1, hr_c/(hr_c+hr_l), 0.9), w_lgbm = 1 - w_chronos
  모델별 표본 < 10 이면 기본값(0.6/0.4) 유지
- API 응답에 saved_shadow_ids 추가 (TS 타입도 동기화)
- README: 동작 모델 메모 섹션을 실제 구현과 일치하도록 갱신

리뷰어 지적 3번 (ensemble_weights 가 영원히 갱신 안됨, upsert_weights 미호출) 해결.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 16:27:21 +09:00
tkrmagid
4fb6cec383 feat(phase-6): Next.js UI + TypeScript strict + 백엔드 mypy 설정
UI:
- web/lib/api.ts: 백엔드 모든 엔드포인트의 클라이언트 + 타입 (Symbol,
  ChartPayload, PredictResponse, LatestPredictionResponse, MetricsResponse,
  NewsResponse). NEXT_PUBLIC_API_BASE 자동 정규화.
- web/components/SearchBox: 디바운스 검색, seed_only 토글, trigram + prefix.
- web/components/StockChart: lightweight-charts 캔들 + 예측 overlay
  (median dashed + q10/q90 점선). base_date 에서 target_date 까지 이어 붙임.
- web/components/PredictionPanel: "예상차트 보기" 버튼 → POST /api/predict
  → user_triggered=TRUE 저장 → onResult 콜백으로 StockChart 에 반영.
  표로 +1/+3/+5거래일 direction, prob_up/flat/down, expected_return,
  ci_low~ci_high 표시.
- web/components/MetricsPanel: 최근 30일 hit_rate / mae.
- web/components/NewsList: 최근 뉴스 + 감성 라벨/점수.
- web/app/page.tsx: 검색 페이지.
- web/app/[code]/page.tsx: 종목 상세 (차트 + 패널 + 메트릭 + 뉴스).

TypeScript 보강 (사용자 요청 "typescript도 추가해서 나중에 수정하기 쉽게"):
- tsconfig.json: strict 외에 forceConsistentCasingInFileNames,
  noFallthroughCasesInSwitch, noImplicitOverride 추가.
- package.json: typecheck (tsc --noEmit), check (typecheck + lint) 스크립트,
  eslint + eslint-config-next 14.2.3.
- .eslintrc.json: next/core-web-vitals.
- package-lock.json 커밋 (재현 가능한 dep).

백엔드:
- pyproject.toml: [tool.mypy] 추가. strict_optional, no_implicit_optional,
  check_untyped_defs. 3rd-party stub 없는 pykrx/chronos 등은 ignore.

검증: `npx tsc --noEmit` 통과 (exit=0).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 16:10:24 +09:00