This commit is contained in:
zhouxhere 2025-02-20 16:55:31 +08:00
commit 2e0c9b34d7
34 changed files with 12063 additions and 0 deletions

24
.gitignore vendored Executable file
View File

@ -0,0 +1,24 @@
# Nuxt dev/build outputs
.output
.data
.nuxt
.nitro
.cache
dist
# Node dependencies
node_modules
# Logs
logs
*.log
# Misc
.DS_Store
.fleet
.idea
# Local env files
.env
.env.*
!.env.example

25
.vscode/launch.json vendored Executable file
View File

@ -0,0 +1,25 @@
{
// 使 IntelliSense
//
// 访: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Launch Package in Debug Mode",
"type": "go",
"request": "launch",
"mode": "debug",
"program": "./main.go",
"args": ["start"]
},
{
"name": "Launch Package in Prod Mode",
"type": "go",
"request": "launch",
"mode": "debug",
"preLaunchTask": "prepare",
"program": "./main.go",
"args": ["start", "--mode", "prod"]
}
]
}

14
.vscode/settings.json vendored Executable file
View File

@ -0,0 +1,14 @@
{
"sqltools.connections": [
{
"previewLimit": 50,
"server": "localhost",
"port": 5432,
"driver": "PostgreSQL",
"name": "local-maptile",
"database": "maptile",
"username": "maptile",
"password": "maptile_passwd"
}
]
}

28
.vscode/tasks.json vendored Executable file
View File

@ -0,0 +1,28 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"label": "prepare-swaggo",
"type": "shell",
"command": "go install github.com/swaggo/swag/cmd/swag@latest"
},
{
"label": "prepare-go",
"type": "shell",
"command": "go mod tidy",
},
{
"label": "swaggo-init",
"type": "shell",
"command": "swag init --ot json"
}, {
"label": "prepare",
"dependsOn":[
"prepare-go",
"swaggo-init",
]
}
]
}

105
api/api.go Executable file
View File

@ -0,0 +1,105 @@
package api
import (
"context"
"log/slog"
"net/http"
"reflect"
"regexp"
"strings"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
echoSwagger "github.com/swaggo/echo-swagger"
_ "git.zhouxhere.com/zhouxhere/maptile/docs"
"git.zhouxhere.com/zhouxhere/maptile/store"
)
type API struct {
*echo.Echo
store *store.Store
}
func NewAPI(e *echo.Echo, s *store.Store) {
api := &API{e, s}
api.Validator = NewCustomValidator()
api.Use(middleware.Recover())
api.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{LogStatus: true,
LogURI: true,
LogError: true,
HandleError: true, // forwards error to the global error handler, so it can decide appropriate status code
LogValuesFunc: func(c echo.Context, v middleware.RequestLoggerValues) error {
if v.Error != nil {
slog.LogAttrs(context.Background(), slog.LevelError, "REQUEST", slog.String("method", v.Method), slog.String("uri", v.URI), slog.Int("status", v.Status), slog.String("latency", v.Latency.String()), slog.String("error", v.Error.Error()))
} else {
slog.LogAttrs(context.Background(), slog.LevelInfo, "REQUEST", slog.String("method", v.Method), slog.String("uri", v.URI), slog.Int("status", v.Status), slog.String("latency", v.Latency.String()))
}
return nil
},
}))
api.HTTPErrorHandler = func(err error, c echo.Context) {
req := c.Request()
slog.LogAttrs(context.Background(), slog.LevelError, "REQUEST", slog.String("method", req.Method), slog.String("uri", req.RequestURI), slog.String("error", err.Error()))
c.JSON(http.StatusOK, Response[interface{}]{
Code: http.StatusInternalServerError,
Message: err.Error(),
Data: nil,
})
}
// swagger
api.GET("/swagger/*", echoSwagger.WrapHandler)
api.GET("/swagger", func(c echo.Context) error {
return c.Redirect(http.StatusMovedPermanently, "/swagger/index.html")
})
api.Register()
// for _, route := range api.Routes() {
// fmt.Println(route.Method, route.Path)
// }
}
func (a *API) Register() {
v := reflect.ValueOf(a)
t := reflect.TypeOf(a)
group := a.Group("/api/v1")
for i := 0; i < v.NumMethod(); i++ {
method := v.Method(i)
methodType := t.Method(i)
methodName := methodType.Name
// 使用正则表达式匹配方法名
re := regexp.MustCompile(`^(Post|Get|Put|Delete)([A-Za-z]+)(By[A-Za-z]+)?$`)
matches := re.FindStringSubmatch(methodName)
if len(matches) == 0 {
continue
}
action := matches[1]
route := strings.ToLower(matches[2])
if matches[3] != "" {
param := strings.ToLower(matches[3][2:])
route = "/" + route + "/:" + param
} else {
route = "/" + route
}
switch action {
case "Post":
group.POST(route, method.Interface().(func(echo.Context) error))
case "Get":
group.GET(route, method.Interface().(func(echo.Context) error))
case "Put":
group.PUT(route, method.Interface().(func(echo.Context) error))
case "Delete":
group.DELETE(route, method.Interface().(func(echo.Context) error))
}
}
}

