On this page
1. 애플리케이션 역할
apps/hub-ui는 hub 운영 화면 전용 프런트엔드 앱임.
hub-api가 제공하는 서버 렌더링 HTML 골격 위에 React 화면을 마운트하는 역할을 담당함.
dashboard, portal, portal-menus, users, codes, metas, ai-logs 화면 엔트리를 소유함.
공통 스타일과 공통 React 유틸은 libs/shared-ui를 소비함.
codes, metas 엔트리는 hub 네비게이션에 노출되는 공통 마스터데이터 관리 화면임.
2. 런타임 구성
번들러는 Vite 6 기반 React + TypeScript 구조를 사용함.
운영 산출물은 apps/hub-ui/dist에 생성됨.
hub-api는 이 디렉터리를 /hub-ui 경로로 직접 마운트함.
개발 서버 기본 포트는 15001임.
루트 진입 /, /hub-ui, /hub-ui/는 /portal로 리다이렉트됨.
3. 엔트리 구조
src/entries/portal.tsx
src/entries/dashboard.tsx
src/entries/portal-menus.tsx
src/entries/users.tsx
src/entries/codes.tsx
src/entries/metas.tsx
src/entries/ai-logs.tsx
4. 페이지 구조
각 엔트리는 서버 템플릿이 주입한 hub-ui-root, hub-ui-page-props를 기준으로 마운트함.
페이지 구현은 src/pages/*.tsx에 위치함.
API 응답 계약과 화면 props 계약은 src/types/models.ts 기준으로 관리함.
브라우저 전역 타입과 import.meta.env 타입은 src/global.d.ts 기준으로 관리함.
5. TypeScript 및 Vite 설정 구조
브라우저 런타임 코드는 tsconfig.json을 기준으로 타입체크함.
Vite 설정과 Node 타입이 필요한 파일은 tsconfig.node.json을 기준으로 별도 타입체크함.
vite.config.ts는 ESM 기준으로 작성하며, 경로 계산은 import.meta.url과 fileURLToPath() 기반 구조를 사용함.
typecheck 스크립트는 브라우저 코드와 Node 설정 파일을 각각 검사하는 구조를 사용함.
6. 공통 의존 구조
공통 React 유틸은 @shared-ui/* alias를 통해 libs/shared-ui/src/react를 참조함.
이 alias는 TypeScript paths와 Vite resolve.alias가 함께 해석하는 구조를 사용함.
공통 토스트, HTTP 요청, 페이지 props 파서, Tabulator 점진 로딩 유틸을 직접 복제하지 않음.
공통 CSS는 서버 템플릿이 shared-ui stylesheet 매크로를 호출해 hub-ui-base.css와 화면 모드별 hub-ui-mobile.css, hub-ui-desktop.css 조합으로 로드함.
포털은 본문을 모바일 폭으로 유지하되 상단 메뉴는 FHD/모바일 반응형으로 동작해야 하므로 base + mobile + desktop 조합을 사용함.
로그인 같은 모바일 화면은 base + mobile 조합을 사용함.
ai-logs, codes, metas, users, portal-menus 같은 운영 관리자 화면은 base + desktop 조합을 사용함.
hub 공통 메뉴바는 각 화면의 본문 폭 정책과 분리해서 뷰포트 기준 반응형으로 동작해야 함.
7. 개발 서버 프록시 구조
/api, /login, /dashboard, /portal, /portal-menus, /users, /codes, /metas, /ai-logs, /error, /shared-ui, /health는 hub-api로 프록시됨.
개발 모드 엔트리 자산 경로는 /hub-ui/src/entries/*.tsx 기준임.
Vite dev client 경로는 /hub-ui/@vite/client임.
React refresh runtime 경로는 /hub-ui/@react-refresh임.
8. 구현 원칙
hub 전용 화면만 포함하고 workout 화면 엔트리와 타입은 포함하지 않음.
페이지 전용 CSS 파일을 추가하지 않음.
공통 시각 규칙은 shared-ui 클래스 조합으로만 표현함.
공통 유틸 또는 아이콘이 필요하면 먼저 libs/shared-ui/src/react 확장을 우선함.
사용자 노출 텍스트는 화면 컴포넌트에 한국어 문구로 직접 작성함.
공통코드 라벨과 메타 설정값처럼 데이터 자체가 마스터데이터인 값만 DB 조회 구조를 사용함.
9. 모바일 화면 구조
/login과 /portal은 모바일 전용 360px 고정 폭 기준을 사용함.
/portal의 본문은 모바일 전용 360px 고정 폭 기준을 유지하지만, 공통 헤더와 메뉴바는 PC 기준에서는 데스크톱 네비게이션, 모바일 기준에서는 드로어 네비게이션을 사용함.
포털 화면은 모바일에서도 카드 2열 구조를 유지함.
포털 카드 메인 텍스트는 1rem, 서브 텍스트는 0.8rem 기준을 사용함.
모바일 포털 카드는 축소 비율 대응이 아니라 공통 폭, 여백, 아이콘 크기, 카드 패딩 보정으로 사용성을 확보함.
/dashboard는 모바일 max-width: 360px 기준에서 모든 카드를 1열 세로 목록으로 표시하며, 화면 폭을 초과하는 별도 좌우 스크롤을 만들지 않음.
/dashboard의 모바일 보기 전환은 현재 선택된 탭을 표시하는 단일 버튼과 shared-ui의 BottomOptionPicker 하단 선택 시트 조합을 사용함.
/portal-menus, /users, /codes, /metas, /ai-logs는 FHD PC-only 본문 정책을 유지하지만, 공통 메뉴바만큼은 모바일 뷰포트에서 드로어 메뉴로 전환됨.
운영 화면, JSON/텍스트 뷰어, 그리드, 필드 라벨은 모두 shared-ui의 Pretendard 기준 폰트 규칙을 따름.
10. 주요 화면 동작 계약
10.1 공통 코드 화면
/codes는 좌측 코드 타입 Tabulator와 우측 메타/코드 상세 영역으로 구성됨.
좌측 type_code, type_name 클릭은 현재 코드 타입을 선택하고 우측 코드 목록과 메타를 로드한 뒤 같은 셀을 편집 모드로 전환하는 동작임.
좌측 count 클릭은 현재 코드 타입을 선택하고 우측 코드 목록과 메타를 로드하는 동작임.
코드 타입의 논리 식별자는 code_type_code이며, 물리 식별자는 code_type_code_id이므로 type_code는 저장 시 하위 공통코드의 code_type_code와 함께 변경될 수 있음.
좌측 Tabulator는 체크박스 기반 멀티 선택 삭제와 현재 코드 타입 활성화를 함께 지원함.
우측 하단 Tabulator는 선택된 코드 타입의 실제 코드 목록을 편집하는 용도임.
우측 하단 Tabulator는 체크박스 기반 멀티 선택 삭제를 지원하며, 삭제 대상 코드는 저장 시 batch API로 반영함.
우측 타입 삭제 전용 버튼은 사용하지 않으며, 코드 타입 삭제는 좌측 Tabulator의 체크박스 선택과 선택 삭제 버튼으로 수행함.
코드 타입 저장은 code_type_code 기준 중복을 저장 전에 확인하고, 서버 409 응답도 중복 저장 실패로 처리함.
공통코드 저장은 같은 code_type_code 안의 common_code 기준 중복을 저장 전에 확인하고, 서버 409 응답도 중복 저장 실패로 처리함.
중복 저장 시 한국어 중복 안내 토스트를 표시하고 저장 요청 또는 저장 후 반영을 중단함.
버튼 영역은 삭제, 조회, 저장 성격 순서를 따르며, 삭제 계열과 조회 계열 사이에는 jb-action-divider를 사용할 수 있음.
좌측 코드 타입 목록과 우측 코드 목록은 각각 선택 삭제, 조회/새로고침, 저장 계열 액션을 같은 성격 순서로 배치함.
10.2 공통 메타/사용자 화면
/metas는 좌측 목록, 우측 상세 편집 구조를 사용하며 좌측 목록에서 체크박스 기반 멀티 선택 삭제를 지원함.
/metas 저장은 meta_key 기준 중복을 저장 전에 확인하고, 서버 409 응답도 중복 저장 실패로 처리함.
/metas 좌측 목록의 선택 삭제 버튼과 우측 상세 삭제 버튼은 함께 유지됨.
/metas 우측 상세 편집 영역은 현재 편집 중인 meta_key, meta_val 기준의 MD 다운로드 버튼을 제공함.
MD 다운로드는 서버 저장 여부와 무관하게 현재 폼 상태의 meta_val을 text/markdown;charset=utf-8 Blob으로 생성하고, 파일명은 meta_key 기반 ${meta_key}.md 형식을 사용함.
MD 다운로드 파일명은 브라우저와 운영체제 파일명에 부적합한 제어문자와 <>:"/\|?* 문자를 _로 치환함.
/users는 점진 로딩형 목록과 상세 편집 구조를 사용하며, 사용자 물리 식별자는 id, 논리 식별자는 user_id를 사용함.
/users 저장은 user_id 기준 중복을 저장 전에 확인하고, 서버 409 응답도 중복 저장 실패로 처리함.
/users 좌측 목록은 체크박스 기반 멀티 선택 삭제를 지원하되 현재 로그인 사용자는 삭제할 수 없음.
/users 상세 편집의 user_id, password는 텍스트 입력 필드이며 select 성격 UI로 렌더링하지 않음.
메타, 사용자 화면의 중복 저장 피드백은 화면 컴포넌트의 한국어 문구로 직접 표시함.
/metas, /users 버튼 영역은 삭제, 조회, 저장 성격 순서를 따르며, 삭제 계열과 조회 계열 사이에는 jb-action-divider를 사용할 수 있음.
/portal-menus도 같은 관리자 화면 액션 정렬 기준을 사용하며, 선택 삭제와 조회/새로고침, 저장 계열 액션을 버튼 성격 기준으로 배치함.
10.3 포털 소비 화면
/portal은 APP, SERVICE 탭 구조를 유지함.
/portal의 APP, SERVICE 탭은 shared-ui의 SegmentedTabs를 사용하며, 모바일 고정 폭 안에서 fill 폭 정책으로 2개 탭을 균등 분할함.
포털 API가 반환한 tab=apps 카드는 APP 탭에 표시함.
하위 호환을 위해 tab=projects 카드도 APP 탭에 포함함.
iconType=app-icon 카드는 icon 값을 shared-ui 앱 아이콘 slug로 해석하여 /shared-ui/app-icons/{icon}/icon.svg를 표시함.
iconType=image 카드는 icon 값을 이미지 URL로 사용하고, 그 외 카드는 icon 값을 텍스트 아이콘으로 표시함.
포털 카드의 앱 이름과 서비스 이름은 jb-portal-card-title의 normal weight 텍스트로 표시하며 별도 굵은 강조를 적용하지 않음.
카드 드래그 정렬은 Sortable을 사용하고, 현재 활성 탭의 카드 순서만 서버에 저장함.
포털 카드 드래그 핸들은 shared-ui 공통 SortableDragHandle과 공통 sortable 옵션을 사용함.
포털 카드 드래그는 핸들을 1초 이상 누른 뒤 이동할 때 정렬 모드로 진입함.
모바일 폭에서도 카드 2열, 드래그 핸들, 설명 표시 규칙을 유지함.
10.4 대시보드 화면
/dashboard는 포털과 별개로 동적 데이터를 보여주는 PC/모바일 겸용 운영 화면임.
대시보드 탭은 즐겨찾기, 소식, 경제, 홈서버 순서로 표시함.
대시보드의 기본 활성 탭은 즐겨찾기임.
PC 기준 대시보드는 즐겨찾기, 소식, 경제, 홈서버를 shared-ui의 SegmentedTabs 인라인 탭으로 표시함.
모바일 max-width: 360px 기준 대시보드는 인라인 SegmentedTabs를 숨기고, 현재 탭 버튼을 누르면 BottomOptionPicker 하단 선택 시트로 같은 탭 목록을 선택함.
대시보드 모바일 하단 선택 시트는 현재 선택된 탭이 열림 직후 목록 안에서 보이는 위치로 정렬되어야 함.
즐겨찾기 탭은 사용자가 즐겨찾기한 큰 카드만 최대 8개까지 표시함.
소식 탭은 WMania 최신뉴스, 네이버 스포츠 야구, 네이버 스포츠 해외야구, FMKorea 핫딜 카드를 표시함.
경제 탭은 펀드, 국내 주식, 해외 주식 카드를 표시함.
홈서버 탭은 AI, CPU, RAM, Docker, 상위 프로세스, 도커 이상 상태, 수집 상태 카드를 표시함.
PC 기준 대시보드는 4열 x 2행 고정 슬롯 배치를 사용하며, 모든 큰 카드는 한 칸만 차지함.
PC 기준 카드 수가 한 행보다 적어도 카드 높이는 4x2 슬롯 기준으로 유지함.
카드 내부 목록이 고정 카드 높이를 초과하면 카드 내부 스크롤을 사용하고, 카드 자체 높이를 늘리지 않음.
모바일 max-width: 360px 기준 대시보드는 1열 세로 카드 목록을 사용하며 카드 높이는 자연 높이로 표시함.
각 카드는 마지막 갱신 시각, 외부 전체보기 링크, 수집 실패 또는 미설정 상태를 자체적으로 표시함.
각 카드는 큰 카드 단위 즐겨찾기 토글 버튼을 제공하며, 즐겨찾기 상태는 서버 preference API로 저장함.
대시보드 헤더의 마지막 생성 시각과 카드별 갱신 시각은 YYYY-MM-DD HH24:MI:SS 형식으로 표시함.
뉴스/핫딜 항목의 publishedAt은 서버가 전달한 원본 문자열을 우선 존중하되, YYYYMMDD, YYYY-MM-DD, YYYY.MM.DD, YYYY-MM-DD HH:MI[:SS], ISO datetime처럼 명확하게 해석 가능한 날짜/시간만 공통 표준 형식으로 정규화함.
WMania처럼 외부 원본이 YY-MM-DD 형태의 기사 날짜를 제공하면 임의로 4자리 연도로 보정하지 않고 원본 문자열을 그대로 표시함.
뉴스/핫딜 카드에서 수집 오류가 있으면 오류 메시지만 표시하고 빈 목록 문구를 중복 표시하지 않음.
핫딜 카드가 차단 상태의 stale 데이터를 표시할 때는 오류 메시지와 마지막 성공 시각을 함께 표시하고, 이전 성공 항목 목록은 계속 표시함.
펀드 카드가 FunETF 일시 제한 이후 서버의 이전 성공 행을 받으면 일반 펀드 항목과 같은 형식으로 표시함.
펀드 카드가 FunETF 일시 제한 상태에서 서버의 오류 항목을 받으면 해당 항목의 가격 영역 대신 오류 상태를 표시함.
새로고침 버튼은 /api/hub/dashboard?forceRefresh=true를 호출함.
외부 HTML 수집 카드의 source별 캐시는 서버에서 보장하므로 사용자가 새로고침을 반복해도 동일 사이트에 과도한 재요청을 보내지 않음.
WMania 최신뉴스 source 캐시는 10분이고, FMKorea 핫딜 source 캐시는 30분임.
대시보드 캐시와 수집 타임아웃 정책은 hub-api의 apps/hub-api/var/dashboard-policy.yml을 기준으로 함.
대시보드 큰 카드는 shared-ui 공통 SortableDragHandle과 공통 sortable 옵션을 사용해 드래그 정렬할 수 있음.
대시보드 큰 카드 드래그는 핸들을 1초 이상 누른 뒤 이동할 때 정렬 모드로 진입함.
대시보드 큰 카드 순서는 탭별로 /api/hub/dashboard/preferences에 저장함.
즐겨찾기 탭의 드래그 순서는 favoriteCardIds 배열 순서로 저장함.
소식, 경제, 홈서버 탭의 드래그 순서는 cardOrders.news, cardOrders.economy, cardOrders.system 배열 순서로 저장함.
대시보드 preference 저장은 브라우저 localStorage를 사용하지 않으며, 같은 사용자 계정 기준으로 기기와 브라우저를 넘어 유지됨.
투자 카드의 펀드/국내 주식/해외 주식 항목 순서는 서버 응답 순서를 따르며, 서버는 공통코드 sort_order_no 기준으로 정렬함.
투자 카드의 펀드/국내 주식/해외 주식 항목은 드래그 핸들을 제공하며, 순서 변경 시 /api/hub/dashboard/investments/reorder로 저장함.
투자 카드의 펀드/국내 주식/해외 주식 항목 드래그 핸들도 shared-ui 공통 SortableDragHandle과 공통 sortable 옵션을 사용함.
투자 카드 현재가는 펀드/국내 주식/국내 지수는 ₩숫자, 해외 주식/해외 지수는 $숫자 형식으로 표시함.
투자 카드 등락 금액은 부호를 통화기호 앞에 두는 +₩숫자, -₩숫자, +$숫자, -$숫자 형식으로 표시함.
투자 카드 등락률은 +숫자%, -숫자% 형식으로 표시하며, 등락 방향 텍스트와 금액/등락률 부호가 같은 방향을 표현해야 함.
투자 카드의 상승/하락 등락 정보는 한국 시장 관례에 맞춰 상승은 빨간색, 하락은 파란색으로 표시함.
AI 카드는 OpenRouter Management API 기준의 금일, 금주, 금월 사용량을 USD로 표시함.
AI 카드는 OpenRouter API key별 주간 사용량을 USD로 표시함.
AI 카드는 OpenRouter 잔여 크레딧과 크레딧 조회 상태를 표시함.
AI 카드의 외부 전체보기 링크는 https://openrouter.ai/settings/credits로 연결함.
AI 카드 사용량은 원화 환산을 수행하지 않으며, AI 로그 화면의 KRW/USD 토글과 별도 표시 계약을 사용함.
AI 카드 내부 지표는 대시보드 다른 카드와 같은 플랫한 저대비 톤을 사용하고 강한 색상 음영이나 그라데이션을 사용하지 않음.
홈서버 CPU 카드와 RAM 카드는 사용률 게이지, 대표 수치, 세부 메트릭을 함께 표시함.
홈서버 Docker 카드는 전체/실행/Healthy/Unhealthy/문제 컨테이너 개수를 표시함.
홈서버 상위 프로세스 카드는 프로세스명, PID, CPU 사용률, 메모리 사용률, 상태를 표시함.
홈서버 도커 이상 상태 카드는 비정상 컨테이너 이름, 이미지, CPU, RAM, 상태를 표시하고, 문제가 없으면 빈 상태 문구를 표시함.
홈서버 수집 상태 카드는 Glances API 수집 상태, CPU/RAM 대표 수치, 도커 이상 개수를 요약함.
10.5 AI 로그 상세 화면
/ai-logs 목록과 요약 영역은 사용된 AI의 토큰 수와 예상 비용을 표시함.
AI 예상 비용의 원본 값은 USD 기준이며, 화면 최초 표시는 원화 기준임.
원화 계산은 메타 EXCHANGE_RATE의 meta_val을 1달러당 원화 숫자값으로 해석해 수행함.
비용 표시는 원화와 달러 모두 소수점 둘째 자리까지 표시함.
그리드 버튼 영역의 통화 전환 버튼은 현재 비용 표시 단위를 KRW와 USD 사이에서 토글함.
통화 전환 버튼 문구는 화면 컴포넌트의 한국어 문구로 직접 작성함.
USD 표시는 API 응답의 USD 예상 비용 값을 그대로 사용함.
KRW 전환 시 환율 메타가 없거나 숫자로 해석되지 않으면 한국어 안내 토스트를 표시하고 현재 통화 상태를 유지함.
/ai-logs 상세 모달은 입력 파라미터와 출력 내용을 JSON 트리 또는 텍스트 뷰어로 표시함.
상세 뷰어는 공통 jb-json-tree, jb-code-view 클래스를 사용함.
ai-logs 화면은 요약 라벨, JSON 트리, 텍스트 뷰어, 그리드까지 shared-ui의 Pretendard 기준 폰트를 일관되게 사용함.
11. 관련 문서
프로젝트 전체 구조는 docs/junkbox/common/guide-jb-core.md를 따름.
hub 백엔드 구조는 docs/junkbox/apps/guide-apps-hub-api.md를 따름.
공통 UI 자산 구조는 docs/junkbox/libs/guide-lib-shared-ui.md를 따름.