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.
 
 
 
 
 
 

1437 lines
60 KiB

package mock
import (
"fmt"
"strconv"
"strings"
"teraclone/internal/config"
)
type PageData struct {
AppName string
MockMode bool
CurrentUser string
CurrentRole string
WorkspaceID 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
Terminal *SerialTerminalData
Modal *ModalData
Message string
}
type SerialTerminalData struct {
Port int
Options []Option
}
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
Fixed bool
Checked bool
Hint string
Rows int
Min string
Max string
Accept string
Options []Option
Group string
Hidden bool
}
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/factory-reset":
return newPage(path, "공장 초기화", "공장 초기화", []string{"공장 초기화"}, nil, []ContentBlock{{
Kind: "form", Title: "공장 초기화", Subtitle: "저장된 장비 구성을 기본값으로 되돌리는 작업입니다. 실행 전 구성 백업을 권장합니다.",
Form: &FormData{Action: "factory-reset", PrimaryLabel: "공장 초기화 실행", Fields: []FormField{
{Label: "현재 계정 비밀번호", Name: "password", Type: "password", Required: true, Hint: "현재 로그인한 계정의 비밀번호로 인증합니다."},
}, Notes: []string{
"네트워크·포트·외부 인증 등 저장된 구성과 보류 중인 변경이 기본값으로 초기화됩니다.",
"사용자·사용자 정의 그룹·세션·로그·인증서·저장된 인증정보와 장비 내 진단 파일을 삭제합니다. 기본 계정 terauser / terauser만 복원됩니다.",
"초기화가 완료되면 자동 재부팅됩니다. 설치된 펌웨어는 유지됩니다.",
"연결이 끊어집니다. 재부팅 후 기본 접속 주소에서 terauser / terauser로 로그인하세요.",
}},
}}), 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 dashboardPage() PageData {
return newPage("/", "대시보드", "대시보드", []string{"대시보드"}, nil, []ContentBlock{
{Kind: "port-links", Title: "포트 링크 상태", PortLinks: portLinks()},
{
Kind: "key-values",
Title: "시스템 정보",
KeyValues: []KeyValue{
{"모델", "TERA CORA-X48 PRO K"},
{"빌드번호", "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 / Unknown"},
{"MAC 주소", "6C:B3:50:0D:36:12"},
{"다운로드 속도", "0 B/s"},
{"업로드 속도", "0 B/s"},
},
},
{
Kind: "key-values",
Title: "리소스 상태",
KeyValues: []KeyValue{
{"CPU 사용률", "14%"},
{"전체 메모리", "2 GB"},
{"사용 중 메모리", "1.3 GB"},
{"남은 메모리", "768 MB"},
{"메모리 사용률", "62.5%"},
{"온도", "53.8°C"},
{"활성 세션", "0"},
{"활성 알람", "0"},
},
},
{
Kind: "key-values",
Title: "포트 사용 현황",
KeyValues: []KeyValue{
{"활성 포트", "0"},
{"누적 수신", "0 B"},
{"누적 송신", "0 B"},
{"1위", "CH01 / 0 B"},
{"2위", "CH02 / 0 B"},
{"3위", "CH03 / 0 B"},
{"4위", "CH04 / 0 B"},
{"5위", "CH05 / 0 B"},
},
},
})
}
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 설정", "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),
groupedField(selectField("IPv6 모드", "ipv6mode", "Auto", []string{"Auto", "Disable", "Static"}), "ipv6", true),
groupedField(textField("IPv6 주소", "ipv6address", "", false), "ipv6", true),
groupedField(textField("Prefix Length", "prefix", "64", false), "ipv6", true),
groupedField(textField("IPv6 게이트웨이", "ipv6gateway", "", false), "ipv6", true),
groupedField(textField("IPv6 DNS 서버 1", "ipv6dns1", "", false), "ipv6", true),
groupedField(textField("IPv6 DNS 서버 2", "ipv6dns2", "", false), "ipv6", true),
}, "적용", ""),
}), true
case "/network/host":
return newPage(path, "네트워크 설정", "네트워크 설정", []string{"네트워크 설정", "호스트 설정"}, subtabs, []ContentBlock{
formBlock("호스트 설정", "network-host", []FormField{
textField("호스트 이름", "hostname", "teraclone-x48", true),
}, "적용", ""),
}), true
case "/network/advanced":
return newPage(path, "네트워크 설정", "네트워크 설정", []string{"네트워크 설정", "고급 설정"}, subtabs, []ContentBlock{
formBlock("운영 모드", "network-advanced", []FormField{
selectOptionsField("운영 모드", "mode", "single", []Option{{Label: "Single", Value: "single"}, {Label: "OOB", Value: "oob"}}),
selectOptionsField("고급 네트워크 모드", "advancedMode", "single", []Option{{Label: "Single", Value: "single"}, {Label: "Bonding", Value: "bonding"}, {Label: "Bridge", Value: "bridge"}}),
groupedField(selectOptionsField("본딩 모드", "bondMode", "active-backup", []Option{{Label: "로드 밸런싱", Value: "balance-rr"}, {Label: "링크 중복", Value: "active-backup"}}), "bond-settings", true),
groupedField(boundedNumberField("링크 감지 시간(ms)", "bondMii", "50", true, "1", "10000"), "bond-settings", true),
groupedField(toggleField("브리지 STP 사용", "bridgeSTP", false), "bridge-settings", true),
groupedField(fixedTextField("OOB 주 인터페이스", "primary", "Interface-1 (eth0)"), "oob-settings", true),
groupedField(fixedTextField("OOB 예비 인터페이스", "fallback", "Interface-2 (eth1)"), "oob-settings", true),
groupedField(textField("상태 확인 대상", "healthTarget", "1.1.1.1", true), "oob-settings", true),
groupedField(textField("확인 주기(초)", "healthInterval", "10", true), "oob-settings", true),
groupedField(textField("전환 실패 횟수", "failureThreshold", "3", true), "oob-settings", true),
}, "적용", ""),
}), true
case "/network/ddns":
return newPage(path, "네트워크 설정", "네트워크 설정", []string{"네트워크 설정", "DDNS 설정"}, subtabs, []ContentBlock{
formBlock("DDNS 설정", "network-ddns", []FormField{
toggleField("DDNS 사용", "ddns", false),
selectField("DNS 공급자", "provider", "dnspod", []string{"dnspod", "dyndns", "cloudflare", "noip", "freedns", "dynv6", "freemyip", "cloudxns", "3322", "custom"}),
toggleField("DDNS IPv4 사용", "ipv4Enabled", true),
textField("사용자 이름(ID)", "username", "", false),
passwordField("비밀번호(Token/Secret)", "password", ""),
textField("IPv4 도메인", "ipv4Domain", "", false),
textField("IP 확인 주소 (Check IP URL)", "checkIp", "", false),
textField("Custom Provider", "customProvider", "", false),
textField("Custom Provider Path", "customPath", "", 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"},
{"동작 설정 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"},
}
switch subPath {
case "parameters":
return newPortPage(path, portID, subtabs, []ContentBlock{
portSelectorBlock(),
{
Kind: "form",
Title: fmt.Sprintf("포트 설정 / Port %d", portID),
Form: &FormData{
Action: "#",
PrimaryLabel: "적용",
Fields: []FormField{
selectField("시리얼 타입", "serialType", "rs232", []string{"rs232"}),
selectField("Baudrate", "baudrate", "115200", []string{"110", "300", "600", "1200", "2400", "4800", "9600", "19200", "38400", "57600", "115200", "230400", "460800"}),
selectField("Databit", "databit", "8", []string{"5", "6", "7", "8"}),
selectField("Parity", "parity", "none", []string{"none", "odd", "even", "space", "mark"}),
selectField("Stopbit", "stopbit", "1", []string{"1", "1.5", "2"}),
selectField("Flow control", "flow", "none", []string{"none", "rtscts", "xonxoff"}),
selectField("RTS control", "rts", "auto", []string{"auto", "on", "off"}),
selectField("DTR control", "dtr", "auto", []string{"auto", "on", "off"}),
toggleField("Ignore jammed", "ignore", false),
},
PortTargets: portTargets(portID),
},
},
}), true
case "operation-1":
return newPortPage(path, portID, subtabs, []ContentBlock{
portSelectorBlock(),
formBlock("동작 설정 1 · eth0", "port-operation-1", []FormField{
portModeField("reverse_ssh"),
fixedTextField("Listen Port", "port", fmt.Sprintf("%d", 10000+portID)),
fixedTextField("Keep alive(초)", "keepalive", "3"),
fixedTextField("ACK 타임아웃(ms)", "timeout", "10000"),
fixedTextField("접속(유휴) 타임아웃(ms)", "connectionTimeout", "0"),
boundedNumberField("최대 세션 수", "maxClients", "4", true, "1", "4"),
authenticationField(),
selectField("줄바꿈", "newline", "cr", []string{"cr", "lf", "crlf"}),
shortcutField("제어 단축키", "controlShortcut", "^]", "Ctrl+] 후 T/Q/B/I/? 명령을 사용합니다."),
shortcutField("전환 단축키", "switchShortcut", "^T", "Reverse 모드의 현재 세션과 동시 접속 유지 상태를 확인합니다."),
shortcutField("세션 종료 단축키", "quitShortcut", "^Q", "현재 터미널 세션만 종료합니다."),
shortcutField("Break 단축키(선택)", "breakShortcut", "", "비워두면 직접 단축키는 사용하지 않습니다. 예: ^B"),
shortcutField("인터럽트 단축키(선택)", "interruptShortcut", "", "비워두면 사용하지 않습니다. 예: ^X"),
}, "적용", ""),
}), true
case "operation-2":
return newPortPage(path, portID, subtabs, []ContentBlock{
portSelectorBlock(),
formBlock("동작 설정 2 · eth1", "port-operation-2", []FormField{
portModeField("disabled"),
fixedTextField("Listen Port", "port", fmt.Sprintf("%d", 10000+portID)),
fixedTextField("Keep alive(초)", "keepalive", "3"),
fixedTextField("ACK 타임아웃(ms)", "timeout", "10000"),
fixedTextField("접속(유휴) 타임아웃(ms)", "connectionTimeout", "0"),
fixedTextField("최대 세션 수 (동작 설정 1과 공유)", "maxClients", "4"),
authenticationField(),
selectField("줄바꿈", "newline", "cr", []string{"cr", "lf", "crlf"}),
shortcutField("제어 단축키", "controlShortcut", "^]", "Ctrl+] 후 T/Q/B/I/? 명령을 사용합니다."),
shortcutField("전환 단축키", "switchShortcut", "^T", "Reverse 모드의 현재 세션과 동시 접속 유지 상태를 확인합니다."),
shortcutField("세션 종료 단축키", "quitShortcut", "^Q", "현재 터미널 세션만 종료합니다."),
shortcutField("Break 단축키(선택)", "breakShortcut", "", "비워두면 직접 단축키는 사용하지 않습니다. 예: ^B"),
shortcutField("인터럽트 단축키(선택)", "interruptShortcut", "", "비워두면 사용하지 않습니다. 예: ^X"),
}, "적용", ""),
}), true
case "data-log":
return newPortPage(path, portID, subtabs, []ContentBlock{
portSelectorBlock(),
formBlock("데이터 로그", "port-data-log", []FormField{
toggleField("Receive 기록", "read", true),
toggleField("Send 기록", "send", true),
toggleField("타임스탬프 표시", "showTimestamp", true),
toggleField("방향 표시", "showDirection", true),
toggleField("사용자 표시", "showUsername", false),
toggleField("IP 주소 표시", "showIP", false),
toggleField("포트 표시", "showPort", false),
selectOptionsField("로그 형식", "format", "ascii", []Option{
{Label: "ASCII", Value: "ascii"},
{Label: "HEX", Value: "hex"},
}),
}, "적용", ""),
}), true
default:
return PageData{}, false
}
}
func portSelectorBlock() ContentBlock {
return ContentBlock{
Kind: "port-selector",
Title: "포트 선택",
PortLinks: portLinks(),
}
}
func portDebugPage(path string) (PageData, bool) {
portID := 1
if path != "/port-debug" {
parts := strings.Split(strings.Trim(path, "/"), "/")
if len(parts) != 2 || parts[0] != "port-debug" {
return PageData{}, false
}
parsed, err := strconv.Atoi(parts[1])
if err != nil || parsed < 1 || parsed > 48 {
return PageData{}, false
}
portID = parsed
}
options := make([]Option, 0, 48)
for number := 1; number <= 48; number++ {
options = append(options, Option{
Label: fmt.Sprintf("CH%02d / Port %d", number, number),
Value: strconv.Itoa(number),
Selected: number == portID,
})
}
return newPage(path, "직접 연결 모드", fmt.Sprintf("직접 연결 모드 / Port %d", portID), []string{"직접 연결 모드"}, nil, []ContentBlock{
{
Kind: "serial-terminal",
Title: "시리얼 포트 CMD 콘솔",
Terminal: &SerialTerminalData{
Port: portID,
Options: options,
},
},
}), true
}
func adminPage(path string) (PageData, bool) {
switch path {
case "/admin/users":
return newPage(path, "관리자 운영", "사용자 계정", []string{"관리자 운영", "사용자 계정"}, nil, []ContentBlock{
tableBlock("사용자 계정", []string{"상태", "사용자 이름", "역할", "그룹", "비밀번호", "포트 권한"}, nil, "사용자 추가", ""),
userAccountModalBlock(),
}), true
case "/admin/auth/radius":
return newPage(path, "관리자 운영", "인증 서버", []string{"관리자 운영", "인증 서버", "RADIUS"}, authServerTabs(path), []ContentBlock{
formBlock("RADIUS 인증", "external-auth-radius", []FormField{
toggleField("RADIUS 사용", "enabled", false),
textField("서버 주소", "host", "", true),
boundedNumberField("인증 포트", "port", "1645", true, "1", "65535"),
passwordField("공유 Secret", "secret", "기존 값을 유지하려면 비워두세요"),
boundedNumberField("응답 타임아웃(초)", "timeout", "5", true, "1", "30"),
selectOptionsField("외부 사용자 기본 역할", "defaultRole", "port_operator", roleOptions()),
}, "적용", ""),
}), true
case "/admin/auth/tacacs":
return newPage(path, "관리자 운영", "인증 서버", []string{"관리자 운영", "인증 서버", "TACACS+"}, authServerTabs(path), []ContentBlock{
formBlock("TACACS+ 인증", "external-auth-tacacs", []FormField{
toggleField("TACACS+ 사용", "enabled", false),
textField("서버 주소", "host", "", true),
boundedNumberField("인증 포트", "port", "49", true, "1", "65535"),
passwordField("공유 Secret", "secret", "기존 값을 유지하려면 비워두세요"),
boundedNumberField("응답 타임아웃(초)", "timeout", "5", true, "1", "30"),
selectOptionsField("외부 사용자 기본 역할", "defaultRole", "port_operator", roleOptions()),
}, "적용", ""),
}), true
case "/admin/address-filter":
return newPage(path, "관리자 운영", "주소 필터", []string{"관리자 운영", "주소 필터"}, addressFilterTabs(path), []ContentBlock{
formBlock("관리 접속 주소 필터", "address-filter-policy", []FormField{
toggleField("주소 필터 사용", "enabled", false),
selectOptionsField("필터 방식", "mode", "blacklist", []Option{{Label: "차단 목록", Value: "blacklist", Selected: true}, {Label: "허용 목록", Value: "whitelist"}}),
}, "적용", ""),
}), true
case "/admin/address-filter/ipv4", "/admin/address-filter/mac":
return addressFilterListPage(path), true
case "/admin/address-filter/ipv4/new", "/admin/address-filter/mac/new":
return addressFilterNewPage(path), true
case "/admin/snmp":
return newPage(path, "관리자 운영", "SNMP", []string{"관리자 운영", "SNMP"}, nil, []ContentBlock{
formBlock("SNMP Agent", "snmp-agent", []FormField{
toggleField("SNMP 사용", "enabled", false),
textField("장비 위치(sysLocation)", "location", "", false),
textField("관리자 연락처(sysContact)", "contact", "", false),
toggleField("SNMP v1", "v1", false),
toggleField("SNMP v2c", "v2c", true),
toggleField("SNMP v3", "v3", false),
passwordField("Read Community", "roCommunity", "기존 값을 유지하려면 비워두세요"),
passwordField("Write Community", "rwCommunity", "기존 값을 유지하려면 비워두세요"),
textField("v3 사용자", "v3Username", "", false),
selectOptionsField("v3 접근 권한", "v3Access", "read", []Option{{Label: "읽기", Value: "read", Selected: true}, {Label: "읽기/쓰기", Value: "write"}}),
selectOptionsField("v3 인증", "v3Auth", "sha256", []Option{{Label: "없음", Value: "none"}, {Label: "MD5", Value: "md5"}, {Label: "SHA-1", Value: "sha1"}, {Label: "SHA-256", Value: "sha256", Selected: true}}),
selectOptionsField("v3 암호화", "v3Privacy", "aes", []Option{{Label: "없음", Value: "none"}, {Label: "DES", Value: "des"}, {Label: "AES", Value: "aes", Selected: true}}),
passwordField("v3 인증 키", "v3AuthKey", "기존 값을 유지하려면 비워두세요"),
passwordField("v3 암호화 키", "v3PrivacyKey", "기존 값을 유지하려면 비워두세요"),
toggleField("v3 사용자 사용", "v3UserEnabled", false),
}, "적용", ""),
}), true
case "/admin/ssl":
return newPage(path, "관리자 운영", "SSL 인증서", []string{"관리자 운영", "SSL 인증서"}, sslTabs(path), []ContentBlock{
formBlock("사용 인증서", "tls-active", []FormField{
selectOptionsField("활성 인증서", "activeCertificate", "", []Option{{Label: "인증서를 불러오는 중", Value: "", Selected: true}}),
}, "적용", ""),
tableBlock("인증서 목록", []string{"ID", "공통 이름", "유효 기간", "키", "상태", "관리"}, nil, "", ""),
}), true
case "/admin/ssl/new":
return newPage(path, "관리자 운영", "SSL 인증서 생성", []string{"관리자 운영", "SSL 인증서", "새로 만들기"}, sslTabs(path), []ContentBlock{
formBlock("자체 서명 인증서 생성", "tls-create", []FormField{
fixedTextField("인증서 ID", "certificateID", "접속 주소로 자동 생성"),
textField("공통 이름(CN)", "commonName", "", true),
textField("국가(C)", "country", "KR", false),
textField("시/도(ST)", "province", "", false),
textField("도시(L)", "locality", "", false),
textField("조직(O)", "organization", "", false),
textField("부서(OU)", "organizationalUnit", "", false),
textField("이메일", "email", "", false),
}, "생성", ""),
}), true
case "/admin/ssl/import":
return newPage(path, "관리자 운영", "SSL 인증서 가져오기", []string{"관리자 운영", "SSL 인증서", "가져오기"}, sslTabs(path), []ContentBlock{
formBlock("PEM 인증서 가져오기", "tls-import", []FormField{
fixedTextField("인증서 ID", "certificateID", "파일명으로 자동 생성"),
fileField("인증서 파일", "certificateFile", ".pem,.crt,.cer", true),
fileField("개인 키 파일", "privateKeyFile", ".pem,.key", false),
passwordField("개인 키 비밀번호", "password", "암호화된 키인 경우 입력"),
}, "가져오기", ""),
}), true
case "/admin/ssl/import-csr":
return newPage(path, "관리자 운영", "CSR 내보내기", []string{"관리자 운영", "SSL 인증서", "CSR 내보내기"}, sslTabs(path), []ContentBlock{
formBlock("인증서 서명 요청(CSR)", "tls-csr", []FormField{
selectOptionsField("인증서", "certificateID", "", []Option{{Label: "인증서를 불러오는 중", Value: "", Selected: true}}),
textField("공통 이름(CN)", "commonName", "", false),
textField("조직(O)", "organization", "", false),
textField("이메일", "email", "", false),
}, "CSR 다운로드", ""),
}), true
case "/admin/backup/export", "/admin/backup/import", "/admin/backup/restore":
if path == "/admin/backup/export" {
return newPage(path, "관리자 운영", "구성 내보내기", []string{"관리자 운영", "백업/복원", "내보내기"}, backupTabs(path), []ContentBlock{
formBlock("암호화 구성 백업", "config-export-encrypted", []FormField{
passwordField("백업 파일 암호", "key", "8~64자"),
passwordField("암호 확인", "confirmKey", "백업 파일 암호 재입력"),
fixedTextField("백업 범위", "scope", "네트워크를 포함한 영구 설정 전체"),
}, "백업 다운로드", ""),
}), true
}
return newPage(path, "관리자 운영", "구성 복원", []string{"관리자 운영", "백업/복원", "복원"}, backupTabs(path), []ContentBlock{
formBlock("암호화 백업 복원", "config-restore-encrypted", []FormField{
fileField("백업 파일", "backupFile", ".json,.tera-config.json", true),
passwordField("백업 파일 암호", "key", "내보낼 때 사용한 암호"),
toggleField("네트워크 설정 포함", "includeNetwork", false),
fixedTextField("안전 안내", "networkWarning", "네트워크 포함 시 현재 웹 연결이 끊길 수 있습니다"),
}, "복원 및 적용", ""),
}), true
case "/admin/firmware":
block := formBlock("펌웨어 업그레이드", "firmware-upgrade", []FormField{
fileField("서명된 펌웨어 패키지", "firmwareFile", ".pkg,application/octet-stream", true),
}, "업데이트 적용", "")
return newPage(path, "관리자 운영", "펌웨어 업그레이드", []string{"관리자 운영", "펌웨어"}, nil, []ContentBlock{block}), true
default:
return PageData{}, false
}
}
func settingsPage(path string) (PageData, bool) {
switch path {
case "/settings/alert/event":
return newPage(path, "기타 설정", "알림 설정", []string{"기타 설정", "알림 설정", "시스템 이벤트"}, alertTabs(path), []ContentBlock{
formBlock("시스템 이벤트 알림", "alarm-system-events", []FormField{
toggleField("Cold start", "coldStart", true),
toggleField("Warm start", "warmStart", true),
toggleField("로그인 실패 임계값", "authFailures", true),
toggleField("OOB 경로 전환", "linkFailover", true),
toggleField("시리얼 링크 DOWN", "serialLinkDown", true),
toggleField("이더넷 링크 DOWN", "ethernetLinkDown", true),
toggleField("저장 공간 경고", "storagePressure", true),
toggleField("메일로 전달", "deliveryEmail", false),
toggleField("SNMP Trap으로 전달", "deliverySNMP", false),
toggleField("Syslog로 전달", "deliverySyslog", true),
}, "적용", ""),
}), true
case "/settings/alert/serial-event":
return newPage(path, "기타 설정", "알림 설정", []string{"기타 설정", "알림 설정", "시리얼 이벤트"}, alertTabs(path), []ContentBlock{
formBlock("포트별 시리얼 이벤트", "alarm-serial-events", []FormField{
selectOptionsField("대상 포트", "channel", "CH01", channelSelectOptions()),
toggleField("DCD 상태 변경", "dcd", false),
toggleField("DSR 상태 변경", "dsr", false),
toggleField("수신 키워드 감지", "keyword", false),
textField("감지 키워드", "keywordValue", "", false),
toggleField("메일로 전달", "deliveryEmail", false),
toggleField("SNMP Trap으로 전달", "deliverySNMP", false),
toggleField("Syslog로 전달", "deliverySyslog", true),
}, "적용", ""),
}), true
case "/settings/alert/mail":
return newPage(path, "기타 설정", "알림 설정", []string{"기타 설정", "알림 설정", "메일"}, alertTabs(path), []ContentBlock{
formBlock("메일 알림", "alarm-email", []FormField{
toggleField("메일 알림 사용", "enabled", false),
textField("SMTP 서버", "smtp", "", true),
boundedNumberField("SMTP 포트", "port", "587", true, "1", "65535"),
toggleField("STARTTLS 사용", "starttls", true),
toggleField("SMTP 인증 사용", "auth", false),
textField("SMTP 사용자", "username", "", false),
passwordField("SMTP 비밀번호", "password", "기존 값을 유지하려면 비워두세요"),
textField("발신자", "from", "", true),
textField("수신자", "to", "", true),
textField("참조(CC)", "cc", "", false),
textField("제목", "title", "TERA 장비 알림", false),
textAreaField("본문", "content", "", 4),
}, "적용", "테스트 발송"),
}), true
case "/settings/alert/snmp-trap-filter":
return newPage(path, "기타 설정", "알림 설정", []string{"기타 설정", "알림 설정", "SNMP Trap"}, alertTabs(path), []ContentBlock{
formBlock("SNMP Trap 대상", "alarm-snmp-trap", []FormField{
toggleField("Trap 전송 사용", "enabled", false),
fixedTextField("대상 ID", "targetID", "primary-trap"),
textField("대상 주소", "host", "", true),
boundedNumberField("대상 포트", "port", "162", true, "1", "65535"),
selectOptionsField("버전", "version", "v2c", []Option{{Label: "v1", Value: "v1"}, {Label: "v2c", Value: "v2c", Selected: true}, {Label: "v3", Value: "v3"}}),
textField("v3 사용자", "username", "", false),
selectOptionsField("v3 인증", "authProtocol", "none", []Option{{Label: "없음", Value: "none", Selected: true}, {Label: "MD5", Value: "md5"}, {Label: "SHA-1", Value: "sha1"}, {Label: "SHA-256", Value: "sha256"}}),
selectOptionsField("v3 암호화", "privacyProtocol", "none", []Option{{Label: "없음", Value: "none", Selected: true}, {Label: "DES", Value: "des"}, {Label: "AES", Value: "aes"}}),
textField("v3 Engine ID", "engineID", "", false),
passwordField("Community / 인증 키", "credential", "기존 값을 유지하려면 비워두세요"),
passwordField("v3 암호화 키", "privacyKey", "기존 값을 유지하려면 비워두세요"),
}, "적용", "테스트 전송"),
}), true
case "/settings/system-log/remote":
tabs := systemLogSettingsTabs(path)
block := formBlock("로그 서버", "apply-log-server", []FormField{
toggleField("원격 로그 전송 사용", "remote", false),
{Label: "로그 서버", Name: "server", Type: "text", Placeholder: "도메인 또는 IP[:포트] 예: 192.168.0.233:514", Hint: "포트를 생략하면 표준 Syslog 포트 514를 사용합니다."},
selectField("전송 프로토콜", "protocol", "UDP", []string{"UDP", "TCP"}),
}, "적용", "")
block.Subtitle = "Syslog 서버를 도메인 또는 IP 주소로 지정합니다. 포트는 주소 뒤에 콜론(:)으로 입력할 수 있습니다."
return newPage(path, "기타 설정", "시스템 로그 - 로그 서버", []string{"기타 설정", "시스템 로그", "로그 서버"}, tabs, []ContentBlock{block}), true
case "/settings/system-log/status-event", "/settings/system-log/events":
tabs := systemLogSettingsTabs(path)
block := formBlock("로그 이벤트", "apply-log-events", []FormField{
toggleField("시스템 이벤트 - 로컬 저장", "localSystem", true),
toggleField("시스템 이벤트 - 원격 전송", "remoteSystem", false),
toggleField("설정 변경 이벤트 - 로컬 저장", "localConfiguration", true),
toggleField("설정 변경 이벤트 - 원격 전송", "remoteConfiguration", false),
toggleField("운영 모드 이벤트 - 로컬 저장", "localOperation", true),
toggleField("운영 모드 이벤트 - 원격 전송", "remoteOperation", false),
}, "적용", "")
block.Subtitle = "시스템 시작/재시작/로그인 실패, 설정 변경, 포트 운영 세션 이벤트의 저장 위치를 선택합니다."
block.Form.Notes = []string{
"시스템 이벤트: Cold start, Warm start, 로그인 실패",
"설정 변경 이벤트: 사용자가 웹 또는 콘솔에서 장비 설정을 변경한 기록",
"운영 모드 이벤트: Reverse SSH/Reverse Telnet 세션 연결 및 해제 기록",
}
return newPage(path, "기타 설정", "시스템 로그 - 로그 이벤트", []string{"기타 설정", "시스템 로그", "로그 이벤트"}, tabs, []ContentBlock{block}), 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", "", false),
textField("수동 시간", "time", "", false),
}), true
default:
return PageData{}, false
}
}
func toolsPage(path string) (PageData, bool) {
switch path {
case "/tools/nettest/ping":
fields := []FormField{
textField("IP 주소 / 도메인 이름", "address", "192.168.0.100", true),
numberField("핑 횟수", "count", "4", true),
fixedTextField("핑 패킷 크기", "size", "56 bytes"),
fixedTextField("핑 응답 타임아웃", "replyTimeout", "1초 (고정)"),
}
return newPage(path, "관리자 운영", "진단 도구", []string{"관리자 운영", "진단 도구"}, nil, []ContentBlock{
{
Kind: "tool",
Title: "핑 테스트",
Tool: &ToolData{
Action: "#",
PrimaryLabel: "테스트",
Fields: fields,
Result: "테스트를 실행하면 백엔드 진단 작업 결과가 여기에 표시됩니다.",
},
},
}), true
default:
return PageData{}, false
}
}
func logsPage(path string) (PageData, bool) {
tabs := []SubTab{
{Label: "시스템 로그", Path: "/logs/system", Active: path == "/logs/system"},
{Label: "포트 로그", Path: "/logs/ports", Active: path == "/logs/ports"},
{Label: "포트 상태 이벤트", Path: "/logs/links", Active: path == "/logs/links"},
}
limitOptions := []Option{
{Label: "50건", Value: "50"},
{Label: "100건", Value: "100"},
{Label: "200건", Value: "200"},
{Label: "500건", Value: "500"},
}
switch path {
case "/logs/system":
return newPage(path, "로그", "시스템 로그", []string{"로그", "시스템 로그"}, tabs, []ContentBlock{
{
Kind: "form",
Title: "검색 조건",
Subtitle: "일반 시스템 이벤트와 보존 대상 감사 기록을 조회합니다. 클리어는 감사 기록을 삭제하지 않습니다.",
Form: &FormData{
Action: "#",
PrimaryLabel: "조회",
SecondaryLabel: "검색 초기화",
Fields: []FormField{
{Label: "로그 종류", Name: "category", Type: "text", Hidden: true},
selectOptionsField("등급", "severity", "", []Option{{Label: "전체", Value: ""}, {Label: "정보", Value: "info"}, {Label: "경고", Value: "warning"}, {Label: "치명적", Value: "critical"}}),
{Label: "검색어", Name: "keyword", Type: "text", Placeholder: "소스 또는 메시지"},
{Label: "시작 시간", Name: "from", Type: "datetime-local"},
{Label: "종료 시간", Name: "to", Type: "datetime-local"},
selectOptionsField("페이지당 표시", "limit", "100", limitOptions),
selectField("다운로드 형식", "export_format", "CSV", []string{"CSV", "JSON", "JSONL"}),
selectField("최대 다운로드 건수", "export_limit", "1000", []string{"100", "500", "1000", "5000"}),
toggleField("자동 갱신 (5초)", "auto_refresh", true),
},
},
},
{
Kind: "table",
Title: "시스템 로그",
Subtitle: "최신 이벤트가 위에 표시됩니다.",
Table: &TableData{
Columns: []string{"시간", "등급", "분류", "발생 위치", "내용"},
EmptyMessage: "조건에 맞는 시스템 로그가 없습니다.",
PrimaryLabel: "새로고침",
SecondaryLabel: "로그 다운로드",
},
},
}), true
case "/logs/ports":
channelOptions := []Option{{Label: "전체 포트", Value: ""}}
for port := 1; port <= 48; port++ {
channelOptions = append(channelOptions, Option{Label: fmt.Sprintf("CH%02d", port), Value: fmt.Sprintf("CH%02d", port)})
}
return newPage(path, "로그", "포트 로그", []string{"로그", "포트 로그"}, tabs, []ContentBlock{
{
Kind: "form",
Title: "검색 조건",
Subtitle: "포트별 RX/TX 데이터·명령·접속/종료 기록을 조회합니다. UP/DOWN 변경은 포트 상태 이벤트에서 확인하세요.",
Form: &FormData{
Action: "#",
PrimaryLabel: "조회",
SecondaryLabel: "검색 초기화",
Fields: []FormField{
selectOptionsField("포트", "channel", "", channelOptions),
{Label: "로그 종류", Name: "event_type", Type: "text", Hidden: true},
selectOptionsField("방향", "direction", "", []Option{{Label: "전체", Value: ""}, {Label: "RX", Value: "RX"}, {Label: "TX", Value: "TX"}}),
{Label: "사용자", Name: "actor", Type: "text", Placeholder: "사용자 이름"},
{Label: "검색어", Name: "keyword", Type: "text", Placeholder: "명령, 결과 또는 메시지"},
{Label: "시작 시간", Name: "from", Type: "datetime-local"},
{Label: "종료 시간", Name: "to", Type: "datetime-local"},
selectOptionsField("페이지당 표시", "limit", "100", limitOptions),
selectField("다운로드 형식", "export_format", "CSV", []string{"CSV", "JSON", "JSONL"}),
selectField("최대 다운로드 건수", "export_limit", "1000", []string{"100", "500", "1000", "5000"}),
toggleField("자동 갱신 (5초)", "auto_refresh", true),
},
},
},
{
Kind: "table",
Title: "포트 로그",
Subtitle: "포트별 통신 이력 · 내보내기는 현재 검색 조건과 최대 다운로드 건수 범위에 적용됩니다.",
Table: &TableData{
Columns: []string{"시간", "포트", "유형", "방향", "사용자", "내용"},
EmptyMessage: "조건에 맞는 포트 로그가 없습니다.",
PrimaryLabel: "새로고침",
SecondaryLabel: "로그 다운로드",
},
},
}), true
case "/logs/links":
resourceOptions := []Option{{Label: "전체 포트", Value: ""}}
for port := 1; port <= 48; port++ {
resourceOptions = append(resourceOptions, Option{Label: fmt.Sprintf("CH%02d", port), Value: fmt.Sprintf("CH%02d", port)})
}
return newPage(path, "로그", "포트 상태 이벤트", []string{"로그", "포트 상태 이벤트"}, tabs, []ContentBlock{
{
Kind: "form",
Title: "검색 조건",
Subtitle: "포트의 UP/DOWN 및 제공되는 신호 변경 기록을 조회합니다. RX/TX 데이터·명령·접속 기록은 포트 로그에서 확인하세요.",
Form: &FormData{
Action: "#",
PrimaryLabel: "조회",
SecondaryLabel: "검색 초기화",
Fields: []FormField{
selectOptionsField("포트", "resource", "", resourceOptions),
{Label: "이벤트 종류", Name: "event_type", Type: "text", Value: "transition", Hidden: true},
selectOptionsField("상태", "state", "", []Option{{Label: "전체", Value: ""}, {Label: "UP", Value: "up"}, {Label: "DOWN", Value: "down"}, {Label: "Unknown", Value: "unknown"}}),
{Label: "신호 / 검색어", Name: "keyword", Type: "text", Placeholder: "DCD, DSR, CTS 또는 메시지"},
{Label: "시작 시간", Name: "from", Type: "datetime-local"},
{Label: "종료 시간", Name: "to", Type: "datetime-local"},
selectOptionsField("페이지당 표시", "limit", "100", limitOptions),
selectField("다운로드 형식", "export_format", "CSV", []string{"CSV", "JSON", "JSONL"}),
selectField("최대 다운로드 건수", "export_limit", "1000", []string{"100", "500", "1000", "5000"}),
toggleField("자동 갱신 (5초)", "auto_refresh", true),
},
Notes: []string{"장비 하드웨어가 제공하지 않는 DCD/DSR 신호는 기록할 수 없습니다.", "수신 키워드는 포트 로그의 검색어로 조회할 수 있습니다."},
},
},
{
Kind: "table",
Title: "포트 상태 이벤트",
Subtitle: "상태 변경 기록만 조회·내보내기합니다.",
Table: &TableData{
Columns: []string{"시간", "포트", "신호", "상태 변화", "유형", "내용"},
EmptyMessage: "조건에 맞는 포트 상태 이벤트가 없습니다.",
PrimaryLabel: "새로고침",
SecondaryLabel: "로그 다운로드",
},
},
}), true
default:
return PageData{}, false
}
}
func systemLogSettingsTabs(path string) []SubTab {
return []SubTab{
{Label: "로그 서버", Path: "/settings/system-log/remote", Active: path == "/settings/system-log/remote"},
{Label: "로그 이벤트", Path: "/settings/system-log/events", Active: path == "/settings/system-log/events" || path == "/settings/system-log/status-event"},
}
}
func portStatusPage() PageData {
options := make([]Option, 0, 48)
for number := 1; number <= 48; number++ {
options = append(options, Option{Label: fmt.Sprintf("CH%02d", number), Value: fmt.Sprintf("CH%02d", number)})
}
return newPage("/port-status", "포트 상태", "포트 상태", []string{"포트 상태"}, nil, []ContentBlock{
formBlock("포트 별칭", "port-alias", []FormField{
selectOptionsField("포트 선택", "channel", "CH01", options),
textField("포트 별칭", "alias", "", false),
}, "변경", ""),
{
Kind: "table",
Title: "포트 상태 요약",
Table: &TableData{
Columns: []string{"포트", "상태", "별칭", "프로토콜", "세션", "RX/TX", "세션 1 IP", "세션 2 IP", "세션 3 IP", "세션 4 IP"},
EmptyMessage: "데이터가 없습니다.",
},
},
})
}
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+" 확인", "이 작업은 백엔드 API를 통해 장비 구성에 반영됩니다.", []FormField{
textAreaField("안내", "note", "작업 전 현재 구성과 서비스 상태를 확인하세요.", 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 ApplyRole(page PageData, role string) PageData {
page.CurrentRole = strings.ToLower(strings.TrimSpace(role))
if page.CurrentRole != "port_operator" {
return page
}
allowed := map[string]bool{"대시보드": true, "포트 설정": true, "포트 상태": true}
filtered := make([]MenuGroup, 0, 3)
for _, group := range page.Menu {
if allowed[group.Label] {
filtered = append(filtered, group)
}
}
page.Menu = filtered
tabs := page.SubTabs[:0]
for _, tab := range page.SubTabs {
if !strings.HasSuffix(tab.Path, "/data-log") {
tabs = append(tabs, tab)
}
}
page.SubTabs = tabs
return page
}
func formBlock(title string, action string, fields []FormField, primary string, secondary string) ContentBlock {
return ContentBlock{
Kind: "form",
Title: title,
Form: &FormData{
Action: "#",
PrimaryLabel: primary,
SecondaryLabel: secondary,
Fields: fields,
},
}
}
func groupedField(field FormField, group string, hidden bool) FormField {
field.Group = group
field.Hidden = hidden
return field
}
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 userAccountModalBlock() ContentBlock {
return ContentBlock{
Kind: "modal",
Title: "사용자 추가",
Modal: &ModalData{
Root: "user-create",
Title: "사용자 추가",
Message: "장비의 Local 인증 저장소에 사용자를 추가합니다.",
Fields: []FormField{
{Type: "hidden", Name: "id", Value: ""},
{Label: "사용자 이름", Name: "username", Type: "text", Required: true, Placeholder: "영문 소문자, 숫자, 점, 하이픈, 밑줄", Hint: "최대 64자이며 생성 후에는 변경할 수 없습니다."},
{Label: "비밀번호", Name: "password", Type: "password", Required: true, Placeholder: "8자 이상 입력", Hint: "수정할 때 비워두면 기존 비밀번호를 유지합니다."},
{
Label: "그룹", Name: "group", Type: "select", Required: true, Value: "administrators",
Options: []Option{
{Label: "Administrators (관리자)", Value: "administrators", Selected: true},
{Label: "Port operators (포트 운영자)", Value: "port_operators"},
},
},
{
Label: "계정 상태", Name: "enabled", Type: "select", Value: "true",
Options: []Option{{Label: "사용", Value: "true", Selected: true}, {Label: "중지", Value: "false"}},
},
},
Buttons: []ActionButton{
{Label: "저장", Action: "submit-user-form", 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 newPage(path, "관리자 운영", filterName, []string{"관리자 운영", "주소 필터", filterName}, addressFilterTabs(path), []ContentBlock{
tableBlock(filterName, []string{"규칙 ID", "주소", "프로토콜", "포트", "상태", "관리"}, nil, "규칙 추가", ""),
})
}
func addressFilterNewPage(path string) PageData {
title := "IPv4 필터 추가"
fields := []FormField{
fixedTextField("규칙 ID", "name", "주소와 프로토콜로 자동 생성"),
textField("IPv4 주소/CIDR", "address", "192.168.0.0/24", true),
selectOptionsField("프로토콜", "protocol", "all", protocolOptions()),
fixedTextField("목적지 포트", "port", "0"),
toggleField("규칙 사용", "enabled", true),
}
if strings.Contains(path, "ipv6") {
title = "IPv6 필터 추가"
fields = []FormField{
fixedTextField("규칙 ID", "name", "주소와 프로토콜로 자동 생성"),
textField("IPv6 주소/CIDR", "address", "2001:db8::/64", true),
selectOptionsField("프로토콜", "protocol", "all", protocolOptions()),
fixedTextField("목적지 포트", "port", "0"),
toggleField("규칙 사용", "enabled", true),
}
}
if strings.Contains(path, "mac") {
title = "MAC 필터 추가"
fields = []FormField{
fixedTextField("규칙 ID", "name", "주소와 프로토콜로 자동 생성"),
textField("MAC 주소", "address", "00:11:22:33:44:55", true),
selectOptionsField("프로토콜", "protocol", "all", protocolOptions()),
fixedTextField("목적지 포트", "port", "0"),
toggleField("규칙 사용", "enabled", true),
}
}
return newPage(path, "관리자 운영", title, []string{"관리자 운영", "주소 필터", title}, addressFilterTabs(path), []ContentBlock{formBlock(title, "address-filter-add", fields, "추가", "")})
}
func authServerTabs(path string) []SubTab {
return []SubTab{{"RADIUS", "/admin/auth/radius", path == "/admin/auth/radius"}, {"TACACS+", "/admin/auth/tacacs", path == "/admin/auth/tacacs"}}
}
func addressFilterTabs(path string) []SubTab {
return []SubTab{
{"기본", "/admin/address-filter", path == "/admin/address-filter"},
{"IPv4", "/admin/address-filter/ipv4", strings.Contains(path, "/ipv4")},
{"MAC", "/admin/address-filter/mac", strings.Contains(path, "/mac")},
}
}
func sslTabs(path string) []SubTab {
return []SubTab{
{"인증서", "/admin/ssl", path == "/admin/ssl"},
{"새로 만들기", "/admin/ssl/new", path == "/admin/ssl/new"},
{"가져오기", "/admin/ssl/import", path == "/admin/ssl/import"},
{"CSR 내보내기", "/admin/ssl/import-csr", path == "/admin/ssl/import-csr"},
}
}
func backupTabs(path string) []SubTab {
return []SubTab{{"내보내기", "/admin/backup/export", path == "/admin/backup/export"}, {"복원", "/admin/backup/restore", path != "/admin/backup/export"}}
}
func alertTabs(path string) []SubTab {
return []SubTab{
{"시스템 이벤트", "/settings/alert/event", path == "/settings/alert/event"},
{"시리얼 이벤트", "/settings/alert/serial-event", path == "/settings/alert/serial-event"},
{"메일", "/settings/alert/mail", path == "/settings/alert/mail"},
{"SNMP Trap", "/settings/alert/snmp-trap-filter", path == "/settings/alert/snmp-trap-filter"},
}
}
func roleOptions() []Option {
return []Option{{Label: "포트 운영자", Value: "port_operator", Selected: true}, {Label: "관리자", Value: "administrator"}}
}
func protocolOptions() []Option {
return []Option{{Label: "전체", Value: "all", Selected: true}, {Label: "HTTP", Value: "http"}, {Label: "HTTPS", Value: "https"}, {Label: "SSH", Value: "ssh"}, {Label: "Telnet", Value: "telnet"}}
}
func channelSelectOptions() []Option {
options := make([]Option, 0, 48)
for channel := 1; channel <= 48; channel++ {
value := fmt.Sprintf("CH%02d", channel)
options = append(options, Option{Label: value, Value: value, Selected: channel == 1})
}
return options
}
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: "LG", Path: "/logs/system"},
{Label: "포트 상태", Icon: "PS", Path: "/port-status"},
{Label: "공장 초기화", Icon: "FR", Path: "/device/factory-reset"},
}
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/") || strings.HasPrefix(path, "/tools/nettest/")
groups[i].Active = groups[i].Expanded
groups[i].Children = append(groups[i].Children,
MenuChild{"사용자 계정", "/admin/users", strings.HasPrefix(path, "/admin/users")},
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"},
MenuChild{"진단 도구", "/tools/nettest/ping", strings.HasPrefix(path, "/tools/nettest/")},
)
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, "/logs/")
groups[i].Children = append(groups[i].Children,
MenuChild{"시스템 로그", "/logs/system", path == "/logs/system"},
MenuChild{"포트 로그", "/logs/ports", path == "/logs/ports"},
MenuChild{"포트 상태 이벤트", "/logs/links", path == "/logs/links"},
)
}
}
return groups
}
func portLinks() []PortLink {
links := make([]PortLink, 0, 48)
for i := 1; i <= 48; i++ {
links = append(links, PortLink{
Number: i,
// The real state is populated by /api/v1/ports. Rendering guessed
// values here causes a visible false-UP flash during page changes.
Up: false,
Blink: false,
})
}
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 shortcutField(label, name, value, hint string) FormField {
// Shortcut values remain in the form so the integration layer can preserve
// the running configuration, but they are intentionally not user-facing.
return FormField{Label: label, Name: name, Type: "text", Value: value, Hidden: true}
}
func numberField(label string, name string, value string, required bool) FormField {
return boundedNumberField(label, name, value, required, "1", "20")
}
func boundedNumberField(label string, name string, value string, required bool, min string, max string) FormField {
return FormField{Label: label, Name: name, Type: "number", Value: value, Required: required, Min: min, Max: max}
}
func fixedTextField(label string, name string, value string) FormField {
return FormField{Label: label, Name: name, Type: "text", Value: value, Disabled: true, Fixed: true}
}
func passwordField(label string, name string, value string) FormField {
return FormField{Label: label, Name: name, Type: "password", Placeholder: value}
}
func fileField(label string, name string, accept string, required bool) FormField {
return FormField{Label: label, Name: name, Type: "file", Accept: accept, Required: required}
}
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 selectOptionsField(label string, name string, value string, options []Option) FormField {
field := FormField{Label: label, Name: name, Type: "select", Value: value}
for _, option := range options {
option.Selected = option.Value == value
field.Options = append(field.Options, option)
}
return field
}
func portModeField(value string) FormField {
return selectOptionsField("동작 모드", "mode", value, []Option{
{Label: "사용 안 함", Value: "disabled"},
{Label: "Reverse SSH", Value: "reverse_ssh"},
{Label: "Reverse Telnet", Value: "reverse_telnet"},
})
}
func authenticationField() FormField {
return selectOptionsField("인증 방식", "authentication", "local", []Option{
{Label: "Local", Value: "local"},
{Label: "RADIUS", Value: "radius"},
{Label: "TACACS", Value: "tacacs"},
})
}
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/"):
return "port-settings"
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, "/", "-"), "-")
}
}