79
api/base.go Executable file
View File

@ -0,0 +1,79 @@
package api
import (
"net/http"
"github.com/go-playground/validator/v10"
"github.com/labstack/echo/v4"
)
type Response[T any] struct {
Code int `json:"code"`
Message string `json:"message"`
Data T `json:"data"`
}
type Pagination[T any] struct {
Page int `json:"page"`
Size int `json:"size"`
Total int `json:"total"`
Data []T `json:"data"`
}
type CustomValidator struct {
validator *validator.Validate
}
func (cv *CustomValidator) Validate(i interface{}) error {
return cv.validator.Struct(i)
}
func NewCustomValidator() *CustomValidator {
return &CustomValidator{validator: validator.New()}
}
func BindAndValidate(c echo.Context, i interface{}) error {
if err := c.Bind(i); err != nil {
return err
}
if err := c.Validate(i); err != nil {
return err
}
return nil
}
func Success[T any](c echo.Context, data T, message string) error {
return c.JSON(http.StatusOK, Response[T]{
Code: http.StatusOK,
Message: message,
Data: data,
})
}
func Failed(c echo.Context, message string) error {
return c.JSON(http.StatusOK, Response[interface{}]{
Code: http.StatusInternalServerError,
Message: message,
Data: nil,
})
}
func Error(c echo.Context, code int, message string) error {
return c.JSON(code, Response[interface{}]{
Code: code,
Message: message,
Data: nil,
})
}
func Paginate[T any](c echo.Context, data []T, page, size, total int, message string) error {
return Success(c, Pagination[T]{
Page: page,
Size: size,
Total: total,
Data: data,
}, message,
)
}

108
api/feature.go Executable file
View File

@ -0,0 +1,108 @@
package api
import (
"net/http"
"git.zhouxhere.com/zhouxhere/maptile/model"
"github.com/labstack/echo/v4"
)
// ListFeature 获取要素列表
// @Summary 获取要素列表
// @Description 获取要素列表
// @Tags feature
// @Accept json
// @Produce json
// @Param page query int false "页码"
// @Param size query int false "每页数量"
// @Success 200 {object} Pagination[FeaturePublic]
// @Router /features [get]
func (a *API) GetFeatures(c echo.Context) error {
var featureList model.FeatureList
if err := BindAndValidate(c, &featureList); err != nil {
return Error(c, http.StatusBadRequest, err.Error())
}
features, total, err := a.store.ListFeature(&featureList)
if err != nil {
return Error(c, http.StatusInternalServerError, err.Error())
}
// 将 features 转换为 FeaturePublic
featurePublics := make([]model.FeaturePublic, len(features))
for i, feature := range features {
featurePublics[i] = feature.ToPublic()
}
return Paginate(c, featurePublics, featureList.Page, featureList.Size, total, "获取要素列表成功")
}
// CreateFeature 创建要素
// @Summary 创建要素
// @Description 创建要素
// @Tags feature
// @Accept json
// @Produce json
// @Param feature body store.FeatureCreate true "要素"
// @Success 200 {object} Response[FeaturePublic]
// @Router /feature [post]
func (a *API) PostFeature(c echo.Context) error {
var featureCreate model.FeatureCreate
if err := BindAndValidate(c, &featureCreate); err != nil {
return Error(c, http.StatusBadRequest, err.Error())
}
feature, err := a.store.CreateFeature(&featureCreate)
if err != nil {
return Error(c, http.StatusInternalServerError, err.Error())
}
return Success(c, feature.ToPublic(), "创建要素成功")
}
// GetFeatureByID 获取要素
// @Summary 获取要素
// @Description 获取要素
// @Tags feature
// @Accept json
// @Produce json
// @Param id path string true "要素 ID"
// @Success 200 {object} Response[FeaturePublic]
// @Router /feature/{id} [get]
func (a *API) GetFeatureByID(c echo.Context) error {
return nil
}
// UpdateFeature 更新要素
// @Summary 更新要素
// @Description 更新要素
// @Tags feature
// @Accept json
// @Produce json
// @Param feature body store.FeatureUpdate true "要素"
// @Success 200 {object} Response[FeaturePublic]
// @Router /feature [put]
func (a *API) PutFeature(c echo.Context) error {
return nil
}
// DeleteFeature 删除要素
// @Summary 删除要素
// @Description 删除要素
// @Tags feature
// @Accept json
// @Produce json
// @Param feature body store.FeatureDeleteOrBanned true "要素"
// @Success 200 {object} Response[FeaturePublic]
// @Router /feature/{id} [delete]
func (a *API) DeleteFeature(c echo.Context) error {
return nil
}
// BannedFeature 禁用要素
// @Summary 禁用要素
// @Description 禁用要素
// @Tags feature
// @Accept json
// @Produce json
// @Param feature body store.FeatureDeleteOrBanned true "要素"
// @Success 200 {object} Response[FeaturePublic]
// @Router /feature/banned/{id} [post]
func (a *API) BannedFeature(c echo.Context) error {
return nil
}

