Checkpoints · 함정 · 결정

주요확인사항

개발 중 알아두어야 할 함정, 제품·기술 결정, 개발 규칙을 남깁니다. 재현 가능한 버그는 버그 리포트 페이지를 봅니다.

역할 · 함정·결정·개발 규칙을 app/checkpoints/data.ts에 남깁니다. 재현 가능한 버그는 버그 리포트, 정식 추적은 이슈대장(시트).

확인 목록 (31)

버그 리포트 →
c33002026-08-13인프라 · DNS

[결정] 문서 사이트 도메인 — docs.zenith-dex.xyz (GoDaddy → Railway)

manifast-typescript-test(web) 커스텀 도메인은 docs.zenith-dex.xyz. GoDaddy(zenith-dex.xyz)에 CNAME docs + TXT _railway-verify.docs 둘 다 넣어야 검증·SSL 완료.

미해결

배경 · 내용

CNAME: 2246vi1x.up.railway.app. TXT: railway-verify=… (README·Railway domain status 참고). 예전 docs.zenith.xyz / hzivwvts 값은 폐기. 기본 URL web-production-33532도 유지.

주의 · 우회

DNS 전파 전은 Railway 기본 URL 사용. 404면 TXT 누락·CNAME이 옛 타깃인지 확인.

후속

GoDaddy에서 CNAME·TXT 수정 후 railway domain status docs.zenith-dex.xyz 로 verified 확인

c32002026-08-13보안 · 주문

[결정] WALLET_SECRET_KEY는 load-test 전용 — trade-api UI는 유저 지갑

서비스(Trade UI) 주문은 Phantom 등 유저 지갑 서명. trade-api에 서버 핫월렛 키 불필요. WALLET_SECRET_KEY는 Railway load-test(scripts/load-orders)만.

해결

배경 · 내용

POST /api/orders·executeServerPlaceOrder는 부하테스트·서버 서명 경로. 운영 UI는 prepare→유저 서명. trade-api Variables에 키가 비어 있어도 정상. load-test에만 JSON 배열 시크릿 설정.

후속

trade-api에 키를 ‘하드닝 미완’으로 취급하지 말 것. /schedule m4 반영.

c31002026-08-13인프라 · RPC · 부하테스트

[결정] 주문 부하 1000건 — Helius RPS ≠ 주문수, Professional 권장

Professional 500은 RPC 500회/초이지 주문 500건/초가 아님. 1000건은 15~30초에 나눠 보내고, 플랜은 Professional 권장(Business는 30초·스로틀 시 가능).

해결

배경 · 내용

문서: helius.dev/docs/billing/rate-limits — Free 10 / Dev 50 / Business 200 / Pro 500 RPS, Enterprise=Custom(무제한 아님). 주문 1건≈reload·blockhash·send·confirm 등 RPC 수 회(대략 3~5+). 동시 100건 실측 fail≈98은 429. 1000건/15~30초: 초당 주문 ~33~67 → RPC ~100~335(가정) → Dev 불가, Business는 ~30초+동시 20~30+전용 키면 △, Professional이면 15~30초 여유. 1초 500주문×2초는 Pro로도 불가에 가깝. load-orders에 동시성 제한·배치·429 백오프 권장. ingest/trade-api와 키 공유 시 헤드룸 필요.

주의 · 우회

부하테스트 전 Helius 대시보드에서 플랜·크레딧 확인. load-test 전용 API 키 분리. COUNT 램프 시 429면 동시 N↓·간격↑.

후속

load-orders 스로틀(동시 30~50, 15~30초) 구현 후 Business vs Pro 실측 비교. /load-test-report 주문 실측 갱신.

c30002026-08-13인프라 · Railway

[결정] realtime replica 1대 증설 — CPU/RAM 사용량 기준 추가 비용

Hobby 월 $5 + 초당 사용량 과금. 요율 CPU $20/vCPU/월 · RAM $10/GB/월. 추가 1 replica ≈ (CPU_used×20)+(RAM_used×10).

