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 ) ,
selectField ( "로그 형식" , "format" , "ascii" , [ ] string { "ascii" , "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 , "/" , "-" ) , "-" )
}
}