You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1128 lines
41 KiB
1128 lines
41 KiB
package mock
|
|
|
|
import (
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"teraclone/internal/config"
|
|
)
|
|
|
|
type PageData struct {
|
|
AppName string
|
|
MockMode bool
|
|
CurrentUser string
|
|
CurrentPath string
|
|
Title string
|
|
Section string
|
|
Breadcrumbs []string
|
|
WorkspaceTabs []WorkspaceTab
|
|
Menu []MenuGroup
|
|
SubTabs []SubTab
|
|
Blocks []ContentBlock
|
|
}
|
|
|
|
type WorkspaceTab struct {
|
|
Key string
|
|
Label string
|
|
Path string
|
|
Active bool
|
|
}
|
|
|
|
type MenuGroup struct {
|
|
Label string
|
|
Icon string
|
|
Path string
|
|
Active bool
|
|
Expanded bool
|
|
Children []MenuChild
|
|
}
|
|
|
|
type MenuChild struct {
|
|
Label string
|
|
Path string
|
|
Active bool
|
|
}
|
|
|
|
type SubTab struct {
|
|
Label string
|
|
Path string
|
|
Active bool
|
|
}
|
|
|
|
type ContentBlock struct {
|
|
Kind string
|
|
Title string
|
|
Subtitle string
|
|
PortLinks []PortLink
|
|
KeyValues []KeyValue
|
|
Form *FormData
|
|
Table *TableData
|
|
Action *ActionPanel
|
|
Tool *ToolData
|
|
Modal *ModalData
|
|
Message string
|
|
}
|
|
|
|
type PortLink struct {
|
|
Number int
|
|
Up bool
|
|
Blink bool
|
|
}
|
|
|
|
type KeyValue struct {
|
|
Key string
|
|
Value string
|
|
}
|
|
|
|
type FormData struct {
|
|
Action string
|
|
PrimaryLabel string
|
|
SecondaryLabel string
|
|
Fields []FormField
|
|
PortTargets []PortTarget
|
|
Notes []string
|
|
}
|
|
|
|
type FormField struct {
|
|
Label string
|
|
Name string
|
|
Type string
|
|
Value string
|
|
Placeholder string
|
|
Required bool
|
|
Disabled bool
|
|
Checked bool
|
|
Hint string
|
|
Rows int
|
|
Options []Option
|
|
}
|
|
|
|
type Option struct {
|
|
Label string
|
|
Value string
|
|
Selected bool
|
|
}
|
|
|
|
type PortTarget struct {
|
|
Number int
|
|
Checked bool
|
|
}
|
|
|
|
type TableData struct {
|
|
Columns []string
|
|
Rows []TableRow
|
|
EmptyMessage string
|
|
PrimaryLabel string
|
|
SecondaryLabel string
|
|
}
|
|
|
|
type TableRow struct {
|
|
Cells []string
|
|
Actions []RowAction
|
|
}
|
|
|
|
type RowAction struct {
|
|
Label string
|
|
Action string
|
|
Variant string
|
|
Target string
|
|
}
|
|
|
|
type ActionPanel struct {
|
|
Description string
|
|
Buttons []ActionButton
|
|
}
|
|
|
|
type ActionButton struct {
|
|
Label string
|
|
Action string
|
|
Variant string
|
|
}
|
|
|
|
type ToolData struct {
|
|
Action string
|
|
PrimaryLabel string
|
|
Tabs []SubTab
|
|
Fields []FormField
|
|
Result string
|
|
}
|
|
|
|
type ModalData struct {
|
|
Root string
|
|
Title string
|
|
Message string
|
|
Fields []FormField
|
|
Buttons []ActionButton
|
|
}
|
|
|
|
func ResolvePage(path string) (PageData, bool) {
|
|
switch {
|
|
case path == "/":
|
|
return dashboardPage(), true
|
|
case path == "/config/save":
|
|
return actionPage(path, "설정 저장", "현재 화면의 설정을 장비 구성 파일로 저장하는 목업 화면입니다.", "save-config"), true
|
|
case path == "/ports/restart":
|
|
return actionPage(path, "포트 재시작", "선택한 포트를 재시작하는 동작을 흉내 내는 목업 화면입니다.", "restart-ports"), true
|
|
case path == "/device/restart":
|
|
return actionPage(path, "장치 재시작", "장치 전체 재시작을 시뮬레이션하는 목업 화면입니다.", "restart-device"), true
|
|
case strings.HasPrefix(path, "/network/"):
|
|
return networkPage(path)
|
|
case strings.HasPrefix(path, "/ports/"):
|
|
return portPage(path)
|
|
case strings.HasPrefix(path, "/port-debug"):
|
|
return portDebugPage(path)
|
|
case strings.HasPrefix(path, "/admin/"):
|
|
return adminPage(path)
|
|
case strings.HasPrefix(path, "/settings/"):
|
|
return settingsPage(path)
|
|
case strings.HasPrefix(path, "/tools/"):
|
|
return toolsPage(path)
|
|
case strings.HasPrefix(path, "/logs/"):
|
|
return logsPage(path)
|
|
case path == "/port-status":
|
|
return portStatusPage(), true
|
|
case path == "/password":
|
|
return passwordPage(), true
|
|
default:
|
|
return PageData{}, false
|
|
}
|
|
}
|
|
|
|
func StatusAPIData() map[string]interface{} {
|
|
return map[string]interface{}{
|
|
"portsUp": 32,
|
|
"portsDown": 16,
|
|
"model": "TERACLONE-X48PRO",
|
|
"uptime": "2일 17시간 24분",
|
|
"temperature": "53.8C",
|
|
"mock": true,
|
|
}
|
|
}
|
|
|
|
func NetworkAPIData() map[string]interface{} {
|
|
return map[string]interface{}{
|
|
"interface1": map[string]string{
|
|
"address": "192.168.0.233",
|
|
"netmask": "255.255.255.0",
|
|
"gateway": "192.168.0.1",
|
|
},
|
|
"interface2": map[string]string{
|
|
"address": "192.168.200.233",
|
|
"netmask": "255.255.255.0",
|
|
"gateway": "192.168.200.1",
|
|
},
|
|
"mock": true,
|
|
}
|
|
}
|
|
|
|
func SystemAPIData() map[string]interface{} {
|
|
return map[string]interface{}{
|
|
"hostname": "teraclone-x48",
|
|
"firmware": "6.4.6004",
|
|
"time": "2026-06-24 14:03:22 KST",
|
|
"mock": true,
|
|
}
|
|
}
|
|
|
|
func SystemLogsAPIData() []map[string]string {
|
|
return []map[string]string{
|
|
{"time": "2026-06-24 13:58:10", "event": "관리자 로그인", "level": "info"},
|
|
{"time": "2026-06-24 13:59:45", "event": "구성 저장 요청", "level": "warn"},
|
|
}
|
|
}
|
|
|
|
func PortLogsAPIData() []map[string]string {
|
|
return []map[string]string{
|
|
{"time": "2026-06-24 13:57:31", "port": "Port 1", "event": "데이터 수신 128 bytes"},
|
|
{"time": "2026-06-24 13:58:02", "port": "Port 24", "event": "링크 상태 변경"},
|
|
}
|
|
}
|
|
|
|
func dashboardPage() PageData {
|
|
return newPage("/", "대시보드", "대시보드", []string{"대시보드"}, nil, []ContentBlock{
|
|
{Kind: "port-links", Title: "포트 링크 상태", PortLinks: portLinks()},
|
|
{
|
|
Kind: "key-values",
|
|
Title: "시스템 정보",
|
|
KeyValues: []KeyValue{
|
|
{"모델", "TERACLONE-X48PRO"},
|
|
{"빌드번호", "N010-0020-2609-0062"},
|
|
{"시리얼번호", "RMM4027N850000026090062"},
|
|
{"펌웨어", "6.4.6004"},
|
|
{"가동 시간", "2일 17시간 24분 50초"},
|
|
{"시스템 시간", "2026년 6월 24일 수요일 오후 2시 03분 22초"},
|
|
{"CPU 평균", "0.46 / 0.45 / 0.41"},
|
|
},
|
|
},
|
|
{
|
|
Kind: "key-values",
|
|
Title: "네트워크 정보",
|
|
KeyValues: []KeyValue{
|
|
{"인터페이스 1", "eth0 / 192.168.0.233 / 1000Mbps / 링크 연결"},
|
|
{"인터페이스 2", "eth1 / 192.168.200.233 / 1000Mbps / 링크 대기"},
|
|
{"MAC 주소", "6C:B3:50:0D:36:12"},
|
|
{"다운로드 속도", "0 B/s"},
|
|
{"업로드 속도", "0 B/s"},
|
|
},
|
|
},
|
|
{
|
|
Kind: "key-values",
|
|
Title: "리소스 상태",
|
|
KeyValues: []KeyValue{
|
|
{"CPU 사용률", "14%"},
|
|
{"메모리 사용률", "38%"},
|
|
{"할당 메모리", "182 MB"},
|
|
{"최대 RSS", "264 MB"},
|
|
},
|
|
},
|
|
})
|
|
}
|
|
|
|
func networkPage(path string) (PageData, bool) {
|
|
subtabs := []SubTab{
|
|
{"인터페이스 1", "/network/interface-1", path == "/network/interface-1"},
|
|
{"인터페이스 2", "/network/interface-2", path == "/network/interface-2"},
|
|
{"호스트 설정", "/network/host", path == "/network/host"},
|
|
{"고급 설정", "/network/advanced", path == "/network/advanced"},
|
|
{"DDNS 설정", "/network/ddns", path == "/network/ddns"},
|
|
}
|
|
|
|
switch path {
|
|
case "/network/interface-1", "/network/interface-2":
|
|
label := "인터페이스 1"
|
|
addr := "192.168.0.233"
|
|
gateway := "192.168.0.1"
|
|
if path == "/network/interface-2" {
|
|
label = "인터페이스 2"
|
|
addr = "192.168.200.233"
|
|
gateway = "192.168.200.1"
|
|
}
|
|
return newPage(path, "네트워크 설정", "네트워크 설정", []string{"네트워크 설정", label}, subtabs, []ContentBlock{
|
|
formBlock(label+" IPv4 / IPv6 설정", "network-apply", []FormField{
|
|
toggleField("DHCP 사용", "dhcp", false),
|
|
textField("IPv4 주소", "address", addr, true),
|
|
textField("서브넷 마스크", "netmask", "255.255.255.0", true),
|
|
textField("게이트웨이", "gateway", gateway, false),
|
|
textField("DNS 서버 1", "dns1", "8.8.8.8", false),
|
|
textField("DNS 서버 2", "dns2", "1.1.1.1", false),
|
|
selectField("IPv6 모드", "ipv6mode", "Auto", []string{"Auto", "Disable", "Static"}),
|
|
textField("IPv6 주소", "ipv6address", "", false),
|
|
textField("Prefix Length", "prefix", "64", false),
|
|
textField("IPv6 게이트웨이", "ipv6gateway", "", false),
|
|
}, "변경", "적용"),
|
|
}), true
|
|
case "/network/host":
|
|
return newPage(path, "네트워크 설정", "네트워크 설정", []string{"네트워크 설정", "호스트 설정"}, subtabs, []ContentBlock{
|
|
formBlock("호스트 설정", "network-host", []FormField{
|
|
textField("호스트 이름", "hostname", "teraclone-x48", true),
|
|
textField("도메인 이름", "domain", "local.lan", false),
|
|
textField("기본 URL", "url", "http://192.168.0.233", false),
|
|
textAreaField("메모", "memo", "장비 목업 UI를 위한 더미 호스트 설정입니다.", 4),
|
|
}, "변경", "적용"),
|
|
}), true
|
|
case "/network/advanced":
|
|
return newPage(path, "네트워크 설정", "네트워크 설정", []string{"네트워크 설정", "고급 설정"}, subtabs, []ContentBlock{
|
|
formBlock("고급 네트워크 설정", "network-advanced", []FormField{
|
|
toggleField("HTTP 활성화", "http", true),
|
|
toggleField("HTTPS 활성화", "https", true),
|
|
textField("HTTP 포트", "httpPort", "80", true),
|
|
textField("HTTPS 포트", "httpsPort", "443", true),
|
|
toggleField("ICMP 응답 허용", "icmp", true),
|
|
toggleField("SSH 활성화", "ssh", false),
|
|
}, "변경", "적용"),
|
|
}), true
|
|
case "/network/ddns":
|
|
return newPage(path, "네트워크 설정", "네트워크 설정", []string{"네트워크 설정", "DDNS 설정"}, subtabs, []ContentBlock{
|
|
formBlock("DDNS 설정", "network-ddns", []FormField{
|
|
toggleField("DDNS 사용", "ddns", false),
|
|
selectField("서비스 제공자", "provider", "DynDNS", []string{"DynDNS", "No-IP", "Custom"}),
|
|
textField("도메인", "domain", "teraclone-demo.example.com", false),
|
|
textField("사용자 이름", "username", "demo-user", false),
|
|
passwordField("비밀번호", "password", "********"),
|
|
textField("업데이트 주기(초)", "interval", "300", false),
|
|
}, "변경", "적용"),
|
|
}), true
|
|
default:
|
|
return PageData{}, false
|
|
}
|
|
}
|
|
|
|
func portPage(path string) (PageData, bool) {
|
|
parts := strings.Split(strings.Trim(path, "/"), "/")
|
|
if len(parts) != 3 || parts[0] != "ports" {
|
|
return PageData{}, false
|
|
}
|
|
|
|
portID, err := strconv.Atoi(parts[1])
|
|
if err != nil || portID < 1 || portID > 48 {
|
|
return PageData{}, false
|
|
}
|
|
|
|
subPath := parts[2]
|
|
subtabs := []SubTab{
|
|
{"매개변수", fmt.Sprintf("/ports/%d/parameters", portID), subPath == "parameters"},
|
|
{"데이터 패킷", fmt.Sprintf("/ports/%d/data-packet", portID), subPath == "data-packet"},
|
|
{"동작 설정 1", fmt.Sprintf("/ports/%d/operation-1", portID), subPath == "operation-1"},
|
|
{"동작 설정 2", fmt.Sprintf("/ports/%d/operation-2", portID), subPath == "operation-2"},
|
|
{"데이터 로그", fmt.Sprintf("/ports/%d/data-log", portID), subPath == "data-log"},
|
|
{"고급 설정", fmt.Sprintf("/ports/%d/advanced", portID), subPath == "advanced"},
|
|
}
|
|
|
|
switch subPath {
|
|
case "parameters":
|
|
return newPortPage(path, portID, subtabs, []ContentBlock{
|
|
{
|
|
Kind: "port-selector",
|
|
Title: "포트 선택",
|
|
PortLinks: portLinks(),
|
|
},
|
|
{
|
|
Kind: "form",
|
|
Title: fmt.Sprintf("포트 설정 / Port %d", portID),
|
|
Form: &FormData{
|
|
Action: "/api/mock/action",
|
|
PrimaryLabel: "변경",
|
|
SecondaryLabel: "적용",
|
|
Fields: []FormField{
|
|
textField("별칭 이름", "alias", fmt.Sprintf("Port-%02d", portID), false),
|
|
selectField("시리얼 타입", "serialType", "RS232", []string{"RS232", "RS422", "RS485"}),
|
|
selectField("Baudrate", "baudrate", "115200", []string{"9600", "19200", "38400", "57600", "115200"}),
|
|
selectField("Databit", "databit", "8", []string{"5", "6", "7", "8"}),
|
|
selectField("Parity", "parity", "None", []string{"None", "Odd", "Even"}),
|
|
selectField("Stopbit", "stopbit", "1", []string{"1", "1.5", "2"}),
|
|
selectField("Flow control", "flow", "None", []string{"None", "RTS/CTS", "XON/XOFF"}),
|
|
selectField("RTS control", "rts", "Auto", []string{"Auto", "Enable", "Disable"}),
|
|
selectField("DTR control", "dtr", "Auto", []string{"Auto", "Enable", "Disable"}),
|
|
toggleField("Ignore jammed", "ignore", false),
|
|
},
|
|
PortTargets: portTargets(portID),
|
|
Notes: []string{
|
|
"실제 장비 저장 없이 화면 상태만 갱신됩니다.",
|
|
"동일 패턴으로 Port 1~48 설정 화면을 재사용합니다.",
|
|
},
|
|
},
|
|
},
|
|
}), true
|
|
case "data-packet":
|
|
return newPortPage(path, portID, subtabs, []ContentBlock{
|
|
formBlock("데이터 패킷", "port-packet", []FormField{
|
|
selectField("패킷 모드", "packetMode", "Delimiter", []string{"Delimiter", "Timeout", "Fixed Length"}),
|
|
textField("패킷 길이", "packetLen", "256", false),
|
|
textField("종료 문자", "delimiter", "\\r\\n", false),
|
|
textField("타임아웃(ms)", "timeout", "50", false),
|
|
}, "변경", "적용"),
|
|
}), true
|
|
case "operation-1":
|
|
return newPortPage(path, portID, subtabs, []ContentBlock{
|
|
formBlock("동작 설정 1", "port-operation-1", []FormField{
|
|
selectField("동작 모드", "mode", "TCP Server", []string{"TCP Server", "TCP Client", "UDP"}),
|
|
textField("로컬 포트", "localPort", fmt.Sprintf("40%02d", portID), false),
|
|
textField("원격 IP", "remoteIP", "192.168.0.100", false),
|
|
textField("원격 포트", "remotePort", "4001", false),
|
|
toggleField("자동 연결", "autoConnect", true),
|
|
}, "변경", "적용"),
|
|
}), true
|
|
case "operation-2":
|
|
return newPortPage(path, portID, subtabs, []ContentBlock{
|
|
formBlock("동작 설정 2", "port-operation-2", []FormField{
|
|
toggleField("TLS 사용", "tls", false),
|
|
toggleField("Keepalive 사용", "keepalive", true),
|
|
textField("Keepalive 시간", "keepaliveSeconds", "30", false),
|
|
toggleField("Nagle 비활성화", "nodelay", true),
|
|
textAreaField("비고", "remarks", "추후 실제 연결 시 포트별 통신 정책을 붙일 수 있도록 남겨둔 더미 영역입니다.", 4),
|
|
}, "변경", "적용"),
|
|
}), true
|
|
case "data-log":
|
|
return newPortPage(path, portID, subtabs, []ContentBlock{
|
|
{
|
|
Kind: "table",
|
|
Title: "데이터 로그",
|
|
Table: &TableData{
|
|
Columns: []string{"시간", "방향", "길이", "데이터"},
|
|
PrimaryLabel: "다운로드",
|
|
SecondaryLabel: "새로고침",
|
|
EmptyMessage: "데이터가 없습니다.",
|
|
Rows: []TableRow{
|
|
{Cells: []string{"2026-06-24 14:05:12", "RX", "128", "01 03 00 00 00 02 C4 0B"}},
|
|
{Cells: []string{"2026-06-24 14:05:13", "TX", "128", "01 03 04 00 01 00 02 2A 32"}},
|
|
},
|
|
},
|
|
},
|
|
}), true
|
|
case "advanced":
|
|
return newPortPage(path, portID, subtabs, []ContentBlock{
|
|
formBlock("고급 설정", "port-advanced", []FormField{
|
|
toggleField("이벤트 로그 기록", "eventlog", true),
|
|
toggleField("라인 상태 감시", "linestate", true),
|
|
toggleField("포트 보호", "protect", false),
|
|
textField("세션 제한", "sessionLimit", "4", false),
|
|
textField("Idle Timeout", "idle", "600", false),
|
|
}, "변경", "적용"),
|
|
}), true
|
|
default:
|
|
return PageData{}, false
|
|
}
|
|
}
|
|
|
|
func portDebugPage(path string) (PageData, bool) {
|
|
subtabs := []SubTab{
|
|
{"포트 디버그", "/port-debug", path == "/port-debug"},
|
|
{"포트 1", "/port-debug/1", path == "/port-debug/1"},
|
|
}
|
|
if path == "/port-debug" || path == "/port-debug/1" {
|
|
return newPage(path, "포트 디버그", "포트 디버그", []string{"포트 디버그"}, subtabs, []ContentBlock{
|
|
{
|
|
Kind: "tool",
|
|
Title: "포트 디버그 콘솔",
|
|
Tool: &ToolData{
|
|
Action: "/api/mock/action",
|
|
PrimaryLabel: "전송",
|
|
Fields: []FormField{
|
|
textField("대상 포트", "port", "Port 1", false),
|
|
selectField("인코딩", "encoding", "HEX", []string{"HEX", "ASCII", "UTF-8"}),
|
|
textAreaField("전송 데이터", "payload", "01 03 00 00 00 02 C4 0B", 8),
|
|
},
|
|
Result: "RX: 01 03 04 00 01 00 02 2A 32\nTX: 01 03 00 00 00 02 C4 0B",
|
|
},
|
|
},
|
|
}), true
|
|
}
|
|
return PageData{}, false
|
|
}
|
|
|
|
func adminPage(path string) (PageData, bool) {
|
|
switch path {
|
|
case "/admin/web-console":
|
|
return simpleFormPage(path, "관리자 운영", []string{"관리자 운영", "웹 콘솔"}, "웹 콘솔 접속 설정", []FormField{
|
|
toggleField("웹 콘솔 사용", "console", true),
|
|
textField("세션 타임아웃", "timeout", "600", false),
|
|
toggleField("동시 로그인 차단", "multi", false),
|
|
}), true
|
|
case "/admin/users":
|
|
return newPage(path, "관리자 운영", "사용자 계정", []string{"관리자 운영", "사용자 계정"}, userTabs(path), []ContentBlock{
|
|
tableBlock("사용자 계정", []string{"활성", "사용자 이름", "그룹", "포트 권한"}, []TableRow{
|
|
{Cells: []string{"사용", "terauser", "administrator", "전체 허용"}, Actions: []RowAction{{Label: "수정", Action: "edit-user", Variant: "primary"}, {Label: "삭제", Action: "delete-user", Variant: "danger"}}},
|
|
{Cells: []string{"사용", "guest", "guest", "조회 전용"}, Actions: []RowAction{{Label: "수정", Action: "edit-user", Variant: "primary"}, {Label: "삭제", Action: "delete-user", Variant: "danger"}}},
|
|
}, "사용자 추가", "변경"),
|
|
modalBlock("사용자 추가", "사용자 생성 팝업을 흉내 낸 목업입니다.", []FormField{
|
|
textField("사용자 이름", "username", "", true),
|
|
passwordField("비밀번호", "password", ""),
|
|
selectField("그룹", "group", "administrator", []string{"administrator", "guest", "operator"}),
|
|
}),
|
|
}), true
|
|
case "/admin/groups":
|
|
return newPage(path, "관리자 운영", "그룹 관리", []string{"관리자 운영", "그룹 관리"}, userTabs(path), []ContentBlock{
|
|
tableBlock("그룹", []string{"그룹 이름", "권한", "설명"}, []TableRow{
|
|
{Cells: []string{"administrator", "모든 메뉴", "전체 접근 가능"}, Actions: []RowAction{{Label: "수정", Action: "edit-group", Variant: "primary"}}},
|
|
{Cells: []string{"guest", "조회 전용", "구성 변경 금지"}, Actions: []RowAction{{Label: "수정", Action: "edit-group", Variant: "primary"}}},
|
|
}, "그룹 추가", "변경"),
|
|
}), true
|
|
case "/admin/auth/radius":
|
|
return simpleFormPage(path, "관리자 운영", []string{"관리자 운영", "인증 서버", "RADIUS"}, "RADIUS", []FormField{
|
|
toggleField("RADIUS 사용", "radius", false),
|
|
textField("서버 주소", "host", "192.168.0.10", false),
|
|
textField("포트", "port", "1812", false),
|
|
passwordField("Secret", "secret", "radius-secret"),
|
|
textField("타임아웃", "timeout", "5", false),
|
|
}), true
|
|
case "/admin/auth/tacacs":
|
|
return simpleFormPage(path, "관리자 운영", []string{"관리자 운영", "인증 서버", "TACACS+"}, "TACACS+", []FormField{
|
|
toggleField("TACACS+ 사용", "tacacs", false),
|
|
textField("서버 주소", "host", "192.168.0.20", false),
|
|
textField("포트", "port", "49", false),
|
|
passwordField("Secret", "secret", "tacacs-secret"),
|
|
textField("타임아웃", "timeout", "5", false),
|
|
}), true
|
|
case "/admin/address-filter":
|
|
return tablePage(path, "주소 필터", []string{"관리자 운영", "주소 필터"}, []string{"유형", "규칙 수", "설명"}, []TableRow{
|
|
{Cells: []string{"IPv4 필터", "4", "기본 IPv4 접근 제어"}},
|
|
{Cells: []string{"IPv6 필터", "2", "기본 IPv6 접근 제어"}},
|
|
{Cells: []string{"MAC 필터", "3", "등록 MAC 제한"}},
|
|
}, []SubTab{
|
|
{"기본", "/admin/address-filter", path == "/admin/address-filter"},
|
|
{"IPv4 필터", "/admin/address-filter/ipv4", false},
|
|
{"IPv6 필터", "/admin/address-filter/ipv6", false},
|
|
{"MAC 필터", "/admin/address-filter/mac", false},
|
|
}), true
|
|
case "/admin/address-filter/ipv4", "/admin/address-filter/ipv6", "/admin/address-filter/mac":
|
|
return addressFilterListPage(path), true
|
|
case "/admin/address-filter/ipv4/new", "/admin/address-filter/ipv6/new", "/admin/address-filter/mac/new":
|
|
return addressFilterNewPage(path), true
|
|
case "/admin/snmp":
|
|
return simpleFormPage(path, "관리자 운영", []string{"관리자 운영", "SNMP"}, "SNMP", []FormField{
|
|
toggleField("SNMP 사용", "snmp", true),
|
|
selectField("SNMP 버전", "version", "v3", []string{"v1", "v2c", "v3"}),
|
|
textField("Read Community", "roCommunity", "public", false),
|
|
textField("Write Community", "rwCommunity", "private", false),
|
|
textField("Trap Host", "trapHost", "192.168.0.50", false),
|
|
textField("Trap Port", "trapPort", "162", false),
|
|
}), true
|
|
case "/admin/ssl", "/admin/ssl/new", "/admin/ssl/import", "/admin/ssl/import-csr":
|
|
title := map[string]string{
|
|
"/admin/ssl": "SSL 인증서",
|
|
"/admin/ssl/new": "SSL 인증서 - 새로 만들기",
|
|
"/admin/ssl/import": "SSL 인증서 - 가져오기",
|
|
"/admin/ssl/import-csr": "SSL 인증서 - CSR 가져오기",
|
|
}[path]
|
|
return simpleFormPage(path, "관리자 운영", []string{"관리자 운영", "SSL 인증서"}, title, []FormField{
|
|
textField("인증서 이름", "name", "device-cert", false),
|
|
textField("공통 이름(CN)", "cn", "192.168.0.233", false),
|
|
textAreaField("인증서 내용", "certificate", "-----BEGIN CERTIFICATE-----\nMOCK\n-----END CERTIFICATE-----", 6),
|
|
textAreaField("개인 키", "privateKey", "-----BEGIN PRIVATE KEY-----\nMOCK\n-----END PRIVATE KEY-----", 6),
|
|
}), true
|
|
case "/admin/backup/export", "/admin/backup/import", "/admin/backup/restore":
|
|
title := map[string]string{
|
|
"/admin/backup/export": "구성 내보내기",
|
|
"/admin/backup/import": "구성 가져오기",
|
|
"/admin/backup/restore": "구성 복원",
|
|
}[path]
|
|
return actionPage(path, title, "백업 및 복원 관련 액션을 시뮬레이션하는 목업 화면입니다.", "backup-action"), true
|
|
case "/admin/firmware":
|
|
return actionPage(path, "펌웨어 업그레이드", "펌웨어 파일 업로드와 적용 절차를 재현하는 목업 화면입니다.", "firmware-upgrade"), true
|
|
default:
|
|
return PageData{}, false
|
|
}
|
|
}
|
|
|
|
func settingsPage(path string) (PageData, bool) {
|
|
switch path {
|
|
case "/settings/alert/event":
|
|
return simpleFormPage(path, "기타 설정", []string{"기타 설정", "알림 설정", "이벤트"}, "알림 설정 - 이벤트", []FormField{
|
|
toggleField("이벤트 알림 사용", "event", true),
|
|
toggleField("링크 다운", "linkDown", true),
|
|
toggleField("인증 실패", "authFail", true),
|
|
toggleField("설정 변경", "configChange", true),
|
|
}), true
|
|
case "/settings/alert/serial-event":
|
|
return simpleFormPage(path, "기타 설정", []string{"기타 설정", "알림 설정", "시리얼 이벤트"}, "알림 설정 - 시리얼 이벤트", []FormField{
|
|
toggleField("시리얼 이벤트 사용", "serialEvent", false),
|
|
toggleField("포트 에러", "portError", true),
|
|
toggleField("수신 타임아웃", "rxTimeout", false),
|
|
}), true
|
|
case "/settings/alert/mail":
|
|
return simpleFormPage(path, "기타 설정", []string{"기타 설정", "알림 설정", "메일"}, "알림 설정 - 메일", []FormField{
|
|
textField("SMTP 서버", "smtp", "smtp.example.com", false),
|
|
textField("포트", "port", "587", false),
|
|
textField("발신자", "from", "alert@example.com", false),
|
|
textField("수신자", "to", "admin@example.com", false),
|
|
toggleField("TLS 사용", "tls", true),
|
|
}), true
|
|
case "/settings/alert/snmp-trap-filter":
|
|
return simpleFormPage(path, "기타 설정", []string{"기타 설정", "알림 설정", "SNMP Trap 필터"}, "SNMP Trap 필터", []FormField{
|
|
textField("대상 주소", "address", "192.168.0.50", false),
|
|
textField("커뮤니티", "community", "public", false),
|
|
selectField("버전", "version", "v2c", []string{"v1", "v2c", "v3"}),
|
|
}), true
|
|
case "/settings/system-log/remote":
|
|
return simpleFormPage(path, "기타 설정", []string{"기타 설정", "시스템 로그", "원격 로그"}, "시스템 로그 - 원격 로그", []FormField{
|
|
toggleField("원격 로그 사용", "remote", false),
|
|
textField("서버 주소", "host", "192.168.0.60", false),
|
|
textField("포트", "port", "514", false),
|
|
selectField("프로토콜", "protocol", "UDP", []string{"UDP", "TCP"}),
|
|
}), true
|
|
case "/settings/system-log/status-event":
|
|
return simpleFormPage(path, "기타 설정", []string{"기타 설정", "시스템 로그", "상태 이벤트"}, "시스템 로그 - 상태 이벤트", []FormField{
|
|
toggleField("링크 업/다운", "link", true),
|
|
toggleField("사용자 로그인", "login", true),
|
|
toggleField("환경 알림", "env", true),
|
|
}), true
|
|
case "/settings/system-log/events":
|
|
return simpleFormPage(path, "기타 설정", []string{"기타 설정", "시스템 로그", "이벤트"}, "시스템 로그 - 이벤트", []FormField{
|
|
toggleField("정보 로그", "info", true),
|
|
toggleField("경고 로그", "warn", true),
|
|
toggleField("오류 로그", "error", true),
|
|
}), true
|
|
case "/settings/datetime":
|
|
return simpleFormPage(path, "기타 설정", []string{"기타 설정", "날짜와 시간"}, "날짜와 시간", []FormField{
|
|
selectField("시간대", "timezone", "Asia/Seoul", []string{"Asia/Seoul", "UTC", "America/Los_Angeles"}),
|
|
toggleField("NTP 사용", "ntp", true),
|
|
textField("NTP 서버 1", "ntp1", "time.google.com", false),
|
|
textField("NTP 서버 2", "ntp2", "pool.ntp.org", false),
|
|
textField("수동 날짜", "date", "2026-06-24", false),
|
|
textField("수동 시간", "time", "14:03:22", false),
|
|
}), true
|
|
default:
|
|
return PageData{}, false
|
|
}
|
|
}
|
|
|
|
func toolsPage(path string) (PageData, bool) {
|
|
switch path {
|
|
case "/tools/nettest/ping", "/tools/nettest/traceroute", "/tools/nettest/tcp":
|
|
active := path
|
|
title := map[string]string{
|
|
"/tools/nettest/ping": "핑 테스트",
|
|
"/tools/nettest/traceroute": "경로 추적 테스트",
|
|
"/tools/nettest/tcp": "TCP 연결 테스트",
|
|
}[path]
|
|
result := map[string]string{
|
|
"/tools/nettest/ping": "PING 1.1.1.1: 56 data bytes\n64 bytes from 1.1.1.1: icmp_seq=0 ttl=58 time=7.2 ms",
|
|
"/tools/nettest/traceroute": "1 192.168.0.1 0.7 ms\n2 10.10.0.1 2.8 ms\n3 1.1.1.1 8.4 ms",
|
|
"/tools/nettest/tcp": "Connecting to 192.168.0.100:4001 ... success\nSession established in mock mode.",
|
|
}[path]
|
|
return newPage(path, "진단 도구", "네트워크 테스트", []string{"진단 도구", "네트워크 테스트"}, []SubTab{
|
|
{"핑", "/tools/nettest/ping", active == "/tools/nettest/ping"},
|
|
{"경로 추적", "/tools/nettest/traceroute", active == "/tools/nettest/traceroute"},
|
|
{"TCP 연결", "/tools/nettest/tcp", active == "/tools/nettest/tcp"},
|
|
}, []ContentBlock{
|
|
{
|
|
Kind: "tool",
|
|
Title: title,
|
|
Tool: &ToolData{
|
|
Action: "/api/mock/action",
|
|
PrimaryLabel: "테스트",
|
|
Tabs: []SubTab{
|
|
{"핑", "/tools/nettest/ping", active == "/tools/nettest/ping"},
|
|
{"경로 추적", "/tools/nettest/traceroute", active == "/tools/nettest/traceroute"},
|
|
{"TCP 연결", "/tools/nettest/tcp", active == "/tools/nettest/tcp"},
|
|
},
|
|
Fields: []FormField{
|
|
textField("주소", "address", "1.1.1.1", false),
|
|
textField("횟수", "count", "4", false),
|
|
textField("패킷 크기", "size", "56", false),
|
|
textField("타임아웃", "timeout", "1000", false),
|
|
},
|
|
Result: result,
|
|
},
|
|
},
|
|
}), true
|
|
case "/tools/alias":
|
|
return simpleFormPage(path, "진단 도구", []string{"진단 도구", "포트 별칭"}, "포트 별칭", []FormField{
|
|
textField("포트 1", "port1", "PLC-A", false),
|
|
textField("포트 2", "port2", "센서-라인1", false),
|
|
textField("포트 3", "port3", "RTU-3", false),
|
|
textField("포트 4", "port4", "예비 포트", false),
|
|
}), true
|
|
default:
|
|
return PageData{}, false
|
|
}
|
|
}
|
|
|
|
func logsPage(path string) (PageData, bool) {
|
|
switch path {
|
|
case "/logs/system":
|
|
return tablePage(path, "시스템 로그", []string{"로그", "시스템 로그"}, []string{"No.", "이벤트"}, []TableRow{
|
|
{Cells: []string{"1", "관리자 로그인"}},
|
|
{Cells: []string{"2", "구성 저장 요청"}},
|
|
{Cells: []string{"3", "포트 1 링크 상태 변경"}},
|
|
}, nil), true
|
|
case "/logs/ports":
|
|
return tablePage(path, "포트 로그", []string{"로그", "포트 로그"}, []string{"No.", "포트", "이벤트"}, []TableRow{
|
|
{Cells: []string{"1", "Port 1", "데이터 수신 128 bytes"}},
|
|
{Cells: []string{"2", "Port 24", "링크 복구"}},
|
|
{Cells: []string{"3", "Port 12", "세션 종료"}},
|
|
}, nil), true
|
|
default:
|
|
return PageData{}, false
|
|
}
|
|
}
|
|
|
|
func portStatusPage() PageData {
|
|
return newPage("/port-status", "포트 상태", "포트 상태", []string{"포트 상태"}, nil, []ContentBlock{
|
|
{Kind: "port-links", Title: "포트 상태 맵", PortLinks: portLinks()},
|
|
{
|
|
Kind: "table",
|
|
Title: "포트 상태 요약",
|
|
Table: &TableData{
|
|
Columns: []string{"포트", "상태", "프로토콜", "세션", "RX/TX"},
|
|
EmptyMessage: "데이터가 없습니다.",
|
|
Rows: []TableRow{
|
|
{Cells: []string{"Port 1", "연결", "TCP Server", "3", "12.3KB / 9.8KB"}},
|
|
{Cells: []string{"Port 2", "대기", "TCP Client", "0", "0 / 0"}},
|
|
{Cells: []string{"Port 24", "연결", "UDP", "1", "1.2KB / 0.8KB"}},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
}
|
|
|
|
func passwordPage() PageData {
|
|
return newPage("/password", "비밀번호 변경", "비밀번호 변경", []string{"비밀번호 변경"}, nil, []ContentBlock{
|
|
formBlock("비밀번호 변경", "change-password", []FormField{
|
|
{Label: "사용자 이름", Name: "username", Type: "text", Value: "terauser", Disabled: true},
|
|
passwordField("기존 비밀번호", "oldPassword", ""),
|
|
passwordField("새 비밀번호", "newPassword", ""),
|
|
passwordField("비밀번호 확인", "confirmPassword", ""),
|
|
}, "변경", "적용"),
|
|
})
|
|
}
|
|
|
|
func actionPage(path string, title string, description string, action string) PageData {
|
|
return newPage(path, title, title, []string{title}, nil, []ContentBlock{
|
|
{
|
|
Kind: "action",
|
|
Title: title,
|
|
Action: &ActionPanel{
|
|
Description: description,
|
|
Buttons: []ActionButton{
|
|
{Label: "실행", Action: action, Variant: "primary"},
|
|
{Label: "취소", Action: "cancel", Variant: "secondary"},
|
|
},
|
|
},
|
|
},
|
|
modalBlock(title+" 확인", "실제 장비에는 반영하지 않고 화면 알림만 표시합니다.", []FormField{
|
|
textAreaField("안내", "note", "MOCK_MODE에서는 실제 저장, 재시작, 복원 작업을 수행하지 않습니다.", 4),
|
|
}),
|
|
})
|
|
}
|
|
|
|
func simpleFormPage(path string, section string, breadcrumbs []string, title string, fields []FormField) PageData {
|
|
return newPage(path, section, title, breadcrumbs, nil, []ContentBlock{
|
|
formBlock(title, "apply-"+sanitizePath(path), fields, "변경", "적용"),
|
|
})
|
|
}
|
|
|
|
func tablePage(path string, title string, breadcrumbs []string, columns []string, rows []TableRow, subtabs []SubTab) PageData {
|
|
return newPage(path, title, title, breadcrumbs, subtabs, []ContentBlock{
|
|
tableBlock(title, columns, rows, "추가", "변경"),
|
|
})
|
|
}
|
|
|
|
func newPortPage(path string, portID int, subtabs []SubTab, blocks []ContentBlock) PageData {
|
|
return newPage(path, "포트 설정", fmt.Sprintf("포트 설정 / Port %d", portID), []string{"포트 설정", fmt.Sprintf("Port %d", portID)}, subtabs, blocks)
|
|
}
|
|
|
|
func newPage(path string, section string, title string, breadcrumbs []string, subtabs []SubTab, blocks []ContentBlock) PageData {
|
|
return PageData{
|
|
AppName: "TERACLONE",
|
|
MockMode: config.MockMode,
|
|
CurrentPath: path,
|
|
Title: title,
|
|
Section: section,
|
|
Breadcrumbs: breadcrumbs,
|
|
WorkspaceTabs: workspaceTabs(path, title),
|
|
Menu: menu(path),
|
|
SubTabs: subtabs,
|
|
Blocks: blocks,
|
|
}
|
|
}
|
|
|
|
func formBlock(title string, action string, fields []FormField, primary string, secondary string) ContentBlock {
|
|
return ContentBlock{
|
|
Kind: "form",
|
|
Title: title,
|
|
Form: &FormData{
|
|
Action: "/api/mock/action",
|
|
PrimaryLabel: primary,
|
|
SecondaryLabel: secondary,
|
|
Fields: fields,
|
|
Notes: []string{
|
|
fmt.Sprintf("Mock action: %s", action),
|
|
"실제 장비 설정 저장이나 통신은 수행하지 않습니다.",
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func tableBlock(title string, columns []string, rows []TableRow, primary string, secondary string) ContentBlock {
|
|
return ContentBlock{
|
|
Kind: "table",
|
|
Title: title,
|
|
Table: &TableData{
|
|
Columns: columns,
|
|
Rows: rows,
|
|
EmptyMessage: "데이터가 없습니다.",
|
|
PrimaryLabel: primary,
|
|
SecondaryLabel: secondary,
|
|
},
|
|
}
|
|
}
|
|
|
|
func modalBlock(title string, message string, fields []FormField) ContentBlock {
|
|
return ContentBlock{
|
|
Kind: "modal",
|
|
Title: title,
|
|
Modal: &ModalData{
|
|
Root: sanitizePath(title),
|
|
Title: title,
|
|
Message: message,
|
|
Fields: fields,
|
|
Buttons: []ActionButton{
|
|
{Label: "확인", Action: "confirm-modal", Variant: "primary"},
|
|
{Label: "취소", Action: "cancel-modal", Variant: "secondary"},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func addressFilterListPage(path string) PageData {
|
|
filterName := "IPv4 필터"
|
|
if strings.Contains(path, "ipv6") {
|
|
filterName = "IPv6 필터"
|
|
}
|
|
if strings.Contains(path, "mac") {
|
|
filterName = "MAC 필터"
|
|
}
|
|
|
|
return tablePage(path, filterName, []string{"관리자 운영", "주소 필터", filterName}, []string{"규칙 이름", "조건", "설명"}, []TableRow{
|
|
{Cells: []string{"규칙 1", "허용", "내부 운영망"}},
|
|
{Cells: []string{"규칙 2", "차단", "임시 테스트 장비"}},
|
|
}, []SubTab{
|
|
{"기본", "/admin/address-filter", false},
|
|
{"IPv4 필터", "/admin/address-filter/ipv4", strings.Contains(path, "ipv4")},
|
|
{"IPv6 필터", "/admin/address-filter/ipv6", strings.Contains(path, "ipv6")},
|
|
{"MAC 필터", "/admin/address-filter/mac", strings.Contains(path, "mac")},
|
|
})
|
|
}
|
|
|
|
func addressFilterNewPage(path string) PageData {
|
|
title := "IPv4 필터 추가"
|
|
fields := []FormField{
|
|
textField("규칙 이름", "name", "allow-office", false),
|
|
selectField("동작", "policy", "허용", []string{"허용", "차단"}),
|
|
textField("주소", "address", "192.168.0.0", false),
|
|
textField("마스크", "mask", "255.255.255.0", false),
|
|
}
|
|
if strings.Contains(path, "ipv6") {
|
|
title = "IPv6 필터 추가"
|
|
fields = []FormField{
|
|
textField("규칙 이름", "name", "allow-v6", false),
|
|
selectField("동작", "policy", "허용", []string{"허용", "차단"}),
|
|
textField("주소", "address", "2001:db8::", false),
|
|
textField("Prefix Length", "prefix", "64", false),
|
|
}
|
|
}
|
|
if strings.Contains(path, "mac") {
|
|
title = "MAC 필터 추가"
|
|
fields = []FormField{
|
|
textField("규칙 이름", "name", "allow-maintenance", false),
|
|
selectField("동작", "policy", "허용", []string{"허용", "차단"}),
|
|
textField("MAC 주소", "address", "00:11:22:33:44:55", false),
|
|
textField("설명", "desc", "점검 장비", false),
|
|
}
|
|
}
|
|
|
|
return simpleFormPage(path, "관리자 운영", []string{"관리자 운영", "주소 필터", title}, title, fields)
|
|
}
|
|
|
|
func userTabs(path string) []SubTab {
|
|
return []SubTab{
|
|
{"사용자", "/admin/users", path == "/admin/users"},
|
|
{"그룹", "/admin/groups", path == "/admin/groups"},
|
|
}
|
|
}
|
|
|
|
func workspaceTabs(path string, title string) []WorkspaceTab {
|
|
return []WorkspaceTab{
|
|
{Key: tabKeyForPath(path), Label: tabLabelForPath(path, title), Path: path, Active: true},
|
|
}
|
|
}
|
|
|
|
func menu(path string) []MenuGroup {
|
|
groups := []MenuGroup{
|
|
{Label: "대시보드", Icon: "DB", Path: "/"},
|
|
{Label: "네트워크 설정", Icon: "NW", Path: "/network/interface-1"},
|
|
{Label: "포트 설정", Icon: "PT", Path: "/ports/1/parameters"},
|
|
{Label: "포트 디버그", Icon: "DG", Path: "/port-debug"},
|
|
{Label: "관리자 운영", Icon: "AD", Path: "/admin/users"},
|
|
{Label: "기타 설정", Icon: "ST", Path: "/settings/alert/event"},
|
|
{Label: "진단 도구", Icon: "TL", Path: "/tools/nettest/ping"},
|
|
{Label: "로그", Icon: "LG", Path: "/logs/system"},
|
|
{Label: "포트 상태", Icon: "PS", Path: "/port-status"},
|
|
{Label: "비밀번호 변경", Icon: "PW", Path: "/password"},
|
|
{Label: "설정 저장", Icon: "SV", Path: "/config/save"},
|
|
{Label: "포트 재시작", Icon: "PR", Path: "/ports/restart"},
|
|
{Label: "장치 재시작", Icon: "DR", Path: "/device/restart"},
|
|
}
|
|
|
|
for i := range groups {
|
|
if groups[i].Path == "/" {
|
|
groups[i].Active = path == "/"
|
|
continue
|
|
}
|
|
groups[i].Active = path == groups[i].Path || strings.HasPrefix(path, trimMenuRoot(groups[i].Path))
|
|
}
|
|
|
|
for i := range groups {
|
|
switch groups[i].Label {
|
|
case "포트 설정":
|
|
groups[i].Expanded = false
|
|
case "관리자 운영":
|
|
groups[i].Expanded = strings.HasPrefix(path, "/admin/")
|
|
groups[i].Children = append(groups[i].Children,
|
|
MenuChild{"웹 콘솔", "/admin/web-console", path == "/admin/web-console"},
|
|
MenuChild{"사용자 계정", "/admin/users", strings.HasPrefix(path, "/admin/users") || path == "/admin/groups"},
|
|
MenuChild{"인증 서버", "/admin/auth/radius", strings.HasPrefix(path, "/admin/auth/")},
|
|
MenuChild{"주소 필터", "/admin/address-filter", strings.HasPrefix(path, "/admin/address-filter")},
|
|
MenuChild{"SNMP", "/admin/snmp", path == "/admin/snmp"},
|
|
MenuChild{"SSL 인증서", "/admin/ssl", strings.HasPrefix(path, "/admin/ssl")},
|
|
MenuChild{"백업/복원", "/admin/backup/export", strings.HasPrefix(path, "/admin/backup/")},
|
|
MenuChild{"펌웨어", "/admin/firmware", path == "/admin/firmware"},
|
|
)
|
|
case "기타 설정":
|
|
groups[i].Expanded = strings.HasPrefix(path, "/settings/")
|
|
groups[i].Children = append(groups[i].Children,
|
|
MenuChild{"알림 설정", "/settings/alert/event", strings.HasPrefix(path, "/settings/alert/")},
|
|
MenuChild{"시스템 로그", "/settings/system-log/remote", strings.HasPrefix(path, "/settings/system-log/")},
|
|
MenuChild{"날짜와 시간", "/settings/datetime", path == "/settings/datetime"},
|
|
)
|
|
case "진단 도구":
|
|
groups[i].Expanded = strings.HasPrefix(path, "/tools/")
|
|
groups[i].Children = append(groups[i].Children,
|
|
MenuChild{"네트워크 테스트", "/tools/nettest/ping", strings.HasPrefix(path, "/tools/nettest/")},
|
|
MenuChild{"포트 별칭", "/tools/alias", path == "/tools/alias"},
|
|
)
|
|
case "로그":
|
|
groups[i].Expanded = strings.HasPrefix(path, "/logs/")
|
|
groups[i].Children = append(groups[i].Children,
|
|
MenuChild{"시스템 로그", "/logs/system", path == "/logs/system"},
|
|
MenuChild{"포트 로그", "/logs/ports", path == "/logs/ports"},
|
|
)
|
|
}
|
|
}
|
|
|
|
return groups
|
|
}
|
|
|
|
func portLinks() []PortLink {
|
|
links := make([]PortLink, 0, 48)
|
|
for i := 1; i <= 48; i++ {
|
|
links = append(links, PortLink{
|
|
Number: i,
|
|
Up: i%3 != 0,
|
|
Blink: i == 1 || i == 24,
|
|
})
|
|
}
|
|
return links
|
|
}
|
|
|
|
func portTargets(selected int) []PortTarget {
|
|
targets := make([]PortTarget, 0, 48)
|
|
for i := 1; i <= 48; i++ {
|
|
targets = append(targets, PortTarget{
|
|
Number: i,
|
|
Checked: i == selected,
|
|
})
|
|
}
|
|
return targets
|
|
}
|
|
|
|
func textField(label string, name string, value string, required bool) FormField {
|
|
return FormField{Label: label, Name: name, Type: "text", Value: value, Required: required}
|
|
}
|
|
|
|
func passwordField(label string, name string, value string) FormField {
|
|
return FormField{Label: label, Name: name, Type: "password", Value: value}
|
|
}
|
|
|
|
func textAreaField(label string, name string, value string, rows int) FormField {
|
|
return FormField{Label: label, Name: name, Type: "textarea", Value: value, Rows: rows}
|
|
}
|
|
|
|
func toggleField(label string, name string, checked bool) FormField {
|
|
return FormField{Label: label, Name: name, Type: "toggle", Checked: checked}
|
|
}
|
|
|
|
func selectField(label string, name string, value string, options []string) FormField {
|
|
field := FormField{Label: label, Name: name, Type: "select", Value: value}
|
|
for _, option := range options {
|
|
field.Options = append(field.Options, Option{
|
|
Label: option,
|
|
Value: option,
|
|
Selected: option == value,
|
|
})
|
|
}
|
|
return field
|
|
}
|
|
|
|
func sanitizePath(path string) string {
|
|
return strings.NewReplacer("/", "-", ":", "-", "_", "-").Replace(strings.Trim(path, "/"))
|
|
}
|
|
|
|
func trimMenuRoot(path string) string {
|
|
if path == "/" {
|
|
return "/"
|
|
}
|
|
parts := strings.Split(strings.Trim(path, "/"), "/")
|
|
if len(parts) == 0 {
|
|
return "/"
|
|
}
|
|
return "/" + parts[0] + "/"
|
|
}
|
|
|
|
func shortLabelForPath(path string) string {
|
|
switch {
|
|
case strings.HasPrefix(path, "/network/"):
|
|
return "네트워크 설정"
|
|
case strings.HasPrefix(path, "/ports/"):
|
|
return "포트 설정"
|
|
case strings.HasPrefix(path, "/admin/"):
|
|
return "관리자 운영"
|
|
case strings.HasPrefix(path, "/settings/"):
|
|
return "기타 설정"
|
|
case strings.HasPrefix(path, "/tools/"):
|
|
return "진단 도구"
|
|
case strings.HasPrefix(path, "/logs/"):
|
|
return "로그"
|
|
default:
|
|
return "장비 화면"
|
|
}
|
|
}
|
|
|
|
func tabLabelForPath(path string, title string) string {
|
|
switch {
|
|
case path == "/":
|
|
return "대시보드"
|
|
case strings.HasPrefix(path, "/ports/"):
|
|
parts := strings.Split(strings.Trim(path, "/"), "/")
|
|
if len(parts) >= 2 {
|
|
if portID, err := strconv.Atoi(parts[1]); err == nil {
|
|
return fmt.Sprintf("포트 %d 설정", portID)
|
|
}
|
|
}
|
|
case strings.HasPrefix(path, "/admin/snmp"):
|
|
return "SNMP"
|
|
case strings.HasPrefix(path, "/tools/nettest/"):
|
|
return "네트워크 테스트"
|
|
}
|
|
|
|
if title != "" {
|
|
return title
|
|
}
|
|
|
|
return shortLabelForPath(path)
|
|
}
|
|
|
|
func tabKeyForPath(path string) string {
|
|
switch {
|
|
case path == "/":
|
|
return "dashboard"
|
|
case strings.HasPrefix(path, "/network/"):
|
|
return "network"
|
|
case strings.HasPrefix(path, "/ports/"):
|
|
parts := strings.Split(strings.Trim(path, "/"), "/")
|
|
if len(parts) >= 2 {
|
|
if portID, err := strconv.Atoi(parts[1]); err == nil {
|
|
return fmt.Sprintf("port-%d", portID)
|
|
}
|
|
}
|
|
return "ports"
|
|
case strings.HasPrefix(path, "/port-debug"):
|
|
return "port-debug"
|
|
case strings.HasPrefix(path, "/admin/snmp"):
|
|
return "admin-snmp"
|
|
case strings.HasPrefix(path, "/admin/"):
|
|
return "admin"
|
|
case strings.HasPrefix(path, "/settings/"):
|
|
return "settings"
|
|
case strings.HasPrefix(path, "/tools/nettest/"):
|
|
return "network-test"
|
|
case strings.HasPrefix(path, "/tools/"):
|
|
return "tools"
|
|
case strings.HasPrefix(path, "/logs/"):
|
|
return "logs"
|
|
case path == "/port-status":
|
|
return "port-status"
|
|
case path == "/password":
|
|
return "password"
|
|
case path == "/config/save":
|
|
return "config-save"
|
|
case path == "/ports/restart":
|
|
return "ports-restart"
|
|
case path == "/device/restart":
|
|
return "device-restart"
|
|
default:
|
|
return strings.Trim(strings.ReplaceAll(path, "/", "-"), "-")
|
|
}
|
|
}
|
|
|