27
api/tag.go Executable file
View File

@ -0,0 +1,27 @@
package api
// // CreateTag 创建标签
// // @Summary 创建标签
// // @Description 创建标签
// // @Tags tag
// // @Accept json
// // @Produce json
// // @Param tag body TagCreate true "标签"
// // @Success 200 {object} TagCreate
// // @Router /tag [post]
// func PostTag(c echo.Context) error {
// return nil
// }
// // GetTag 获取标签
// // @Summary 获取标签
// // @Description 获取标签
// // @Tags tag
// // @Accept json
// // @Produce json
// // @Param id path string true "标签 ID"
// // @Success 200 {object} TagGet
// // @Router /tag/{id} [get]
// func GetTagByID(c echo.Context) error {
// return nil
// }

95
bin/maptile.go Executable file
View File

@ -0,0 +1,95 @@
package bin
import (
"context"
"log/slog"
"net/http"
"os"
"os/signal"
"syscall"
"git.zhouxhere.com/zhouxhere/maptile/config"
_ "git.zhouxhere.com/zhouxhere/maptile/log"
"git.zhouxhere.com/zhouxhere/maptile/server"
"git.zhouxhere.com/zhouxhere/maptile/store"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var rootCmd = &cobra.Command{
Use: "maptile",
Short: "一个简单的地图vector tile 管理",
Run: func(cmd *cobra.Command, args []string) {
config := &config.Config{
Mode: viper.GetString("mode"),
Addr: viper.GetString("addr"),
Port: viper.GetInt("port"),
DSN: viper.GetString("dsn"),
}
if err := config.Validate(); err != nil {
panic(err)
}
ctx, cancel := context.WithCancel(context.Background())
store, err := store.NewStore(config.DSN)
if err != nil {
cancel()
slog.Error("store init failed", "err", err)
return
}
if err := store.Migrate(ctx); err != nil {
cancel()
slog.Error("store migrate failed", "err", err)
return
}
s := server.NewServer(ctx, config, store)
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
if err := s.Start(ctx); err != nil {
if err != http.ErrServerClosed {
slog.Error("server start failed", "err", err)
cancel()
}
}
go func() {
<-c
s.Stop(ctx)
cancel()
}()
<-ctx.Done()
},
}
func init() {
viper.SetDefault("mode", "dev")
viper.SetDefault("addr", "0.0.0.0")
viper.SetDefault("port", 8080)
rootCmd.PersistentFlags().String("mode", "dev", `mode of server, can be "prod" or "dev"`)
rootCmd.PersistentFlags().String("addr", "0.0.0.0", "address of server")
rootCmd.PersistentFlags().Int("port", 8080, "port of server")
rootCmd.PersistentFlags().String("dsn", "postgresql://maptile:maptile_passwd@localhost:5432/maptile?sslmode=disable", "database source name")
if err := viper.BindPFlags(rootCmd.PersistentFlags()); err != nil {
panic(err)
}
viper.SetEnvPrefix("maptile")
viper.AutomaticEnv()
}
func RunCMD() {
if err := rootCmd.Execute(); err != nil {
panic(err)
}
}

30
config/config.go Executable file
View File

@ -0,0 +1,30 @@
package config
import "fmt"
type Config struct {
Mode string
Addr string
Port int
DSN string
}
func (c *Config) Validate() error {
if c.Mode != "dev" && c.Mode != "prod" {
return fmt.Errorf("invalid mode: %s", c.Mode)
}
if c.Addr == "" {
return fmt.Errorf("invalid address: %s", c.Addr)
}
if c.Port == 0 {
return fmt.Errorf("invalid port: %d", c.Port)
}
if c.DSN == "" {
return fmt.Errorf("invalid DSN: %s", c.DSN)
}
return nil
}

32
docs/docs.go Executable file
View File

@ -0,0 +1,32 @@
package docs
import (
"embed"
"github.com/swaggo/swag"
)
//go:embed swagger.json
var swaggerJSON embed.FS
var SwaggerInfo = &swag.Spec{
Version: "1.0",
Host: "localhost:8080",
BasePath: "/api/v1",
Schemes: []string{"http"},
Title: "地图图层 API",
Description: "这是一个使用 Gin 和 GORM 实现的地图图层数据管理 API。",
InfoInstanceName: "swagger",
LeftDelim: "{{",
RightDelim: "}}",
}
func init() {
swaggerData, err := swaggerJSON.ReadFile("swagger.json")
if err != nil {
panic(err)
}
SwaggerInfo.SwaggerTemplate = string(swaggerData)
swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo)
}

313
docs/swagger.json Executable file
View File

