본문으로 건너뛰기

JunkBox apps/hub-ui 가이드

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.urlfileURLToPath() 기반 구조를 사용함.
  • 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, /healthhub-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-uiBottomOptionPicker 하단 선택 시트 조합을 사용함.
  • /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_valtext/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 포털 소비 화면

  • /portalAPP, SERVICE 탭 구조를 유지함.
  • /portalAPP, SERVICE 탭은 shared-uiSegmentedTabs를 사용하며, 모바일 고정 폭 안에서 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-uiSegmentedTabs 인라인 탭으로 표시함.
  • 모바일 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-apiapps/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_RATEmeta_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를 따름.