해결

배경 · 내용

2026-08-12 스냅샷(realtime 1대): CPU≈0.12 vCPU · RAM≈792MB(0.792GB) → 증분 약 $2.4+$7.92≈$10.3/월. 부하로 상한(8 vCPU·8GB)까지 쓰면 증분 최대 약 $240/월(160+80). Hobby included usage $5는 워크스페이스 전체 사용량과 상쇄되어 실제 청구 증분은 크레딧 잔액에 따라 더 낮을 수 있음. 볼륨·CPU/RAM 구분·한도는 c2600.

주의 · 우회

`railway metrics --service realtime`로 current/avg CPU·RAM 확인 후 같은 식으로 재산정. 청구는 대시보드 Usage/빌링에서 $5 크레딧 잔액까지 같이 볼 것.

후속

full-tab ~900유저/대 가정(부하보고서)과 맞춰 replica 수 결정. /network-costs는 egress 별도.

c29002026-08-12인프라 · 부하테스트

[결정] load-sse --ramp — 점진 입장(예: 2초당 100유저)

동시 몰림 대신 --ramp 100/2s 또는 LOAD_SSE_RAMP로 가상 탭을 웨이브로 연다. --ramp가 있으면 --batch보다 우선.

해결

배경 · 내용

형식: 탭수/간격 (100/2s, 50/500ms). 웨이브마다 해당 탭의 SSE를 한꺼번에 연 뒤 interval 대기. full-tab 500유저·ramp 100/2s면 입장 ~10초 + hold 기본 1분(60초). burst는 --batch·--batch-gap-ms.

후속

1500~2000 full-tab 또는 load-orders. /load-test-report 갱신 완료.

c28002026-08-12인프라 · 부하테스트

[결정] load-sse --full-tab — Trade UI와 동일 SSE 묶음

count=가상 탭 수. --full-tab이면 presence+호가(티커·차트)+체결+미체결(기본). raw SSE≈count×5. presence만 테스트는 실사용과 다름.

해결

배경 · 내용

스트림: /presence/stream, /markets/price/stream( presence=1 ), /markets/price/stream?presence=0, /markets/fills/stream, /orders/open/stream(가짜 trader). --no-open-orders로 지갑 미연결(4 SSE). 기본 batch full-tab=20. LOAD_SSE_FULL_TAB=1. dev만, 후 realtime 재시작.

후속

1500~2000 full-tab 또는 load-orders 램프. /load-test-report 참고.

c27002026-08-12인프라 · 부하테스트

[결정] 접속 부하는 load-sse — Online=presence SSE, 램프 후 주문과 겹침

5k ‘접속’은 브라우저 탭이 아니라 scripts/load-sse.ts가 realtime /presence/stream N개. 주문 1k는 load-orders와 별개·나중에 겹침.

해결

배경 · 내용

확인: load-sse 로그 presenceOpened/fail·health.online before/peak/after, railway metrics realtime·Redis CPU/RAM, 헤더 Online, 연결 중 끊김. 주의: presence마다 daily_visitors +1(dev만). 연결당 Redis subscribe 1개라 고수가 먼저 터질 수 있음. 램프 100→500→1k→2k→5k. --with-price는 호가 SSE(presence=0) 추가.

주의 · 우회

한 번에 5k 말고 COUNT만 올려 재기동. REALTIME_URL은 realtime 공개 URL.

후속

SSE 안정 후 LOAD_TEST_COUNT 램프와 동시 실행.

c26002026-08-12인프라 · Railway

[결정] Railway CPU·RAM ≠ 볼륨 — 한도 안에서 사용량 과금

볼륨은 디스크 용량을 정해 붙이고, CPU·RAM은 서비스마다 고정 할당이 아니라 컨테이너 상한(현 dev 8 vCPU / 8 GB) 안에서 쓴 만큼 과금.

해결

배경 · 내용

