// D:\Study\go\003_random_gui\random.go package main import ( "encoding/base64" "encoding/hex" "errors" "fmt" "math/rand" "net/url" "os" "path/filepath" "runtime" "strconv" "strings" "time" "fyne.io/fyne/v2" "fyne.io/fyne/v2/app" "fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/data/binding" "fyne.io/fyne/v2/dialog" "fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/widget" ) // --- 전역 변수 및 상수 정의 --- var ( // 비밀번호 생성에 사용될 문자 집합 lowerCharSet = "abcdefghijklmnopqrstuvwxyz" upperCharSet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" specialCharSet = "!@#$%&*" // Fyne의 Label에서 문제를 일으킬 수 있는 일부 특수문자 제외 고려 numberSet = "0123456789" allCharSet = lowerCharSet + upperCharSet + specialCharSet + numberSet letterCharSet = lowerCharSet + upperCharSet currentPassword string // 현재 생성된 비밀번호 저장 notificationTimer *time.Timer // 하단 알림 메시지 타이머 ) const ( // 비밀번호 정책 관련 상수 minSpecialChar = 1 minNum = 1 minUpperCase = 2 maxLength = 64 notificationDuration = 3 * time.Second // 알림 메시지 표시 시간 (초) ) // --- UI 생성 헬퍼 함수 --- // createEntryWithBackground 함수는 배경이 있는 Entry 위젯 영역을 생성합니다. // placeholder: Entry에 표시될 안내 문구 // isMultiLine: 여러 줄 입력 가능 여부 // isReadOnlyStyle: 읽기 전용 스타일 적용 여부 (굵게 표시) // 반환값: UI 객체 (fyne.CanvasObject), 생성된 Entry 위젯 (*widget.Entry) func createEntryWithBackground(placeholder string, isMultiLine bool, isReadOnlyStyle bool) (fyne.CanvasObject, *widget.Entry) { entry := widget.NewEntry() entry.SetPlaceHolder(placeholder) // 텍스트 스타일 설정 textStyle := fyne.TextStyle{Monospace: true} if isReadOnlyStyle { // 읽기 전용으로 사용될 Entry는 굵게 표시 textStyle.Bold = true } entry.TextStyle = textStyle // 여러 줄 입력 설정 if isMultiLine { entry.MultiLine = true entry.Wrapping = fyne.TextWrapWord // 단어 단위 줄 바꿈 entry.SetMinRowsVisible(2) // 여러 줄 Entry는 최소 2줄 높이로 시작 (시각적 균형) } // 배경 사각형 생성 background := canvas.NewRectangle(theme.InputBackgroundColor()) background.CornerRadius = theme.InputRadiusSize() // 테마 기본 모서리 둥글기 // 내용물 (Entry 또는 스크롤된 Entry) var content fyne.CanvasObject if isMultiLine { content = container.NewScroll(entry) // 여러 줄은 스크롤 가능하게 } else { content = entry // 한 줄은 그대로 } // 배경과 내용물을 겹쳐서 최종 UI 객체 생성 entryArea := container.NewStack( background, container.NewPadded(content), // 내용물 주변에 기본 패딩 적용 ) return entryArea, entry } // --- 메인 애플리케이션 로직 --- func main() { // 패닉 복구: 예기치 않은 오류 발생 시 애플리케이션 비정상 종료 방지 및 안내 defer func() { if r := recover(); r != nil { // 개발 및 사용자 안내용 오류 메시지 출력 fmt.Println("====================================================================") fmt.Println("⚠️ 프로그램 실행 중 심각한 오류 발생 ⚠️") fmt.Println("--------------------------------------------------------------------") fmt.Println("오류 내용:", r) fmt.Println("--------------------------------------------------------------------") fmt.Println("\nFyne GUI 라이브러리 또는 관련 시스템 의존성 문제일 수 있습니다.") fmt.Println("1. Go 패키지 확인: 'go get fyne.io/fyne/v2'") fmt.Println("2. 시스템 의존성 확인 (C 컴파일러, 그래픽 라이브러리 등)") fmt.Println(" - Fyne 공식 문서 (fyne.io)의 설치 가이드를 참조하세요.") fmt.Println("--------------------------------------------------------------------") fmt.Println("\n10초 후 자동으로 종료됩니다...") time.Sleep(10 * time.Second) } }() // Fyne 애플리케이션 객체 생성 myApp := app.NewWithID("com.yourdomain.securetools.final") // 고유 앱 ID myApp.Settings().SetTheme(theme.DarkTheme()) // 어두운 테마 적용 // 메인 윈도우 생성 myWindow := myApp.NewWindow("🔑 Secure Tools 🛠️") // 창 제목 // 하단 알림 메시지용 레이블 초기화 (처음에는 숨김) notificationLabel := widget.NewLabel("") notificationLabel.Alignment = fyne.TextAlignCenter notificationLabel.Hide() // 랜덤 생성기 초기화 localRand := rand.New(rand.NewSource(time.Now().UnixNano())) minRequiredLength := minSpecialChar + minNum + minUpperCase + 1 // 비밀번호 최소 길이 계산 // --- 비밀번호 생성 섹션 UI 정의 --- lengthLabel := widget.NewLabel("길이:") passwordLengthArea, passwordLengthEntry := createEntryWithBackground( fmt.Sprintf("%d-%d", minRequiredLength, maxLength), // 플레이스홀더 false, // 한 줄 입력 false, // 일반 스타일 ) passwordResultArea, passwordResultEntry := createEntryWithBackground( "", // 생성된 비밀번호가 표시될 곳 (초기값 없음) false, // 한 줄 표시 true, // 읽기 전용 스타일 (굵게) ) passwordResultEntry.Disable() // 초기에는 비활성화 (읽기 전용) // "복사" 버튼 copyPasswordButton := widget.NewButtonWithIcon("복사", theme.ContentCopyIcon(), func() { if currentPassword == "" { showNotification("먼저 비밀번호를 생성하세요.", notificationLabel) return } myWindow.Clipboard().SetContent(currentPassword) showNotification("비밀번호가 클립보드에 복사되었습니다.", notificationLabel) }) copyPasswordButton.Disable() // 초기 비활성화 // "저장" 버튼 savePasswordButton := widget.NewButtonWithIcon("저장", theme.DocumentSaveIcon(), func() { if currentPassword == "" { showNotification("먼저 비밀번호를 생성하세요.", notificationLabel) return } showSavePopUp(myApp, currentPassword, myWindow) // 파일 저장 팝업 호출 }) savePasswordButton.Disable() // 초기 비활성화 // "생성하기" 버튼 generatePasswordButton := widget.NewButtonWithIcon("생성하기", theme.ConfirmIcon(), func() { input := strings.TrimSpace(passwordLengthEntry.Text) length, err := strconv.Atoi(input) if err != nil || length < minRequiredLength || length > maxLength { errorMsg := fmt.Sprintf("숫자를 입력하고, 길이는 %d자 이상 %d자 이하여야 합니다.", minRequiredLength, maxLength) showNotification(errorMsg, notificationLabel) passwordResultEntry.Enable() passwordResultEntry.SetText("- 오류 -") passwordResultEntry.Disable() currentPassword = "" copyPasswordButton.Disable() savePasswordButton.Disable() return } currentPassword = generatePassword(localRand, length, minSpecialChar, minNum, minUpperCase) passwordResultEntry.Enable() passwordResultEntry.SetText(currentPassword) passwordResultEntry.Disable() copyPasswordButton.Enable() savePasswordButton.Enable() }) generatePasswordButton.Importance = widget.HighImportance // 주요 실행 버튼으로 강조 // 비밀번호 생성 섹션 레이아웃 조립 passwordInputControls := container.NewBorder(nil, nil, lengthLabel, generatePasswordButton, passwordLengthArea) passwordResultControls := container.NewVBox(widget.NewLabel("생성된 비밀번호:"), passwordResultArea) passwordActionButtons := container.NewGridWithColumns(2, copyPasswordButton, savePasswordButton) passwordGeneratorSection := container.NewVBox( widget.NewLabelWithStyle("비밀번호 생성", fyne.TextAlignLeading, fyne.TextStyle{Bold: true, Italic: true}), container.NewPadded(passwordInputControls), container.NewPadded(passwordResultControls), container.NewPadded(passwordActionButtons), ) // --- 인코딩/디코딩 섹션 UI 정의 --- encodeDecodeInputArea, encodeDecodeInputEntry := createEntryWithBackground( "인코딩 또는 디코딩할 텍스트 입력...", true, // 여러 줄 입력 가능 false, // 일반 스타일 ) encodeDecodeResultArea, encodeDecodeResultEntry := createEntryWithBackground( "결과가 여기에 표시됩니다.", true, // 여러 줄 표시 가능 false, // 일반 스타일 (읽기 전용이므로 Disable 처리) ) encodeDecodeResultEntry.Disable() // 결과 표시는 읽기 전용 // 인코딩/디코딩 옵션 encodingTypeOptions := []string{"Base64", "Hex", "URL", "ROT13"} encodingTypeBinding := binding.NewString() _ = encodingTypeBinding.Set(encodingTypeOptions[0]) // 초기값 설정 encodingSelect := widget.NewSelect(encodingTypeOptions, func(s string) { _ = encodingTypeBinding.Set(s) }) encodingSelect.SetSelected(encodingTypeOptions[0]) operationBinding := binding.NewString() _ = operationBinding.Set("encode") // 초기값 설정 operationRadioGroup := widget.NewRadioGroup([]string{"인코딩", "디코딩"}, func(s string) { if s == "인코딩" { _ = operationBinding.Set("encode") } else { _ = operationBinding.Set("decode") } }) operationRadioGroup.SetSelected("인코딩") operationRadioGroup.Horizontal = true // "생성된 비밀번호 사용" 버튼 useGeneratedPwdButton := widget.NewButton("생성된 비밀번호 사용", func() { if currentPassword != "" { encodeDecodeInputEntry.SetText(currentPassword) } else { showNotification("먼저 비밀번호를 생성하세요.", notificationLabel) } }) // "실행" (인코딩/디코딩) 버튼 executeEncodeDecodeButton := widget.NewButtonWithIcon("실행", theme.MediaPlayIcon(), func() { inputText := encodeDecodeInputEntry.Text if strings.TrimSpace(inputText) == "" { showNotification("입력값이 없습니다.", notificationLabel) encodeDecodeResultEntry.Enable() encodeDecodeResultEntry.SetText("") encodeDecodeResultEntry.Disable() return } selectedEncoding, _ := encodingTypeBinding.Get() selectedOperation, _ := operationBinding.Get() var resultText string var err error // 인코딩/디코딩 로직 switch selectedEncoding { case "Base64": if selectedOperation == "encode" { resultText = base64.StdEncoding.EncodeToString([]byte(inputText)) } else { /* decode */ decodedBytes, decErr := base64.StdEncoding.DecodeString(inputText) if decErr != nil { err = fmt.Errorf("Base64 디코딩 오류: %w", decErr) } else { resultText = string(decodedBytes) } } case "Hex": if selectedOperation == "encode" { resultText = hex.EncodeToString([]byte(inputText)) } else { /* decode */ decodedBytes, decErr := hex.DecodeString(inputText) if decErr != nil { err = fmt.Errorf("HEX 디코딩 오류: %w", decErr) } else { resultText = string(decodedBytes) } } case "URL": if selectedOperation == "encode" { resultText = url.QueryEscape(inputText) } else { /* decode */ decodedText, decErr := url.QueryUnescape(inputText) if decErr != nil { err = fmt.Errorf("URL 디코딩 오류: %w", decErr) } else { resultText = decodedText } } case "ROT13": resultText = rot13(inputText) // ROT13은 인코딩/디코딩 동일 default: err = errors.New("알 수 없는 인코딩 방식") } encodeDecodeResultEntry.Enable() // 결과 표시 전 활성화 if err != nil { showNotification(fmt.Sprintf("오류: %v", err), notificationLabel) encodeDecodeResultEntry.SetText("") } else { encodeDecodeResultEntry.SetText(resultText) } encodeDecodeResultEntry.Disable() // 결과 표시 후 다시 비활성화 }) executeEncodeDecodeButton.Importance = widget.HighImportance // 주요 실행 버튼으로 강조 // "결과 복사" 버튼 copyResultButton := widget.NewButtonWithIcon("결과 복사", theme.ContentCopyIcon(), func() { if encodeDecodeResultEntry.Text == "" { showNotification("복사할 결과가 없습니다.", notificationLabel) return } myWindow.Clipboard().SetContent(encodeDecodeResultEntry.Text) showNotification("결과가 클립보드에 복사되었습니다.", notificationLabel) }) // 인코딩/디코딩 섹션 레이아웃 조립 encodeDecodeInputLabel := widget.NewLabel("입력 텍스트:") encodeDecodeInputHeader := container.NewBorder(nil, nil, encodeDecodeInputLabel, useGeneratedPwdButton, layout.NewSpacer()) encodeDecodeInputSectionWidget := container.NewVBox(encodeDecodeInputHeader, encodeDecodeInputArea) encodeDecodeOptionsLabel := widget.NewLabel("옵션:") encodeDecodeOptionsGrid := container.NewGridWithColumns(2, widget.NewLabel("방식:"), encodingSelect, widget.NewLabel("작업:"), operationRadioGroup, ) encodeDecodeOptionsSectionWidget := container.NewVBox(encodeDecodeOptionsLabel, encodeDecodeOptionsGrid, layout.NewSpacer(), executeEncodeDecodeButton) encodeDecodeResultLabel := widget.NewLabel("결과:") encodeDecodeResultHeader := container.NewBorder(nil, nil, encodeDecodeResultLabel, copyResultButton, layout.NewSpacer()) encodeDecodeResultSectionWidget := container.NewVBox(encodeDecodeResultHeader, encodeDecodeResultArea) encodeDecodeSection := container.NewVBox( widget.NewLabelWithStyle("인코딩 / 디코딩", fyne.TextAlignLeading, fyne.TextStyle{Bold: true, Italic: true}), container.NewPadded(encodeDecodeInputSectionWidget), container.NewPadded(encodeDecodeOptionsSectionWidget), container.NewPadded(encodeDecodeResultSectionWidget), ) // --- 전체 창 레이아웃 --- // 메인 컨텐츠 VBox (스크롤 대상) mainContent := container.NewVBox( passwordGeneratorSection, widget.NewSeparator(), // 섹션 구분선 encodeDecodeSection, ) // 스크롤 가능한 컨테이너로 메인 컨텐츠를 감쌈 scrollableMainContent := container.NewScroll(mainContent) // 최종 윈도우 레이아웃: 중앙에 스크롤 컨텐츠, 하단에 알림 레이블 finalWindowContent := container.NewBorder( nil, // Top notificationLabel, // Bottom nil, // Left nil, // Right scrollableMainContent, // Center ) myWindow.SetContent(finalWindowContent) myWindow.Resize(fyne.NewSize(500, 700)) // 창 초기 크기 (내용에 따라 조절) myWindow.SetFixedSize(false) // 창 크기 조절 가능 myWindow.CenterOnScreen() // 화면 중앙에 표시 myWindow.ShowAndRun() // 애플리케이션 시작 } // main 함수 끝 // --- 유틸리티 함수 --- // showNotification 함수는 화면 하단에 임시 알림 메시지를 표시합니다. func showNotification(message string, label *widget.Label) { if notificationTimer != nil { notificationTimer.Stop() // 이전 타이머가 있으면 중지 } label.SetText(message) label.Show() // 일정 시간 후 알림 자동 숨김 notificationTimer = time.AfterFunc(notificationDuration, func() { label.Hide() }) } // rot13 함수는 입력된 문자열에 대해 ROT13 변환을 수행합니다. func rot13(input string) string { var result strings.Builder for _, r := range input { switch { case r >= 'a' && r <= 'z': result.WriteRune('a' + (r-'a'+13)%26) case r >= 'A' && r <= 'Z': result.WriteRune('A' + (r-'A'+13)%26) default: result.WriteRune(r) } } return result.String() } // showSavePopUp 함수는 파일 저장 위치와 이름을 입력받는 팝업 다이얼로그를 표시합니다. func showSavePopUp(a fyne.App, contentToSave string, parentWindow fyne.Window) { popUpWindow := a.NewWindow("파일 정보 입력") // 새 창으로 팝업 구현 // 선택된 폴더 경로 바인딩 및 레이블 selectedFolderBinding := binding.NewString() _ = selectedFolderBinding.Set("저장할 폴더를 선택하세요...") // 초기 메시지 selectedFolderLabel := widget.NewLabelWithData(selectedFolderBinding) selectedFolderLabel.Wrapping = fyne.TextWrapBreak // 긴 경로 줄 바꿈 // 파일 이름 입력 필드 fileNameEntry := widget.NewEntry() fileNameEntry.SetText("generated_data") // 기본 파일 이름 // 확장자 선택 extOptions := []string{".txt", ".log", ".md", ".dat"} extSelect := widget.NewSelect(extOptions, nil) // 선택 시 콜백 없음 extSelect.SetSelected(".txt") // 기본 확장자 // 폴더 선택 버튼 selectFolderButton := widget.NewButtonWithIcon("폴더 찾기...", theme.FolderOpenIcon(), func() { dialog.ShowFolderOpen(func(uri fyne.ListableURI, err error) { if err != nil { dialog.ShowError(err, popUpWindow) // 오류는 팝업 창에 표시 _ = selectedFolderBinding.Set("오류: 폴더 접근 불가") return } if uri == nil { return } // 사용자가 선택 취소한 경우 localPath := uri.Path() // Windows 경로 형식 보정 if runtime.GOOS == "windows" { if strings.HasPrefix(localPath, "/") && len(localPath) > 2 && localPath[2] == ':' { localPath = strings.ToUpper(localPath[1:2]) + localPath[2:] } localPath = filepath.FromSlash(localPath) } _ = selectedFolderBinding.Set(localPath) }, popUpWindow) // 부모 윈도우는 팝업 자신 }) // 확인 버튼 (저장 실행) confirmButton := widget.NewButtonWithIcon("확인", theme.ConfirmIcon(), func() { folderPath, _ := selectedFolderBinding.Get() // 유효성 검사 if folderPath == "" || folderPath == "저장할 폴더를 선택하세요..." || strings.HasPrefix(folderPath, "오류:") { dialog.ShowError(errors.New("유효한 저장 폴더를 선택하세요"), popUpWindow) return } baseName := strings.TrimSpace(fileNameEntry.Text) if baseName == "" { dialog.ShowError(errors.New("파일 이름을 입력하세요"), popUpWindow) return } extension := extSelect.Selected finalFileName := baseName + extension fullPath := filepath.Join(folderPath, finalFileName) saveToFile(fullPath, contentToSave, popUpWindow, parentWindow) // 파일 저장 함수 호출 popUpWindow.Close() // 팝업 닫기 }) confirmButton.Importance = widget.HighImportance // 취소 버튼 cancelButton := widget.NewButtonWithIcon("취소", theme.CancelIcon(), func() { popUpWindow.Close() }) // 팝업 레이아웃 popUpContent := container.NewVBox( widget.NewLabel("파일 저장 정보 입력"), widget.NewSeparator(), container.NewBorder(nil, nil, widget.NewLabel(" 저장 폴더:"), selectFolderButton, selectedFolderLabel), widget.NewSeparator(), container.New(layout.NewFormLayout(), widget.NewLabel("파일 이름:"), fileNameEntry, widget.NewLabel("확장자:"), extSelect, ), widget.NewSeparator(), container.NewGridWithColumns(2, cancelButton, confirmButton), ) popUpWindow.SetContent(container.NewPadded(popUpContent)) popUpWindow.Resize(fyne.NewSize(600, 280)) popUpWindow.SetFixedSize(true) // 팝업 크기 고정 popUpWindow.CenterOnScreen() popUpWindow.Show() } // saveToFile 함수는 실제로 파일 시스템에 내용을 저장합니다. func saveToFile(fullPath string, content string, dialogParentOnFail fyne.Window, dialogParentOnSuccess fyne.Window) { err := os.WriteFile(fullPath, []byte(content), 0644) // 파일 권한 0644 if err != nil { dialog.ShowError(fmt.Errorf("파일 저장 실패: %w", err), dialogParentOnFail) return } dialog.ShowInformation("성공", fmt.Sprintf("'%s'(으)로 저장되었습니다.", filepath.Base(fullPath)), dialogParentOnSuccess) } // generatePassword 함수는 지정된 조건에 따라 무작위 비밀번호를 생성합니다. func generatePassword(r *rand.Rand, length, minSpecial, minNums, minUpper int) string { if length <= 0 { return "" } // 유효하지 않은 길이 요청 시 빈 문자열 반환 var builder strings.Builder // 1. 첫 글자는 알파벳으로 시작 builder.WriteByte(letterCharSet[r.Intn(len(letterCharSet))]) // 2. 최소 필수 문자 (특수문자, 숫자, 대문자) 추가 for i := 0; i < minSpecial; i++ { builder.WriteByte(specialCharSet[r.Intn(len(specialCharSet))]) } for i := 0; i < minNums; i++ { builder.WriteByte(numberSet[r.Intn(len(numberSet))]) } for i := 0; i < minUpper; i++ { builder.WriteByte(upperCharSet[r.Intn(len(upperCharSet))]) } // 3. 필수 문자 추가 후 길이 조정 (목표 길이 초과 시 자르기) if builder.Len() > length { tempStr := builder.String()[:length] builder.Reset() builder.WriteString(tempStr) } // 4. 나머지 길이만큼 모든 문자 집합에서 무작위 문자 추가 remaining := length - builder.Len() for i := 0; i < remaining; i++ { builder.WriteByte(allCharSet[r.Intn(len(allCharSet))]) } // 5. 생성된 문자열 섞기 (첫 글자 제외) runes := []rune(builder.String()) if len(runes) > 1 { head := runes[0] tail := runes[1:] r.Shuffle(len(tail), func(i, j int) { tail[i], tail[j] = tail[j], tail[i] }) return string(head) + string(tail) } return string(runes) // 한 글자이거나 그 이하면 그대로 반환 }