이 페이지에서
1. 애플리케이션 역할
apps/hub-api는 JunkBox의 상시 운영 허브임.
인증 API, 공통 로그인 화면, 운영 관리자 화면, 공통 마스터 데이터 관리 API, 공통 마스터 데이터 런타임 제공자를 동시에 담당함.
다른 앱과 모듈이 재사용할 공통 코드, 코드 타입, 메타, 포털 메뉴의 단일 관리 주체임.
chess-api, workout-api, rename-api, raid-api 같은 타 앱의 공통 로그인 진입점 역할을 수행함.
workout 앱의 운동 계획 생성은 workout-api 규칙 엔진이 담당하며, hub-api의 현재 역할 범위에 포함되지 않음.
2. 런타임 구성
FastAPI 애플리케이션 엔트리포인트는 junkbox_hub_api.main:app임.
기본 루트 /는 /portal로 리다이렉트됨.
상태 확인 경로는 /health임.
OpenAPI 노출 경로는 /docs, /redoc, /openapi.json임.
인증, 예외 처리, 공통 정적 자산 마운트, 웹 페이지 라우터, API 라우터를 하나의 앱에서 통합함.
apps/hub-ui/dist를 /hub-ui 경로로 직접 마운트함.
libs/shared-ui 산출 자산은 /shared-ui 경로로 직접 마운트함.
CORS 허용 origin 목록은 shared-core 설정의 server.cors_origins를 공통으로 사용함.
2.1 로컬 개발 서버 연동
hub-api는 18001 포트 기준 백엔드로 실행할 수 있음.
apps/hub-ui는 15001 포트 기준 Vite 개발 서버로 실행할 수 있음.
VS Code task는 uv run --package junkbox-hub-api uvicorn ... 형태로 실행하며, hub-api 패키지 의존성을 기준으로 jinja2, python-multipart, uvicorn[standard] 같은 런타임 의존성을 해석함.
페이지 HTML은 18001이 렌더링하고, React 엔트리와 HMR 자산은 15001/hub-ui/* 경로에서 로드함.
workout-ui와 rename-ui 개발 서버 복귀를 위해 다른 로컬 UI origin도 같은 공통 CORS 목록으로 허용함.
2.2 운영 배포 기준
운영 컨테이너 기본 포트는 8001임.
운영 이미지는 apps/hub-ui/dist, libs/shared-ui 자산, hub-api 실행 코드를 함께 포함함.
운영 환경변수는 /sorc001/junkbox/env/.env.shared와 /sorc001/junkbox/env/.env.prod를 함께 참조하고, 컨테이너 내부 ENV_FILE=/app/env/.env.prod 기준으로 로딩함.
운영 로그 볼륨은 /logs001/junkbox/hub-api 호스트 경로를 컨테이너 내부 /app/logs에 연결함.
공통 애플리케이션 로그 파일 경로는 /app/logs/app.log임.
Hub SQLite DB 파일은 호스트 /sorc001/junkbox/db/hub.sqlite3에 위치하고, 운영 컨테이너에서는 /sorc001/junkbox/db를 /app/db로 마운트해 /app/db/hub.sqlite3로 접근함.
운영 HUB_DATABASE_URL은 sqlite+aiosqlite:////app/db/hub.sqlite3 값을 사용함.
운영 AI 라우팅 파일은 호스트 /sorc001/junkbox/master-data/ai-routing.yml를 컨테이너 내부 /app/var/master-data/ai-routing.yml로 마운트해서 사용함.
대시보드 런타임 정책값은 apps/hub-api/var/dashboard-policy.yml에서 관리함.
대시보드 정책 파일은 로컬 Git 원본 기준 apps/hub-api/var/dashboard-policy.yml, 컨테이너 기준 /app/apps/hub-api/var/dashboard-policy.yml 경로를 사용함.
대시보드 AI 카드의 OpenRouter credits와 API key별 사용량 조회에는 DASHBOARD_OPENROUTER_MANAGEMENT_API_KEY가 필요함.
운영 OpenRouter Management API Key는 /sorc001/junkbox/env/.env.prod 같은 profile별 secret env에서 관리하며 .env.shared에 값을 두지 않음.
공통 shared-ui CSS/JS 링크는 템플릿 컨텍스트의 shared_ui_asset() 헬퍼가 파일 수정 시각 기준 버전 쿼리를 붙여 렌더링함.
공통 CSS 링크 선언은 shared-ui stylesheet 매크로를 통해 중앙화함.
공통 CSS는 hub-ui-base.css와 화면 모드별 hub-ui-mobile.css, hub-ui-desktop.css를 조합해서 로드하며, 화면별 필요에 따라 두 모드 CSS를 함께 로드할 수 있음.
hub 공통 메뉴바는 화면 본문 폭 정책과 분리해서 뷰포트 기준으로 반응형 동작하며, PC-only 본문 화면도 모바일 폭에서는 드로어 메뉴를 사용함.
2.3 공통 로깅 초기화
앱 import 시점에 shared-core의 setup_app_logging()을 호출함.
설정값은 settings.modules.hub.logging에서 읽음.
로그 sink는 콘솔 출력과 파일 출력 두 축으로 동시에 구성됨.
파일 정책은 rotation, retention, compression, enqueue 옵션을 공통 설정값 기준으로 적용함.
LOG_DIAGNOSE_TOGGLE이 켜진 profile에서는 diagnose, backtrace를 함께 활성화함.
3. 라우팅 구조
3.1 인증 API
POST /api/auth/login
POST /api/auth/logout
GET /api/auth/verify
GET /api/auth/encrypt
3.2 hub API
공통코드 API
GET /api/hub/codes/types
GET /api/hub/codes/types/with-counts
POST /api/hub/codes/types
GET /api/hub/codes/types/{code_type_code}/meta
PATCH /api/hub/codes/types/{code_type_code}/meta
DELETE /api/hub/codes/types/{code_type_code}
GET /api/hub/codes/{code_type_code}
GET /api/hub/codes/{code_type_code}/all
POST /api/hub/codes/batch
메타 API
GET /api/hub/metas
GET /api/hub/metas/progressive
GET /api/hub/metas/{meta_id}
GET /api/hub/metas/by-key/{meta_key}
POST /api/hub/metas
PATCH /api/hub/metas/{meta_id}
DELETE /api/hub/metas/{meta_id}
포털 메뉴 API
GET /api/hub/portal-menus
GET /api/hub/portal-menus/{menu_id}
POST /api/hub/portal-menus
PATCH /api/hub/portal-menus/{menu_id}
DELETE /api/hub/portal-menus/{menu_id}
사용자 API
GET /api/hub/users
GET /api/hub/users/progressive
GET /api/hub/users/{user_id}
POST /api/hub/users
PATCH /api/hub/users/{user_id}
DELETE /api/hub/users/{user_id}
AI 로그 API
GET /api/hub/ai-logs
GET /api/hub/ai-logs/summary
GET /api/hub/ai-logs/{usage_log_id}
대시보드 API
GET /api/hub/dashboard
GET /api/hub/dashboard?forceRefresh=true
GET /api/hub/dashboard/preferences
PATCH /api/hub/dashboard/preferences
POST /api/hub/dashboard/investments/reorder
3.3 내부 master-data API
GET /api/internal/master-data/codes/{code_type_code}
GET /api/internal/master-data/metas/by-key/{meta_key}
GET /api/internal/master-data/portal-menus
내부 API는 서버 간 호출 전용이며, HUB_API_INTERNAL_TOKEN_HEADER 헤더와 토큰 검증을 통과한 요청만 허용함.
3.4 내부 인증 API
POST /api/internal/auth/validate
POST /api/internal/auth/refresh
내부 인증 API는 서버 간 호출 전용이며, HUB_API_INTERNAL_TOKEN_HEADER 헤더와 토큰 검증을 통과한 요청만 허용함.
POST /api/internal/auth/validate는 요청 body의 현재 JWT를 검증하고 Hub SQLite tb_user_session_n 기준 세션 유효성을 확인한 뒤 현재 사용자, 권한, role, session_id를 반환함.
POST /api/internal/auth/refresh는 요청 body의 현재 JWT를 검증하고, 세션 테이블과 세션 정책의 슬라이딩 임계값 기준으로 새 JWT 발급 필요 여부를 반환함.
토큰이 유효하고 갱신이 필요 없으면 refreshed=false, token=null, max_age_seconds를 반환함.
토큰이 유효하고 갱신이 필요하면 refreshed=true, 새 token, max_age_seconds를 반환함.
3.5 AI master-data API
GET /api/v1/master/all
/api/v1/master/all은 AI 에이전트 표준 통합 조회 경로이며 ROLE_AI JWT를 요구함.
통합 snapshot API는 exported_at, code_types, codes, metas를 하나의 JSON 응답으로 반환함.
3.6 포털 API
GET /api/portal/config
POST /api/portal/config
3.7 웹 화면
GET /login
POST /login
GET /dashboard
GET /portal
GET /portal-menus
GET /users
GET /codes
GET /metas
GET /ai-logs
GET /error/403
/error/403은 하위 호환 진입점이며 별도 권한 오류 페이지를 렌더링하지 않고 로그인 화면의 권한 없음 alert 흐름으로 연결함.
4. 인증 및 권한 흐름
4.1 토큰 입력 경로
API는 Authorization: Bearer 헤더와 COOKIE_NAME 설정값 기준 HttpOnly 쿠키를 모두 수용함.
현재 기본 쿠키 이름은 JUNKBOX_AUTH임.
인증 해석 우선순위는 Authorization 헤더가 먼저임.
일반 사용자 JWT는 서명, 만료, sid claim, Hub SQLite tb_user_session_n의 활성 세션 상태를 모두 통과해야 유효함.
Swagger 인증도 같은 JWT 검증과 세션 유효성 확인 경로를 사용함.
AI 에이전트 표준 master-data API는 Bearer JWT의 role=ROLE_AI claim을 검증함.
ROLE_AI JWT는 일반 require_module(...) 기반 비즈니스 API 접근에 사용할 수 없으며 권한 의존성 단계에서 403으로 차단됨.
4.2 로그인과 페이지 접근 흐름
보호 페이지는 강제 인증 구조를 사용함.
비로그인 요청은 /login?redirect=...&required_module=...로 리다이렉트됨.
로그인 성공 시 redirect 값이 허용된 경로 또는 허용된 앱 origin이면 해당 위치로 이동함.
redirect가 비어 있거나 허용되지 않으면 required_module 기준 기본 위치로 이동함.
HUB 보호 페이지는 HUB 권한을 요구함.
allowed_modules에 과거 SYSTEM 값이 있어도 내부 판단은 HUB 권한으로 정규화하여 처리함.
CHESS 앱에서 공통 로그인으로 들어온 경우 required_module=CHESS 기준 검증을 수행함.
WORKOUT 앱에서 공통 로그인으로 들어온 경우 required_module=WORKOUT 기준 검증을 수행함.
RAID 앱에서 공통 로그인으로 들어온 경우 required_module=RAID 기준 검증을 수행함.
요구 모듈 권한이 없으면 공통 로그인 화면으로 돌아가며 permission_denied=1을 전달함.
로그인 화면은 permission_denied=1을 감지하면 권한 없음 alert를 한 번 표시하고 현재 URL에서 해당 쿼리 플래그를 제거함.
현재 세션 검증 결과의 allowed_modules에 요구 모듈이 없으면 로그인 화면을 다시 렌더링하여 credential 재검증 후 최신 권한 claim이 포함된 JWT를 발급할 수 있음.
최근 로그인에 성공한 아이디는 공통 쿠키에 기록되며, 이후 로그인 화면 진입 시 기본 아이디 값으로 다시 채워짐.
4.3 JWT 갱신 흐름
사용자 세션 만료 시간과 슬라이딩 갱신 임계값은 apps/hub-api/var/auth-policy.yml에서 관리함.
현재 세션 정책은 expiration_minutes=120, sliding_threshold_minutes=30임.
로그인 성공 시 hub-api는 세션 정책의 만료 시간을 기준으로 Hub SQLite tb_user_session_n에 사용자 세션 row를 생성하고 JWT sid, JWT exp, HttpOnly 쿠키 max_age를 설정함.
사용자 세션은 session_id, user_id, issued_at, expires_at, revoked_at, last_seen_at, user_agent, ip_address, is_active 값을 포함함.
JWT의 sid claim은 Hub SQLite tb_user_session_n.session_id와 매칭되어야 함.
일반 사용자 JWT 검증은 JWT 서명과 만료뿐 아니라 세션 row의 활성 상태, revoke 상태, 만료 시각, 사용자 활성 상태를 확인함.
hub-api도 공통 SlidingSessionMiddleware를 등록하여 브라우저 쿠키 기반 요청의 세션 갱신을 처리함.
POST /api/internal/auth/validate는 개별 앱과 공통 인증 의존성이 일반 사용자 JWT의 세션 유효성을 hub에 위임할 때 사용하는 내부 API임.
POST /api/internal/auth/refresh는 현재 JWT와 세션 row를 검증하고, 만료까지 남은 시간이 슬라이딩 임계값 이하이면 같은 sid와 DB의 최신 사용자 권한 기준으로 새 JWT를 발급함.
개별 앱의 슬라이딩 세션 미들웨어는 직접 JWT를 재발급하지 않고 POST /api/internal/auth/refresh에 갱신 판단과 토큰 발급을 위임함.
로그아웃은 현재 요청 토큰의 sid에 해당하는 세션 row를 revoke하고 인증 쿠키를 만료시킴.
사용자 비밀번호 변경, 계정 비활성화, 권한 변경, 사용자 삭제는 해당 사용자의 활성 세션을 revoke함.
만료되었거나 유효하지 않거나 revoke된 토큰은 슬라이딩 갱신 대상이 아니며, 표준 인증 실패 흐름에서 로그인 재진입 또는 JSON 401로 처리됨.
4.4 API 실패 정책
인증 실패는 JSON 401 응답임.
권한 부족은 JSON 403 응답임.
HTML 페이지는 JSON 오류를 직접 노출하지 않고 로그인 또는 로그인 화면의 권한 없음 alert 흐름으로 분기함.
HTML 권한 부족에 대한 별도 권한 오류 템플릿은 사용하지 않음.
5. 데이터 저장 책임
5.1 Hub SQLite 공통 마스터 데이터
공통코드와 코드 타입은 Hub SQLite tb_common_c, tb_code_type_c를 소스 오브 트루스로 사용함.
메타는 Hub SQLite tb_meta_m을 소스 오브 트루스로 사용함.
포털 메뉴는 Hub SQLite tb_portal_menu_m을 소스 오브 트루스로 사용함.
일반 UI 텍스트와 사용자 메시지는 DB 기반 공통 마스터 데이터 대상이 아니며 각 앱의 소스 코드에 한국어 문구로 직접 작성함.
apps/hub-api는 DB를 직접 다루지 않고 shared-core의 공통 마스터 데이터 저장소를 통해 읽고 저장함.
저장소 클래스 이름은 JsonMasterDataStore를 유지하지만 현재 구현은 DB 기반임.
AI 전용 read-only API도 같은 저장소 계층을 재사용하며 별도 복제 저장소를 두지 않음.
hub-api는 앱 import 시점에 settings.modules.hub.database_url을 사용해 shared-core 전역 async engine을 Hub SQLite로 초기화함.
로컬 비컨테이너 실행의 기본 Hub DSN은 sqlite+aiosqlite:////sorc001/junkbox/db/hub.sqlite3임.
Docker 실행의 Hub DSN은 sqlite+aiosqlite:////app/db/hub.sqlite3이며 compose가 호스트 /sorc001/junkbox/db를 컨테이너 /app/db로 마운트함.
SQLite 연결은 DB 파일 부모 디렉터리를 준비하고 PRAGMA journal_mode=WAL, PRAGMA busy_timeout=5000, PRAGMA foreign_keys=ON을 적용함.
scripts/migrate_hub_pg_dump_to_sqlite.py는 PostgreSQL dump의 hub schema DDL/DML 중 Hub 대상 테이블만 읽어 Hub SQLite 파일을 생성하는 운영 보조 스크립트임.
이관 스크립트의 기본 dump 입력은 dbjbp1_20260719-080001.sql이고 기본 출력은 /sorc001/junkbox/db/hub.sqlite3임.
--force로 기존 Hub SQLite 파일을 덮어쓸 때는 /sorc001/junkbox/backups/sqlite 하위 백업 생성을 전제로 함.
5.2 YAML 기반 AI 라우팅 데이터
AI 기능별 라우팅은 YAML 파일을 소스 오브 트루스로 사용함.
로컬 Git 원본 경로는 apps/hub-api/var/master-data/ai-routing.yml임.
컨테이너 런타임 기본 경로는 /app/var/master-data/ai-routing.yml임.
운영 호스트 공통 파일 경로는 /sorc001/junkbox/master-data/ai-routing.yml임.
APP_PROFILE 값에 따라 ai-routing.local.yml, ai-routing.prod.yml 같은 profile별 파일을 우선 선택함.
profile별 파일이 없으면 기본 ai-routing.yml을 사용함.
5.3 Hub SQLite 리소스
사용자는 DB 기반 구조를 사용함.
AI 로그 엔티티는 shared-ai가 소유하고, hub-api는 조회 API와 운영 화면을 제공함.
사용자 리소스는 Hub SQLite tb_user_m를 기준으로 하며 id 물리 PK, user_id 논리 식별자 구조를 사용함.
사용자 allowed_modules는 HUB, CHESS, WORKOUT, RENAME, RAID 같은 앱 모듈 접근 권한을 SQLite JSON 배열로 저장함.
사용자 로그인 세션은 Hub SQLite tb_user_session_n를 기준으로 하며 session_id가 JWT sid claim과 매칭됨.
세션 row는 다중 기기 로그인을 허용하는 사용자별 세션 단위 상태이며, 로그아웃 또는 사용자 보안 속성 변경 시 revoke됨.
AI 사용 로그는 Hub SQLite tb_ai_usage_logs_n를 기준으로 함.
사용자 생성과 수정 시 created_by, updated_by는 사용자 id 기준으로 기록함.
6. workout 계획 생성과의 경계
workout 앱의 운동 계획 생성 공개 진입점과 실행 주체는 workout-api임.
hub-api는 workout 운동 계획 생성용 LLM 호출, LangGraph 실행, SSE trace, AI 응답 직렬화, 계획 저장 orchestration을 현재 시스템 계약으로 소유하지 않음.
hub-api는 workout이 참조하는 BODY_PART, WORKOUT_ROUTINE_MODE, WORKOUT_STATUS 공통코드와 로그인/세션 검증, internal master-data API를 제공함.
workout 운동 종목 기준정보는 workout SQLite tb_workout_exercise_m이 런타임 소스 오브 트루스이며, hub 공통코드 EXERCISE는 초기 bootstrap source로만 사용할 수 있음.
7. 공통 AI 연계 구조
7.1 공통 라이브러리 의존
hub-api는 provider 구현 자체를 직접 소유하지 않음.
공통 AI 기능은 libs/shared-ai를 통해 사용함.
7.2 라우팅과 프로필
기능별 provider/model 선택은 ai-routing.yml 계열 YAML이 결정함.
provider API 키와 base URL은 .env.shared + ENV_FILE 조합이 결정함.
Jeevtrap Markdown 변환, Jeevtrap URL 요약/키워드 추출 route는 LiteLLM provider와 OpenRouter DeepSeek V4 Flash 모델을 사용할 수 있음.
AI_MODEL 공통코드는 모델 단가 계산용 기준 데이터로 사용함.
AI_MODEL.common_code는 provider:model 형식의 full model id와 일치해야 함.
단가 계산은 라우팅 YAML의 provider와 model을 조합한 full model id를 AI_MODEL.common_code와 비교해 수행하며, 공통코드 값을 : 기준으로 분해해 provider와 model을 별도 해석하지 않음.
AI_MODEL.attribute01은 입력 토큰 USD 단가, AI_MODEL.attribute02는 출력 토큰 USD 단가이며 둘 다 100만 토큰 기준 숫자값으로 해석함.
AI 로그 화면의 원화 표시는 메타 EXCHANGE_RATE의 meta_val을 1달러당 원화 숫자값으로 사용함.
어떤 provider 어댑터가 선택되는지는 model이 아니라 라우팅 YAML의 provider 값이 결정함.
7.3 structured output과 fallback
shared-ai의 공통 structured generation 계층이 JSON 파싱, Pydantic 검증, JSON 재생성, usage log 누적을 처리함.
LiteLLM proxy는 route의 structured_output_mode와 모델 계열에 따라 response format을 선택함.
Gemini 계열 auto 전략은 response_format={"type":"json_object","response_schema":...}를 우선 시도함.
LiteLLM 또는 하위 모델이 response format 옵션을 거부하면 JSON only 지시문 강화 fallback 요청을 1회 재시도함.
따라서 AI route의 provider/model이 변경되어도 structured output 계약을 우선 유지하는 구조임.
8. 화면 구조
8.1 공통 구조
모든 관리자 화면은 공통 베이스 템플릿과 shared-ui 산출 자산을 사용함.
React 운영 화면 소스는 apps/hub-ui에서 .ts/.tsx 기준으로 관리함.
공통 베이스 템플릿은 dev client와 React refresh preamble의 단일 선언 지점임.
공통 베이스 템플릿은 shared-ui stylesheet 매크로를 import하고, hub-ui-base.css, hub-ui-mobile.css, hub-ui-desktop.css, junkbox-tabulator.css, app-shell.js에 버전 쿼리를 붙여 운영 캐시 잔존 영향을 줄임.
공통 베이스 템플릿은 favicon_url 컨텍스트가 있으면 <link rel="icon">을 렌더링함.
공통 베이스 템플릿은 기본적으로 body_mode_class에 mobile 문자열이 포함되면 mobile CSS를, 그 외에는 desktop CSS를 로드함.
화면 컨텍스트에서 include_mobile_css 또는 include_desktop_css를 지정하면 기본 CSS 조합에 필요한 모드 CSS를 추가로 로드할 수 있음. /portal은 본문 모바일 폭을 유지하면서 상단 메뉴를 PC/모바일 반응형으로 표시하기 위해 mobile CSS와 desktop CSS를 함께 로드함.
공통 베이스 템플릿은 inline <style> 블록을 포함하지 않으며, 모든 스타일은 shared-ui 산출 CSS에서 공급됨.
hub 웹 화면의 favicon은 포털 메뉴의 menu_type_code=APP, icon_type_code=APP_ICON, icon_val 조합에서 hub 앱 아이콘 slug를 해석하고, 없으면 hub slug를 fallback으로 사용함.
서버 템플릿 문구는 템플릿 또는 라우터 컨텍스트에 한국어 문구로 직접 작성함.
공통코드 라벨과 메타 설정값처럼 데이터 자체가 마스터데이터인 값만 DB 조회 구조를 사용함.
8.2 로그인 화면
로그인 화면은 서버 렌더링 폼 구조임.
공통 로그인 화면은 최소 텍스트 중심의 단순한 구조를 사용함.
로그인 화면 상단 eyebrow 브랜드 텍스트는 JUNKBOX를 사용함.
인증 오류는 서버에서 다시 로그인 페이지를 렌더링하면서 메시지를 주입하는 방식임.
최근 로그인 성공 아이디가 있으면 공통 쿠키 값을 기본 아이디 입력값으로 사용함.
8.3 포털 화면
포털은 운영 허브 홈 화면임.
APP와 SERVICE 탭을 사용하여 카드 그룹을 분리함.
APP 탭은 포털 메뉴의 menu_type_code=APP 값을 기준으로 구성함.
하위 호환을 위해 DB에 남아 있는 menu_type_code=PROJECT 값은 포털 소비 화면과 정렬 저장에서 APP 탭으로 해석함.
포털 소비 화면의 탭 외형과 선택 상호작용은 hub-ui가 shared-ui 공통 segmented tab 계약으로 렌더링함.
카드 설명 노출 여부와 아이콘 렌더링 방식은 포털 메뉴 저장소의 is_desc_visible, icon_type_code, icon_val 기준으로 결정함.
icon_type_code=TEXT는 icon_val을 텍스트 아이콘으로 표시함.
icon_type_code=IMAGE는 icon_val을 이미지 URL로 사용함.
icon_type_code=APP_ICON은 icon_val을 shared-ui 앱 아이콘 slug로 해석하여 /shared-ui/app-icons/{icon_val}/icon.svg를 포털 카드 아이콘으로 사용함.
포털 메뉴의 APP_ICON + icon_val 조합은 포털 카드 아이콘, 앱 favicon, PWA 아이콘을 같은 앱 아이콘 세트로 연결하는 단일 관리 지점임.
로컬 프로필에서는 local_menu_url, 그 외 프로필에서는 menu_url을 소비함.
8.4 대시보드 화면
/dashboard는 포털과 분리된 동적 데이터 대시보드 화면임.
대시보드는 소식/핫딜, AI 사용량, 투자 관심 목록, 홈서버 상태를 카드 단위로 표시함.
뉴스 카드는 wmania 최신뉴스와 네이버 스포츠 야구/해외야구 인기 기사 API를 HTTP 방식으로 수집함.
네이버 스포츠 야구/해외야구 수집은 m.sports.naver.com의 모바일 섹션 뉴스 URL에 KST 기준 오늘 날짜를 yyyyMMdd 형식으로 넣은 popular 목록을 기준으로 하며, 카드의 전체보기 URL은 sports.news.naver.com/{section}/index 계열 URL을 유지함.
핫딜 카드는 FMKorea 핫딜 인기순 페이지의 HTML 목록에서 최신 5건의 제목, 링크, 쇼핑몰/가격/배송 정보를 수집함.
WMania/핫딜처럼 HTML을 파싱하는 외부 사이트 수집은 source별 캐시를 두며, 사용자가 새로고침을 반복해도 캐시 TTL 안에는 같은 사이트를 다시 호출하지 않음.
WMania 최신뉴스 source 캐시는 10분이고, FMKorea 핫딜 source 캐시는 30분임.
WMania/핫딜 수집 응답이 Retry-After를 포함한 일시 제한 상태이면 해당 시간 동안 재요청을 피하고, 이전 성공 카드가 있으면 stale 데이터를 우선 표시함.
FMKorea 핫딜 수집이 HTTP 430 차단 상태를 반환하고 이전 성공 카드가 있으면 카드의 항목 목록과 updatedAt은 마지막 성공 데이터 기준으로 유지하고, status=error와 차단 상태 오류 메시지를 함께 반환함.
FMKorea 핫딜 수집이 HTTP 430 차단 상태를 반환하지만 이전 성공 카드가 없으면 항목 없는 오류 카드로 표시함.
외부 HTML source별 성공 카드와 차단 상태는 hub-api 프로세스 인메모리 캐시에 저장되며, 프로세스 재시작 후에는 이전 성공 카드가 유지되지 않음.
국내 주식 카드는 DASHBOARD_STOCK_DOMESTIC 공통코드 목록을 기준으로 네이버 금융 국내 주식과 국내 지수 API를 조회함.
해외 주식 카드는 DASHBOARD_STOCK_OVERSEAS 공통코드 목록을 기준으로 네이버 금융 해외 주식과 해외 지수 API를 조회함.
펀드 카드는 DASHBOARD_FUND 공통코드 목록을 기준으로 FunETF 공개 기준가 API를 조회함.
펀드 카드는 성공 수집 행을 hub-api 프로세스 인메모리 캐시에 저장하며, FunETF가 HTTP 429 또는 Cloudflare 확인 응답을 반환하면 재요청을 잠시 피하고 이전 성공 펀드 행을 우선 반환함.
FunETF 일시 제한 상태에서 이전 성공 펀드 캐시가 없으면 펀드 항목은 카드 내부 오류 항목으로 반환됨.
투자 카드 항목 순서는 공통코드의 sort_order_no를 따름.
POST /api/hub/dashboard/investments/reorder는 펀드/국내 주식/해외 주식 카드의 드래그 정렬 결과를 받아 해당 공통코드의 sort_order_no를 10 단위로 재부여함.
POST /api/hub/dashboard/investments/reorder의 assetType은 FUND, STOCK_DOMESTIC, STOCK_OVERSEAS만 사용함.
DASHBOARD_STOCK_DOMESTIC, DASHBOARD_STOCK_OVERSEAS, DASHBOARD_FUND 공통코드의 common_code는 외부 조회 식별자, common_code_name은 표시명으로 사용함.
DASHBOARD_STOCK_DOMESTIC.attribute01과 DASHBOARD_STOCK_OVERSEAS.attribute01은 투자 항목 종류이며 STOCK, INDEX 값을 사용함.
DASHBOARD_STOCK_DOMESTIC.attribute02, DASHBOARD_STOCK_OVERSEAS.attribute02, DASHBOARD_FUND.attribute02는 상세 URL로 사용함.
DASHBOARD_STOCK_DOMESTIC.attribute03, DASHBOARD_STOCK_OVERSEAS.attribute03, DASHBOARD_FUND.attribute01, DASHBOARD_FUND.attribute03은 대시보드 수집 공급자 선택 기준으로 사용하지 않음.
DASHBOARD_STOCK_DOMESTIC.common_code는 attribute01=STOCK이면 네이버 국내 주식 API의 6자리 종목코드, attribute01=INDEX이면 네이버 국내 지수 API의 지수 코드이며 코스피는 KOSPI 형식을 사용함.
DASHBOARD_STOCK_OVERSEAS.common_code는 attribute01=STOCK이면 네이버 해외 주식 API의 Reuters code이며 NVDA.O, GOOGL.O 같은 형식을 사용하고, attribute01=INDEX이면 네이버 해외 지수 API의 지수 Reuters code임.
대시보드 투자 수집 provider는 공통코드 속성값으로 전환하지 않으며, 주식/지수는 네이버 금융, 펀드는 FunETF 수집기로 고정됨.
GET /api/hub/dashboard/preferences는 현재 로그인 사용자의 대시보드 즐겨찾기 카드 id 목록과 탭별 카드 순서 preference를 반환함.
PATCH /api/hub/dashboard/preferences는 현재 로그인 사용자의 favoriteCardIds, cardOrders를 저장함.
대시보드 사용자 preference는 Hub SQLite tb_dashboard_user_preference_n에 사용자별 1 row로 저장함.
tb_dashboard_user_preference_n.user_id는 tb_user_m.user_id를 참조하며, 사용자 삭제 시 cascade 삭제됨.
favorite_card_ids는 SQLite JSON 배열이며 즐겨찾기 카드 id를 표시 순서대로 저장하고 최대 8개까지만 허용함.
card_orders는 SQLite JSON object이며 news, economy, system 같은 탭 key별 카드 id 배열을 저장함.
즐겨찾기와 카드 순서 preference는 대시보드 데이터 수집 캐시와 분리된 DB 사용자 설정이며, /api/hub/dashboard 응답 TTL에 포함되지 않음.
preference API는 중복/빈 카드 id를 정규화하고, 지원하지 않는 탭 key의 카드 순서 값은 저장 대상에서 제외함.
AI 카드는 OpenRouter Management API를 기준으로 사용량과 크레딧을 조회함.
AI 카드의 금일, 금주, 금월 사용량은 OpenRouter GET /api/v1/keys 응답의 usage_daily, usage_weekly, usage_monthly를 API key 목록 전체 기준으로 합산한 USD 값임.
AI 카드의 key별 주간 사용량은 같은 GET /api/v1/keys 응답의 각 key usage_weekly 값을 사용함.
AI 카드의 잔여 크레딧은 OpenRouter GET /api/v1/credits 응답의 total_credits - total_usage 계산값을 사용함.
AI 카드 사용량은 Hub SQLite tb_ai_usage_logs_n 로그 집계를 fallback으로 사용하지 않으며, OpenRouter 조회가 실패하거나 Management API Key가 없으면 미설정 또는 오류 상태를 반환함.
AI 카드 사용량은 OpenRouter 응답의 USD 값을 그대로 제공하며, 메타 EXCHANGE_RATE를 통한 KRW 환산을 수행하지 않음.
OpenRouter Management API Key는 completion endpoint 호출에 사용하지 않는 관리 API 전용 secret이며, DASHBOARD_OPENROUTER_MANAGEMENT_API_KEY로 주입함.
홈서버 상태 응답은 DASHBOARD_GLANCES_BASE_URL이 설정된 경우 Glances REST API v4의 CPU, 메모리, processlist, containers 정보를 조회함.
홈서버 상태 응답은 cpu, memory, topProcesses, problemContainers, containersSummary, updatedAt을 포함함.
topProcesses는 Glances processlist 응답을 CPU 사용률 내림차순으로 정렬한 상위 5건이며, 프로세스명, PID, CPU 사용률, 메모리 사용률, 상태를 포함함.
problemContainers는 Docker containers 응답 중 비정상 상태만 최대 10건 반환함.
Docker 컨테이너 상태가 running, up, healthy, Up ... (healthy) 계열이면 정상으로 분류하고, unhealthy, exited처럼 정상 조건에 해당하지 않는 상태만 문제 컨테이너로 분류함.
containersSummary는 Docker 컨테이너의 total, running, healthy, unhealthy, problem 개수를 제공함.
DASHBOARD_GLANCES_BASE_URL이 비어 있으면 홈서버 상태 응답은 미설정 상태와 빈 메트릭/목록/요약을 반환함.
대시보드 전체 응답 TTL, 일반 HTML source별 TTL, 핫딜 source별 TTL, 외부 HTTP 수집 타임아웃, Glances 호출 타임아웃은 apps/hub-api/var/dashboard-policy.yml에서 관리함.
대시보드 전체 응답 TTL과 WMania 같은 일반 HTML source별 TTL의 기본값은 각각 600초임.
FMKorea 핫딜 source별 TTL의 기본값은 1800초임.
대시보드 정책 파일은 collection.http_timeout_seconds, cache.dashboard_ttl_seconds, cache.external_source_ttl_seconds, cache.hotdeal_source_ttl_seconds, glances.timeout_seconds 값을 제공함.
외부 사이트/API 오류는 전체 화면 실패가 아니라 카드별 오류 상태로 노출함.
HTTP/HTML 수집 보조 기능은 libs/shared-crawler를 사용하며, Playwright 브라우저 크롤링은 대시보드 기본 경로에서 사용하지 않음.
8.5 포털 메뉴 관리 화면
/portal-menus는 단일 Tabulator 기반 저장형 관리자 화면임.
목록 조회, 신규 생성, 수정, 체크박스 기반 멀티 선택 삭제를 하나의 그리드에서 처리함.
소비 화면 /portal과 동일한 DB 포털 메뉴 저장소를 기준으로 동작함.
메뉴 타입 선택값은 APP, SERVICE를 사용하며, 기존 PROJECT 값은 화면 정규화 단계에서 APP로 표시함.
아이콘 타입 선택값은 APP_ICON, TEXT, IMAGE를 사용함.
8.6 공통코드 화면
/codes는 좌측 코드 타입 목록과 우측 작업 영역으로 구성된 2패널 화면임.
코드 타입 메타와 실제 코드는 저장소 레벨에서 분리되어 관리됨.
좌측 type_code, type_name 선택은 현재 코드 타입을 활성화하고 우측 코드 목록과 메타를 조회한 뒤 같은 셀에서 직접 편집하는 동작을 수행함.
코드 타입의 code_type_code 변경 시 저장소는 해당 코드 타입 하위 공통코드의 code_type_code도 함께 갱신함.
좌측 코드 타입 목록과 우측 코드 목록은 각각 체크박스 기반 멀티 선택 삭제를 지원함.
우측 코드 목록의 선택 삭제는 저장 대기 목록에 삭제 키를 쌓고 POST /api/hub/codes/batch 저장 시 실제 삭제를 반영함.
8.7 메타 화면
/metas는 좌측 목록과 우측 상세 편집 구조를 사용함.
메타 값은 긴 텍스트를 직접 편집할 수 있는 textarea 중심 구조를 가짐.
좌측 목록은 체크박스 기반 멀티 선택 삭제를 지원하며, 우측 상세 삭제 버튼도 유지함.
9. 관련 문서
프로젝트 전체 구조는 docs/junkbox/common/guide-jb-core.md를 따름.
공통 AI 라이브러리 구조는 docs/junkbox/libs/guide-lib-shared-ai.md를 따름.
공통 크롤링 보조 구조는 docs/junkbox/libs/guide-lib-shared-crawler.md를 따름.
운동 계획 도메인 소비 구조는 docs/junkbox/apps/guide-apps-workout-api.md, docs/junkbox/apps/guide-apps-workout-ui.md를 따름.