실측(dev, 최근 1h): web ~138MB·CPU≈0, trade-api ~70MB, realtime ~48MB, ingest ~155MB·0.07vCPU, Redis ~9MB, Mongo ~331MB. 한도 대비 여유 큼. 부족하면 OOM/스로틀 → 플랜·리소스 한도 검토. 부하·동시접속이 늘면 ingest·Mongo 램부터 보면 됨. 리전·볼륨은 c2300·c2400.

주의 · 우회

대시보드/CLI `railway metrics --cpu --memory`로 서비스별 current/avg/max/limit 확인. 볼륨 크기와 CPU/RAM을 혼동하지 말 것.

후속

스케일업은 실사용이 한도에 근접할 때. replica 규칙은 c2000.

c25002026-08-12차트 · 성능 · 캐시

[보류] 캔들 과거 봉 클라이언트 캐시 — 닫힌 봉은 API 재호출 불필요

과거(닫힌) 캔들은 불변이니 한 번 받은 구간은 클라이언트가 들고, 새 구간·최신 tip만 API/SSE. 구현은 나중에. 캐시 시 오류 추적이 어려워질 수 있어 관측 가능해야 함.

미해결

배경 · 내용

방향: 닫힌 봉 캐시 찬성. staleTime 예 5분(과거는 더 길어도 됨). 맨 끝(라이브) 봉은 캐시 말고 SSE. React Query가 적합하고 zustand는 필수는 아님(서버 캐시와 겹침). 기존 candlesApi 8s 인플라이트는 Strict Mode용 — 키·TTL만 키우는 방법도 있음. 우려: Network에 요청이 안 보이면 원인 오해, hit/miss·에러 삼킴. 넣을 때: hit/miss 로그 또는 PERF, 실패 시 조용한 캐시 폴백 금지, ?nocache=1/캐시 비우기, 키는 address|resolution|before|after|limit 단순화, 윈도우 상한 유지.

주의 · 우회

지금은 매 스크롤·초기 로드마다 trade-api→Mongo. 중복은 짧은 공유 캐시·loadOlder suppress로만 완화(c2200).

후속

나중에 구현 시 RQ vs candlesApi 확장 중 하나 선택 + 디버그 요건(hit/miss·nocache)을 스펙에 먼저 고정.

c24002026-08-12인프라 · 성능

[측정] Railway 리전 SFO → Singapore 후 REST 지연

한국에서 DevTools Network(Fetch/XHR)로 비교. REST는 대략 160~200ms(SFO) → 110~135ms(Singapore). SSE의 초 단위 시간은 연결 유지 시간이라 리전 비교에 쓰지 않음.

해결

배경 · 내용

실측(초기 진입, trade-api): candles SFO ~164ms → SG ~125ms(약 24%↓). fills ~195ms → ~111ms(약 43%↓). volume24h ~161ms → ~133ms(약 17%↓). 전체로 왕복 약 30~80ms 감소. SSE stream이 2~4초로 길게 보이는 것은 EventSource가 열린 채로 경과 시간이 쌓인 것(완료 지연 아님). Waiting(TTFB)로 개통만 보면 됨. Solana RPC가 미국이면 주문·체인 쪽 지연은 리전과 별개로 남을 수 있음.

주의 · 우회

리전 성적표는 candles/fills/volume24h 시간만 본다. stream 초 단위는 무시.

후속

체감이 더 필요하면 RPC 아시아 엔드포인트 검토. 리전 이전 절차는 c2300.

c23002026-08-12인프라 · Railway

[결정] Railway dev 리전 — SFO → Southeast Asia (Singapore)

한국·동남아 지연을 줄이려고 compute를 싱가포르로 옮김. Railway 아시아 컴퓨트는 싱가포르만 있음(도쿄는 미제공).

해결

배경 · 내용

순서: Redis·Mongo(볼륨 마이그레이션) → ingest → trade-api → realtime → web. CLI: railway service scale --service X southeast-asia=1 sfo=0. Solana RPC가 미국이면 체인 왕복은 별개. 실측 비교는 c2400.

