Ver3.0 ๐ Go์ธ์ด๋ก ๋ง๋๋ CLI ์ ํ๋ฆฌ์ผ์ด์

๐ Go์ธ์ด๋ก ๋ง๋๋ CLI ์ ํ๋ฆฌ์ผ์ด์
Cobra & Viper๋ก ํ๋ก์ฒ๋ผ ๋ช ๋ นํ ๋๊ตฌ ๊ฐ๋ฐํ๊ธฐ
โข ๋น ๋ฅธ ์คํ ์๋ - GUI ์ค๋ฒํค๋ ์์ด ์ฆ์ ์คํ
โข ์๋ํ ์นํ์ - ์คํฌ๋ฆฝํธ์ ํ์ดํ๋ผ์ธ์ ์ฝ๊ฒ ํตํฉ
โข ์๊ฒฉ ์์ ์ต์ ํ - SSH๋ฅผ ํตํ ์๋ฒ ๊ด๋ฆฌ์ ์๋ฒฝ
โข ๋ฆฌ์์ค ํจ์จ์ฑ - ๋ฉ๋ชจ๋ฆฌ์ CPU ์ฌ์ฉ๋ ์ต์ํ
โข ๋ฒ์ ๊ด๋ฆฌ ์ฉ์ด - ์ค์ ํ์ผ์ Git์ผ๋ก ์ฝ๊ฒ ๊ด๋ฆฌ
1. ์๋ธ์ปค๋งจ๋ ๊ตฌ์กฐ - git add, git commit์ฒ๋ผ ๊ณ์ธต์ ๋ช ๋ น์ด ๊ตฌ์กฐ
2. ํ๋๊ทธ ๊ด๋ฆฌ - ์งง์ ํ๋๊ทธ(-v)์ ๊ธด ํ๋๊ทธ(--verbose) ์๋ ์ฒ๋ฆฌ
3. ์๋ ์์ฑ - Bash, Zsh, Fish ๋ฑ์ ์ ์๋์์ฑ ์์ฑ
4. ๋์๋ง ์์ฑ - ๋ช ๋ น์ด ์ค๋ช ๊ณผ ์ฌ์ฉ๋ฒ์ ์๋์ผ๋ก ์์ฑ
5. ์๋ฌ ์ฒ๋ฆฌ - ์ผ๊ด๋ ์๋ฌ ๋ฉ์์ง์ ์ ์ ๊ธฐ๋ฅ
go get -u github.com/spf13/cobra@latest
go install github.com/spf13/cobra-cli@latest
์ค์น๊ฐ ์๋ฃ๋๋ฉด ์๋ก์ด CLI ํ๋ก์ ํธ๋ฅผ ์์ํ ์ ์์ด. Cobra CLI ๋๊ตฌ๋ฅผ ์ฌ์ฉํ๋ฉด ํ๋ก์ ํธ ๊ตฌ์กฐ๋ฅผ ์๋์ผ๋ก ๋ง๋ค์ด์ค:
mkdir myapp
cd myapp
go mod init myapp
cobra-cli init
์ด ๋ช
๋ น์ด๋ฅผ ์คํํ๋ฉด ๋ค์๊ณผ ๊ฐ์ ๊ตฌ์กฐ๊ฐ ์์ฑ๋ผ:
myapp/
โโโ cmd/
โ โโโ root.go
โโโ main.go
โโโ go.mod
root.go ํ์ผ์ด ํต์ฌ์ด์ผ. ์ฌ๊ธฐ์ ๋ฃจํธ ์ปค๋งจ๋๊ฐ ์ ์๋์ด ์๊ณ , ๋ชจ๋ ์๋ธ์ปค๋งจ๋๋ค์ด ์ด ๋ฃจํธ์ ์ฐ๊ฒฐ๋์ง.
### ์ฒซ ๋ฒ์งธ CLI ์ ํ๋ฆฌ์ผ์ด์
๋ง๋ค๊ธฐ
๊ฐ๋จํ ์์ ๋ก ์์ํด๋ณด์. ์ฌ์ฉ์์๊ฒ ์ธ์ฌํ๋ CLI ๋๊ตฌ๋ฅผ ๋ง๋ค์ด๋ณผ๊ฒ:
package main
import (
"fmt"
"github.com/spf13/cobra"
"os"
)
var rootCmd = &cobra.Command{
Use: "greet",
Short: "์ธ์ฌ๋ฅผ ๊ฑด๋ค๋ CLI ๋๊ตฌ",
Long: `์ด๋ฆ์ ์
๋ ฅํ๋ฉด ์น๊ทผํ๊ฒ ์ธ์ฌํด์ฃผ๋ ๊ฐ๋จํ ๋๊ตฌ์
๋๋ค.`,
Run: func(cmd *cobra.Command, args []string) {
name, _ := cmd.Flags().GetString("name")
if name == "" {
name = "์น๊ตฌ"
}
fmt.Printf("์๋
ํ์ธ์, %s๋! ๐\n", name)
},
}
func init() {
rootCmd.Flags().StringP("name", "n", "", "์ธ์ฌํ ๋์์ ์ด๋ฆ")
}
func main() {
if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}
์ด ์ฝ๋๋ฅผ ์คํํ๋ฉด:
go run main.go --name ์ฒ ์
# ์ถ๋ ฅ: ์๋
ํ์ธ์, ์ฒ ์๋! ๐
go run main.go -n ์ํฌ
# ์ถ๋ ฅ: ์๋
ํ์ธ์, ์ํฌ๋! ๐
go run main.go
# ์ถ๋ ฅ: ์๋
ํ์ธ์, ์น๊ตฌ๋! ๐
์งง์ ํ๋๊ทธ(-n)์ ๊ธด ํ๋๊ทธ(--name) ๋ ๋ค ์๋์ผ๋ก ์๋ํ๋ ๊ฒ ๋ณด์ด์ง? ์ด๊ฒ Cobra์ ๋งค๋ ฅ์ด์ผ! ๐
### ์๋ธ์ปค๋งจ๋ ์ถ๊ฐํ๊ธฐ
์ด์ ์ข ๋ ๋ณต์กํ ๊ตฌ์กฐ๋ฅผ ๋ง๋ค์ด๋ณด์. ์๋ธ์ปค๋งจ๋๋ฅผ ์ถ๊ฐํ๋ ๋ฐฉ๋ฒ์ ์์๋ณผ๊ฒ:
cobra-cli add hello
cobra-cli add goodbye
์ด ๋ช
๋ น์ด๋ฅผ ์คํํ๋ฉด cmd/hello.go์ cmd/goodbye.go ํ์ผ์ด ์์ฑ๋ผ. ๊ฐ ํ์ผ์ ์์ ํด์ ๊ธฐ๋ฅ์ ์ถ๊ฐํ ์ ์์ด:
// cmd/hello.go
package cmd
import (
"fmt"
"github.com/spf13/cobra"
)
var helloCmd = &cobra.Command{
Use: "hello [์ด๋ฆ]",
Short: "๋ฐ๊ฐ๊ฒ ์ธ์ฌํฉ๋๋ค",
Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
enthusiastic, _ := cmd.Flags().GetBool("enthusiastic")
name := args[0]
if enthusiastic {
fmt.Printf("์๋
ํ์ธ์, %s๋!!! ์ ๋ง ๋ฐ๊ฐ์ต๋๋ค!!! ๐๐๐\n", name)
} else {
fmt.Printf("์๋
ํ์ธ์, %s๋. ๋ฐ๊ฐ์ต๋๋ค.\n", name)
}
},
}
func init() {
rootCmd.AddCommand(helloCmd)
helloCmd.Flags().BoolP("enthusiastic", "e", false, "์ด์ ์ ์ผ๋ก ์ธ์ฌํ๊ธฐ")
}
์ด์ ๋ค์๊ณผ ๊ฐ์ด ์ฌ์ฉํ ์ ์์ด:
go run main.go hello ๋ฏผ์
# ์ถ๋ ฅ: ์๋
ํ์ธ์, ๋ฏผ์๋. ๋ฐ๊ฐ์ต๋๋ค.
go run main.go hello ๋ฏผ์ --enthusiastic
# ์ถ๋ ฅ: ์๋
ํ์ธ์, ๋ฏผ์๋!!! ์ ๋ง ๋ฐ๊ฐ์ต๋๋ค!!! ๐๐๐
## ๐ง Viper๋ก ์ค์ ๊ด๋ฆฌ ๋ง์คํฐํ๊ธฐ
CLI ๋๊ตฌ๋ฅผ ๋ง๋ค๋ค ๋ณด๋ฉด ์ค์ ์ ๊ด๋ฆฌํด์ผ ํ ์ผ์ด ๋ง์. ์ฌ์ฉ์๋ง๋ค ๋ค๋ฅธ API ํค๋ฅผ ์ฌ์ฉํ๋ค๊ฑฐ๋, ๊ธฐ๋ณธ ๋์์ ์ปค์คํฐ๋ง์ด์งํ๊ณ ์ถ์ ๋ ๋ง์ด์ผ. ์ด๋ Viper๊ฐ ์ ๋ง ์ ์ฉํด! ๐ฏ
Viper๋ Cobra๋ฅผ ๋ง๋ ๊ฐ์ ํ์์ ๊ฐ๋ฐํ ์ค์ ๊ด๋ฆฌ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ผ. ์ฌ๋ฌ ์์ค์์ ์ค์ ์ ์ฝ์ด์ฌ ์ ์์ด:
- JSON, YAML, TOML, HCL ๋ฑ์ ์ค์ ํ์ผ
- ํ๊ฒฝ ๋ณ์
- ์ปค๋งจ๋ ๋ผ์ธ ํ๋๊ทธ
- ์๊ฒฉ ์ค์ ์์คํ
(etcd, Consul ๋ฑ)
### Viper ์ค์น ๋ฐ ๊ธฐ๋ณธ ์ฌ์ฉ๋ฒ
๋จผ์ Viper๋ฅผ ์ค์นํ์:
go get github.com/spf13/viper
๊ธฐ๋ณธ์ ์ธ ์ฌ์ฉ ์์ ๋ฅผ ๋ณด์:
package main
import (
"fmt"
"github.com/spf13/viper"
)
func main() {
// ์ค์ ํ์ผ ์ด๋ฆ๊ณผ ๊ฒฝ๋ก ์ง์
viper.SetConfigName("config")
viper.SetConfigType("yaml")
viper.AddConfigPath(".")
viper.AddConfigPath("$HOME/.myapp")
// ์ค์ ํ์ผ ์ฝ๊ธฐ
if err := viper.ReadInConfig(); err != nil {
fmt.Printf("์ค์ ํ์ผ์ ์ฐพ์ ์ ์์ต๋๋ค: %v\n", err)
}
// ์ค์ ๊ฐ ์ฝ๊ธฐ
apiKey := viper.GetString("api.key")
timeout := viper.GetInt("timeout")
debug := viper.GetBool("debug")
fmt.Printf("API Key: %s\n", apiKey)
fmt.Printf("Timeout: %d\n", timeout)
fmt.Printf("Debug: %v\n", debug)
}
๊ทธ๋ฆฌ๊ณ config.yaml ํ์ผ์ ๋ง๋ค์ด:
api:
key: "your-secret-api-key"
endpoint: "https://api.example.com"
timeout: 30
debug: true
retries: 3
### Cobra์ Viper ํตํฉํ๊ธฐ
Cobra์ Viper๋ฅผ ํจ๊ป ์ฌ์ฉํ๋ฉด ์ ๋ง ๊ฐ๋ ฅํ ์กฐํฉ์ด ๋ผ. ํ๋๊ทธ, ์ค์ ํ์ผ, ํ๊ฒฝ ๋ณ์๋ฅผ ๋ชจ๋ ํตํฉํด์ ๊ด๋ฆฌํ ์ ์๊ฑฐ๋ :
package cmd
import (
"fmt"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var cfgFile string
var rootCmd = &cobra.Command{
Use: "myapp",
Short: "์ค์ ๊ด๋ฆฌ๊ฐ ์๋ฒฝํ CLI ๋๊ตฌ",
}
func init() {
cobra.OnInitialize(initConfig)
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "",
"์ค์ ํ์ผ ๊ฒฝ๋ก (๊ธฐ๋ณธ๊ฐ: $HOME/.myapp.yaml)")
rootCmd.PersistentFlags().Bool("debug", false, "๋๋ฒ๊ทธ ๋ชจ๋ ํ์ฑํ")
// ํ๋๊ทธ๋ฅผ Viper์ ๋ฐ์ธ๋ฉ
viper.BindPFlag("debug", rootCmd.PersistentFlags().Lookup("debug"))
}
func initConfig() {
if cfgFile != "" {
viper.SetConfigFile(cfgFile)
} else {
viper.AddConfigPath("$HOME")
viper.SetConfigName(".myapp")
viper.SetConfigType("yaml")
}
// ํ๊ฒฝ ๋ณ์ ์๋ ๋ฐ์ธ๋ฉ
viper.AutomaticEnv()
if err := viper.ReadInConfig(); err == nil {
fmt.Println("์ค์ ํ์ผ ์ฌ์ฉ:", viper.ConfigFileUsed())
}
}
์ด๋ ๊ฒ ์ค์ ํ๋ฉด ์ฐ์ ์์๊ฐ ๋ค์๊ณผ ๊ฐ์ด ์ ์ฉ๋ผ:
1. **์ปค๋งจ๋ ๋ผ์ธ ํ๋๊ทธ** (๊ฐ์ฅ ๋์ ์ฐ์ ์์)
2. **ํ๊ฒฝ ๋ณ์**
3. **์ค์ ํ์ผ**
4. **๊ธฐ๋ณธ๊ฐ** (๊ฐ์ฅ ๋ฎ์ ์ฐ์ ์์)
๊ฐ๋ฐ ํ๊ฒฝ์์๋ ์ค์ ํ์ผ์ ์ฌ์ฉํ๊ณ , ํ๋ก๋์ ์์๋ ํ๊ฒฝ ๋ณ์๋ก ๋ฏผ๊ฐํ ์ ๋ณด๋ฅผ ์ฃผ์ ํ๋ ๋ฐฉ์์ด ์ผ๋ฐ์ ์ด์ผ. ์๋ฅผ ๋ค์ด:
export MYAPP_API_KEY="production-key"
myapp --debug
์ด๋ ๊ฒ ํ๋ฉด API ํค๋ ํ๊ฒฝ ๋ณ์์์, debug ํ๋๊ทธ๋ ์ปค๋งจ๋ ๋ผ์ธ์์ ๊ฐ์ ธ์ค๊ฒ ๋ผ.
# config.yaml
database:
host: localhost
port: 5432
credentials:
username: admin
password: secret
pool:
max_connections: 100
min_connections: 10
server:
host: 0.0.0.0
port: 8080
tls:
enabled: true
cert_file: /path/to/cert.pem
key_file: /path/to/key.pem
Go ์ฝ๋์์๋ ์ด๋ ๊ฒ ์ ๊ทผํด:
// ์ (.) ํ๊ธฐ๋ฒ์ผ๋ก ์ค์ฒฉ๋ ๊ฐ ์ ๊ทผ
dbHost := viper.GetString("database.host")
dbPort := viper.GetInt("database.port")
username := viper.GetString("database.credentials.username")
maxConn := viper.GetInt("database.pool.max_connections")
// ์ ์ฒด ์น์
์ ๋งต์ผ๋ก ๊ฐ์ ธ์ค๊ธฐ
dbConfig := viper.GetStringMap("database")
fmt.Printf("Database config: %v\n", dbConfig)
// ๊ตฌ์กฐ์ฒด๋ก ์ธ๋ง์ฌ๋ง
type DatabaseConfig struct {
Host string
Port int
Credentials struct {
Username string
Password string
}
Pool struct {
MaxConnections int `mapstructure:"max_connections"`
MinConnections int `mapstructure:"min_connections"`
}
}
var dbCfg DatabaseConfig
viper.UnmarshalKey("database", &dbCfg)
API ํค๋ ๋น๋ฐ๋ฒํธ ๊ฐ์ ๋ฏผ๊ฐํ ์ ๋ณด๋ ์ค์ ํ์ผ์ ์ง์ ์ ์ฅํ์ง ๋ง๊ณ , ํ๊ฒฝ ๋ณ์๋ ์ํฌ๋ฆฟ ๊ด๋ฆฌ ์์คํ ์ ์ฌ์ฉํ๋ ๊ฒ ์ข์. ์ค์ ํ์ผ์ Git์ ์ปค๋ฐํ ๋ ์ค์๋ก ๋ ธ์ถ๋ ์ ์๊ฑฐ๋ !
viper.WatchConfig()
viper.OnConfigChange(func(e fsnotify.Event) {
fmt.Println("์ค์ ํ์ผ์ด ๋ณ๊ฒฝ๋์์ต๋๋ค:", e.Name)
// ์๋ก์ด ์ค์ ๊ฐ ์ ์ฉ
newTimeout := viper.GetInt("timeout")
fmt.Printf("์๋ก์ด ํ์์์ ๊ฐ: %d\n", newTimeout)
// ์ฌ๊ธฐ์ ์ ํ๋ฆฌ์ผ์ด์
๋์์ ์
๋ฐ์ดํธ
updateApplicationConfig()
})
์ด ๊ธฐ๋ฅ์ ์ฅ์๊ฐ ์คํ๋๋ ์๋น์ค๋ ๋ฐ๋ชฌ ํ๋ก์ธ์ค์์ ํนํ ์ ์ฉํด. ์ค์ ์ ๋ฐ๊พธ๊ณ ์ถ์ ๋๋ง๋ค ์๋น์ค๋ฅผ ์ฌ์์ํ ํ์๊ฐ ์์ผ๋๊น!
## ๐จ ์ค์ ํ๋ก์ ํธ: ํ์ผ ๊ด๋ฆฌ CLI ๋๊ตฌ ๋ง๋ค๊ธฐ
์ด๋ก ์ ์ถฉ๋ถํ ๋ฐฐ์ ์ผ๋, ์ด์ ์ค์ ๋ก ์ ์ฉํ CLI ๋๊ตฌ๋ฅผ ๋ง๋ค์ด๋ณด์! ํ์ผ์ ๊ฒ์ํ๊ณ , ์ ๋ฆฌํ๊ณ , ๋ถ์ํ๋ ๋๊ตฌ๋ฅผ ๋ง๋ค ๊ฑฐ์ผ. ์ด๋ฆ์ filemaster๋ก ํ์! ๐
### ํ๋ก์ ํธ ๊ตฌ์กฐ ์ค๊ณ
๋จผ์ ํ๋ก์ ํธ ๊ตฌ์กฐ๋ฅผ ์ก์๋ณด์:
filemaster/
โโโ cmd/
โ โโโ root.go
โ โโโ search.go
โ โโโ organize.go
โ โโโ analyze.go
โโโ internal/
โ โโโ file/
โ โ โโโ search.go
โ โ โโโ organize.go
โ โ โโโ analyze.go
โ โโโ config/
โ โโโ config.go
โโโ config.yaml
โโโ main.go
โโโ go.mod
์ด ๊ตฌ์กฐ๋ ๊ด์ฌ์ฌ์ ๋ถ๋ฆฌ(Separation of Concerns) ์์น์ ๋ฐ๋ผ. cmd ํจํค์ง๋ CLI ์ธํฐํ์ด์ค๋ง ๋ด๋นํ๊ณ , ์ค์ ๋น์ฆ๋์ค ๋ก์ง์ internal ํจํค์ง์ ๋ถ๋ฆฌํ์ด.
### Root ์ปค๋งจ๋ ๊ตฌํ
๋จผ์ ๋ฃจํธ ์ปค๋งจ๋๋ฅผ ๋ง๋ค์:
// cmd/root.go
package cmd
import (
"fmt"
"os"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var cfgFile string
var verbose bool
var rootCmd = &cobra.Command{
Use: "filemaster",
Short: "๊ฐ๋ ฅํ ํ์ผ ๊ด๋ฆฌ CLI ๋๊ตฌ",
Long: `filemaster๋ ํ์ผ์ ๊ฒ์ํ๊ณ , ์ ๋ฆฌํ๊ณ , ๋ถ์ํ๋
์ฌ์ธ์ CLI ๋๊ตฌ์
๋๋ค. ๋์ฉ๋ ํ์ผ ์์คํ
๋ ๋น ๋ฅด๊ฒ ์ฒ๋ฆฌํ ์ ์์ด์!`,
}
func Execute() {
if err := rootCmd.Execute(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
func init() {
cobra.OnInitialize(initConfig)
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "",
"์ค์ ํ์ผ ๊ฒฝ๋ก (๊ธฐ๋ณธ๊ฐ: $HOME/.filemaster.yaml)")
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false,
"์์ธํ ์ถ๋ ฅ ํ์")
viper.BindPFlag("verbose", rootCmd.PersistentFlags().Lookup("verbose"))
}
func initConfig() {
if cfgFile != "" {
viper.SetConfigFile(cfgFile)
} else {
home, err := os.UserHomeDir()
if err != nil {
fmt.Println(err)
os.Exit(1)
}
viper.AddConfigPath(home)
viper.AddConfigPath(".")
viper.SetConfigName(".filemaster")
viper.SetConfigType("yaml")
}
viper.AutomaticEnv()
if err := viper.ReadInConfig(); err == nil && verbose {
fmt.Println("์ค์ ํ์ผ ์ฌ์ฉ:", viper.ConfigFileUsed())
}
}
### Search ์ปค๋งจ๋ ๊ตฌํ
ํ์ผ์ ๊ฒ์ํ๋ ๊ธฐ๋ฅ์ ๋ง๋ค์ด๋ณด์. ์ด๋ฆ, ํ์ฅ์, ํฌ๊ธฐ ๋ฑ์ผ๋ก ๊ฒ์ํ ์ ์๊ฒ ํ ๊ฑฐ์ผ:
// cmd/search.go
package cmd
import (
"fmt"
"github.com/spf13/cobra"
"filemaster/internal/file"
)
var searchCmd = &cobra.Command{
Use: "search [๊ฒฝ๋ก]",
Short: "ํ์ผ ๊ฒ์",
Long: `์ง์ ๋ ๊ฒฝ๋ก์์ ์กฐ๊ฑด์ ๋ง๋ ํ์ผ์ ๊ฒ์ํฉ๋๋ค.`,
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
path := args[0]
pattern, _ := cmd.Flags().GetString("pattern")
ext, _ := cmd.Flags().GetString("ext")
minSize, _ := cmd.Flags().GetInt64("min-size")
maxSize, _ := cmd.Flags().GetInt64("max-size")
opts := file.SearchOptions{
Pattern: pattern,
Extension: ext,
MinSize: minSize,
MaxSize: maxSize,
}
results, err := file.Search(path, opts)
if err != nil {
fmt.Printf("๊ฒ์ ์ค ์ค๋ฅ ๋ฐ์: %v\n", err)
return
}
fmt.Printf("์ด %d๊ฐ์ ํ์ผ์ ์ฐพ์์ต๋๋ค:\n\n", len(results))
for _, result := range results {
fmt.Printf("๐ %s (%s)\n", result.Path, formatSize(result.Size))
}
},
}
func init() {
rootCmd.AddCommand(searchCmd)
searchCmd.Flags().StringP("pattern", "p", "", "ํ์ผ๋ช
ํจํด (์ ๊ท์)")
searchCmd.Flags().StringP("ext", "e", "", "ํ์ผ ํ์ฅ์")
searchCmd.Flags().Int64("min-size", 0, "์ต์ ํ์ผ ํฌ๊ธฐ (๋ฐ์ดํธ)")
searchCmd.Flags().Int64("max-size", 0, "์ต๋ ํ์ผ ํฌ๊ธฐ (๋ฐ์ดํธ)")
}
func formatSize(bytes int64) string {
const unit = 1024
if bytes < unit {
return fmt.Sprintf("%d B", bytes)
}
div, exp := int64(unit), 0
for n := bytes / unit; n >= unit; n /= unit {
div *= unit
exp++
}
return fmt.Sprintf("%.1f %cB", float64(bytes)/float64(div), "KMGTPE"[exp])
}
### ์ค์ ๊ฒ์ ๋ก์ง ๊ตฌํ
์ด์ internal ํจํค์ง์ ์ค์ ๊ฒ์ ๋ก์ง์ ๊ตฌํํ์:
// internal/file/search.go
package file
import (
"os"
"path/filepath"
"regexp"
"strings"
)
type SearchOptions struct {
Pattern string
Extension string
MinSize int64
MaxSize int64
}
type SearchResult struct {
Path string
Size int64
IsDir bool
}
func Search(root string, opts SearchOptions) ([]SearchResult, error) {
var results []SearchResult
var pattern *regexp.Regexp
var err error
if opts.Pattern != "" {
pattern, err = regexp.Compile(opts.Pattern)
if err != nil {
return nil, err
}
}
err = filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
if err != nil {
return nil // ์๋ฌ๊ฐ ์์ด๋ ๊ณ์ ์งํ
}
// ๋๋ ํ ๋ฆฌ๋ ๊ฑด๋๋ฐ๊ธฐ
if info.IsDir() {
return nil
}
// ํจํด ๋งค์นญ
if pattern != nil && !pattern.MatchString(filepath.Base(path)) {
return nil
}
// ํ์ฅ์ ํํฐ๋ง
if opts.Extension != "" {
ext := strings.TrimPrefix(filepath.Ext(path), ".")
if ext != opts.Extension {
return nil
}
}
// ํฌ๊ธฐ ํํฐ๋ง
size := info.Size()
if opts.MinSize > 0 && size < opts.MinSize {
return nil
}
if opts.MaxSize > 0 && size > opts.MaxSize {
return nil
}
results = append(results, SearchResult{
Path: path,
Size: size,
IsDir: false,
})
return nil
})
return results, err
}
### Organize ์ปค๋งจ๋๋ก ํ์ผ ์ ๋ฆฌํ๊ธฐ
์ด์ ํ์ผ์ ํ์ฅ์๋ณ๋ก ์ ๋ฆฌํ๋ ๊ธฐ๋ฅ์ ์ถ๊ฐํด๋ณด์:
// cmd/organize.go
package cmd
import (
"fmt"
"github.com/spf13/cobra"
"filemaster/internal/file"
)
var organizeCmd = &cobra.Command{
Use: "organize [๊ฒฝ๋ก]",
Short: "ํ์ผ ์ ๋ฆฌ",
Long: `ํ์ผ์ ํ์ฅ์๋ณ๋ก ํด๋์ ์ ๋ฆฌํฉ๋๋ค.`,
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
path := args[0]
dryRun, _ := cmd.Flags().GetBool("dry-run")
byDate, _ := cmd.Flags().GetBool("by-date")
opts := file.OrganizeOptions{
DryRun: dryRun,
ByDate: byDate,
}
result, err := file.Organize(path, opts)
if err != nil {
fmt.Printf("์ ๋ฆฌ ์ค ์ค๋ฅ ๋ฐ์: %v\n", err)
return
}
if dryRun {
fmt.Println("๐ ์๋ฎฌ๋ ์ด์
๋ชจ๋ (์ค์ ๋ก ํ์ผ์ ์ด๋ํ์ง ์์ต๋๋ค)")
}
fmt.Printf("\nโจ ์ ๋ฆฌ ์๋ฃ!\n")
fmt.Printf("์ด๋๋ ํ์ผ: %d๊ฐ\n", result.MovedFiles)
fmt.Printf("์์ฑ๋ ํด๋: %d๊ฐ\n", result.CreatedDirs)
if len(result.Errors) > 0 {
fmt.Printf("\nโ ๏ธ %d๊ฐ์ ์ค๋ฅ ๋ฐ์:\n", len(result.Errors))
for _, err := range result.Errors {
fmt.Printf(" - %v\n", err)
}
}
},
}
func init() {
rootCmd.AddCommand(organizeCmd)
organizeCmd.Flags().Bool("dry-run", false, "์ค์ ๋ก ์ด๋ํ์ง ์๊ณ ์๋ฎฌ๋ ์ด์
๋ง")
organizeCmd.Flags().Bool("by-date", false, "๋ ์ง๋ณ๋ก ์ ๋ฆฌ")
}
ํ์ผ์ ์ค์ ๋ก ์ด๋ํ๊ฑฐ๋ ์ญ์ ํ๋ ๋ช ๋ น์ด๋ฅผ ๋ง๋ค ๋๋ ํญ์ --dry-run ์ต์ ์ ์ ๊ณตํ๋ ๊ฒ ์ข์. ์ฌ์ฉ์๊ฐ ์ค์๋ก ์ค์ํ ํ์ผ์ ๋ ๋ฆฌ๋ ๊ฑธ ๋ฐฉ์งํ ์ ์๊ฑฐ๋ . ์ด๊ฑด Git, Docker ๊ฐ์ ์ ๋ช ํ ๋๊ตฌ๋ค๋ ์ฌ์ฉํ๋ ํจํด์ด์ผ!
// cmd/analyze.go
package cmd
import (
"fmt"
"github.com/spf13/cobra"
"filemaster/internal/file"
"sort"
)
var analyzeCmd = &cobra.Command{
Use: "analyze [๊ฒฝ๋ก]",
Short: "ํ์ผ ๋ถ์",
Long: `๋๋ ํ ๋ฆฌ์ ํ์ผ ํต๊ณ๋ฅผ ๋ถ์ํฉ๋๋ค.`,
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
path := args[0]
showChart, _ := cmd.Flags().GetBool("chart")
topN, _ := cmd.Flags().GetInt("top")
stats, err := file.Analyze(path)
if err != nil {
fmt.Printf("๋ถ์ ์ค ์ค๋ฅ ๋ฐ์: %v\n", err)
return
}
fmt.Printf("\n๐ ๋๋ ํ ๋ฆฌ ๋ถ์ ๊ฒฐ๊ณผ: %s\n\n", path)
fmt.Printf("์ด ํ์ผ ์: %d๊ฐ\n", stats.TotalFiles)
fmt.Printf("์ด ํฌ๊ธฐ: %s\n", formatSize(stats.TotalSize))
fmt.Printf("ํ๊ท ํ์ผ ํฌ๊ธฐ: %s\n\n", formatSize(stats.AverageSize))
// ํ์ฅ์๋ณ ํต๊ณ
fmt.Println("๐ ํ์ฅ์๋ณ ๋ถํฌ:")
type extStat struct {
ext string
count int
size int64
}
var extStats []extStat
for ext, count := range stats.ExtensionCount {
extStats = append(extStats, extStat{
ext: ext,
count: count,
size: stats.ExtensionSize[ext],
})
}
sort.Slice(extStats, func(i, j int) bool {
return extStats[i].size > extStats[j].size
})
displayCount := topN
if len(extStats) < displayCount {
displayCount = len(extStats)
}
for i := 0; i < displayCount; i++ {
stat := extStats[i]
percentage := float64(stat.size) / float64(stats.TotalSize) * 100
fmt.Printf(" .%-10s %4d๊ฐ %10s (%.1f%%)\n",
stat.ext, stat.count, formatSize(stat.size), percentage)
if showChart {
barLength := int(percentage / 2)
bar := strings.Repeat("โ", barLength)
fmt.Printf(" %s\n", bar)
}
}
// ๊ฐ์ฅ ํฐ ํ์ผ๋ค
fmt.Println("\n๐ฆ ๊ฐ์ฅ ํฐ ํ์ผ๋ค:")
for i, f := range stats.LargestFiles {
if i >= 5 {
break
}
fmt.Printf(" %d. %s (%s)\n", i+1, f.Name, formatSize(f.Size))
}
},
}
func init() {
rootCmd.AddCommand(analyzeCmd)
analyzeCmd.Flags().Bool("chart", false, "์ฐจํธ ํ์")
analyzeCmd.Flags().IntP("top", "t", 10, "์์ N๊ฐ ํ์ฅ์ ํ์")
}
์ด์ ์ด๋ ๊ฒ ์ฌ์ฉํ ์ ์์ด:
# ํ์ผ ๊ฒ์
filemaster search ~/Documents --pattern "report.*\.pdf" --min-size 1048576
# ํ์ผ ์ ๋ฆฌ (์๋ฎฌ๋ ์ด์
)
filemaster organize ~/Downloads --dry-run
# ํ์ผ ์ ๋ฆฌ (์ค์ ์คํ)
filemaster organize ~/Downloads
# ๋๋ ํ ๋ฆฌ ๋ถ์
filemaster analyze ~/Projects --chart --top 5
## ๐ ๊ณ ๊ธ ๊ธฐ๋ฅ ๊ตฌํํ๊ธฐ
๊ธฐ๋ณธ ๊ธฐ๋ฅ์ ๋ค ๋ง๋ค์์ผ๋, ์ด์ ์ข ๋ ๊ณ ๊ธ ๊ธฐ๋ฅ๋ค์ ์ถ๊ฐํด๋ณด์. ํ๋กํ์
๋ํ CLI ๋๊ตฌ๊ฐ ๋๋ ค๋ฉด ์ด๋ฐ ๊ธฐ๋ฅ๋ค์ด ํ์ํด!
### ์งํ ์ํฉ ํ์ํ๊ธฐ
๋์ฉ๋ ํ์ผ์ ์ฒ๋ฆฌํ ๋๋ ์งํ ์ํฉ์ ๋ณด์ฌ์ฃผ๋ ๊ฒ ์ค์ํด. ์ฌ์ฉ์๊ฐ ํ๋ก๊ทธ๋จ์ด ๋ฉ์ถ ๊ฑด์ง ์๋๋ฉด ์ด์ฌํ ์๋ ์ค์ธ์ง ์ ์ ์์ด์ผ ํ๊ฑฐ๋ . progressbar ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํ๋ฉด ์ฝ๊ฒ ๊ตฌํํ ์ ์์ด:
go get github.com/schollz/progressbar/v3
package file
import (
"github.com/schollz/progressbar/v3"
"os"
"path/filepath"
)
func SearchWithProgress(root string, opts SearchOptions) ([]SearchResult, error) {
// ๋จผ์ ์ด ํ์ผ ์๋ฅผ ์ธ๊ธฐ
var totalFiles int
filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
if err == nil && !info.IsDir() {
totalFiles++
}
return nil
})
// ํ๋ก๊ทธ๋ ์ค ๋ฐ ์์ฑ
bar := progressbar.NewOptions(totalFiles,
progressbar.OptionSetDescription("ํ์ผ ๊ฒ์ ์ค..."),
progressbar.OptionSetTheme(progressbar.Theme{
Saucer: "=",
SaucerHead: ">",
SaucerPadding: " ",
BarStart: "[",
BarEnd: "]",
}),
progressbar.OptionShowCount(),
progressbar.OptionShowIts(),
progressbar.OptionSetWidth(50),
)
var results []SearchResult
err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
if err != nil {
return nil
}
if !info.IsDir() {
bar.Add(1)
// ๊ฒ์ ๋ก์ง...
if matchesOptions(path, info, opts) {
results = append(results, SearchResult{
Path: path,
Size: info.Size(),
})
}
}
return nil
})
bar.Finish()
return results, err
}
### ๋ณ๋ ฌ ์ฒ๋ฆฌ๋ก ์ฑ๋ฅ ํฅ์ํ๊ธฐ
Go์ ๊ฐ์ ์ธ ๊ณ ๋ฃจํด์ ํ์ฉํด์ ํ์ผ ์ฒ๋ฆฌ๋ฅผ ๋ณ๋ ฌํํ๋ฉด ์ฑ๋ฅ์ด ํฌ๊ฒ ํฅ์๋ผ. ํนํ ๋ง์ ํ์ผ์ ์ฒ๋ฆฌํ ๋ ํจ๊ณผ๊ฐ ์ข์:
package file
import (
"sync"
"runtime"
)
func AnalyzeParallel(root string) (*Stats, error) {
numWorkers := runtime.NumCPU()
// ์์
ํ์ ๊ฒฐ๊ณผ ์ฑ๋
jobs := make(chan string, 100)
results := make(chan FileInfo, 100)
// ์์ปค ํ ์์ฑ
var wg sync.WaitGroup
for i := 0; i < numWorkers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for path := range jobs {
info, err := analyzeFile(path)
if err == nil {
results <- info
}
}
}()
}
// ํ์ผ ๋ชฉ๋ก ์์ง ๋ฐ ์์
๋ถ๋ฐฐ
go func() {
filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
if err == nil && !info.IsDir() {
jobs <- path
}
return nil
})
close(jobs)
}()
// ์์ปค๋ค์ด ์๋ฃ๋๋ฉด ๊ฒฐ๊ณผ ์ฑ๋ ๋ซ๊ธฐ
go func() {
wg.Wait()
close(results)
}()
// ๊ฒฐ๊ณผ ์์ง
stats := &Stats{
ExtensionCount: make(map[string]int),
ExtensionSize: make(map[string]int64),
}
for info := range results {
stats.TotalFiles++
stats.TotalSize += info.Size
ext := filepath.Ext(info.Path)
stats.ExtensionCount[ext]++
stats.ExtensionSize[ext] += info.Size
}
if stats.TotalFiles > 0 {
stats.AverageSize = stats.TotalSize / int64(stats.TotalFiles)
}
return stats, nil
}
1. ์์ปค ์ ์กฐ์ - CPU ์ฝ์ด ์๋งํผ ์์ปค๋ฅผ ๋ง๋๋ ๊ฒ ์ผ๋ฐ์ ์ด์ง๋ง, I/O ์์ ์ด ๋ง์ผ๋ฉด ๋ ๋ง์ด ๋ง๋ค ์๋ ์์ด
2. ๋ฒํผ ํฌ๊ธฐ - ์ฑ๋ ๋ฒํผ๋ฅผ ์ ์ ํ ์ค์ ํ๋ฉด ๊ณ ๋ฃจํด ๊ฐ ํต์ ์ค๋ฒํค๋๋ฅผ ์ค์ผ ์ ์์ด
3. ์ปจํ ์คํธ ์ฌ์ฉ - context.Context๋ฅผ ์ฌ์ฉํ๋ฉด ์์ ์ทจ์๋ ํ์์์์ ์ฝ๊ฒ ๊ตฌํํ ์ ์์ด
go get github.com/fatih/color
package cmd
import (
"github.com/fatih/color"
"fmt"
)
var (
successColor = color.New(color.FgGreen, color.Bold)
errorColor = color.New(color.FgRed, color.Bold)
warningColor = color.New(color.FgYellow, color.Bold)
infoColor = color.New(color.FgCyan)
highlightColor = color.New(color.FgMagenta, color.Bold)
)
func printSuccess(msg string) {
successColor.Printf("โ %s\n", msg)
}
func printError(msg string) {
errorColor.Printf("โ %s\n", msg)
}
func printWarning(msg string) {
warningColor.Printf("โ %s\n", msg)
}
func printInfo(msg string) {
infoColor.Printf("โน %s\n", msg)
}
func printHighlight(msg string) {
highlightColor.Println(msg)
}
// ์ฌ์ฉ ์์
func showResults(results []SearchResult) {
if len(results) == 0 {
printWarning("๊ฒ์ ๊ฒฐ๊ณผ๊ฐ ์์ต๋๋ค")
return
}
printSuccess(fmt.Sprintf("์ด %d๊ฐ์ ํ์ผ์ ์ฐพ์์ต๋๋ค", len(results)))
for _, result := range results {
infoColor.Printf(" ๐ %s ", result.Path)
highlightColor.Printf("(%s)\n", formatSize(result.Size))
}
}
### ์๋ ์์ฑ ๊ธฐ๋ฅ ์ถ๊ฐํ๊ธฐ
Cobra๋ ์ ์๋์์ฑ ์คํฌ๋ฆฝํธ๋ฅผ ์๋์ผ๋ก ์์ฑํด์ค. ์ฌ์ฉ์ ๊ฒฝํ์ด ํจ์ฌ ์ข์์ง์ง:
// cmd/completion.go
package cmd
import (
"os"
"github.com/spf13/cobra"
)
var completionCmd = &cobra.Command{
Use: "completion [bash|zsh|fish|powershell]",
Short: "์ ์๋์์ฑ ์คํฌ๋ฆฝํธ ์์ฑ",
Long: `์ง์ ๋ ์์ ๋ํ ์๋์์ฑ ์คํฌ๋ฆฝํธ๋ฅผ ์์ฑํฉ๋๋ค.
์ฌ์ฉ ๋ฐฉ๋ฒ:
Bash:
$ source <(filemaster completion bash)
๋๋
$ filemaster completion bash > /etc/bash_completion.d/filemaster
Zsh:
$ filemaster completion zsh > "${fpath[1]}/_filemaster"
Fish:
$ filemaster completion fish > ~/.config/fish/completions/filemaster.fish
PowerShell:
PS> filemaster completion powershell | Out-String | Invoke-Expression
`,
DisableFlagsInUseLine: true,
ValidArgs: []string{"bash", "zsh", "fish", "powershell"},
Args: cobra.ExactValidArgs(1),
Run: func(cmd *cobra.Command, args []string) {
switch args[0] {
case "bash":
cmd.Root().GenBashCompletion(os.Stdout)
case "zsh":
cmd.Root().GenZshCompletion(os.Stdout)
case "fish":
cmd.Root().GenFishCompletion(os.Stdout, true)
case "powershell":
cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout)
}
},
}
func init() {
rootCmd.AddCommand(completionCmd)
}
์ฌ์ฉ์๋ ์ด๋ ๊ฒ ์ค์นํ ์ ์์ด:
# Bash
echo 'source <(filemaster completion bash)' >> ~/.bashrc
# Zsh
filemaster completion zsh > ~/.zsh/completion/_filemaster
echo 'fpath=(~/.zsh/completion $fpath)' >> ~/.zshrc
# Fish
filemaster completion fish > ~/.config/fish/completions/filemaster.fish
## ๐งช ํ
์คํธ์ ๋๋ฒ๊น
ํ๋กํ์
๋ํ CLI ๋๊ตฌ๋ฅผ ๋ง๋ค๋ ค๋ฉด ํ
์คํธ๊ฐ ํ์์ผ. Go๋ ํ
์คํธ ์์ฑ์ด ์ ๋ง ์ฌ์!
### ์ ๋ ํ
์คํธ ์์ฑํ๊ธฐ
// internal/file/search_test.go
package file
import (
"os"
"path/filepath"
"testing"
)
func TestSearch(t *testing.T) {
// ํ
์คํธ์ฉ ์์ ๋๋ ํ ๋ฆฌ ์์ฑ
tmpDir := t.TempDir()
// ํ
์คํธ ํ์ผ๋ค ์์ฑ
testFiles := []struct {
name string
size int64
}{
{"test1.txt", 100},
{"test2.pdf", 1000},
{"test3.txt", 500},
{"large.bin", 10000},
}
for _, tf := range testFiles {
path := filepath.Join(tmpDir, tf.name)
f, err := os.Create(path)
if err != nil {
t.Fatal(err)
}
f.Truncate(tf.size)
f.Close()
}
// ํ
์คํธ ์ผ์ด์ค๋ค
tests := []struct {
name string
opts SearchOptions
expected int
}{
{
name: "txt ํ์ผ๋ง ๊ฒ์",
opts: SearchOptions{Extension: "txt"},
expected: 2,
},
{
name: "500๋ฐ์ดํธ ์ด์ ํ์ผ",
opts: SearchOptions{MinSize: 500},
expected: 3,
},
{
name: "ํจํด ๋งค์นญ",
opts: SearchOptions{Pattern: "test[0-9]"},
expected: 3,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
results, err := Search(tmpDir, tt.opts)
if err != nil {
t.Errorf("Search() error = %v", err)
return
}
if len(results) != tt.expected {
t.Errorf("Search() got %d results, want %d",
len(results), tt.expected)
}
})
}
}
func BenchmarkSearch(b *testing.B) {
tmpDir := b.TempDir()
// ๋ฒค์น๋งํฌ์ฉ ํ์ผ ์์ฑ
for i := 0; i < 1000; i++ {
path := filepath.Join(tmpDir, fmt.Sprintf("file%d.txt", i))
os.Create(path)
}
opts := SearchOptions{Extension: "txt"}
b.ResetTimer()
for i := 0; i < b.N; i++ {
Search(tmpDir, opts)
}
}
ํ
์คํธ ์คํ:
# ๋ชจ๋ ํ
์คํธ ์คํ
go test ./...
# ์ปค๋ฒ๋ฆฌ์ง ํ์ธ
go test -cover ./...
# ๋ฒค์น๋งํฌ ์คํ
go test -bench=. ./...
# ์์ธํ ์ถ๋ ฅ
go test -v ./...
### ํตํฉ ํ
์คํธ
CLI ๋ช
๋ น์ด ์์ฒด๋ฅผ ํ
์คํธํ๋ ๊ฒ๋ ์ค์ํด:
// cmd/root_test.go
package cmd
import (
"bytes"
"testing"
)
func TestRootCommand(t *testing.T) {
// ์ถ๋ ฅ ์บก์ฒ๋ฅผ ์ํ ๋ฒํผ
buf := new(bytes.Buffer)
rootCmd.SetOut(buf)
rootCmd.SetErr(buf)
// ๋ช
๋ น์ด ์คํ
rootCmd.SetArgs([]string{"--help"})
err := rootCmd.Execute()
if err != nil {
t.Errorf("rootCmd.Execute() error = %v", err)
}
output := buf.String()
if !strings.Contains(output, "filemaster") {
t.Error("Help output doesn't contain 'filemaster'")
}
}
func TestSearchCommand(t *testing.T) {
tmpDir := t.TempDir()
// ํ
์คํธ ํ์ผ ์์ฑ
os.Create(filepath.Join(tmpDir, "test.txt"))
buf := new(bytes.Buffer)
rootCmd.SetOut(buf)
rootCmd.SetArgs([]string{"search", tmpDir, "--ext", "txt"})
err := rootCmd.Execute()
if err != nil {
t.Errorf("search command error = %v", err)
}
output := buf.String()
if !strings.Contains(output, "test.txt") {
t.Error("Search didn't find test.txt")
}
}
1. Verbose ํ๋๊ทธ - ๋๋ฒ๊ทธ ์ ๋ณด๋ฅผ ์ถ๋ ฅํ๋ --verbose ํ๋๊ทธ๋ฅผ ํญ์ ์ ๊ณตํด
2. ๋ก๊น - log ํจํค์ง๋ logrus ๊ฐ์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ก ์์ธํ ๋ก๊ทธ๋ฅผ ๋จ๊ฒจ
3. Delve ๋๋ฒ๊ฑฐ - Go ์ ์ฉ ๋๋ฒ๊ฑฐ์ธ Delve๋ฅผ ์ฌ์ฉํ๋ฉด ๋ธ๋ ์ดํฌํฌ์ธํธ ์ค์ ์ด ๊ฐ๋ฅํด
4. pprof - ์ฑ๋ฅ ํ๋กํ์ผ๋ง์ด ํ์ํ๋ฉด pprof๋ฅผ ์ฌ์ฉํด
# Linux (64๋นํธ)
GOOS=linux GOARCH=amd64 go build -o filemaster-linux-amd64
# Windows (64๋นํธ)
GOOS=windows GOARCH=amd64 go build -o filemaster-windows-amd64.exe
# macOS (Intel)
GOOS=darwin GOARCH=amd64 go build -o filemaster-darwin-amd64
# macOS (Apple Silicon)
GOOS=darwin GOARCH=arm64 go build -o filemaster-darwin-arm64
# Linux (ARM, ๋ผ์ฆ๋ฒ ๋ฆฌํ์ด ๋ฑ)
GOOS=linux GOARCH=arm GOARM=7 go build -o filemaster-linux-arm
### ๋น๋ ์คํฌ๋ฆฝํธ ๋ง๋ค๊ธฐ
๋ชจ๋ ํ๋ซํผ์ฉ ๋ฐ์ด๋๋ฆฌ๋ฅผ ํ ๋ฒ์ ๋น๋ํ๋ ์คํฌ๋ฆฝํธ๋ฅผ ๋ง๋ค์:
#!/bin/bash
# build.sh
VERSION="1.0.0"
APP_NAME="filemaster"
platforms=(
"linux/amd64"
"linux/arm64"
"darwin/amd64"
"darwin/arm64"
"windows/amd64"
)
for platform in "${platforms[@]}"
do
platform_split=(${platform//\// })
GOOS=${platform_split[0]}
GOARCH=${platform_split[1]}
output_name=$APP_NAME'-'$VERSION'-'$GOOS'-'$GOARCH
if [ $GOOS = "windows" ]; then
output_name+='.exe'
fi
echo "Building $output_name..."
env GOOS=$GOOS GOARCH=$GOARCH go build \
-ldflags="-s -w -X main.version=$VERSION" \
-o dist/$output_name
if [ $? -ne 0 ]; then
echo "Failed to build $output_name"
exit 1
fi
done
echo "Build complete!"
### ๋ฐ์ด๋๋ฆฌ ํฌ๊ธฐ ์ต์ ํ
Go ๋ฐ์ด๋๋ฆฌ๋ ๊ธฐ๋ณธ์ ์ผ๋ก ์ข ํฐ ํธ์ด์ผ. ํ์ง๋ง ์ต์ ํํ๋ฉด ํฌ๊ธฐ๋ฅผ ๋ง์ด ์ค์ผ ์ ์์ด:
# ๊ธฐ๋ณธ ๋น๋
go build -o filemaster
# ์ต์ ํ๋ ๋น๋
go build -ldflags="-s -w" -o filemaster
# UPX๋ก ์ถ๊ฐ ์์ถ (์ ํ์ฌํญ)
upx --best --lzma filemaster
| ๋น๋ ์ต์ | ์ค๋ช | ํฌ๊ธฐ ๊ฐ์ |
|---|---|---|
| -s | ์ฌ๋ณผ ํ ์ด๋ธ ์ ๊ฑฐ | ~10% |
| -w | DWARF ๋๋ฒ๊ทธ ์ ๋ณด ์ ๊ฑฐ | ~20% |
| UPX | ์คํ ํ์ผ ์์ถ | ~50-70% |
UPX๋ก ์์ถํ๋ฉด ๋ฐ์ด๋๋ฆฌ ํฌ๊ธฐ๋ ํฌ๊ฒ ์ค์ด๋ค์ง๋ง, ์ผ๋ถ ์ํฐ๋ฐ์ด๋ฌ์ค ํ๋ก๊ทธ๋จ์ด ์คํ์งํ ์ ์์ด. ๋ํ ์คํ ์ ์์ถ ํด์ ์๊ฐ์ด ์ถ๊ฐ๋๋ฏ๋ก ์์ ์๋๊ฐ ์ฝ๊ฐ ๋๋ ค์ง ์ ์์ด.
# .github/workflows/release.yml
name: Release
on:
push:
tags:
- 'v*'
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.21'
- name: Run tests
run: go test -v ./...
- name: Build binaries
run: |
chmod +x build.sh
./build.sh
- name: Create Release
uses: softprops/action-gh-release@v1
with:
files: dist/*
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
์ด์ ํ๊ทธ๋ฅผ ํธ์ํ๋ฉด ์๋์ผ๋ก ๋ฆด๋ฆฌ์ค๊ฐ ์์ฑ๋ผ:
git tag v1.0.0
git push origin v1.0.0
### Homebrew๋ก ๋ฐฐํฌํ๊ธฐ (macOS)
macOS ์ฌ์ฉ์๋ค์ ์ํด Homebrew Formula๋ฅผ ๋ง๋ค ์๋ ์์ด:
# Formula/filemaster.rb
class Filemaster < Formula
desc "๊ฐ๋ ฅํ ํ์ผ ๊ด๋ฆฌ CLI ๋๊ตฌ"
homepage "https://github.com/yourusername/filemaster"
url "https://github.com/yourusername/filemaster/archive/v1.0.0.tar.gz"
sha256 "your-sha256-hash"
license "MIT"
depends_on "go" => :build
def install
system "go", "build", *std_go_args(ldflags: "-s -w")
end
test do
system "#{bin}/filemaster", "--version"
end
end
์ฌ์ฉ์๋ ์ด๋ ๊ฒ ์ค์นํ ์ ์์ด:
brew tap yourusername/tap
brew install filemaster
## ๐ ์ค์ ์์ : ์ฌ๋ฅ๋ท API ํด๋ผ์ด์ธํธ ๋ง๋ค๊ธฐ
์ด์ ๋ฐฐ์ด ๋ด์ฉ์ ํ์ฉํด์ ์ค์ ๋ก ์ ์ฉํ ๋๊ตฌ๋ฅผ ๋ง๋ค์ด๋ณด์! ์ฌ๋ฅ๋ท์ API๋ฅผ ์ฌ์ฉํ๋ CLI ํด๋ผ์ด์ธํธ๋ฅผ ๋ง๋ค์ด๋ณผ๊ฒ. ์ฌ๋ฅ๋ท์ ๋ค์ํ ์ฌ๋ฅ์ ๊ฑฐ๋ํ๋ ํ๋ซํผ์ด๋๊น, ์ฌ๋ฅ ๊ฒ์์ด๋ ๋ฑ๋ก์ CLI๋ก ํ ์ ์์ผ๋ฉด ํธ๋ฆฌํ๊ฒ ์ง? ๐
### ํ๋ก์ ํธ ๊ตฌ์กฐ
jaenung-cli/
โโโ cmd/
โ โโโ root.go
โ โโโ search.go
โ โโโ post.go
โ โโโ profile.go
โโโ internal/
โ โโโ api/
โ โ โโโ client.go
โ โโโ config/
โ โโโ config.go
โโโ .jaenung.yaml
โโโ main.go
### API ํด๋ผ์ด์ธํธ ๊ตฌํ
๋จผ์ ์ฌ๋ฅ๋ท API์ ํต์ ํ๋ ํด๋ผ์ด์ธํธ๋ฅผ ๋ง๋ค์:
// internal/api/client.go
package api
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
)
type Client struct {
BaseURL string
APIKey string
HTTPClient *http.Client
}
type Talent struct {
ID int `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
Category string `json:"category"`
Price int `json:"price"`
Author string `json:"author"`
CreatedAt time.Time `json:"created_at"`
}
type SearchOptions struct {
Query string
Category string
MinPrice int
MaxPrice int
Limit int
}
func NewClient(baseURL, apiKey string) *Client {
return &Client{
BaseURL: baseURL,
APIKey: apiKey,
HTTPClient: &http.Client{
Timeout: 30 * time.Second,
},
}
}
func (c *Client) SearchTalents(opts SearchOptions) ([]Talent, error) {
url := fmt.Sprintf("%s/api/talents/search", c.BaseURL)
params := map[string]interface{}{
"query": opts.Query,
"category": opts.Category,
"min_price": opts.MinPrice,
"max_price": opts.MaxPrice,
"limit": opts.Limit,
}
body, err := json.Marshal(params)
if err != nil {
return nil, err
}
req, err := http.NewRequest("POST", url, bytes.NewBuffer(body))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+c.APIKey)
resp, err := c.HTTPClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("API error: %s", string(body))
}
var talents []Talent
if err := json.NewDecoder(resp.Body).Decode(&talents); err != nil {
return nil, err
}
return talents, nil
}
func (c *Client) PostTalent(talent Talent) error {
url := fmt.Sprintf("%s/api/talents", c.BaseURL)
body, err := json.Marshal(talent)
if err != nil {
return err
}
req, err := http.NewRequest("POST", url, bytes.NewBuffer(body))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+c.APIKey)
resp, err := c.HTTPClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusCreated {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("API error: %s", string(body))
}
return nil
}
### Search ์ปค๋งจ๋ ๊ตฌํ
์ฌ๋ฅ์ ๊ฒ์ํ๋ ๋ช
๋ น์ด๋ฅผ ๋ง๋ค์:
// cmd/search.go
package cmd
import (
"fmt"
"github.com/fatih/color"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"jaenung-cli/internal/api"
)
var searchCmd = &cobra.Command{
Use: "search [ํค์๋]",
Short: "์ฌ๋ฅ๋ท์์ ์ฌ๋ฅ ๊ฒ์",
Long: `ํค์๋์ ํํฐ๋ฅผ ์ฌ์ฉํด์ ์ฌ๋ฅ๋ท์์ ์ฌ๋ฅ์ ๊ฒ์ํฉ๋๋ค.`,
Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
query := args[0]
category, _ := cmd.Flags().GetString("category")
minPrice, _ := cmd.Flags().GetInt("min-price")
maxPrice, _ := cmd.Flags().GetInt("max-price")
limit, _ := cmd.Flags().GetInt("limit")
// API ํด๋ผ์ด์ธํธ ์์ฑ
client := api.NewClient(
viper.GetString("api.base_url"),
viper.GetString("api.key"),
)
// ๊ฒ์ ์คํ
opts := api.SearchOptions{
Query: query,
Category: category,
MinPrice: minPrice,
MaxPrice: maxPrice,
Limit: limit,
}
talents, err := client.SearchTalents(opts)
if err != nil {
color.Red("โ ๊ฒ์ ์คํจ: %v", err)
return
}
// ๊ฒฐ๊ณผ ์ถ๋ ฅ
if len(talents) == 0 {
color.Yellow("โ ๏ธ ๊ฒ์ ๊ฒฐ๊ณผ๊ฐ ์์ต๋๋ค")
return
}
color.Green("โ %d๊ฐ์ ์ฌ๋ฅ์ ์ฐพ์์ต๋๋ค:\n", len(talents))
for i, talent := range talents {
fmt.Printf("\n%d. ", i+1)
color.Cyan(talent.Title)
fmt.Printf(" ์นดํ
๊ณ ๋ฆฌ: %s | ๊ฐ๊ฒฉ: %s์\n",
talent.Category,
formatPrice(talent.Price))
fmt.Printf(" ์์ฑ์: %s\n", talent.Author)
if viper.GetBool("verbose") {
fmt.Printf(" ์ค๋ช
: %s\n", talent.Description)
}
}
fmt.Println("\n๐ก ์์ธ ์ ๋ณด๋ฅผ ๋ณด๋ ค๋ฉด --verbose ํ๋๊ทธ๋ฅผ ์ฌ์ฉํ์ธ์")
},
}
func init() {
rootCmd.AddCommand(searchCmd)
searchCmd.Flags().StringP("category", "c", "", "์นดํ
๊ณ ๋ฆฌ ํํฐ")
searchCmd.Flags().Int("min-price", 0, "์ต์ ๊ฐ๊ฒฉ")
searchCmd.Flags().Int("max-price", 0, "์ต๋ ๊ฐ๊ฒฉ")
searchCmd.Flags().IntP("limit", "l", 10, "๊ฒฐ๊ณผ ๊ฐ์ ์ ํ")
}
func formatPrice(price int) string {
if price >= 1000000 {
return fmt.Sprintf("%.1f๋ง", float64(price)/10000)
}
return fmt.Sprintf("%d", price)
}
### Post ์ปค๋งจ๋ ๊ตฌํ
์๋ก์ด ์ฌ๋ฅ์ ๋ฑ๋กํ๋ ๋ช
๋ น์ด๋ ๋ง๋ค์:
// cmd/post.go
package cmd
import (
"bufio"
"fmt"
"os"
"strings"
"github.com/fatih/color"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"jaenung-cli/internal/api"
)
var postCmd = &cobra.Command{
Use: "post",
Short: "์๋ก์ด ์ฌ๋ฅ ๋ฑ๋ก",
Long: `์ฌ๋ฅ๋ท์ ์๋ก์ด ์ฌ๋ฅ์ ๋ฑ๋กํฉ๋๋ค.`,
Run: func(cmd *cobra.Command, args []string) {
interactive, _ := cmd.Flags().GetBool("interactive")
var talent api.Talent
if interactive {
talent = promptTalentInfo()
} else {
title, _ := cmd.Flags().GetString("title")
description, _ := cmd.Flags().GetString("description")
category, _ := cmd.Flags().GetString("category")
price, _ := cmd.Flags().GetInt("price")
if title == "" || description == "" || category == "" {
color.Red("โ ํ์ ์ ๋ณด๊ฐ ๋๋ฝ๋์์ต๋๋ค")
fmt.Println("--interactive ํ๋๊ทธ๋ฅผ ์ฌ์ฉํ๊ฑฐ๋ ๋ชจ๋ ํ์ ํ๋๊ทธ๋ฅผ ์ง์ ํ์ธ์")
return
}
talent = api.Talent{
Title: title,
Description: description,
Category: category,
Price: price,
}
}
// ํ์ธ
fmt.Println("\n๐ ๋ฑ๋กํ ์ฌ๋ฅ ์ ๋ณด:")
fmt.Printf("์ ๋ชฉ: %s\n", talent.Title)
fmt.Printf("์นดํ
๊ณ ๋ฆฌ: %s\n", talent.Category)
fmt.Printf("๊ฐ๊ฒฉ: %d์\n", talent.Price)
fmt.Printf("์ค๋ช
: %s\n", talent.Description)
fmt.Print("\n๋ฑ๋กํ์๊ฒ ์ต๋๊น? (y/N): ")
reader := bufio.NewReader(os.Stdin)
confirm, _ := reader.ReadString('\n')
confirm = strings.TrimSpace(strings.ToLower(confirm))
if confirm != "y" && confirm != "yes" {
color.Yellow("โ ๏ธ ๋ฑ๋ก์ด ์ทจ์๋์์ต๋๋ค")
return
}
// API ํด๋ผ์ด์ธํธ ์์ฑ
client := api.NewClient(
viper.GetString("api.base_url"),
viper.GetString("api.key"),
)
// ์ฌ๋ฅ ๋ฑ๋ก
if err := client.PostTalent(talent); err != nil {
color.Red("โ ๋ฑ๋ก ์คํจ: %v", err)
return
}
color.Green("โ ์ฌ๋ฅ์ด ์ฑ๊ณต์ ์ผ๋ก ๋ฑ๋ก๋์์ต๋๋ค!")
fmt.Println("์ฌ๋ฅ๋ท(https://www.jaenung.net)์์ ํ์ธํ์ธ์.")
},
}
func init() {
rootCmd.AddCommand(postCmd)
postCmd.Flags().BoolP("interactive", "i", false, "๋ํํ ๋ชจ๋")
postCmd.Flags().String("title", "", "์ฌ๋ฅ ์ ๋ชฉ")
postCmd.Flags().String("description", "", "์ฌ๋ฅ ์ค๋ช
")
postCmd.Flags().String("category", "", "์นดํ
๊ณ ๋ฆฌ")
postCmd.Flags().Int("price", 0, "๊ฐ๊ฒฉ")
}
func promptTalentInfo() api.Talent {
reader := bufio.NewReader(os.Stdin)
fmt.Print("์ ๋ชฉ: ")
title, _ := reader.ReadString('\n')
title = strings.TrimSpace(title)
fmt.Print("์นดํ
๊ณ ๋ฆฌ: ")
category, _ := reader.ReadString('\n')
category = strings.TrimSpace(category)
fmt.Print("๊ฐ๊ฒฉ (์): ")
var price int
fmt.Scanln(&price)
reader.ReadString('\n') // ๋ฒํผ ๋น์ฐ๊ธฐ
fmt.Print("์ค๋ช
: ")
description, _ := reader.ReadString('\n')
description = strings.TrimSpace(description)
return api.Talent{
Title: title,
Description: description,
Category: category,
Price: price,
}
}
### ์ค์ ํ์ผ
# .jaenung.yaml
api:
base_url: "https://api.jaenung.net"
key: "your-api-key-here"
defaults:
search_limit: 10
category: ""
output:
format: "table" # table, json, yaml
color: true
verbose: false
์ด์ ์ด๋ ๊ฒ ์ฌ์ฉํ ์ ์์ด:
# ์ฌ๋ฅ ๊ฒ์
jaenung-cli search "ํ๋ก๊ทธ๋๋ฐ" --category "๊ฐ๋ฐ" --limit 5
# ๋ํํ ๋ชจ๋๋ก ์ฌ๋ฅ ๋ฑ๋ก
jaenung-cli post --interactive
# ํ๋๊ทธ๋ก ์ฌ๋ฅ ๋ฑ๋ก
jaenung-cli post \
--title "Go ์ธ์ด ๊ณผ์ธ" \
--description "Go ์ธ์ด๋ฅผ ์ฒ์๋ถํฐ ์๋ ค๋๋ฆฝ๋๋ค" \
--category "๊ต์ก" \
--price 50000
## ๐ ๋ฒ ์คํธ ํ๋ํฐ์ค์ ํ
CLI ๋๊ตฌ๋ฅผ ๋ง๋ค๋ฉด์ ๋ฐฐ์ด ๋ฒ ์คํธ ํ๋ํฐ์ค๋ค์ ์ ๋ฆฌํด๋ณผ๊ฒ!
### 1. ์ฌ์ฉ์ ์นํ์ ์ธ ์๋ฌ ๋ฉ์์ง
์๋ฌ ๋ฉ์์ง๋ ๋ช
ํํ๊ณ ํด๊ฒฐ ๋ฐฉ๋ฒ์ ์ ์ํด์ผ ํด:
// ๋์ ์
fmt.Println("Error: invalid input")
// ์ข์ ์
color.Red("โ ์ค๋ฅ: ์ ํจํ์ง ์์ ์
๋ ฅ์
๋๋ค")
fmt.Println("\nํด๊ฒฐ ๋ฐฉ๋ฒ:")
fmt.Println(" 1. ํ์ผ ๊ฒฝ๋ก๋ฅผ ํ์ธํ์ธ์")
fmt.Println(" 2. ํ์ผ ๊ถํ์ ํ์ธํ์ธ์")
fmt.Println(" 3. --help ์ต์
์ผ๋ก ์ฌ์ฉ๋ฒ์ ํ์ธํ์ธ์")
### 2. ์งํ ์ํฉ ํ์
์ค๋ ๊ฑธ๋ฆฌ๋ ์์
์ ๋ฐ๋์ ์งํ ์ํฉ์ ๋ณด์ฌ์ค:
spinner := spinner.New(spinner.CharSets[14], 100*time.Millisecond)
spinner.Suffix = " ํ์ผ ์ฒ๋ฆฌ ์ค..."
spinner.Start()
// ์์
์ํ
processFiles()
spinner.Stop()
color.Green("โ ์๋ฃ!")
### 3. ์ค์ ์ ์ฐ์ ์์
์ค์ ๊ฐ์ ์ฐ์ ์์๋ฅผ ๋ช
ํํ ํด:
1. ์ปค๋งจ๋ ๋ผ์ธ ํ๋๊ทธ
2. ํ๊ฒฝ ๋ณ์
3. ์ค์ ํ์ผ
4. ๊ธฐ๋ณธ๊ฐ
### 4. ๋์๋ง ์์ฑ
๋ช
ํํ๊ณ ์์ ๊ฐ ํฌํจ๋ ๋์๋ง์ ์์ฑํด:
var searchCmd = &cobra.Command{
Use: "search [ํค์๋]",
Short: "์ฌ๋ฅ ๊ฒ์",
Long: `์ฌ๋ฅ๋ท์์ ํค์๋๋ก ์ฌ๋ฅ์ ๊ฒ์ํฉ๋๋ค.
๊ฒ์ ์ต์
:
- ์นดํ
๊ณ ๋ฆฌ ํํฐ๋ง
- ๊ฐ๊ฒฉ ๋ฒ์ ์ง์
- ๊ฒฐ๊ณผ ๊ฐ์ ์ ํ`,
Example: ` # ๊ธฐ๋ณธ ๊ฒ์
jaenung-cli search "ํ๋ก๊ทธ๋๋ฐ"
# ์นดํ
๊ณ ๋ฆฌ์ ๊ฐ๊ฒฉ ํํฐ
jaenung-cli search "๋์์ธ" --category "๊ทธ๋ํฝ" --max-price 100000
# ์์ธ ์ ๋ณด ํฌํจ
jaenung-cli search "๋ฒ์ญ" --verbose`,
Run: searchRun,
}
### 5. ๋ฒ์ ์ ๋ณด ์ ๊ณต
ํญ์ ๋ฒ์ ์ ๋ณด๋ฅผ ์ ๊ณตํด:
var (
version = "dev"
commit = "none"
date = "unknown"
)
var versionCmd = &cobra.Command{
Use: "version",
Short: "๋ฒ์ ์ ๋ณด ์ถ๋ ฅ",
Run: func(cmd *cobra.Command, args []string) {
fmt.Printf("jaenung-cli version %s\n", version)
fmt.Printf("commit: %s\n", commit)
fmt.Printf("built at: %s\n", date)
},
}
๋น๋ ์ ๋ฒ์ ์ ๋ณด๋ฅผ ์ฃผ์
:
go build -ldflags="-X main.version=1.0.0 -X main.commit=$(git rev-parse HEAD) -X main.date=$(date -u +%Y-%m-%dT%H:%M:%SZ)"
### 6. ๋ก๊น
๋ ๋ฒจ
๋ค์ํ ๋ก๊น
๋ ๋ฒจ์ ์ง์ํด:
import "github.com/sirupsen/logrus"
var log = logrus.New()
func init() {
if viper.GetBool("debug") {
log.SetLevel(logrus.DebugLevel)
} else if viper.GetBool("verbose") {
log.SetLevel(logrus.InfoLevel)
} else {
log.SetLevel(logrus.WarnLevel)
}
}
### 7. ์์ ํ ์ข
๋ฃ
์๊ทธ๋์ ์ฒ๋ฆฌํด์ ์์ ํ๊ฒ ์ข
๋ฃ:
import (
"context"
"os"
"os/signal"
"syscall"
)
func main() {
ctx, cancel := context.WithCancel(context.Background())
// ์๊ทธ๋ ์ฒ๋ฆฌ
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
go func() {
<-sigChan
fmt.Println("\n์ข
๋ฃ ์ค...")
cancel()
}()
// ๋ฉ์ธ ๋ก์ง ์คํ
if err := run(ctx); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}
- โ ๋ช ํํ ์๋ฌ ๋ฉ์์ง์ ํด๊ฒฐ ๋ฐฉ๋ฒ ์ ์
- โ ์งํ ์ํฉ ํ์ (์คํผ๋, ํ๋ก๊ทธ๋ ์ค ๋ฐ)
- โ ์์ธํ ๋์๋ง๊ณผ ์์
- โ ๋ฒ์ ์ ๋ณด ์ ๊ณต
- โ ๋ค์ํ ์ถ๋ ฅ ํ์ ์ง์ (JSON, YAML, ํ ์ด๋ธ)
- โ ์ค์ ํ์ผ๊ณผ ํ๊ฒฝ ๋ณ์ ์ง์
- โ ์๋ ์์ฑ ์คํฌ๋ฆฝํธ ์ ๊ณต
- โ ์ ์ ํ ์ข ๋ฃ ์ฝ๋ ๋ฐํ
- โ ๋ก๊น ๊ณผ ๋๋ฒ๊ทธ ๋ชจ๋
- โ ์์ ํ ์๊ทธ๋ ์ฒ๋ฆฌ
go get github.com/charmbracelet/bubbletea
go get github.com/charmbracelet/lipgloss
๊ฐ๋จํ ๋ฉ๋ด ์์ :
package main
import (
"fmt"
"os"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
type model struct {
choices []string
cursor int
selected map[int]struct{}
}
func initialModel() model {
return model{
choices: []string{"๊ฒ์", "๋ฑ๋ก", "ํ๋กํ", "์ข
๋ฃ"},
selected: make(map[int]struct{}),
}
}
func (m model) Init() tea.Cmd {
return nil
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "ctrl+c", "q":
return m, tea.Quit
case "up", "k":
if m.cursor > 0 {
m.cursor--
}
case "down", "j":
if m.cursor < len(m.choices)-1 {
m.cursor++
}
case "enter", " ":
_, ok := m.selected[m.cursor]
if ok {
delete(m.selected, m.cursor)
} else {
m.selected[m.cursor] = struct{}{}
}
}
}
return m, nil
}
func (m model) View() string {
s := "์ฌ๋ฅ๋ท CLI - ๋ฉ๋ด๋ฅผ ์ ํํ์ธ์:\n\n"
for i, choice := range m.choices {
cursor := " "
if m.cursor == i {
cursor = ">"
}
checked := " "
if _, ok := m.selected[i]; ok {
checked = "x"
}
s += fmt.Sprintf("%s [%s] %s\n", cursor, checked, choice)
}
s += "\n(โ/โ: ์ด๋, Space: ์ ํ, Enter: ์คํ, q: ์ข
๋ฃ)\n"
return s
}
func main() {
p := tea.NewProgram(initialModel())
if err := p.Start(); err != nil {
fmt.Printf("Error: %v", err)
os.Exit(1)
}
}
### ํ๋ฌ๊ทธ์ธ ์์คํ
ํ์ฅ ๊ฐ๋ฅํ CLI ๋๊ตฌ๋ฅผ ๋ง๋ค๊ณ ์ถ๋ค๋ฉด ํ๋ฌ๊ทธ์ธ ์์คํ
์ ๊ตฌํํ ์ ์์ด:
// plugin/plugin.go
package plugin
import "plugin"
type Plugin interface {
Name() string
Execute(args []string) error
}
func LoadPlugin(path string) (Plugin, error) {
p, err := plugin.Open(path)
if err != nil {
return nil, err
}
symPlugin, err := p.Lookup("Plugin")
if err != nil {
return nil, err
}
var plug Plugin
plug, ok := symPlugin.(Plugin)
if !ok {
return nil, fmt.Errorf("unexpected type from module symbol")
}
return plug, nil
}
### ๋ฐ์ดํฐ๋ฒ ์ด์ค ํตํฉ
๋ก์ปฌ ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํด์ผ ํ๋ค๋ฉด SQLite๋ฅผ ์ฌ์ฉํ ์ ์์ด:
go get github.com/mattn/go-sqlite3
package db
import (
"database/sql"
_ "github.com/mattn/go-sqlite3"
)
type DB struct {
conn *sql.DB
}
func New(path string) (*DB, error) {
conn, err := sql.Open("sqlite3", path)
if err != nil {
return nil, err
}
// ํ
์ด๋ธ ์์ฑ
_, err = conn.Exec(`
CREATE TABLE IF NOT EXISTS history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
command TEXT NOT NULL,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
)
`)
if err != nil {
return nil, err
}
return &DB{conn: conn}, nil
}
func (db *DB) SaveCommand(command string) error {
_, err := db.conn.Exec("INSERT INTO history (command) VALUES (?)", command)
return err
}
func (db *DB) GetHistory(limit int) ([]string, error) {
rows, err := db.conn.Query(
"SELECT command FROM history ORDER BY timestamp DESC LIMIT ?",
limit,
)
if err != nil {
return nil, err
}
defer rows.Close()
var history []string
for rows.Next() {
var cmd string
if err := rows.Scan(&cmd); err != nil {
return nil, err
}
history = append(history, cmd)
}
return history, nil
}
### ์น ์๋ฒ ๋ชจ๋
CLI ๋๊ตฌ์ ์น ์ธํฐํ์ด์ค๋ฅผ ์ถ๊ฐํ ์๋ ์์ด:
var serveCmd = &cobra.Command{
Use: "serve",
Short: "์น ์๋ฒ ๋ชจ๋๋ก ์คํ",
Run: func(cmd *cobra.Command, args []string) {
port, _ := cmd.Flags().GetInt("port")
http.HandleFunc("/api/search", handleSearch)
http.HandleFunc("/api/post", handlePost)
addr := fmt.Sprintf(":%d", port)
color.Green("๐ ์น ์๋ฒ ์์: http://localhost%s", addr)
if err := http.ListenAndServe(addr, nil); err != nil {
color.Red("โ ์๋ฒ ์์ ์คํจ: %v", err)
}
},
}
func init() {
rootCmd.AddCommand(serveCmd)
serveCmd.Flags().IntP("port", "p", 8080, "ํฌํธ ๋ฒํธ")
}
๋ ๊น์ด ๊ณต๋ถํ๊ณ ์ถ๋ค๋ฉด ์ด๋ฐ ๋ฆฌ์์ค๋ค์ ์ถ์ฒํด:
โข Cobra ๊ณต์ ๋ฌธ์ - https://cobra.dev
โข Viper GitHub - https://github.com/spf13/viper
โข Awesome Go CLI - CLI ๊ด๋ จ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ๋ชจ์
โข 12 Factor CLI Apps - CLI ๋๊ตฌ ์ค๊ณ ์์น
โข Command Line Interface Guidelines - CLI UX ๊ฐ์ด๋
๊ด๋ จ ํค์๋
๋๊ธ 0
์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.

๋๊ธ ์์ฑ
์ด ๊ธ์ ๋ํ ์ฌ๋ฌ๋ถ์ ์๊ฐ์ ๋ค๋ ค์ฃผ์ธ์
๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค
๋๊ธ์ ์์ฑํ๋ ค๋ฉด ๋จผ์ ๋ก๊ทธ์ธํด์ฃผ์ธ์.