@ -0,0 +1,313 @@
{
"swagger": "2.0",
"info": {
"contact": {}
},
"paths": {
"/feature": {
"put": {
"description": "更新要素",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"feature"
],
"summary": "更新要素",
"parameters": [
{
"description": "要素",
"name": "feature",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/store.FeatureUpdate"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/api.Response-model_Feature"
}
}
}
},
"post": {
"description": "创建要素",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"feature"
],
"summary": "创建要素",
"parameters": [
{
"description": "要素",
"name": "feature",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/store.FeatureCreate"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/api.Response-model_Feature"
}
}
}
}
},
"/feature/banned/{id}": {
"post": {
"description": "禁用要素",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"feature"
],
"summary": "禁用要素",
"parameters": [
{
"description": "要素",
"name": "feature",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/store.FeatureDeleteOrBanned"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/api.Response-model_Feature"
}
}
}
}
},
"/feature/{id}": {
"get": {
"description": "获取要素",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"feature"
],
"summary": "获取要素",
"parameters": [
{
"type": "string",
"description": "要素 ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/api.Response-model_Feature"
}
}
}
},
"delete": {
"description": "删除要素",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"feature"
],
"summary": "删除要素",
"parameters": [
{
"description": "要素",
"name": "feature",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/store.FeatureDeleteOrBanned"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/api.Response-model_Feature"
}
}
}
}
},
"/features": {
"get": {
"description": "获取要素列表",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"feature"
],
"summary": "获取要素列表",
"parameters": [
{
"type": "integer",
"description": "页码",
"name": "page",
"in": "query"
},
{
"type": "integer",
"description": "每页数量",
"name": "size",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/api.Pagination-model_Feature"
}
}
}
}
}
},
"definitions": {
"api.Pagination-model_Feature": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/definitions/model.Feature"
}
},
"page": {
"type": "integer"
},
"size": {
"type": "integer"
},
"total": {
"type": "integer"
}
}
},
"api.Response-model_Feature": {
"type": "object",
"properties": {
"code": {
"type": "integer"
},
"data": {
"$ref": "#/definitions/model.Feature"
},
"message": {
"type": "string"
}
}
},
"model.Feature": {
"type": "object",
"properties": {
"created_at": {
"type": "integer"
},
"geometry": {
"type": "string"
},
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"status": {
"type": "string"
},
"updated_at": {
"type": "integer"
}
}
},
"store.FeatureCreate": {
"type": "object",
"required": [
"geometry",
"name"
],
"properties": {
"geometry": {
"type": "string"
},
"name": {
"type": "string"
}
}
},
"store.FeatureDeleteOrBanned": {
"type": "object",
"required": [
"id"
],
"properties": {
"id": {
"type": "string"
}
}
},
"store.FeatureUpdate": {
"type": "object",
"required": [
"geometry",
"id",
"name"
],
"properties": {
"geometry": {
"type": "string"
},
"id": {
"type": "string"
},
"name": {
"type": "string"
}
}
}
}
}

68
go.mod Executable file
View File

@ -0,0 +1,68 @@
module git.zhouxhere.com/zhouxhere/maptile
go 1.22.5
require github.com/twpayne/go-geom v1.6.0
require (
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/ghodss/yaml v1.0.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/labstack/gommon v0.4.2 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.11.0 // indirect
github.com/spf13/cast v1.6.0 // indirect
github.com/spf13/pflag v1.0.6 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/swaggo/files/v2 v2.0.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.9.0 // indirect
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
golang.org/x/time v0.8.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)
require (
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/jsonreference v0.21.0 // indirect
github.com/go-openapi/spec v0.21.0 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/go-playground/validator/v10 v10.25.0
github.com/golang-migrate/migrate/v4 v4.18.2
github.com/google/uuid v1.6.0
github.com/jmoiron/sqlx v1.4.0
github.com/josharian/intern v1.0.0 // indirect
github.com/labstack/echo/v4 v4.13.3
github.com/lib/pq v1.10.9
github.com/mailru/easyjson v0.9.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
github.com/pkg/errors v0.9.1
github.com/spf13/cobra v1.9.1
github.com/spf13/viper v1.19.0
github.com/swaggo/echo-swagger v1.4.1
github.com/swaggo/swag v1.16.4
golang.org/x/crypto v0.33.0 // indirect
golang.org/x/net v0.35.0 // indirect
golang.org/x/sys v0.30.0 // indirect
golang.org/x/text v0.22.0 // indirect
golang.org/x/tools v0.30.0 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1
gopkg.in/yaml.v3 v3.0.1 // indirect
)

206
go.sum Executable file
View File