후속

체감이 부족하면 RPC도 아시아 엔드포인트 검토.

c22002026-08-12차트 · 성능

[결정] 초기 candles는 차트 1회 — before=는 스크롤 시에만

초기 Network candles 2번(limit=60 + before=)은 setData auto-fit이 loadOlder를 오발한 것. 첫 진입에는 before 불필요.

완화됨

배경 · 내용

24h%는 volume24h.price24hAgo. 차트만 /candles(1m×60). setData·showRecentBars 직후 300ms는 range 페이지 로드 suppress. 사용자가 왼쪽으로 스크롤하면 before= 호출이 정상.

주의 · 우회

초기 진입에 before=가 보이면 suppress 타이밍을 의심. 스크롤 중 before=는 정상.

후속

가격 SSE(티커+차트 presence=0) 통합은 별도.

c21002026-08-12차트 · 캔들

[결정] 차트 과거 봉은 Mongo candle_bars — 첫 로드·스크롤 페이지 60

브라우저 → trade-api GET /api/markets/candles → DB. 화면에 보이는 건 20봉이지만 페이지는 60봉(이전 120).

해결

배경 · 내용

정본은 Mongo `candle_bars`(봉 1문서). ingest가 실시간 mid로 쓰고, 과거 대량은 scripts/backfill-fills. 예외: 최초 조회인데 봉이 거의 없으면 Manifest mid RPC로 시드한 뒤 DB에 채운다. 그다음 스크롤(before/after)은 다시 Mongo만.

주의 · 우회

차트가 비면 MONGO_URL·ingest·backfill을 본다. RPC 시드는 cold start뿐.

후속

VISIBLE_BARS=20 · CANDLE_PAGE_SIZE=60 · CANDLE_WINDOW_MAX=360.

c20002026-08-12아키텍처 · 스케일

[결정] Railway replica — realtime·trade-api·web OK, ingest는 복제 금지

부하가 큰 서비스를 복사본으로 늘린다. ingest를 늘리면 Solana WS가 마켓당 N배가 된다.

해결

배경 · 내용

replica = 같은 코드의 인스턴스 개수. realtime(SSE)·trade-api(REST)·web은 수평 확장. ingest는 마켓당 구독 1개 유지. load-test는 HTTP 서버가 아니라 scripts/load-orders.ts 원샷. 상세는 /architecture.

후속

동접이 커지면 realtime·trade-api부터 replica. ingest는 1대 또는 마켓 샤딩.

c19002026-08-11부하테스트

[결정] 주문 부하는 HTTP 서버가 아니라 scripts/load-orders.ts

services/load-test Express(POST /api/orders/load-test)는 삭제. 같은 프로세스에서 executeServerPlaceOrder를 동시 호출한다.

해결

배경 · 내용

로컬 tsx는 RPS·지연이 배포와 달라 실측이 아니다. Railway load-test 서비스가 기동 시 스크립트를 한 번 실행하고 종료(restart NEVER). LOAD_TEST_COUNT·LOAD_TEST_MARKET·WALLET_SECRET_KEY·SOLANA_RPC_URL.

후속

실측은 railway up --service load-test. 건수는 Variables의 LOAD_TEST_COUNT.

c18002026-08-11차트 · 모바일

[함정] 실기기 기간 버튼 줄바꿈 ≠ DevTools 기기 프리셋

폰이 물리적으로 더 커도 CSS 폭은 더 좁을 수 있다. PC DevTools Galaxy S20 Ultra(412px)와 실기기(Phantom)는 같은 화면이 아니다.

완화됨

배경 · 내용

