본문으로 건너뛰기

JunkBox apps/hub-api 가이드

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-api18001 포트 기준 백엔드로 실행할 수 있음.
  • apps/hub-ui15001 포트 기준 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-uirename-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_URLsqlite+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-coresetup_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_modulesHUB, 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_codeprovider:model 형식의 full model id와 일치해야 함.
  • 단가 계산은 라우팅 YAML의 providermodel을 조합한 full model id를 AI_MODEL.common_code와 비교해 수행하며, 공통코드 값을 : 기준으로 분해해 provider와 model을 별도 해석하지 않음.
  • AI_MODEL.attribute01은 입력 토큰 USD 단가, AI_MODEL.attribute02는 출력 토큰 USD 단가이며 둘 다 100만 토큰 기준 숫자값으로 해석함.
  • AI 로그 화면의 원화 표시는 메타 EXCHANGE_RATEmeta_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_classmobile 문자열이 포함되면 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 포털 화면

  • 포털은 운영 허브 홈 화면임.
  • APPSERVICE 탭을 사용하여 카드 그룹을 분리함.
  • APP 탭은 포털 메뉴의 menu_type_code=APP 값을 기준으로 구성함.
  • 하위 호환을 위해 DB에 남아 있는 menu_type_code=PROJECT 값은 포털 소비 화면과 정렬 저장에서 APP 탭으로 해석함.
  • 포털 소비 화면의 탭 외형과 선택 상호작용은 hub-uishared-ui 공통 segmented tab 계약으로 렌더링함.
  • 카드 설명 노출 여부와 아이콘 렌더링 방식은 포털 메뉴 저장소의 is_desc_visible, icon_type_code, icon_val 기준으로 결정함.
  • icon_type_code=TEXTicon_val을 텍스트 아이콘으로 표시함.
  • icon_type_code=IMAGEicon_val을 이미지 URL로 사용함.
  • icon_type_code=APP_ICONicon_valshared-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/reorderassetTypeFUND, STOCK_DOMESTIC, STOCK_OVERSEAS만 사용함.
  • DASHBOARD_STOCK_DOMESTIC, DASHBOARD_STOCK_OVERSEAS, DASHBOARD_FUND 공통코드의 common_code는 외부 조회 식별자, common_code_name은 표시명으로 사용함.
  • DASHBOARD_STOCK_DOMESTIC.attribute01DASHBOARD_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_codeattribute01=STOCK이면 네이버 국내 주식 API의 6자리 종목코드, attribute01=INDEX이면 네이버 국내 지수 API의 지수 코드이며 코스피는 KOSPI 형식을 사용함.
  • DASHBOARD_STOCK_OVERSEAS.common_codeattribute01=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_idtb_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를 따름.