Commit Graph

90 Commits

Author SHA1 Message Date
claude-owner
b46f6f1b3a feat(targets): wrap PriceTargets in Collapsible
사이드바 5번째(이자 마지막 정보성) 패널인 PriceTargets 도 공용 Collapsible
로 통일. SymbolSidebar(상단 종목 카드 — 자체 톤 유지가 맞음) 만 남기고
나머지 사이드 패널 5개(Orderbook / Targets / Alerts / Note / Related)가
모두 같은 토글/영속 패턴.

subtitle 설계 — 접힌 상태에서도 저장값 한눈에:
- 둘 다 저장됨: "목표 12,500 · 손절 9,800"
- 한쪽만: "목표 12,500" / "손절 9,800"
- 둘 다 없음: "비공개 · 브라우저 저장" (InvestmentNote 와 동일 톤)
- dirty 입력값이 아닌 saved 기준만 표시 — 토글 사이클 사이에 안내가
  깜빡이지 않게.

storageKey="targets" → panel-open:targets 영속.
defaultOpen=true — 입력값은 Collapsible 의 hidden 본문 덕에 토글에도
보존되지만, 처음 보는 사용자에겐 input 이 펴져 있어야 "여기서 적는다"
가 자명. 입력 흐름 우선.

기존 rounded-lg 는 Collapsible 기본 rounded-md 로 흡수 — 다른 사이드
패널과 모서리 톤 통일.

검증:
- npx tsc --noEmit clean
- npx next lint "✔ No ESLint warnings or errors"
2026-06-01 10:40:04 +09:00
claude-owner
70d56b14ec feat(orderbook): wrap OrderbookPanel in Collapsible (sticky-aware)
사이드바 6 패널 중 마지막 남은 비-collapsible 패널이던 OrderbookPanel 을
공용 Collapsible 로 통일. 이제 SymbolSidebar/PriceTargets 제외 모든 사이드
패널이 같은 토글/영속 패턴.

sticky 충돌 해소:
- 기존 sticky 스타일은 OrderbookPanel 자체 outer div 에 직접 박혀 있었음.
  Collapsible 로 감싸면 outer 가 바뀌므로 Collapsible 의 className prop 으로
  sticky 톤(bg-zinc-900/80 backdrop-blur)을 sticky=true 일 때만 주입.
- 접혔을 때: 헤더 한 줄(호가 + ts) 만 sticky top-4 로 상단에 떠 있음 →
  차트/지표 영역 길게 스크롤 중에도 호가 헤더 접근 가능, 한 번 클릭이면
  본문 펴짐. 펼쳤을 때: 본문 포함 전체 sticky.
- 내부 p-3 패딩은 제거 — Collapsible 의 px-4/pb-4/pt-3 본문 패딩이
  카드 안쪽 여백을 대체.

defaultOpen=true:
- 호가는 사이드바에서 정보 밀도/시의성 모두 가장 높은 패널 →
  진입 즉시 펴진 상태가 자연스러움. RelatedStocks(밀도 낮음) 만 false.

storageKey="orderbook": panel-open:orderbook 으로 영속.

검증:
- npx tsc --noEmit clean
- npx next lint "✔ No ESLint warnings or errors"
2026-06-01 10:37:23 +09:00
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
b1323373a5 feat(price-hero): 오늘의 흐름 OHLV mini strip under change line
PriceHero gains an optional flow={open,high,low,volume} prop. Rendered
as a compact 4-KPI inline strip (시/고/저/거래량) below the change
line. Wraps naturally on narrow widths via flex-wrap.

page.tsx computes flow from chart.ohlcv tail inside the existing
useMemo:
- 1D (10m): groups bars by same KST date as last bar, aggregates as
  session OHLV (open=first, high=max, low=min, volume=sum).
- 1d/1w/1mo: just the last valid bar's OHLV (each bar already
  represents one unit).