@ -0,0 +1,206 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/alecthomas/assert/v2 v2.10.0 h1:jjRCHsj6hBJhkmhznrCzoNpbA3zqy0fYiUcYZP/GkPY=
github.com/alecthomas/assert/v2 v2.10.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dhui/dktest v0.4.4 h1:+I4s6JRE1yGuqflzwqG+aIaMdgXIorCf5P98JnaAWa8=
github.com/dhui/dktest v0.4.4/go.mod h1:4+22R4lgsdAXrDyaH4Nqx2JEz2hLp49MqQmm9HLCQhM=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/docker v27.2.0+incompatible h1:Rk9nIVdfH3+Vz4cyI/uhbINhEZ/oLmc+CBXmH6fbNk4=
github.com/docker/docker v27.2.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY=
github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk=
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.25.0 h1:5Dh7cjvzR7BRZadnsVOzPhWsrwUr0nmsZJxEAnFLNO8=
github.com/go-playground/validator/v10 v10.25.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-migrate/migrate/v4 v4.18.2 h1:2VSCMz7x7mjyTXx3m2zPokOY82LTRgxK1yQYKo6wWQ8=
github.com/golang-migrate/migrate/v4 v4.18.2/go.mod h1:2CM6tJvn2kqPXwnXO/d3rAQYiyoIm180VsO8PRX6Rpk=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY=
github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g=
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=
github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/swaggo/echo-swagger v1.4.1 h1:Yf0uPaJWp1uRtDloZALyLnvdBeoEL5Kc7DtnjzO/TUk=
github.com/swaggo/echo-swagger v1.4.1/go.mod h1:C8bSi+9yH2FLZsnhqMZLIZddpUxZdBYuNHbtaS1Hljc=
github.com/swaggo/files/v2 v2.0.0 h1:hmAt8Dkynw7Ssz46F6pn8ok6YmGZqHSVLZ+HQM7i0kw=
github.com/swaggo/files/v2 v2.0.0/go.mod h1:24kk2Y9NYEJ5lHuCra6iVwkMjIekMCaFq/0JQj66kyM=
github.com/swaggo/swag v1.16.4 h1:clWJtd9LStiG3VeijiCfOVODP6VpHtKdQy9ELFG3s1A=
github.com/swaggo/swag v1.16.4/go.mod h1:VBsHJRsDvfYvqoiMKnsdwhNV9LEMHgEDZcyVYX0sxPg=
github.com/twpayne/go-geom v1.6.0 h1:WPOJLCdd8OdcnHvKQepLKwOZrn5BzVlNxtQB59IDHRE=
github.com/twpayne/go-geom v1.6.0/go.mod h1:Kr+Nly6BswFsKM5sd31YaoWS5PeDDH2NftJTK7Gd028=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8=
go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw=
go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8=
go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc=
go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8=
go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4=
go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ=
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM=
golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY=
golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

20
log/log.go Executable file
View File

@ -0,0 +1,20 @@
package log
import (
"log/slog"
"gopkg.in/natefinch/lumberjack.v2"
)
func init() {
logFile := &lumberjack.Logger{
Filename: "logs/app.log", // 日志文件路径
MaxSize: 10, // 单个日志文件的最大大小MB
MaxBackups: 5, // 保留的旧日志文件最大数量
MaxAge: 30, // 保留旧日志文件的最大天数
Compress: true, // 是否压缩旧日志文件
}
logger := slog.New(slog.NewJSONHandler(logFile, nil))
slog.SetDefault(logger)
}

7
main.go Executable file
View File

@ -0,0 +1,7 @@
package main
import "git.zhouxhere.com/zhouxhere/maptile/bin"
func main() {
bin.RunCMD()
}

6
model/base.go Executable file
View File

@ -0,0 +1,6 @@
package model
type ListQuery struct {
Page int `json:"page" query:"page"`
Size int `json:"size" query:"size"`
}

68
model/feature.go Executable file
View File

