Screen: Dashboard (대시보드)
진입 화면. App = 로그인 후 첫 도착 (bottom nav 홈 탭). Web = 비로그인 랜딩 (/).
- Flutter (App):
mobile/lib/features/watchlist/presentation/pages/dashboard_page.dart - Next.js (Web):
frontend/src/app/page.tsx+frontend/src/app/layout.tsx
💡 시각 미리보기:
플랫폼별 차이
| 항목 | App (Flutter) | Web (Next.js) |
|---|---|---|
| 인증 | 로그인 필수 | 비로그인 한정 (로그인 UI 없음) |
| 워치리스트 | 있음 (chips + 공시 필터) | 없음 |
| Quota | QuotaIndicator floating | 없음 |
| 추천 종목 | EmptyWatchlist 상태에서만 (워치리스트 = 0) | 프리셋 칩 = 최근 7일 공시(요약 보유) 발생 종목 (API), 필터 |
| 공시 list | 워치리스트 기반 + 무한 스크롤 | summary 있는 최근 20건 (페이징 없음, 종목 필터 가능) |
| 종목 선택 | watchlist 추가 (API 호출) | 필터 — 해당 종목 공시만 (/?corp=), 추가/이동 X |
| 더보기 | 무한 스크롤 | AppCTA 모달 (앱 다운로드 유도) |
| Nav | bottom nav 3탭 (실동작) | 없음 (FloatingNavBar 제거) |
| 뷰포트 | 모바일 디바이스 | 모바일 폭 고정 (max-w-[420px], desktop 도 동일) |
App — 화면 구성
| 영역 | 컴포넌트 | 조건 |
|---|---|---|
| AppBar | (시스템, title="공시") | 항상 |
| 종목 추가 검색 | StockSearch | 항상 |
| 워치리스트 칩 row | WatchStockChip × N | 워치리스트 ≥ 1 |
| 본문 — 공시 목록 | DisclosureItem × N (페이지네이션) | 워치리스트 ≥ 1 |
| 본문 — 온보딩 | EmptyWatchlist | 워치리스트 = 0 |
| Floating | QuotaIndicator | 📋 target — 현재 dashboard 미구현 (detail 화면에만 존재), 추가는 별도 PR |
App — 상태 분기
- S1. 정상 (워치리스트 ≥ 1) — 검색박스 + 칩 row + DisclosureItem list (무한 스크롤)
- S2. 빈 워치리스트 (신규 사용자) — 검색박스 + EmptyWatchlist (온보딩 + 추천 8개 + 최근 공시 피드)
- S3. 로딩 — 워치리스트 / 대시보드 둘 다 spinner
- S4. 에러 — 영역별 메시지 + 재시도 안내
App — 진입 시 API 호출 (waterfall)
로그인 후 대시보드 마운트 시 — 병렬:
GET /api/v1/watchlist→ 워치리스트 칩 row 렌더 결정 (S1 vs S2)GET /api/v1/dashboard/disclosures?page=0&size=20→ DisclosureItem list (마운트 직후 항상 호출 —DashboardDisclosuresController.build()의Future.microtask(reload), 워치리스트 0/N 무관)- (📋 target)
GET /api/v1/quota→ QuotaIndicator — dashboard 에 quota 추가 시
페이지네이션: page 는 0-based (백엔드 queryInt(r,"page",0) + mobile fetchDashboardPage(page=0)). 첫 진입 = page 0.
워치리스트 = 0 (EmptyWatchlist): 추가로 GET /api/v1/disclosures/recent?limit=10 → 전체 최신 공시 피드
App — 액션
종목 추가 (S2 → S1 전환)
- 검색 박스 입력 → 300ms debounce →
GET /api/v1/stocks/search?query=<q> - 드롭다운에서 선택 →
POST /api/v1/watchlist {corp_code} - 성공 → 워치리스트 reload + dashboard disclosures reload (페이지 리셋)
추천 칩 (EmptyWatchlist 상태)
- 추천 칩 탭 →
POST /api/v1/watchlist {corp_code} - 성공 → S1 으로 전환
종목 제거
- WatchStockChip 의 X 탭 →
DELETE /api/v1/watchlist/{corp_code} - 성공 → 워치리스트 reload + dashboard disclosures reload
- 마지막 종목 제거 시 S2 (EmptyWatchlist) 로 전환
공시 탭
DisclosureItem 탭 → screen-disclosure-detail (예정) 진입.
Pull to refresh
워치리스트 + 대시보드 disclosures 동시 reload.
무한 스크롤
스크롤 끝 도달 → GET /api/v1/dashboard/disclosures?page=N+1&size=20
Quota 인디케이터 탭
breakdown dialog (Modal/M4) 표시.
Web — 화면 구성
웹은 summary 있는 공시만 노출 + 종목 필터 중심. 종목 선택(검색 or 프리셋 칩)은 워치리스트 추가가 아니라 URL 쿼리(/?corp={corpCode}) 필터 — SSR 재렌더, 공유 가능.
| 영역 | 컴포넌트 | 조건 |
|---|---|---|
| AppBar | AppBar (title="공시") | 항상 |
| 종목 검색 | SearchBox | 항상 (선택 시 /?corp= 필터) |
| 프리셋 칩 | SuggestedStocks (최근 7일 공시 발생 종목, API) | 항상 (필터 활성 칩은 selected 표기 + ✕) |
| 본문 — 공시 목록 | DisclosureItem × ≤20 (summary 있는 것만) | 항상 |
| 더보기 CTA | "더 많은 공시는 앱에서" 버튼 | 항상 (목록 하단) |
| Footer | © + 정책 링크 (privacy / terms / support) | 항상 |
❌ FloatingNavBar 제거 (웹 전체) — 단일 대시보드 + 필터 구조라 탭 네비 불필요.
Web — 상태 분기
비로그인 한정. 필터 유무(?corp)에 따라:
- 로딩 — SSR (Next.js
force-dynamic), fetch 실패 시 "공시 목록을 불러올 수 없습니다." - 빈 결과 — 필터 없음: "아직 등록된 공시가 없습니다." / 필터 활성: "이 종목의 최근 공시가 없습니다." (종목명 비의존 — 직접
/?corp=진입 + 0건 시 종목명 복원 불가하므로 추가 API 호출 없이 닫음) - 정상 — 공시 list 표시 (필터 활성 시 헤딩 "{종목명} 공시" — 종목명은 응답 disclosure 의
corp_name에서 취득 + "전체 보기" reset)
Web — 진입 시 API 호출
SSR 시점 (Next.js force-dynamic + fetchCache: default-cache, revalidate=60) — 병렬:
GET /api/v1/web/disclosures?limit=20[&corpCode={corp}]→ DisclosureItem list (summary-only.corpCode는?corp쿼리 있을 때만)GET /api/v1/web/active-stocks?days=7→ 프리셋 칩 (최근 7일 summary 있는 공시 발생 종목)
모바일과 분리된 웹 전용 엔드포인트(
/api/v1/web/*). 성격이 다름 — 비로그인 · summary-only · 필터 · 앱 유도. 모바일/recent·/dashboard/disclosures는 불변. 백엔드 설계는 PR2.
Web — 액션
프리셋 칩 탭
- SuggestedStocks chip 탭 →
router.push('/?corp={corp_code}') - 해당 종목의 summary 있는 최근 20건으로 필터 (SSR 재렌더)
- watchlist API 호출 안 함 — 비로그인 · 추가 개념 없음. 활성 칩 재탭(✕) 또는 "전체 보기" →
/로 필터 해제
종목 검색
- SearchBox 입력 →
/api/stocks/search(Next.js route handler) → 백엔드/api/v1/stocks/search - 결과 선택 (
selectStock) →router.push('/?corp={corp_code}')필터 (페이지 이동 X — 프리셋 칩과 동일)
공시 탭
DisclosureItem 탭 → /disclosures/[rceptNo] (공시 상세 페이지, 비로그인 가능, 분석 본문은 일부 마스킹).
더보기
"더 많은 공시는 앱에서" 버튼 탭 → AppCTA 모달 (App Store / Google Play, 현재 disabled placeholder). 웹은 페이징 없이 최근 20건만 제공 — 그 이상은 앱 유도.
액션 카탈로그 (전체)
| 액션 | endpoint | method | request | response | App | Web |
|---|---|---|---|---|---|---|
| 워치리스트 조회 | /api/v1/watchlist | GET | — | [WatchStock] | ✅ | — |
| 워치리스트 추가 | /api/v1/watchlist | POST | {corp_code} | WatchStock | ✅ | — |
| 워치리스트 제거 | /api/v1/watchlist/{corp_code} | DELETE | — | 204 | ✅ | — |
| 종목 검색 | /api/v1/stocks/search | GET | ?query= | [StockSearchResult] | ✅ | ✅ (route handler 경유) |
| 대시보드 공시 페이지 | /api/v1/dashboard/disclosures | GET | ?page&size | DashboardPage | ✅ | — |
| 최근 공시 (전체) | /api/v1/disclosures/recent | GET | ?limit | [DashboardDisclosure] | ✅ (EmptyWatchlist) | — |
| 웹 공시 목록 (summary-only) | /api/v1/web/disclosures | GET | ?limit&corpCode | [DashboardDisclosure] | — | ✅ (메인 list, PR2) |
| 웹 프리셋 종목 | /api/v1/web/active-stocks | GET | ?days&limit | [StockSearchResult] | — | ✅ (프리셋 칩, PR2) |
| Quota | /api/v1/quota | GET | — | QuotaStatus | ✅ | — |
Edge cases
App
- 인증 만료 (401):
requireAuth흐름으로 auth-gate 띄움 → flow-auth-gate (예정) - bootstrapped 안 됨: 첫 cold start
/auth/me응답까지 ~2초 폴링, 그 동안 화면 보이지만 보호된 액션 차단 - 동시 등록/제거 race: 마지막 reload 가 진실 — 중간 상태 UI 일시 깜빡임 허용
- 종목 검색 결과 0건: 드롭다운 자체 hide (별도 "결과 없음" 메시지 X)
- 광고 보상 사용 시: weeklyUsed 음수 가능, QuotaIndicator label remaining > 10 표시 (bar clamp)
Web
- 백엔드 다운 / 403:
getJSONSoft가 null 삼킴 → "공시 목록을 불러올 수 없습니다." Empty 표시 (에러 throw 안 함) - CF 빌드 시 prerender:
force-dynamic으로 회피 — Worker secret 살아있는 런타임 시점 fetch - Smart App Banner (iOS Safari):
apple-itunes-appmeta 가 App Store 진입 유도 (앱 등록 전엔 placeholder app-id, 배너 안 뜸)
관련 docs
- domain-watchlist.md — 워치리스트 비즈니스 룰 (App only)
- domain-quota.md — quota 정책 (App only)
- domain-disclosure.md — 공시 sync / 분석 정책 (App + Web 공통)
Status
- ✅ App — 검색 / 워치리스트 / 공시 list / 페이지네이션 (0-based) 구현 일치
- 📋 Web 개편 (진행 중, 0520) — 종목 필터 + summary-only + 프리셋(active-stocks) + FloatingNavBar 제거 + 더보기 AppCTA. 디자인/스펙 = 본 PR, 백엔드(
/api/v1/web/*) = PR2, FE = PR3 - 📋
/companies/[corpCode]페이지 제거 (PR3) — 종목 필터가 흡수. 검색/프리셋 선택 + disclosure-detail HeaderCard 의 기업 링크 모두/?corp={corpCode}로 통일 (PR3 에서/companies/*진입점 전부 전환 — 잔존 링크 404 방지) - 📋 target (현재 미구현, 별도 PR) — dashboard 에 QuotaIndicator floating 추가. 현재 quota 위젯은 disclosure-detail 화면에만 존재
- 📋 상대 시간 라벨 ("오늘 오후 5:30" / "어제") — 디자인 박제, BE
created_at노출 미구현