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.

1129 lines
41 KiB

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