기간 버튼은 `.candle-resolutions { flex-wrap: wrap; max-width: 70%; justify-content: flex-end }`라서 8개가 안 들어가면 1W만 다음 줄 오른쪽으로 떨어진다. DevTools 프리셋 폭(S20 Ultra=412 CSS px)은 레이아웃용이지 실기기 mm가 아니다. S23 Ultra는 패널이 더 크고 PPI가 높아 CSS 폭이 384px대인 경우가 많고, 삼성 화면 크기/글자 크기·Android text-size-adjust가 10px 버튼을 더 넓힌다. Phantom 인앱 웹뷰는 데스크톱 Chrome 디바이스 모드와 폰트·뷰포트가 다르다.

주의 · 우회

비교할 때 DevTools 기기 이름 대신 `innerWidth`·`devicePixelRatio`를 본다. 프리셋 125%는 폰 프레임 확대일 뿐 CSS 폭을 바꾸지 않는다.

후속

≤720만 기간 nowrap·OHLC ellipsis·text-size-adjust 100%. PC(1101+)는 기존 wrap 유지.

c17002026-08-11트레이드 UI · 레이아웃

[결정] PC 레이아웃은 태블릿~풀HD를 비율 보간 — 줌 전용 분기는 없다

노트북 작은 화면·브라우저 125~150% 줌까지 화면마다 맞추지 않는다. 줌은 CSS 뷰포트가 작아진 것과 같아서 vw/vh clamp로 같이 커버한다. 페이지 전체 zoom/scale은 쓰지 않는다(줌과 겹침).

해결

배경 · 내용

1101~풀HD: 호가·주문 폭 `clamp(280px, 28vw, 440px)`, 미체결 높이 `clamp(240px, 36vh, 480px)`. QHD+는 상한(440/480) 유지. ≤720 모바일 셸, 721–1100 태블릿 스택은 그대로.

후속

풀HD 100%·150%, 1366 노트북, QHD에서 차트 폭·호가 스크롤만 스모크.

c16002026-08-11트레이드 UI · 레이아웃

[결정] PC 미체결 높이 — 풀HD~QHD는 vh 보간, QHD+는 480px

미체결을 480px 고정하면 풀HD에서 호가가 눌려 스크롤이 난다. QHD 비율은 유지하고 그보다 낮은 세로는 36vh로 줄인다.

해결

배경 · 내용

`--bottom-h: clamp(240px, 36vh, 480px)`. 태블릿(721–1100)은 세로 스택이라 height:auto 그대로.

후속

풀HD에서 차트·호가 공간이 충분한지, QHD에서 미체결이 이전과 비슷한지 확인.

c15002026-08-11헤더 · presence

[함정] 헤더 Online — 는 지갑 연결과 무관 · realtime SSE

지갑이 연결되어 있어도 Online이 — 이면 탭 presence SSE가 안 붙은 것이다. realtime(:4002) `/presence/stream`을 본다.

미해결

배경 · 내용

Online은 접속 탭 수다. 첫 SSE 이벤트 전·realtime 미기동·EventSource 실패면 null이라 — 로 둔다. 로컬에서 web·trade-api·ingest만 살아 있고 4002가 LISTEN이 아니면 이 증상.

주의 · 우회

`npm run dev`는 web·api·rt·ingest를 같이 켠다. 로그에 `[rt] listening`이 없고 4002가 비면 realtime이 죽은 것. Redis error에 rt만 내려가던 문제는 핸들러로 막음.

c14002026-08-11차트 · 모바일

[결정] TradingView 고지는 차트 안 로고 — 툴바 문구 금지

툴바에 「차트: TradingView」를 넣으면 모바일에서 OHLC·해상도와 겹쳐 두 줄이 된다. 차트 안 attribution 로고로 되돌린다.

해결

배경 · 내용

lightweight-charts `attributionLogo: true`. 툴바는 OHLC와 간격 버튼만.

후속

툴바에 TV 문구를 다시 넣지 않는다.

c13002026-08-11트레이드 UI

[결정] 첫 진입 전체 화면 로딩은 두지 않는다

유저가 처음 와도 스플래시·전체 스켈레톤은 없다. 셸·주문창은 바로 그리고, 차트·호가·지갑만 뒤에서 채운다.