@ -0,0 +1,68 @@
package model
import (
"encoding/json"
"time"
"github.com/google/uuid"
)
const (
StatusNormal = "normal"
StatusDeleted = "deleted"
StatusBanned = "banned"
)
type Feature struct {
ID uuid.UUID `db:"id" json:"id"`
Name string `db:"name" json:"name"`
Geometry []byte `db:"geometry" json:"geometry" swaggertype:"object"`
Status string `db:"status" json:"status"`
CreatedAt int64 `db:"created_at" json:"created_at"`
UpdatedAt int64 `db:"updated_at" json:"updated_at"`
}
type FeatureList struct {
ListQuery
Name string `json:"name" query:"name"`
Status string `json:"status" query:"status"`
}
type FeatureCreate struct {
Name string `json:"name" binding:"required"`
Geometry map[string]interface{} `json:"geometry" binding:"required"`
}
type FeatureUpdate struct {
ID uuid.UUID `json:"id" binding:"required"`
Name string `json:"name" binding:"required"`
Geometry map[string]interface{} `json:"geometry" binding:"required"`
}
type FeatureDeleteOrBanned struct {
ID uuid.UUID `json:"id" binding:"required"`
}
type FeaturePublic struct {
ID string `json:"id"`
Name string `json:"name"`
Geometry map[string]interface{} `json:"geometry"`
Status string `json:"status"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
func (f *Feature) ToPublic() FeaturePublic {
return FeaturePublic{
ID: f.ID.String(),
Name: f.Name,
Geometry: func() map[string]interface{} {
var geom map[string]interface{}
_ = json.Unmarshal(f.Geometry, &geom)
return geom
}(),
Status: f.Status,
CreatedAt: time.Unix(f.CreatedAt, 0),
UpdatedAt: time.Unix(f.UpdatedAt, 0),
}
}

103
server/server.go Executable file
View File

@ -0,0 +1,103 @@
package server
import (
"context"
"fmt"
"log/slog"
"net"
"net/http"
"time"
"git.zhouxhere.com/zhouxhere/maptile/api"
"git.zhouxhere.com/zhouxhere/maptile/config"
"git.zhouxhere.com/zhouxhere/maptile/store"
"github.com/labstack/echo/v4"
"github.com/pkg/errors"
// "github.com/soheilhy/cmux"
// "google.golang.org/grpc"
)
type Server struct {
config *config.Config
Store *store.Store
httpServer *http.Server
// grpcServer *grpc.Server
}
func NewServer(ctx context.Context, config *config.Config, store *store.Store) *Server {
s := &Server{
Store: store,
}
s.config = config
echoServer := echo.New()
api.NewAPI(echoServer, store)
addr := fmt.Sprintf("%s:%d", config.Addr, config.Port)
s.httpServer = &http.Server{
Addr: addr,
Handler: echoServer,
}
// grpcServer := grpc.NewServer()
// s.grpcServer = grpcServer
return s
}
func (s *Server) Start(ctx context.Context) error {
addr := fmt.Sprintf("%s:%d", s.config.Addr, s.config.Port)
listener, err := net.Listen("tcp", addr)
if err != nil {
return errors.Wrap(err, "failed to listen")
}
// muxServer := cmux.New(listener)
// go func() {
// grpcListener := muxServer.MatchWithWriters(cmux.HTTP2MatchHeaderFieldSendSettings("content-type", "application/grpc"))
// if err := s.grpcServer.Serve(grpcListener); err != nil {
// slog.Error("failed to start grpc server", "error", err)
// }
// }()
// go func() {
// httpListener := muxServer.Match(cmux.HTTP1Fast(http.MethodPatch))
// if err := s.httpServer.Serve(httpListener); err != nil {
// slog.Error("failed to start http server", "error", err)
// }
// }()
// go func() {
// if err := muxServer.Serve(); err != nil {
// slog.Error("failed to start server", "error", err)
// }
// }()
go func() {
if err := s.httpServer.Serve(listener); err != nil {
slog.Error("failed to start http server", "error", err)
}
}()
return nil
}
func (s *Server) Stop(ctx context.Context) {
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
if err := s.httpServer.Shutdown(ctx); err != nil {
slog.Error("failed to stop http server", "error", err)
}
// s.grpcServer.GracefulStop()
if err := s.Store.Close(); err != nil {
slog.Error("failed to close store", "error", err)
}
slog.Info("server stopped")
}

167
store/feature.go Executable file
View File

@ -0,0 +1,167 @@
package store
import (
"encoding/json"
"time"
"git.zhouxhere.com/zhouxhere/maptile/model"
"github.com/google/uuid"
"github.com/pkg/errors"
"github.com/twpayne/go-geom"
"github.com/twpayne/go-geom/encoding/geojson"
)
// ListFeature 获取要素列表
func (s *Store) ListFeature(query *model.FeatureList) ([]model.Feature, int, error) {
queryStr := `
SELECT id, name, ST_AsGeoJSON(geometry) AS geometry, status, created_at, updated_at
FROM feature WHERE 1 = 1
`
if query.Status != "" {
queryStr += " AND status = :status"
}
if query.Name != "" {
queryStr += " AND name LIKE :name"
}
queryStr += " ORDER BY status, created_at DESC"
queryStr += " LIMIT :limit OFFSET :offset"
namedParams := map[string]interface{}{
"status": query.Status,
"name": "'%" + query.Name + "%'",
"limit": query.Size,
"offset": (query.Page - 1) * query.Size,
}
queryStr, args, err := s.DB.BindNamed(queryStr, namedParams)
if err != nil {
return nil, 0, err
}
// 执行查询
features := []model.Feature{}
if err := s.DB.Select(&features, queryStr, args...); err != nil {
return nil, 0, err
}
countQuery := `
SELECT COUNT(*) FROM feature WHERE 1 = 1
`
if query.Status != "" {
countQuery += " AND status = :status"
}
if query.Name != "" {
countQuery += " AND name LIKE :name"
}
countQuery, countArgs, err := s.DB.BindNamed(countQuery, namedParams)
if err != nil {
return nil, 0, err
}
var count int
if err := s.DB.Get(&count, countQuery, countArgs...); err != nil {
return nil, 0, err
}
return features, count, nil
}
// GetFeatureByID 获取要素
func (s *Store) GetFeatureByID(id uuid.UUID) (*model.Feature, error) {
query := `
SELECT id, name, ST_AsGeoJSON(geometry) AS geometry, status, created_at, updated_at
FROM feature
WHERE id = :id
`
f := &model.Feature{}
if err := s.DB.Get(f, query, id); err != nil {
return nil, err
}
return f, nil
}
// CreateFeature 创建要素
func (s *Store) CreateFeature(feature *model.FeatureCreate) (*model.Feature, error) {
// 将传入的 JSON 格式的 geometry 转换为 geom.T 类型
geometryBytes, err := json.Marshal(feature.Geometry)
if err != nil {
return nil, err
}
var geomT geom.T
if err := geojson.Unmarshal(geometryBytes, &geomT); err != nil {
return nil, errors.New("invalid GeoJSON format")
}
query := `
INSERT INTO feature (id, name, geometry, status, created_at, updated_at)
VALUES (:id, :name, ST_GeomFromGeoJSON(:geometry), :status, :created_at, :updated_at)
`
f := &model.Feature{
ID: uuid.New(),
Name: feature.Name,
Geometry: geometryBytes,
Status: model.StatusNormal,
CreatedAt: time.Now().Unix(),
UpdatedAt: time.Now().Unix(),
}
_, err = s.DB.NamedExec(query, f)
return f, err
}
// UpdateFeature 更新要素
func (s *Store) UpdateFeature(feature *model.FeatureUpdate) (*model.Feature, error) {
// 将传入的 JSON 格式的 geometry 转换为 geom.T 类型
geometryBytes, err := json.Marshal(feature.Geometry)
if err != nil {
return nil, err
}
var geomT geom.T
if err := geojson.Unmarshal(geometryBytes, &geomT); err != nil {
return nil, errors.New("invalid GeoJSON format")
}
query := `
UPDATE feature
SET name = :name, geometry = ST_GeomFromGeoJSON(:geometry), updated_at = :updated_at
WHERE id = :id
`
f := &model.Feature{
ID: feature.ID,
Name: feature.Name,
Geometry: geometryBytes,
UpdatedAt: time.Now().Unix(),
}
_, err = s.DB.NamedExec(query, f)
return f, err
}
// DeleteFeature 删除要素
func (s *Store) DeleteFeature(feature *model.FeatureDeleteOrBanned) error {
query := `
UPDATE feature SET status = :status, updated_at = :updated_at WHERE id = :id
`
f := &model.Feature{
ID: feature.ID,
Status: model.StatusDeleted,
UpdatedAt: time.Now().Unix(),
}
_, err := s.DB.NamedExec(query, f)
return err
}
// BannedFeature 禁用要素
func (s *Store) BannedFeature(feature *model.FeatureDeleteOrBanned) error {
query := `
UPDATE feature SET status = :status, updated_at = :updated_at WHERE id = :id
`
f := &model.Feature{
ID: feature.ID,
Status: model.StatusBanned,
UpdatedAt: time.Now().Unix(),
}
_, err := s.DB.NamedExec(query, f)
return err
}

View File

@ -0,0 +1,17 @@
-- 删除地图标签关联表
DROP TABLE IF EXISTS feature_tag;
-- 删除标签数据表
DROP TABLE IF EXISTS tag;
-- 删除地图相关数据表
DROP TABLE IF EXISTS feature;
-- 删除地图数据状态枚举类型
DROP TYPE IF EXISTS feature_status;
-- 删除 PostGIS 扩展
DROP EXTENSION IF EXISTS postgis;
-- 删除 UUID 扩展
DROP EXTENSION IF EXISTS "uuid-ossp";

View File

@ -0,0 +1,53 @@
-- Description: 数据库初始化脚本
-- CREATE ROLE maptile WITH LOGIN PASSWORD 'maptile_passwd';
-- CREATE DATABASE maptile WITH OWNER maptile;
-- Postgis extension
CREATE EXTENSION IF NOT EXISTS postgis;
-- UUID extension
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
-- 地图数据状态
CREATE TYPE feature_status AS ENUM ('normal', 'banned', 'deleted');
COMMENT ON TYPE feature_status IS '地图数据状态枚举类型,包括正常、未激活、封禁和删除';
-- 地图相关数据
CREATE TABLE feature (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
name TEXT NOT NULL,
geometry Geometry(Geometry, 4326),
status feature_status NOT NULL DEFAULT 'normal',
created_at BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW()),
updated_at BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW())
);
COMMENT ON COLUMN feature.id IS '地图ID';
COMMENT ON COLUMN feature.name IS '地图名称';
COMMENT ON COLUMN feature.geometry IS '地图几何数据';
COMMENT ON COLUMN feature.created_at IS '创建时间';
COMMENT ON COLUMN feature.updated_at IS '更新时间';
-- 标签数据
CREATE TABLE tag (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
created_at BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW()),
updated_at BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW())
);
COMMENT ON COLUMN tag.id IS '标签ID';
COMMENT ON COLUMN tag.name IS '标签名称';
COMMENT ON COLUMN tag.created_at IS '创建时间';
COMMENT ON COLUMN tag.updated_at IS '更新时间';
-- 地图标签关联表
CREATE TABLE feature_tag (
feature_id UUID NOT NULL,
tag_id INTEGER NOT NULL,
created_at BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW()),
updated_at BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW()),
UNIQUE (feature_id, tag_id)
);
COMMENT ON COLUMN feature_tag.feature_id IS '地图ID';
COMMENT ON COLUMN feature_tag.tag_id IS '标签ID';
COMMENT ON COLUMN feature_tag.created_at IS '创建时间';
COMMENT ON COLUMN feature_tag.updated_at IS '更新时间';

71
store/store.go Executable file
View File

@ -0,0 +1,71 @@
package store
import (
"context"
"embed"
"github.com/golang-migrate/migrate/v4"
_ "github.com/golang-migrate/migrate/v4/database/postgres"
"github.com/golang-migrate/migrate/v4/source/iofs"
"github.com/jmoiron/sqlx"
_ "github.com/lib/pq"
)
//go:embed migrations/*.sql
var sqlMigrations embed.FS
type Store struct {
dsn string
DB *sqlx.DB
}
func NewStore(dsn string) (*Store, error) {
db, err := sqlx.Connect("postgres", dsn)
if err != nil {
return nil, err
}
return &Store{
dsn: dsn,
DB: db,
}, nil
}
func (s *Store) Close() error {
return s.DB.Close()
}
func (s *Store) Migrate(ctx context.Context) error {
source, err := iofs.New(sqlMigrations, "migrations")
if err != nil {
return err
}
m, err := migrate.NewWithSourceInstance("iofs", source, s.dsn)
if err != nil {
return err
}
sourceVersion, err := source.First()
if err != nil {
return err
}
version, dirty, err := m.Version()
if err != nil && err != migrate.ErrNilVersion {
return err
}
if version == sourceVersion && !dirty {
return nil
}
if err = m.Up(); err != nil && err != migrate.ErrNoChange {
return err
}
return nil
}

75
web/README.md Executable file
View File

@ -0,0 +1,75 @@
# Nuxt Minimal Starter
Look at the [Nuxt documentation](https://nuxt.com/docs/getting-started/introduction) to learn more.
## Setup
Make sure to install dependencies:
```bash
# npm
npm install
# pnpm
pnpm install
# yarn
yarn install
# bun
bun install
```
## Development Server
Start the development server on `http://localhost:3000`:
```bash
# npm
npm run dev
# pnpm
pnpm dev
# yarn
yarn dev
# bun
bun run dev
```
## Production
Build the application for production:
```bash
# npm
npm run build
# pnpm
pnpm build
# yarn
yarn build
# bun
bun run build
```
Locally preview production build:
```bash
# npm
npm run preview
# pnpm
pnpm preview
# yarn
yarn preview
# bun
bun run preview
```
Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.

6
web/app.vue Executable file
View File

@ -0,0 +1,6 @@
<template>
<div>
<NuxtRouteAnnouncer />
<NuxtWelcome />
</div>
</template>

3
web/assets/css/main.css Normal file
View File

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

12
web/nuxt.config.ts Executable file
View File

@ -0,0 +1,12 @@
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
compatibilityDate: "2024-11-01",
devtools: { enabled: true },
css: ["@/assets/css/main.css"],
postcss: {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
},
});