Volume formatted with the same 억/만 rule as StockChart fmtKVolume —
small enough to inline at use site instead of carving out a shared
format module. High/low labels get rose-300/sky-300 ink as a soft
directional hint while the numbers themselves stay neutral (zinc-200)
to avoid implying a misleading +/- sign.
2026-06-01 10:20:25 +09:00
claude-owner
31e7a67a0d feat(symbol-page): aria-pressed PeriodTabs + sticky orderbook in 1D mode
PeriodTabs (reviewer 488fb32 concern):
- Drop role=tablist/tab/aria-selected. These imply a full ARIA tab
  pattern (tabpanel, aria-controls, roving tabIndex, internal Arrow
  focus traversal) which we don't implement — page-level left/right keys
  drive period change instead. Switch to plain button group with
  aria-pressed={active}, which accurately describes a segmented
  toggle. Comment updated to match.

OrderbookPanel sticky (new slice):
- Add optional sticky prop. When true, panel uses 'sticky top-4' with
  a slightly heavier bg-zinc-900/80 + backdrop-blur so it stays
  legible over the panels scrolling behind it.
- page.tsx passes sticky={isIntraday} — only 1D (10m bars) mode
  benefits, since orderbook timeliness drops on daily+ charts and the
  fixed slot would harm sidebar layout stability there.
2026-05-30 20:13:44 +09:00
claude-owner
488fb32acb feat(period-tabs): rose accent + role=tab semantics for active period
The active period tab previously used `bg-zinc-700`, which blends with the
zinc-toned header buttons (비교 / 공유 / Star) right beside it — users had to
read the tab label to know which period was loaded.

Switch active style to rose-500 (한국 상승 컬러 톤 — same family as positive
returns and target lines elsewhere in the app), with bold weight and a soft
glow. Inactive tabs unchanged. Also adds tablist/tab/aria-selected semantics
so screen readers announce the active period.
2026-05-29 00:56:19 +09:00
claude-owner
900c56550c feat(chart): split toolbar into indicators row + actions row
When showSma is true, the indicator chips (MA5/20/60 + RSI + MACD) and the
action buttons (수평선 / CSV / 전체화면, plus the fullscreen symbol label) were
sharing one flex-wrap row. On narrow widths the actions wrapped into the
middle of the indicators because `ml-auto` only enforces right-pull while
everything fits on one line — once wrap kicks in the right-side group loses
its anchor.

Split into two rows so wrap in either group stays local:
- Indicators row: rendered only when showSma. Hidden in 1D (intraday) where
  none of these are meaningful, matching prior behavior.
- Actions row: always rendered. Leading fullscreen label + `ml-auto` to push
  the three action buttons right; on narrow widths they wrap among themselves
  without colliding with indicator chips.
2026-05-29 00:54:04 +09:00
claude-owner
a6cd6f3a35 feat(symbol-page): PeriodTabs overflow fix + fullscreen chart symbol label
- page.tsx: wrap PeriodTabs in overflow-x-auto + inner flex w-max min-w-full
  justify-end so the leftmost tab is reachable when content exceeds container
  (justify-end + overflow-x-auto alone clips negative direction in some browsers).
- StockChart: when fullscreen, render small "{name} {code}" leading label in the
  toolbar — PriceHero is covered by the fixed inset overlay so the chart loses
  symbol context otherwise. Toolbar (not canvas) so it doesn't overlap the
  existing CrosshairLabel at top-left of the chart area.
2026-05-29 00:51:13 +09:00
claude-owner
faa0163089 feat(symbol-page): split header into 2 rows so PeriodTabs has room on mobile
The single-row header packed nav + 비교/공유/Star + 7-tab PeriodTabs side by
side. On narrow widths the line wrapped with PeriodTabs landing mid-row, which
looked broken. Move PeriodTabs to its own row underneath, right-aligned with
overflow-x-auto fallback. Action group keeps flex-wrap as a defensive fallback.

Pure layout — no behavior change.
2026-05-29 00:46:20 +09:00
claude-owner
7312d0aadc feat(chart): Alt+Click removes nearest horizontal line
IPriceLine has no native click event, so use a coordinate-matching pattern:
  - On Alt+Click, compute candle.priceToCoordinate for each hline
  - Find the one whose y is closest to the click y
  - If within ±6px, remove it via removeLine