해결

배경 · 내용

마켓 목록이 번들에 있어 크롬은 즉시 그릴 수 있다. 전체를 기다리면 체감만 느리고 지갑 연결·마켓 선택까지 막힌다. 차트는 배지 Loading…, 호가는 비었다가 채움, 지갑 복원은 조용히. 신경 쓸 것은 잘못된 숫자 깜빡임(목업 lastPrice→실호가, SSR PC→모바일 셸)이지 전체 로딩이 아니다.

후속

가격/셸 플래시가 거슬리면 그때 해당 값만 비우거나 기본값을 맞춘다. 전체 로딩 오버레이는 넣지 않는다.

c12002026-08-06호가 · RPC

[함정] 호가창 빈 화면 ≠ 보안 패치 — Helius RPC 쿼터(429 max usage)

호가가 No orders로만 보이면 CSP/SSE 한도보다 Solana RPC(Helius) 크레딧·레이트리밋을 먼저 본다.

완화됨

배경 · 내용

로컬 `/api/markets/price`가 502 + `429 Too Many Requests: max usage reached`면 Helius 플랜 한도. Trade는 REST+SSE+fills+open-orders가 RPC를 같이 써서 쿼터가 빨리 닳는다. 보안 헤더·SSE 슬롯은 같은 증상처럼 보이지만, 이 경우 서버 로그에 Helius 429가 찍힌다.

주의 · 우회

Helius 대시보드에서 credits 확인·키 교체. 프로세스 내 호가 캐시(stale fallback)와 SSE 초기 loadFromAddress 제거로 중복 RPC를 줄여 둠.

후속

유료 RPC 여유 확보 후에도 비면 해당 마켓 on-chain L2가 실제로 비었는지(또는 Zenith 등 다른 배포) 확인

c11002026-08-06보안 · API

[결정] 서버 서명 API — INTERNAL_API_SECRET · 레이트리밋 · 보안 헤더

무인증 POST /api/orders·load-test·history를 시크릿으로 잠그고, tx/send·verify·SSE에 남용 한도를 걸었다. 상세는 /security-report.

완화됨

배경 · 내용

Trade UI는 prepare+지갑 서명 경로라 영향 없음. 부하테스트·스크립트는 헤더 x-internal-api-secret 필요. 키 로테이션(git .env 히스토리)은 운영 작업으로 남김(s0800).

주의 · 우회

로컬만 비상으로 ALLOW_OPEN_SERVER_ORDERS=true (프로덕션·Railway에서는 금지).

후속

Railway에 INTERNAL_API_SECRET 설정. WALLET_SECRET_KEY·RPC 키 재발급 권장. 동접·SSE는 /load-test-report.

c10002026-08-06지갑 · Phantom · 배포

[함정] Phantom — Railway `*.up.railway.app` 주문 시 ‘요청 차단됨’

dev URL(`web-dev-2f02.up.railway.app`)에서 매수 서명하면 Phantom이 ‘이 dApp은 악성일 수 있습니다’로 요청을 막는다. 주문 코드 오류가 아니다.

미해결

배경 · 내용

Phantom은 평판이 없는·공유 호스팅 도메인(Railway 기본 서브도메인 등)에 대해 서명/연결을 차단하거나 경고한다. 피싱에 자주 쓰이는 패턴이라 기본이 보수적이다. 같은 앱이라도 커스텀 도메인·화이트리스트 심사 후에는 덜 뜬다.

주의 · 우회

본인 배포임을 확인한 뒤 팝업 하단 ‘계속 진행(위험)’으로 진행. 반복되면 Phantom에서 해당 사이트를 신뢰/연결 앱으로 둔 뒤 재시도.

후속

프로덕션은 커스텀 도메인 + TLS. 필요 시 Phantom domain review 폼 제출.

c09102026-08-06Mongo · 차트

[결정] 차트 봉 — candle_bars 봉 단위 컬렉션으로 전환

