# πŸ“‹ Headplane UI ν•œκΈ€ν™” κ³„νšμ„œ ## πŸ” ν˜„μž¬ 상황 뢄석 ### Headplane κ΅­μ œν™” ν˜„ν™© - ❌ i18n 라이브러리 λ―Έμ‚¬μš© (react-i18next, next-intl λ“± μ—†μŒ) - ❌ λͺ¨λ“  UI ν…μŠ€νŠΈκ°€ μ»΄ν¬λ„ŒνŠΈμ— ν•˜λ“œμ½”λ”© - ❌ μ–Έμ–΄ μ„€μ • μ˜΅μ…˜ μ—†μŒ - ❌ GitHub에 λ‹€κ΅­μ–΄ 지원 μš”μ²­μ΄λ‚˜ λ…Όμ˜ μ—†μŒ ### 기술 μŠ€νƒ - βœ… React 19.1.0 + TypeScript - βœ… Vite λΉŒλ“œ μ‹œμŠ€ν…œ - βœ… Docker λ©€ν‹°μŠ€ν…Œμ΄μ§€ λΉŒλ“œ - βœ… pnpm νŒ¨ν‚€μ§€ λ§€λ‹ˆμ € ### ν˜„μž¬ 접속 정보 - **둜컬**: http://localhost:3000/admin/ - **μ™ΈλΆ€**: http://192.168.0.151:3000/admin/ - **API Key**: `8qRr1IB.tV95CmA0fLaCiGGIgBfeoN9daHceFkzI` ## 🎯 ν•œκΈ€ν™” λͺ©ν‘œ ### μš°μ„ μˆœμœ„ 1: 핡심 UI μš”μ†Œ - [ ] 상단 헀더 "Headplane" β†’ "ν—€λ“œν”Œλ ˆμΈ" - [ ] λ„€λΉ„κ²Œμ΄μ…˜ 메뉴 (Machines β†’ μž₯치 관리, Users β†’ μ‚¬μš©μž 관리, Settings β†’ μ„€μ • λ“±) - [ ] 둜그인 νŽ˜μ΄μ§€ ν…μŠ€νŠΈ (API Key β†’ API ν‚€, Sign In β†’ 둜그인) - [ ] 메인 λŒ€μ‹œλ³΄λ“œ 라벨듀 ### μš°μ„ μˆœμœ„ 2: 상세 νŽ˜μ΄μ§€ - [ ] ν…Œμ΄λΈ” 헀더 (Name β†’ 이름, IP Address β†’ IP μ£Όμ†Œ, Status β†’ μƒνƒœ λ“±) - [ ] λ²„νŠΌ ν…μŠ€νŠΈ (Create β†’ 생성, Delete β†’ μ‚­μ œ, Edit β†’ νŽΈμ§‘ λ“±) - [ ] 폼 라벨 및 ν”Œλ ˆμ΄μŠ€ν™€λ” ### μš°μ„ μˆœμœ„ 3: λ©”μ‹œμ§€ 및 μ•Œλ¦Ό - [ ] μ—λŸ¬ λ©”μ‹œμ§€ - [ ] 성곡/확인 λ©”μ‹œμ§€ - [ ] 도움말 ν…μŠ€νŠΈ ## πŸ› οΈ κ΅¬ν˜„ λ°©μ•ˆλ³„ 비ꡐ ### λ°©μ•ˆ 1: ν™˜κ²½ λ³€μˆ˜ 기반 (ꢌμž₯ - 단기) **λ‚œμ΄λ„**: ⭐⭐ **μ†Œμš” μ‹œκ°„**: 2-3μ‹œκ°„ **μž₯점**: - λΉ λ₯Έ κ΅¬ν˜„ κ°€λŠ₯ - κΈ°μ‘΄ μ½”λ“œ μ΅œμ†Œ λ³€κ²½ - Docker ν™˜κ²½λ³€μˆ˜λ‘œ μ‰½κ²Œ μ œμ–΄ - ν˜„μž¬ ν™˜κ²½μ—μ„œ μ¦‰μ‹œ 적용 κ°€λŠ₯ **단점**: - μ œν•œμ μΈ μœ μ—°μ„± - ν…μŠ€νŠΈλ³„ κ°œλ³„ ν™˜κ²½λ³€μˆ˜ ν•„μš” **κ΅¬ν˜„ 방법**: ```typescript // app/config/texts.ts 생성 export const UI_TEXTS = { app_title: process.env.HEADPLANE_TITLE || 'Headplane', nav: { machines: process.env.HEADPLANE_NAV_MACHINES || 'Machines', users: process.env.HEADPLANE_NAV_USERS || 'Users', settings: process.env.HEADPLANE_NAV_SETTINGS || 'Settings' } }; ``` ### λ°©μ•ˆ 2: react-i18next λ„μž… (ꢌμž₯ - μž₯κΈ°) **λ‚œμ΄λ„**: ⭐⭐⭐⭐ **μ†Œμš” μ‹œκ°„**: 1-2일 **μž₯점**: - μ™„μ „ν•œ κ΅­μ œν™” 지원 - μ–Έμ–΄ μ „ν™˜ κΈ°λŠ₯ - ν‘œμ€€μ μΈ μ ‘κ·Ό 방식 - ν–₯ν›„ ν™•μž₯μ„± μ’‹μŒ **단점**: - λͺ¨λ“  μ»΄ν¬λ„ŒνŠΈ μˆ˜μ • ν•„μš” - 라이브러리 μ˜μ‘΄μ„± μΆ”κ°€ - μƒλ‹Ήν•œ μ½”λ“œ λ³€κ²½ ν•„μš” **κ΅¬ν˜„ ꡬ쑰**: ``` public/locales/ β”œβ”€β”€ en/ β”‚ └── translation.json └── ko/ └── translation.json ``` ### λ°©μ•ˆ 3: λΈŒλΌμš°μ € ν™•μž₯ν”„λ‘œκ·Έλž¨ (μ¦‰μ‹œ 적용) **λ‚œμ΄λ„**: ⭐ **μ†Œμš” μ‹œκ°„**: 30λΆ„ **μž₯점**: - μ¦‰μ‹œ 적용 κ°€λŠ₯ - μ½”λ“œ μˆ˜μ • λΆˆν•„μš” - ν…ŒμŠ€νŠΈ λͺ©μ μœΌλ‘œ 졜적 **단점**: - 개인 ν™˜κ²½μ—μ„œλ§Œ λ™μž‘ - 동적 μ½˜ν…μΈ  ν•œκ³„ - μΌμ‹œμ  ν•΄κ²°μ±… ### λ°©μ•ˆ 4: 포크 ν›„ ν•˜λ“œμ½”λ”© μˆ˜μ • **λ‚œμ΄λ„**: ⭐⭐⭐ **μ†Œμš” μ‹œκ°„**: 4-6μ‹œκ°„ **μž₯점**: - μ™„μ „ν•œ μ œμ–΄ - μ¦‰μ‹œ 적용 κ°€λŠ₯ **단점**: - μ—…μŠ€νŠΈλ¦Ό μ—…λ°μ΄νŠΈ 어렀움 - μœ μ§€λ³΄μˆ˜ λΆ€λ‹΄ ## πŸ“… 단계별 μ‹€ν–‰ κ³„νš ### Phase 1: μ¦‰μ‹œ 적용 (였늘) #### 1-1. λΈŒλΌμš°μ € ν™•μž₯ν”„λ‘œκ·Έλž¨ μ œμž‘ - [ ] Tampermonkey 슀크립트 μž‘μ„± - [ ] 핡심 UI μš”μ†Œ λ²ˆμ—­ λ§€ν•‘ - [ ] ν…ŒμŠ€νŠΈ 및 검증 #### 1-2. λΈŒλΌμš°μ € ν™•μž₯ 슀크립트 μ˜ˆμ‹œ ```javascript // ==UserScript== // @name Headplane ν•œκΈ€ν™” // @match http://192.168.0.151:3000/* // @match http://localhost:3000/* // ==/UserScript== const translations = { 'Headplane': 'ν—€λ“œν”Œλ ˆμΈ', 'Machines': 'μž₯치 관리', 'Users': 'μ‚¬μš©μž 관리', 'Settings': 'μ„€μ •', 'API Key': 'API ν‚€', 'Sign In': '둜그인', 'Welcome to Headplane': 'ν—€λ“œν”Œλ ˆμΈμ— μ˜€μ‹  것을 ν™˜μ˜ν•©λ‹ˆλ‹€', 'Enter an API key to authenticate': 'API ν‚€λ₯Ό μž…λ ₯ν•˜μ—¬ μΈμ¦ν•΄μ£Όμ„Έμš”' }; function translatePage() { document.querySelectorAll('*').forEach(element => { if (element.children.length === 0 && element.textContent.trim()) { const text = element.textContent.trim(); if (translations[text]) { element.textContent = translations[text]; } } }); } // νŽ˜μ΄μ§€ λ‘œλ“œ μ‹œ λ²ˆμ—­ 적용 translatePage(); // 동적 μ½˜ν…μΈ  λ³€κ²½ 감지 및 λ²ˆμ—­ 적용 new MutationObserver(translatePage).observe(document.body, { childList: true, subtree: true }); ``` ### Phase 2: μž„μ‹œ ν•΄κ²°μ±… (1-2일 λ‚΄) #### 2-1. ν™˜κ²½ λ³€μˆ˜ 기반 μ»€μŠ€ν„°λ§ˆμ΄μ§• - [ ] ν…μŠ€νŠΈ μƒμˆ˜ 파일 생성 (`app/config/texts.ts`) - [ ] μ£Όμš” μ»΄ν¬λ„ŒνŠΈμ— ν…μŠ€νŠΈ μƒμˆ˜ 적용 - [ ] Docker Compose ν™˜κ²½λ³€μˆ˜ μ„€μ • - [ ] μ»€μŠ€ν…€ Docker 이미지 λΉŒλ“œ - [ ] μ»¨ν…Œμ΄λ„ˆ 재배포 및 ν…ŒμŠ€νŠΈ #### 2-2. Docker ν™˜κ²½λ³€μˆ˜ μ„€μ • μ˜ˆμ‹œ ```yaml # docker-compose.yml에 μΆ”κ°€ services: headplane: environment: - TZ=Asia/Seoul - HEADSCALE_URL=http://headscale:8080 - HEADSCALE_API_KEY=${HEADSCALE_API_KEY} # ν•œκΈ€ν™” ν™˜κ²½λ³€μˆ˜ - HEADPLANE_TITLE=ν—€λ“œν”Œλ ˆμΈ - HEADPLANE_NAV_MACHINES=μž₯치 관리 - HEADPLANE_NAV_USERS=μ‚¬μš©μž 관리 - HEADPLANE_NAV_SETTINGS=μ„€μ • - HEADPLANE_LOGIN_TITLE=둜그인 - HEADPLANE_API_KEY_LABEL=API ν‚€ ``` ### Phase 3: μ™„μ „ν•œ ν•΄κ²°μ±… (1주일 λ‚΄) #### 3-1. react-i18next 라이브러리 λ„μž… - [ ] GitHubμ—μ„œ tale/headplane 포크 - [ ] 둜컬 개발 ν™˜κ²½ ꡬ성 - [ ] react-i18next 라이브러리 μ„€μΉ˜ - [ ] i18n μ„€μ • 및 λ²ˆμ—­ 파일 ꡬ쑰 생성 - [ ] μ£Όμš” μ»΄ν¬λ„ŒνŠΈ κ΅­μ œν™” 적용 - [ ] μ–Έμ–΄ μ „ν™˜ UI μΆ”κ°€ - [ ] μ»€μŠ€ν…€ Docker 이미지 λΉŒλ“œ 및 배포 #### 3-2. λ²ˆμ—­ 파일 ꡬ쑰 μ˜ˆμ‹œ ```json // public/locales/ko/translation.json { "app": { "title": "ν—€λ“œν”Œλ ˆμΈ", "description": "ν—€λ“œμŠ€μΌ€μΌ 관리 μ›Ή μΈν„°νŽ˜μ΄μŠ€" }, "navigation": { "machines": "μž₯치 관리", "users": "μ‚¬μš©μž 관리", "settings": "μ„€μ •", "accessControl": "μ ‘κ·Ό μ œμ–΄", "dns": "DNS" }, "login": { "title": "ν—€λ“œν”Œλ ˆμΈμ— μ˜€μ‹  것을 ν™˜μ˜ν•©λ‹ˆλ‹€", "description": "API ν‚€λ₯Ό μž…λ ₯ν•˜μ—¬ ν—€λ“œν”Œλ ˆμΈμ— μΈμ¦ν•΄μ£Όμ„Έμš”.", "apiKeyLabel": "API ν‚€", "apiKeyPlaceholder": "API ν‚€λ₯Ό μž…λ ₯ν•΄μ£Όμ„Έμš”", "signInButton": "둜그인", "helpText": "ν„°λ―Έλ„μ—μ„œ 'headscale apikeys create' λͺ…령을 μ‹€ν–‰ν•˜μ—¬ API ν‚€λ₯Ό 생성할 수 μžˆμŠ΅λ‹ˆλ‹€." }, "machines": { "title": "μž₯치 관리", "description": "ν…ŒμΌλ„·μ— μ—°κ²°λœ μž₯μΉ˜λ“€μ„ κ΄€λ¦¬ν•©λ‹ˆλ‹€.", "tableHeaders": { "name": "이름", "addresses": "μ£Όμ†Œ", "version": "버전", "lastSeen": "λ§ˆμ§€λ§‰ 접속" } }, "users": { "title": "μ‚¬μš©μž 관리", "description": "ν—€λ“œμŠ€μΌ€μΌ μ‚¬μš©μžλ“€μ„ κ΄€λ¦¬ν•©λ‹ˆλ‹€." } } ``` ## 🎯 ꢌμž₯ μ‹€ν–‰ λ°©μ•ˆ ### μ¦‰μ‹œ μ‹€ν–‰: λ°©μ•ˆ 3 (λΈŒλΌμš°μ € ν™•μž₯ν”„λ‘œκ·Έλž¨) ν˜„μž¬ μ‚¬μš© 쀑인 ν™˜κ²½μ—μ„œ λ°”λ‘œ ν•œκΈ€ UIλ₯Ό 확인할 수 μžˆλ„λ‘ **Tampermonkey 슀크립트**λΆ€ν„° μ‹œμž‘ ### 단기 λͺ©ν‘œ: λ°©μ•ˆ 1 (ν™˜κ²½ λ³€μˆ˜ 기반) Docker ν™˜κ²½λ³€μˆ˜λ₯Ό ν†΅ν•œ ν…μŠ€νŠΈ μ»€μŠ€ν„°λ§ˆμ΄μ§•μœΌλ‘œ **μ•ˆμ •μ μΈ ν•œκΈ€ν™”** κ΅¬ν˜„ ### μž₯κΈ° λͺ©ν‘œ: λ°©μ•ˆ 2 (react-i18next λ„μž…) μ™„μ „ν•œ κ΅­μ œν™” 지원을 μœ„ν•œ **ν‘œμ€€μ μΈ λ‹€κ΅­μ–΄ 지원** κ΅¬ν˜„ ## πŸ“Š μ˜ˆμƒ λ²ˆμ—­ λ²”μœ„ ### 핡심 λ²ˆμ—­ λŒ€μƒ (총 μ•½ 50-80개 ν…μŠ€νŠΈ) #### 곡톡 UI μš”μ†Œ - Headplane β†’ ν—€λ“œν”Œλ ˆμΈ - API Key β†’ API ν‚€ - Sign In β†’ 둜그인 - Settings β†’ μ„€μ • - Profile β†’ ν”„λ‘œν•„ - Logout β†’ λ‘œκ·Έμ•„μ›ƒ #### λ„€λΉ„κ²Œμ΄μ…˜ 메뉴 - Machines β†’ μž₯치 관리 - Users β†’ μ‚¬μš©μž 관리 - Access Control β†’ μ ‘κ·Ό μ œμ–΄ - DNS β†’ DNS μ„€μ • #### ν…Œμ΄λΈ” 및 폼 μš”μ†Œ - Name β†’ 이름 - IP Address β†’ IP μ£Όμ†Œ - Status β†’ μƒνƒœ - Version β†’ 버전 - Last Seen β†’ λ§ˆμ§€λ§‰ 접속 - Create β†’ 생성 - Delete β†’ μ‚­μ œ - Edit β†’ νŽΈμ§‘ ## πŸš€ μ‹œμž‘ν•˜κΈ° ### 1단계: λΈŒλΌμš°μ € ν™•μž₯ν”„λ‘œκ·Έλž¨ μ„€μΉ˜ 1. Chrome/Edgeμ—μ„œ Tampermonkey ν™•μž₯ν”„λ‘œκ·Έλž¨ μ„€μΉ˜ 2. μœ„μ˜ 슀크립트 μ½”λ“œλ₯Ό μƒˆ 슀크립트둜 생성 3. http://192.168.0.151:3000/admin/ μ ‘μ†ν•˜μ—¬ ν•œκΈ€ν™” 확인 ### 2단계: ν™˜κ²½λ³€μˆ˜ 기반 κ΅¬ν˜„ μ€€λΉ„ 1. tale/headplane μ €μž₯μ†Œ 포크 2. 둜컬 κ°œλ°œν™˜κ²½ ꡬ성 3. ν…μŠ€νŠΈ μƒμˆ˜ 파일 생성 및 적용 ## πŸ“ μ§„ν–‰ 상황 체크리슀트 ### Phase 1: λΈŒλΌμš°μ € ν™•μž₯ν”„λ‘œκ·Έλž¨ - [ ] Tampermonkey μ„€μΉ˜ 및 슀크립트 μž‘μ„± - [ ] 둜그인 νŽ˜μ΄μ§€ ν•œκΈ€ν™” ν…ŒμŠ€νŠΈ - [ ] 메인 λŒ€μ‹œλ³΄λ“œ ν•œκΈ€ν™” ν…ŒμŠ€νŠΈ - [ ] λ²ˆμ—­ ν’ˆμ§ˆ 검증 ### Phase 2: ν™˜κ²½λ³€μˆ˜ 기반 - [ ] ν”„λ‘œμ νŠΈ 포크 및 클둠 - [ ] 개발 ν™˜κ²½ ꡬ성 - [ ] ν…μŠ€νŠΈ μƒμˆ˜ 파일 ꡬ쑰 섀계 - [ ] μ£Όμš” μ»΄ν¬λ„ŒνŠΈ μˆ˜μ • - [ ] Docker 이미지 λΉŒλ“œ ν…ŒμŠ€νŠΈ - [ ] ν”„λ‘œλ•μ…˜ 배포 ### Phase 3: react-i18next λ„μž… - [ ] i18n 라이브러리 μ„€μΉ˜ 및 μ„€μ • - [ ] λ²ˆμ—­ 파일 ꡬ쑰 생성 - [ ] μ»΄ν¬λ„ŒνŠΈλ³„ κ΅­μ œν™” 적용 - [ ] μ–Έμ–΄ μ „ν™˜ UI κ΅¬ν˜„ - [ ] 전체 ν…ŒμŠ€νŠΈ 및 검증 ## πŸ”— κ΄€λ ¨ 링크 - **Headplane GitHub**: https://github.com/tale/headplane - **React i18next λ¬Έμ„œ**: https://react.i18next.com/ - **ν˜„μž¬ Headplane UI**: http://192.168.0.151:3000/admin/ ## πŸ“… 생성일: 2025-09-09 ## πŸ‘€ μž‘μ„±μž: Claude Code Assistant