Complements the chips ✕ button — clicking the line itself on the chart is
more natural than aiming at a small ✕. Shift+Click for add still wins when
both modifiers are held (more frequent, checked first).

- StockChart toolbar title and ShortcutsHelp updated to surface the gesture
2026-05-29 00:42:31 +09:00
claude-owner
cab2ed13a3 feat(chart): Shift+Click adds horizontal line at clicked y-coordinate
H key and toolbar button use hover-or-last-close, so mouse users had no way
to drop a line at an arbitrary price they hadn't hovered. Shift+Click reads
the click's y via candle.coordinateToPrice — direct positional control.

- Plain click stays as native chart interaction (crosshair/drag)
- Only intercepts when shiftKey held; preventDefault to avoid text selection
- Honors the same 10-line cap as the H key / button
- ShortcutsHelp adds "Shift+클릭" entry next to H so it's discoverable
2026-05-29 00:39:45 +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
093ac86d1a feat(symbol-page): keyboard navigation for period tabs and watchlist
- ←/→ : 기간 탭 이전/다음 (양끝 clamp — wrap-around 안 함, 사용자가
  끝에 갇혔는지 인지 가능).
- S : 관심종목 토글 (watchlist.toggle, StarButton 자동 동기화).
- 입력 필드 포커스 시 무시 — 검색창/메모 텍스트 편집 방해 금지.
- modifier(Ctrl/Cmd/Alt) 가 눌리면 단축키 양보 — 브라우저/OS 단축키
  보존 (ex. Cmd+→ 줄 끝 이동, Alt+← 뒤로가기 등).
- ShortcutsHelp 에 새 단축키 등록 → ? 키 헬프 오버레이에서 발견 가능.
- PeriodTabs 에 `periodNeighbor(id, dir)` 헬퍼 export — 페이지가
  PERIODS 순서를 직접 알 필요 없도록.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 00:24:03 +09:00
claude-owner
6f71aef451 feat(shortcuts): keyboard shortcuts help overlay + F modifier guard
- 글로벌 `ShortcutsHelp` 컴포넌트 추가 — `?` 키로 토글, ESC 또는 바깥
  클릭 / 닫기 버튼 / 우하단 floating ? 버튼으로 닫음. /, Ctrl+K, F,
  ESC, ↑↓, Enter 등 현재 사이트의 모든 단축키를 scope 별로 표시.
- 입력 필드 포커스 시 단축키 무시 가드 — `?` 입력 의도를 방해하지 않음.
- 열릴 때 닫기 버튼에 포커스 → 키보드 사용자 Tab 흐름 자연스럽게.
- 모달 click-outside → setOpen(false), aria-modal/role=dialog 로 접근성.
- 리뷰어 9f215ac 지적: StockChart F 단축키가 Ctrl+F/Cmd+F/Alt+F 도
  가로채던 것을 modifier guard 로 막음. shift/ctrl/meta/alt 어느 것도
  눌리지 않은 단순 F 일 때만 toggleFullscreen.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 00:21:07 +09:00
claude-owner
9f215acaaa feat(chart): fullscreen mode + CSV scope label clarification
- StockChart 툴바에 `⛶ 전체화면` 버튼 추가. Fullscreen API 우선,
  미지원/거부 시 fixed inset 오버레이 폴백. F 키 토글, ESC 해제.
- 입력 필드(INPUT/TEXTAREA/SELECT/contentEditable) 포커스 시 단축키 무시.
- fullscreenchange 이벤트로 native 상태 동기화 → ESC 로 빠져나와도 UI 정합.
- CSV 버튼 title 을 '현재 로드된 기간(from ~ to)' 으로 명시.
  리뷰어 지적 (1e0c917): "현재 보이는 봉" 이라는 표현은 timeScale
  visible range 와 혼동 — 실제는 PeriodTabs 가 선택한 range 전체.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 00:17:00 +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
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