기간별·해상도별 range 조회가 필요해져 candle_series(시리즈+bars[]) 대신 candle_bars(문서=봉 1개)를 정본으로 쓴다. Redis는 여전히 호가 pub/sub만 — 봉 저장과 무관.

완화됨

배경 · 내용

스키마: _id=`{market}:{resolution}:{time}`, pair/marketId 라벨, OHLC. 인덱스 {market,resolution,time} unique. mid 틱은 해당 버킷 문서만 upsert(같은 주면 H/L/C 갱신). API `/api/markets/candles` 응답 shape·before/after는 유지해 웹 차트 사이드이펙트 최소화. 레거시 candle_series는 조회 시 비어 있으면 1회 flatten 마이그레이션 후 신규 write는 bars만. fills와 별개(차트=mid OHLC).

주의 · 우회

Compass는 test(또는 revolution) DB의 candle_bars를 pair·resolution·time으로 조회. 구 candle_series는 마이그레이션 전 백업용으로 남을 수 있음.

후속

로컬·dev에서 차트 스크롤·해상도 전환 스모크. 과거 백필은 `backfill-fills.ts`가 fills 후 candle_bars를 체결 시간순으로 재구성(--candles-only 가능). WS mid 폭주 디바운스는 별도.

c08002026-08-06지갑 · Phantom

[함정] Phantom 완전 해지 후에도 재연결이 ‘바로 붙음’

disconnect만 하면 Wallet Standard에 accounts가 남아 connect가 승인 없이 기존 주소로 단축된다. Connected apps 철회는 웹 API로 불가.

해당 없음

배경 · 내용

구 코드: accounts[0] 있으면 connect() 스킵. 완전 해지 UI·forceFresh 실험은 제거(2026-08-06). 명시적 connect는 항상 connect() 호출(b0100). Phantom ‘Connected apps’에서만 사이트 철회 가능.

주의 · 우회

승인 UI가 필요하면 Phantom 앱 → 설정 → 연결된 앱에서 사이트 제거 후 재연결.

후속

웹 완전 해지 UI 제거. accounts 단축은 tryRestore(silent/onlyIfTrusted)만.

c06002026-08-06지갑 · 모바일

[함정] 모바일 Phantom — 웹은 앱 설치를 알 수 없고 provider 주입만 감지

Chrome 등 일반 모바일 브라우저에서는 Phantom 앱이 설치돼 있어도 `window.phantom`이 없다. ‘미설치’로 보이면 OS 설치 여부가 아니라 provider 미주입이다.

완화됨

배경 · 내용

Phantom 공식: provider는 확장·인앱 브라우저에서만 inject. OS 앱 설치 여부를 웹 JS로 조회하는 API는 없다. `isPhantomInstalled()`는 Wallet Standard/`isPhantom`만 본다. 모바일 일반 브라우저는 browse 딥링크(`phantom.app/ul/browse/<url>?ref=`)로 인앱을 열게 하고, 미설치 문구는 PC(확장 없음)에만 쓴다.

주의 · 우회

Android: 모달에서 ‘Phantom 앱에서 열기’ → 인앱에서 Connect. 또는 Phantom 앱 → 브라우저로 사이트 직접 열기.

후속

카피·CTA 분리 적용(2026-08-06). iOS 딥링크는 추후.

c05002026-08-06헤더 · 모바일

[함정] 모바일 헤더 Online — 문서만 제거·platform 미커밋으로 ‘원복’처럼 보임

Online은 PC만 표시하기로 했는데 MobileTradeShell에 online-pill이 다시 있어 ‘Phantom이 되돌린 것’처럼 보였다. 실제는 제거가 커밋·배포에 안 들어간 것.

해결

배경 · 내용

extras e0200·에이전트 응답상 모바일 Online 제거로 적혀 있었으나, manifast-platform git에는 제거 커밋이 없다(`git log -S online-pill -- MobileTradeShell` = 추가만 `3bdb592`). 제거는 2026-08-05 로컬 편집만 하고 커밋/푸시하지 않았고, 버전 bump(0.1.2)에도 MobileTradeShell이 포함되지 않았다. Phantom 커밋(`467957b`)은 online-pill을 변경하지 않음 — 부모부터 계속 존재.