10252
web/package-lock.json generated Executable file

File diff suppressed because it is too large Load Diff

23
web/package.json Executable file
View File

@ -0,0 +1,23 @@
{
"name": "nuxt-app",
"private": true,
"type": "module",
"scripts": {
"build": "nuxt build",
"dev": "nuxt dev",
"generate": "nuxt generate",
"preview": "nuxt preview",
"postinstall": "nuxt prepare"
},
"dependencies": {
"nuxt": "^3.15.4",
"vue": "latest",
"vue-router": "latest"
},
"devDependencies": {
"autoprefixer": "^10.4.20",
"daisyui": "^4.12.23",
"postcss": "^8.5.3",
"tailwindcss": "^3.4.17"
}
}

BIN
web/public/favicon.ico Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

1
web/public/robots.txt Executable file
View File

@ -0,0 +1 @@

3
web/server/tsconfig.json Executable file
View File

@ -0,0 +1,3 @@
{
"extends": "../.nuxt/tsconfig.server.json"
}

18
web/tailwind.config.js Normal file
View File

@ -0,0 +1,18 @@
import daisyui from 'daisyui'
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./components/**/*.{js,vue,ts}",
"./layouts/**/*.vue",
"./pages/**/*.vue",
"./plugins/**/*.{js,ts}",
"./app.vue",
"./error.vue",
],
theme: {
extend: {},
},
plugins: [daisyui],
}

4
web/tsconfig.json Executable file
View File

@ -0,0 +1,4 @@
{
// https://nuxt.com/docs/guide/concepts/typescript
"extends": "./.nuxt/tsconfig.json"
}