주의 · 우회

모바일 정책 변경은 문서(extras)와 같은 날 platform 커밋까지 맞춘다.

후속

2026-08-06 MobileTradeShell에서 online-pill·onlineCount 재제거. 버그 재현 이슈는 b0200.

c04002026-08-06지갑 · web

[적용] Phantom 지갑 — 연결·미설치 안내·해지 모달 (PC·Android)

Zenith web이 MetaMask 경로를 제거하고 Phantom(Wallet Standard + 주입 provider)으로 통일. 연결 해지는 확인 모달. 모바일 미설치 시 설치 안내·Play Store·인앱 딥링크.

완화됨

배경 · 내용

`phantomSolana.ts`: Standard `standard:connect` / `window.phantom.solana`·`window.solana.isPhantom` 폴백. `@metamask/connect-solana` 제거. SelectWalletModal은 미설치 시 설치 문구·다운로드. Android는 `phantom.app/ul/browse/...` 옵션. 헤더 주소 클릭 → DisconnectWalletModal → 확인 시에만 disconnect. iPhone 딥링크는 보류.

주의 · 우회

일반 Chrome(모바일)에서는 Phantom이 주입되지 않음 → 설치 안내 또는 ‘Phantom 앱에서 열기’. PC는 Phantom 확장 필요.

후속

iOS Safari/딥링크 QA는 이후. Android·PC Phantom 확장으로 서명·주문 스모크.

c03002026-08-05Mongo · 개발 규칙

[함정] Mongo 시각 — BSON Date는 UTC, 한국시간은 *Ts/*Kst 쌍

Compass에서 Date 필드가 UTC로 보이거나 ‘한국시간 Date’를 기대하면 헷갈린다. BSON Date는 항상 UTC epoch이라 타임존을 담을 수 없다.

해결

배경 · 내용

Zenith Mongo(`daily_visitors` · `fills` · `candle_series`)는 `*Ts`(Unix ms, 정렬·비교·로직) + `*Kst`(Asia/Seoul `YYYY-MM-DD HH:mm:ss`, Compass 확인용)를 쓴다. 유틸: `@manifast/shared` `time.ts`. 일 경계(`daily_visitors`의 date/_id)는 KST 날짜 키. `blockTime`은 온체인 Unix 초 그대로 두고 표시만 `blockTimeKst`. 구 문서의 `createdAt`/`updatedAt` Date는 남을 수 있음 — 신규 write부터 새 필드.

주의 · 우회

Compass에서 사람 시간은 `*Kst` 필드를 본다. 코드·쿼리·인덱스는 `*Ts`만 쓴다. `*Kst` 문자열로 정렬·필터하지 않는다.

후속

적용됨(2026-08-05). 이후 Mongo 시각 필드는 같은 패턴으로 추가.

c02002026-08-05지갑 · 제품 정책

[결정] 지갑 — PC·모바일 Phantom 통일 (MetaMask 1단계 철회)

Zenith 1단계 지갑을 Phantom(PC 확장 + 모바일 앱)으로 통일. MetaMask 1단계는 철회.

해결

배경 · 내용

Manifest.trade는 PC MetaMask·모바일 Phantom처럼 환경마다 지갑이 갈린다. MetaMask Solana는 모바일·connect-solana QA 부담이 크고, Phantom은 PC 확장과 모바일 앱을 같은 플로우로 쓸 수 있다. dev 단계에서는 지갑 경로를 하나로 두는 편이 낫다고 판단해 Phantom only로 결정했다.

주의 · 우회

PC는 Phantom 확장, Android는 Phantom 앱(또는 인앱 브라우저). iPhone은 추후.

후속

코드 적용됨(2026-08-06). c0400 참고.