From 5b5870f3547a526bc443adaeec81f4f6a5109a6a Mon Sep 17 00:00:00 2001 From: JongYeob Sheen Date: Fri, 17 Nov 2023 01:38:01 +0900 Subject: [PATCH] Swag Docs --- Makefile | 6 +- cmd/main.go | 27 +- configs/common.dev | 15 +- configs/common.go | 15 +- configs/common.local | 16 +- configs/common.prod | 15 +- docs/docs.go | 1051 +++++++++++++++++ docs/swagger.json | 1025 ++++++++++++++++ docs/swagger.yaml | 688 +++++++++++ go.mod | 64 +- go.sum | 162 +++ go.work | 3 + go.work.sum | 5 + internal/controllers/auth.go | 48 +- internal/controllers/program.go | 207 ++++ internal/controllers/quiz.go | 193 +++ internal/controllers/swagger.go | 22 + internal/controllers/token.go | 65 +- internal/controllers/user.go | 72 +- internal/database/database.go | 9 +- internal/helpers/cipher/cipher.go | 86 ++ internal/helpers/helpers.go | 10 - internal/helpers/securityutil/securityutil.go | 150 +++ internal/middleware/auth.go | 2 +- internal/models/auth.go | 20 + internal/models/program.go | 47 + internal/models/quiz.go | 52 + internal/models/token.go | 33 +- internal/models/user.go | 22 +- internal/repositories/program.go | 90 ++ internal/repositories/quiz.go | 84 ++ internal/repositories/token.go | 11 +- internal/repositories/user.go | 34 +- internal/routers/auth.go | 18 +- internal/routers/program.go | 50 + internal/routers/quiz.go | 50 + internal/routers/router.go | 15 +- internal/routers/swagger.go | 37 + internal/routers/token.go | 20 +- internal/routers/user.go | 19 +- internal/services/auth.go | 35 +- internal/services/program.go | 56 + internal/services/quiz.go | 56 + internal/services/token.go | 97 +- internal/services/user.go | 20 +- test/program_test.go | 190 +++ test/quiz_test.go | 183 +++ {tests => test}/token_test.go | 2 +- tests/test.db | Bin 569344 -> 0 bytes 49 files changed, 5004 insertions(+), 193 deletions(-) create mode 100644 docs/docs.go create mode 100644 docs/swagger.json create mode 100644 docs/swagger.yaml create mode 100644 go.work create mode 100644 go.work.sum create mode 100644 internal/controllers/program.go create mode 100644 internal/controllers/quiz.go create mode 100644 internal/controllers/swagger.go create mode 100644 internal/helpers/cipher/cipher.go create mode 100644 internal/helpers/securityutil/securityutil.go create mode 100644 internal/models/program.go create mode 100644 internal/models/quiz.go create mode 100644 internal/repositories/program.go create mode 100644 internal/repositories/quiz.go create mode 100644 internal/routers/program.go create mode 100644 internal/routers/quiz.go create mode 100644 internal/routers/swagger.go create mode 100644 internal/services/program.go create mode 100644 internal/services/quiz.go create mode 100644 test/program_test.go create mode 100644 test/quiz_test.go rename {tests => test}/token_test.go (99%) delete mode 100644 tests/test.db diff --git a/Makefile b/Makefile index b54d592..16e586f 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -FUNCTION_NAME =superrichquiz-octet +FUNCTION_NAME =learsteam-quiz-api BUILD =$(CURDIR)/build BIN =$(CURDIR)/bin MAIN =$(CURDIR)/cmd/main.go @@ -12,6 +12,10 @@ DEV_HANDLER =bootstrap default: help +swag: ## swagger docs + @echo "\033[32mSwagger API Docs ...\033[0m" + @swag init --parseDependency -g cmd/main.go + deps: ## install dependency @echo "\033[32mDependency ...\033[0m" @go install gorm.io/gorm diff --git a/cmd/main.go b/cmd/main.go index a93495a..735a961 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -1,17 +1,25 @@ package main import ( + configs "learnsteam/learsteam-quiz-api/configs" + _ "learnsteam/learsteam-quiz-api/docs" + "learnsteam/learsteam-quiz-api/internal/database" + "learnsteam/learsteam-quiz-api/internal/helpers" + "learnsteam/learsteam-quiz-api/internal/routers" + "log" "net/http" - configs "studioj/boilerplate_go/configs" - "studioj/boilerplate_go/internal/database" - "studioj/boilerplate_go/internal/helpers" - "studioj/boilerplate_go/internal/routers" - "github.com/apex/gateway" ) +// @title Learsteam Quiz API +// @version 1.0 +// @description Learnsteam Quiz 서비스 API + +// @contact.name Jay Sheen +// @contact.email sheen@jongyeob.com + func main() { Init() Run() @@ -20,6 +28,7 @@ func main() { func Init() { database.Init() routers.Init() + database.AutoMigrate() } @@ -30,3 +39,11 @@ func Run() { log.Fatal(http.ListenAndServe(configs.PORT, routers.Router)) } } + +func InitSwagger() { + // swagger:route GET /health health + // Check server health + // + // responses: + // 200: healthResponse +} diff --git a/configs/common.dev b/configs/common.dev index 3ff9f9c..a3193d7 100644 --- a/configs/common.dev +++ b/configs/common.dev @@ -1,5 +1,14 @@ package config -const PORT = ":3030" -const DATABASE_URL = "root:omHO7EEzHm52s9DlZD70P6KPKm2TbODC@tcp(db:3306)/boilerplate?charset=utf8&parseTime=True&loc=Local" -const SECRET_KEY = "5a14e06d-55a3-418c-9f3f-8fde328d6c49" \ No newline at end of file +const ( + BASE_URL = "https://learnsteam-quiz-api.jongyeob.com" + PORT = ":3030" + DATABASE_URL = "learnsteam:fbOgZ6Xxn5VXBYihjqygRXyaK6ZUgKL6@tcp(localhost:3306)/learnsteam_quiz?charset=utf8&parseTime=True&loc=Local" + + KAKAO_CLIENT_ID = "986830" + KAKAO_CLIENT_SECRET = "14f63a8e91c4e0fe88bc40e3ff348233" + + SECRET_KEY = "GmhiMJuAIyF3jwkd97iODSoJoN3bIVkF" + ACCESS_KEY = "NQD9AXmpn13asz84oPf5Dyc9rhLyhmHp" + REFRESH_KEY = "qFzFMlX2PfTSXgA9QPFltcNhJk1SVF0a" +) diff --git a/configs/common.go b/configs/common.go index d2cb9d7..246840b 100644 --- a/configs/common.go +++ b/configs/common.go @@ -1,7 +1,14 @@ package config -const PORT = ":3030" -const DATABASE_URL = "root:sswha123@tcp(localhost:3306)/boilerplate?charset=utf8&parseTime=True&loc=Local" +const ( + BASE_URL = "http://localhost:3030" + PORT = ":3030" + DATABASE_URL = "learnsteam:fbOgZ6Xxn5VXBYihjqygRXyaK6ZUgKL6@tcp(localhost:3306)/learnsteam_quiz?charset=utf8&parseTime=True&loc=Local" -// const DATABASE_URL = "sqlite.db" -const SECRET_KEY = "5a14e06d-55a3-418c-9f3f-8fde328d6c49" + KAKAO_CLIENT_ID = "986830" + KAKAO_CLIENT_SECRET = "14f63a8e91c4e0fe88bc40e3ff348233" + + SECRET_KEY = "GmhiMJuAIyF3jwkd97iODSoJoN3bIVkF" + ACCESS_KEY = "NQD9AXmpn13asz84oPf5Dyc9rhLyhmHp" + REFRESH_KEY = "qFzFMlX2PfTSXgA9QPFltcNhJk1SVF0a" +) diff --git a/configs/common.local b/configs/common.local index 51e3c29..246840b 100644 --- a/configs/common.local +++ b/configs/common.local @@ -1,6 +1,14 @@ package config -const PORT = ":3030" -const DATABASE_URL = "root:sswha123@tcp(localhost:3306)/boilerplate?charset=utf8&parseTime=True&loc=Local" -//const DATABASE_URL = "sqlite.db" -const SECRET_KEY = "5a14e06d-55a3-418c-9f3f-8fde328d6c49" \ No newline at end of file +const ( + BASE_URL = "http://localhost:3030" + PORT = ":3030" + DATABASE_URL = "learnsteam:fbOgZ6Xxn5VXBYihjqygRXyaK6ZUgKL6@tcp(localhost:3306)/learnsteam_quiz?charset=utf8&parseTime=True&loc=Local" + + KAKAO_CLIENT_ID = "986830" + KAKAO_CLIENT_SECRET = "14f63a8e91c4e0fe88bc40e3ff348233" + + SECRET_KEY = "GmhiMJuAIyF3jwkd97iODSoJoN3bIVkF" + ACCESS_KEY = "NQD9AXmpn13asz84oPf5Dyc9rhLyhmHp" + REFRESH_KEY = "qFzFMlX2PfTSXgA9QPFltcNhJk1SVF0a" +) diff --git a/configs/common.prod b/configs/common.prod index 8719dab..a3193d7 100644 --- a/configs/common.prod +++ b/configs/common.prod @@ -1,5 +1,14 @@ package config -const PORT = ":3030" -const DATABASE_URL = "root:omHO7EEzHm52s9DlZD70P6KPKm2TbODC@tcp(localhost:3306)/boilerplate?charset=utf8&parseTime=True&loc=Local" -const SECRET_KEY = "5a14e06d-55a3-418c-9f3f-8fde328d6c49" \ No newline at end of file +const ( + BASE_URL = "https://learnsteam-quiz-api.jongyeob.com" + PORT = ":3030" + DATABASE_URL = "learnsteam:fbOgZ6Xxn5VXBYihjqygRXyaK6ZUgKL6@tcp(localhost:3306)/learnsteam_quiz?charset=utf8&parseTime=True&loc=Local" + + KAKAO_CLIENT_ID = "986830" + KAKAO_CLIENT_SECRET = "14f63a8e91c4e0fe88bc40e3ff348233" + + SECRET_KEY = "GmhiMJuAIyF3jwkd97iODSoJoN3bIVkF" + ACCESS_KEY = "NQD9AXmpn13asz84oPf5Dyc9rhLyhmHp" + REFRESH_KEY = "qFzFMlX2PfTSXgA9QPFltcNhJk1SVF0a" +) diff --git a/docs/docs.go b/docs/docs.go new file mode 100644 index 0000000..41d43bc --- /dev/null +++ b/docs/docs.go @@ -0,0 +1,1051 @@ +// Package docs Code generated by swaggo/swag. DO NOT EDIT +package docs + +import "github.com/swaggo/swag" + +const docTemplate = `{ + "schemes": {{ marshal .Schemes }}, + "swagger": "2.0", + "info": { + "description": "{{escape .Description}}", + "title": "{{.Title}}", + "contact": { + "name": "Jay Sheen", + "email": "sheen@jongyeob.com" + }, + "version": "{{.Version}}" + }, + "host": "{{.Host}}", + "basePath": "{{.BasePath}}", + "paths": { + "/auth/login": { + "post": { + "description": "username, password 를 입력하여 로그인", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "로그인", + "parameters": [ + { + "description": "username", + "name": "username", + "in": "body", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "비밀번호", + "name": "password", + "in": "body", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/learnsteam_learsteam-quiz-api_internal_models.LoginResponse" + } + } + } + } + }, + "/auth/register": { + "post": { + "description": "username, name, password 를 입력하여 회원가입", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "회원가입", + "parameters": [ + { + "description": "username", + "name": "username", + "in": "body", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "이름", + "name": "name", + "in": "body", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "비밀번호", + "name": "password", + "in": "body", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/learnsteam_learsteam-quiz-api_internal_models.RegisterResponse" + } + } + } + } + }, + "/program": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "퀴즈 프로그램 목록을 가져옵니다.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "프로그램 목록 가져오기", + "parameters": [ + { + "type": "string", + "description": "태그", + "name": "tag", + "in": "query" + }, + { + "type": "string", + "description": "검색어", + "name": "q", + "in": "query" + }, + { + "type": "integer", + "description": "페이지", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "페이지 사이즈", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/learnsteam_learsteam-quiz-api_internal_models.ProgramListResponse" + } + } + } + }, + "put": { + "description": "퀴즈 프로그램을 수정합니다.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "퀴즈 프로그램 수정", + "parameters": [ + { + "description": "프로그램 제목", + "name": "subject", + "in": "body", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "프로그램 코스", + "name": "course", + "in": "body", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "프로그램 내용", + "name": "content", + "in": "body", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "프로그램 태그", + "name": "tag", + "in": "body", + "required": true, + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "description": "프로그램 상태 on 또는 off", + "name": "status", + "in": "body", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "프로그램 발행 날짜", + "name": "publish_at", + "in": "body", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/learnsteam_learsteam-quiz-api_internal_models.ProgramResponse" + } + } + } + }, + "post": { + "description": "퀴즈 프로그램을 만듭니다.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "퀴즈 프로그램 생성", + "parameters": [ + { + "description": "프로그램 제목", + "name": "subject", + "in": "body", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "프로그램 코스", + "name": "course", + "in": "body", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "프로그램 내용", + "name": "content", + "in": "body", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "프로그램 태그", + "name": "tag", + "in": "body", + "required": true, + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "description": "프로그램 상태 on 또는 off", + "name": "status", + "in": "body", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "프로그램 발행 날짜", + "name": "publish_at", + "in": "body", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/learnsteam_learsteam-quiz-api_internal_models.ProgramResponse" + } + } + } + } + }, + "/program/{id}": { + "get": { + "description": "ID로 퀴즈 프로그램을 가져옵니다.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "퀴즈 프로그램 가져오기", + "parameters": [ + { + "type": "string", + "description": "퀴즈 프로그램 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/learnsteam_learsteam-quiz-api_internal_models.ProgramResponse" + } + }, + "400": { + "description": "Bad Request" + } + } + } + }, + "/quiz": { + "get": { + "description": "퀴즈 목록을 가져옵니다.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "퀴즈 목록 가져오기", + "parameters": [ + { + "type": "string", + "description": "프로그램 ID", + "name": "program_id", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "검색어", + "name": "q", + "in": "query" + }, + { + "type": "integer", + "description": "페이지", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "페이지 사이즈", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/learnsteam_learsteam-quiz-api_internal_models.QuizListResponse" + } + } + } + }, + "put": { + "description": "퀴즈를 수정합니다.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "퀴즈 수정", + "parameters": [ + { + "type": "string", + "description": "퀴즈 ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "프로그램 ID", + "name": "program_id", + "in": "body", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "퀴즈 순서", + "name": "sequence", + "in": "body", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "description": "퀴즈 타입 : choice, check, ox, input", + "name": "quiz_type", + "in": "body", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "퀴즈 문제", + "name": "question", + "in": "body", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "퀴즈 선택지 : ['선택1','선택2','선택3', '선택4']", + "name": "choice", + "in": "body", + "required": true, + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "description": "퀴즈 정답 : [1,3]", + "name": "answer", + "in": "body", + "required": true, + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "description": "퀴즈 힌트", + "name": "hint", + "in": "body", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "퀴즈 해설", + "name": "comment", + "in": "body", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/learnsteam_learsteam-quiz-api_internal_models.QuizResponse" + } + } + } + }, + "post": { + "description": "퀴즈를 만듭니다.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "퀴즈 생성", + "parameters": [ + { + "description": "프로그램 ID", + "name": "program_id", + "in": "body", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "퀴즈 순서", + "name": "sequence", + "in": "body", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "description": "퀴즈 타입 : choice, check, ox, input", + "name": "quiz_type", + "in": "body", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "퀴즈 문제", + "name": "question", + "in": "body", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "퀴즈 선택지 : ['선택1','선택2','선택3', '선택4']", + "name": "choice", + "in": "body", + "required": true, + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "description": "퀴즈 정답 : [1,3]", + "name": "answer", + "in": "body", + "required": true, + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "description": "퀴즈 힌트", + "name": "hint", + "in": "body", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "퀴즈 해설", + "name": "comment", + "in": "body", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/learnsteam_learsteam-quiz-api_internal_models.QuizResponse" + } + } + } + } + }, + "/quiz/{id}": { + "get": { + "description": "ID로 퀴즈를 가져옵니다.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "퀴즈 가져오기", + "parameters": [ + { + "type": "string", + "description": "퀴즈 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/learnsteam_learsteam-quiz-api_internal_models.QuizResponse" + } + }, + "400": { + "description": "Bad Request" + } + } + } + }, + "/token/refresh": { + "post": { + "description": "AccessToken을 RefreshToken으로 갱신합니다.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "AccessToken Refresh", + "parameters": [ + { + "description": "RefreshToken", + "name": "refresh_token", + "in": "body", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/learnsteam_learsteam-quiz-api_internal_models.ProgramResponse" + } + } + } + } + }, + "/user": { + "get": { + "description": "사용자 목록을 가져옵니다.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "사용자 목록 가져오기", + "parameters": [ + { + "type": "string", + "description": "검색어", + "name": "q", + "in": "query" + }, + { + "type": "integer", + "description": "페이지", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "페이지 사이즈", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/learnsteam_learsteam-quiz-api_internal_models.UserListResponse" + } + } + } + } + }, + "/user/{id}": { + "get": { + "description": "ID로 사용자 정보를 가져옵니다.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "사용자 정보 가져오기", + "parameters": [ + { + "type": "string", + "description": "사용자 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/learnsteam_learsteam-quiz-api_internal_models.User" + } + }, + "400": { + "description": "Bad Request" + } + } + } + } + }, + "definitions": { + "learnsteam_learsteam-quiz-api_internal_models.LoginResponse": { + "type": "object", + "properties": { + "refresh_token": { + "type": "string", + "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdXRob3JpemVkIjp0cnVlLCJleHAiOjE3MDc4OTcwMjcs" + }, + "token": { + "type": "string", + "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdXRob3JpemVkIjp0cnVlLCJleHAiOjE3MDI3MTMwMjcsInN1" + }, + "user": { + "$ref": "#/definitions/learnsteam_learsteam-quiz-api_internal_models.User" + } + } + }, + "learnsteam_learsteam-quiz-api_internal_models.Program": { + "type": "object", + "properties": { + "content": { + "type": "string", + "example": "코스 설명" + }, + "course": { + "type": "string", + "example": "코스 이름" + }, + "id": { + "type": "string", + "example": "ef74c59a-c707-4162-a52b-455906c81ec1" + }, + "publish_at": { + "type": "string", + "example": "2023-11-10T00:00:00+09:00" + }, + "status": { + "type": "string", + "example": "on" + }, + "subject": { + "type": "string", + "example": "프로그램 제목" + }, + "tag": { + "type": "array", + "items": { + "type": "integer" + } + } + } + }, + "learnsteam_learsteam-quiz-api_internal_models.ProgramListResponse": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/learnsteam_learsteam-quiz-api_internal_models.Program" + } + }, + "page": { + "type": "integer", + "example": 1 + }, + "pageSize": { + "type": "integer", + "example": 10 + }, + "total": { + "type": "integer", + "example": 999 + }, + "totalPage": { + "type": "integer", + "example": 99 + } + } + }, + "learnsteam_learsteam-quiz-api_internal_models.ProgramResponse": { + "type": "object", + "properties": { + "content": { + "type": "string", + "example": "코스 설명" + }, + "course": { + "type": "string", + "example": "코스 이름" + }, + "id": { + "type": "string", + "example": "ef74c59a-c707-4162-a52b-455906c81ec1" + }, + "publish_at": { + "type": "string", + "example": "2023-11-10T00:00:00+09:00" + }, + "status": { + "type": "string", + "example": "on" + }, + "subject": { + "type": "string", + "example": "프로그램 제목" + }, + "tag": { + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "tag1", + "tag2" + ] + } + } + }, + "learnsteam_learsteam-quiz-api_internal_models.Quiz": { + "type": "object", + "properties": { + "answer": { + "type": "array", + "items": { + "type": "integer" + } + }, + "choice": { + "type": "array", + "items": { + "type": "integer" + } + }, + "comment": { + "type": "string", + "example": "퀴즈 해설" + }, + "hint": { + "type": "string", + "example": "퀴즈 힌트" + }, + "id": { + "type": "string", + "example": "1b066168-68c4-4b50-bc9a-b6c4fceaf378" + }, + "program_id": { + "type": "string", + "example": "2036023a-fb56-4b6c-b3bb-c787c681ada6" + }, + "question": { + "type": "string", + "example": "퀴즈 질문입니다." + }, + "quiz_type": { + "type": "string", + "example": "choice" + }, + "sequence": { + "type": "integer", + "example": 5 + } + } + }, + "learnsteam_learsteam-quiz-api_internal_models.QuizListResponse": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/learnsteam_learsteam-quiz-api_internal_models.Quiz" + } + }, + "page": { + "type": "integer", + "example": 1 + }, + "pageSize": { + "type": "integer", + "example": 10 + }, + "total": { + "type": "integer", + "example": 5 + }, + "totalPage": { + "type": "integer", + "example": 1 + } + } + }, + "learnsteam_learsteam-quiz-api_internal_models.QuizResponse": { + "type": "object", + "properties": { + "answer": { + "type": "array", + "items": { + "type": "integer" + } + }, + "choice": { + "type": "array", + "items": { + "type": "integer" + } + }, + "comment": { + "type": "string", + "example": "퀴즈 해설" + }, + "hint": { + "type": "string", + "example": "퀴즈 힌트" + }, + "id": { + "type": "string", + "example": "1b066168-68c4-4b50-bc9a-b6c4fceaf378" + }, + "program_id": { + "type": "string", + "example": "2036023a-fb56-4b6c-b3bb-c787c681ada6" + }, + "question": { + "type": "string", + "example": "퀴즈 질문입니다." + }, + "quiz_type": { + "type": "string", + "example": "check" + }, + "sequence": { + "type": "integer", + "example": 5 + } + } + }, + "learnsteam_learsteam-quiz-api_internal_models.RegisterResponse": { + "type": "object", + "properties": { + "refresh_token": { + "type": "string", + "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdXRob3JpemVkIjp0cnVlLCJleHAiOjE3MDc4OTcwMjcs" + }, + "token": { + "type": "string", + "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdXRob3JpemVkIjp0cnVlLCJleHAiOjE3MDI3MTMwMjcsInN1" + }, + "user": { + "$ref": "#/definitions/learnsteam_learsteam-quiz-api_internal_models.User" + } + } + }, + "learnsteam_learsteam-quiz-api_internal_models.User": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "137c1683-2ad6-4201-b256-253828b61c49" + }, + "name": { + "type": "string", + "example": "홍길동" + }, + "score": { + "type": "integer", + "example": 9999 + }, + "username": { + "type": "string", + "example": "user0" + } + } + }, + "learnsteam_learsteam-quiz-api_internal_models.UserListResponse": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/learnsteam_learsteam-quiz-api_internal_models.User" + } + }, + "page": { + "type": "integer", + "example": 1 + }, + "pageSize": { + "type": "integer", + "example": 10 + }, + "total": { + "type": "integer", + "example": 90 + }, + "totalPage": { + "type": "integer", + "example": 9 + } + } + } + } +}` + +// SwaggerInfo holds exported Swagger Info so clients can modify it +var SwaggerInfo = &swag.Spec{ + Version: "1.0", + Host: "", + BasePath: "", + Schemes: []string{}, + Title: "Learsteam Quiz API", + Description: "Learnsteam Quiz 서비스 API", + InfoInstanceName: "swagger", + SwaggerTemplate: docTemplate, + LeftDelim: "{{", + RightDelim: "}}", +} + +func init() { + swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) +} diff --git a/docs/swagger.json b/docs/swagger.json new file mode 100644 index 0000000..81a15ac --- /dev/null +++ b/docs/swagger.json @@ -0,0 +1,1025 @@ +{ + "swagger": "2.0", + "info": { + "description": "Learnsteam Quiz 서비스 API", + "title": "Learsteam Quiz API", + "contact": { + "name": "Jay Sheen", + "email": "sheen@jongyeob.com" + }, + "version": "1.0" + }, + "paths": { + "/auth/login": { + "post": { + "description": "username, password 를 입력하여 로그인", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "로그인", + "parameters": [ + { + "description": "username", + "name": "username", + "in": "body", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "비밀번호", + "name": "password", + "in": "body", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/learnsteam_learsteam-quiz-api_internal_models.LoginResponse" + } + } + } + } + }, + "/auth/register": { + "post": { + "description": "username, name, password 를 입력하여 회원가입", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "회원가입", + "parameters": [ + { + "description": "username", + "name": "username", + "in": "body", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "이름", + "name": "name", + "in": "body", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "비밀번호", + "name": "password", + "in": "body", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/learnsteam_learsteam-quiz-api_internal_models.RegisterResponse" + } + } + } + } + }, + "/program": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "퀴즈 프로그램 목록을 가져옵니다.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "프로그램 목록 가져오기", + "parameters": [ + { + "type": "string", + "description": "태그", + "name": "tag", + "in": "query" + }, + { + "type": "string", + "description": "검색어", + "name": "q", + "in": "query" + }, + { + "type": "integer", + "description": "페이지", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "페이지 사이즈", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/learnsteam_learsteam-quiz-api_internal_models.ProgramListResponse" + } + } + } + }, + "put": { + "description": "퀴즈 프로그램을 수정합니다.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "퀴즈 프로그램 수정", + "parameters": [ + { + "description": "프로그램 제목", + "name": "subject", + "in": "body", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "프로그램 코스", + "name": "course", + "in": "body", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "프로그램 내용", + "name": "content", + "in": "body", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "프로그램 태그", + "name": "tag", + "in": "body", + "required": true, + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "description": "프로그램 상태 on 또는 off", + "name": "status", + "in": "body", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "프로그램 발행 날짜", + "name": "publish_at", + "in": "body", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/learnsteam_learsteam-quiz-api_internal_models.ProgramResponse" + } + } + } + }, + "post": { + "description": "퀴즈 프로그램을 만듭니다.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "퀴즈 프로그램 생성", + "parameters": [ + { + "description": "프로그램 제목", + "name": "subject", + "in": "body", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "프로그램 코스", + "name": "course", + "in": "body", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "프로그램 내용", + "name": "content", + "in": "body", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "프로그램 태그", + "name": "tag", + "in": "body", + "required": true, + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "description": "프로그램 상태 on 또는 off", + "name": "status", + "in": "body", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "프로그램 발행 날짜", + "name": "publish_at", + "in": "body", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/learnsteam_learsteam-quiz-api_internal_models.ProgramResponse" + } + } + } + } + }, + "/program/{id}": { + "get": { + "description": "ID로 퀴즈 프로그램을 가져옵니다.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "퀴즈 프로그램 가져오기", + "parameters": [ + { + "type": "string", + "description": "퀴즈 프로그램 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/learnsteam_learsteam-quiz-api_internal_models.ProgramResponse" + } + }, + "400": { + "description": "Bad Request" + } + } + } + }, + "/quiz": { + "get": { + "description": "퀴즈 목록을 가져옵니다.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "퀴즈 목록 가져오기", + "parameters": [ + { + "type": "string", + "description": "프로그램 ID", + "name": "program_id", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "검색어", + "name": "q", + "in": "query" + }, + { + "type": "integer", + "description": "페이지", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "페이지 사이즈", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/learnsteam_learsteam-quiz-api_internal_models.QuizListResponse" + } + } + } + }, + "put": { + "description": "퀴즈를 수정합니다.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "퀴즈 수정", + "parameters": [ + { + "type": "string", + "description": "퀴즈 ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "프로그램 ID", + "name": "program_id", + "in": "body", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "퀴즈 순서", + "name": "sequence", + "in": "body", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "description": "퀴즈 타입 : choice, check, ox, input", + "name": "quiz_type", + "in": "body", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "퀴즈 문제", + "name": "question", + "in": "body", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "퀴즈 선택지 : ['선택1','선택2','선택3', '선택4']", + "name": "choice", + "in": "body", + "required": true, + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "description": "퀴즈 정답 : [1,3]", + "name": "answer", + "in": "body", + "required": true, + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "description": "퀴즈 힌트", + "name": "hint", + "in": "body", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "퀴즈 해설", + "name": "comment", + "in": "body", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/learnsteam_learsteam-quiz-api_internal_models.QuizResponse" + } + } + } + }, + "post": { + "description": "퀴즈를 만듭니다.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "퀴즈 생성", + "parameters": [ + { + "description": "프로그램 ID", + "name": "program_id", + "in": "body", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "퀴즈 순서", + "name": "sequence", + "in": "body", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "description": "퀴즈 타입 : choice, check, ox, input", + "name": "quiz_type", + "in": "body", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "퀴즈 문제", + "name": "question", + "in": "body", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "퀴즈 선택지 : ['선택1','선택2','선택3', '선택4']", + "name": "choice", + "in": "body", + "required": true, + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "description": "퀴즈 정답 : [1,3]", + "name": "answer", + "in": "body", + "required": true, + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "description": "퀴즈 힌트", + "name": "hint", + "in": "body", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "퀴즈 해설", + "name": "comment", + "in": "body", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/learnsteam_learsteam-quiz-api_internal_models.QuizResponse" + } + } + } + } + }, + "/quiz/{id}": { + "get": { + "description": "ID로 퀴즈를 가져옵니다.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "퀴즈 가져오기", + "parameters": [ + { + "type": "string", + "description": "퀴즈 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/learnsteam_learsteam-quiz-api_internal_models.QuizResponse" + } + }, + "400": { + "description": "Bad Request" + } + } + } + }, + "/token/refresh": { + "post": { + "description": "AccessToken을 RefreshToken으로 갱신합니다.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "AccessToken Refresh", + "parameters": [ + { + "description": "RefreshToken", + "name": "refresh_token", + "in": "body", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/learnsteam_learsteam-quiz-api_internal_models.ProgramResponse" + } + } + } + } + }, + "/user": { + "get": { + "description": "사용자 목록을 가져옵니다.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "사용자 목록 가져오기", + "parameters": [ + { + "type": "string", + "description": "검색어", + "name": "q", + "in": "query" + }, + { + "type": "integer", + "description": "페이지", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "페이지 사이즈", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/learnsteam_learsteam-quiz-api_internal_models.UserListResponse" + } + } + } + } + }, + "/user/{id}": { + "get": { + "description": "ID로 사용자 정보를 가져옵니다.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "사용자 정보 가져오기", + "parameters": [ + { + "type": "string", + "description": "사용자 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/learnsteam_learsteam-quiz-api_internal_models.User" + } + }, + "400": { + "description": "Bad Request" + } + } + } + } + }, + "definitions": { + "learnsteam_learsteam-quiz-api_internal_models.LoginResponse": { + "type": "object", + "properties": { + "refresh_token": { + "type": "string", + "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdXRob3JpemVkIjp0cnVlLCJleHAiOjE3MDc4OTcwMjcs" + }, + "token": { + "type": "string", + "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdXRob3JpemVkIjp0cnVlLCJleHAiOjE3MDI3MTMwMjcsInN1" + }, + "user": { + "$ref": "#/definitions/learnsteam_learsteam-quiz-api_internal_models.User" + } + } + }, + "learnsteam_learsteam-quiz-api_internal_models.Program": { + "type": "object", + "properties": { + "content": { + "type": "string", + "example": "코스 설명" + }, + "course": { + "type": "string", + "example": "코스 이름" + }, + "id": { + "type": "string", + "example": "ef74c59a-c707-4162-a52b-455906c81ec1" + }, + "publish_at": { + "type": "string", + "example": "2023-11-10T00:00:00+09:00" + }, + "status": { + "type": "string", + "example": "on" + }, + "subject": { + "type": "string", + "example": "프로그램 제목" + }, + "tag": { + "type": "array", + "items": { + "type": "integer" + } + } + } + }, + "learnsteam_learsteam-quiz-api_internal_models.ProgramListResponse": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/learnsteam_learsteam-quiz-api_internal_models.Program" + } + }, + "page": { + "type": "integer", + "example": 1 + }, + "pageSize": { + "type": "integer", + "example": 10 + }, + "total": { + "type": "integer", + "example": 999 + }, + "totalPage": { + "type": "integer", + "example": 99 + } + } + }, + "learnsteam_learsteam-quiz-api_internal_models.ProgramResponse": { + "type": "object", + "properties": { + "content": { + "type": "string", + "example": "코스 설명" + }, + "course": { + "type": "string", + "example": "코스 이름" + }, + "id": { + "type": "string", + "example": "ef74c59a-c707-4162-a52b-455906c81ec1" + }, + "publish_at": { + "type": "string", + "example": "2023-11-10T00:00:00+09:00" + }, + "status": { + "type": "string", + "example": "on" + }, + "subject": { + "type": "string", + "example": "프로그램 제목" + }, + "tag": { + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "tag1", + "tag2" + ] + } + } + }, + "learnsteam_learsteam-quiz-api_internal_models.Quiz": { + "type": "object", + "properties": { + "answer": { + "type": "array", + "items": { + "type": "integer" + } + }, + "choice": { + "type": "array", + "items": { + "type": "integer" + } + }, + "comment": { + "type": "string", + "example": "퀴즈 해설" + }, + "hint": { + "type": "string", + "example": "퀴즈 힌트" + }, + "id": { + "type": "string", + "example": "1b066168-68c4-4b50-bc9a-b6c4fceaf378" + }, + "program_id": { + "type": "string", + "example": "2036023a-fb56-4b6c-b3bb-c787c681ada6" + }, + "question": { + "type": "string", + "example": "퀴즈 질문입니다." + }, + "quiz_type": { + "type": "string", + "example": "choice" + }, + "sequence": { + "type": "integer", + "example": 5 + } + } + }, + "learnsteam_learsteam-quiz-api_internal_models.QuizListResponse": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/learnsteam_learsteam-quiz-api_internal_models.Quiz" + } + }, + "page": { + "type": "integer", + "example": 1 + }, + "pageSize": { + "type": "integer", + "example": 10 + }, + "total": { + "type": "integer", + "example": 5 + }, + "totalPage": { + "type": "integer", + "example": 1 + } + } + }, + "learnsteam_learsteam-quiz-api_internal_models.QuizResponse": { + "type": "object", + "properties": { + "answer": { + "type": "array", + "items": { + "type": "integer" + } + }, + "choice": { + "type": "array", + "items": { + "type": "integer" + } + }, + "comment": { + "type": "string", + "example": "퀴즈 해설" + }, + "hint": { + "type": "string", + "example": "퀴즈 힌트" + }, + "id": { + "type": "string", + "example": "1b066168-68c4-4b50-bc9a-b6c4fceaf378" + }, + "program_id": { + "type": "string", + "example": "2036023a-fb56-4b6c-b3bb-c787c681ada6" + }, + "question": { + "type": "string", + "example": "퀴즈 질문입니다." + }, + "quiz_type": { + "type": "string", + "example": "check" + }, + "sequence": { + "type": "integer", + "example": 5 + } + } + }, + "learnsteam_learsteam-quiz-api_internal_models.RegisterResponse": { + "type": "object", + "properties": { + "refresh_token": { + "type": "string", + "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdXRob3JpemVkIjp0cnVlLCJleHAiOjE3MDc4OTcwMjcs" + }, + "token": { + "type": "string", + "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdXRob3JpemVkIjp0cnVlLCJleHAiOjE3MDI3MTMwMjcsInN1" + }, + "user": { + "$ref": "#/definitions/learnsteam_learsteam-quiz-api_internal_models.User" + } + } + }, + "learnsteam_learsteam-quiz-api_internal_models.User": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "137c1683-2ad6-4201-b256-253828b61c49" + }, + "name": { + "type": "string", + "example": "홍길동" + }, + "score": { + "type": "integer", + "example": 9999 + }, + "username": { + "type": "string", + "example": "user0" + } + } + }, + "learnsteam_learsteam-quiz-api_internal_models.UserListResponse": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/learnsteam_learsteam-quiz-api_internal_models.User" + } + }, + "page": { + "type": "integer", + "example": 1 + }, + "pageSize": { + "type": "integer", + "example": 10 + }, + "total": { + "type": "integer", + "example": 90 + }, + "totalPage": { + "type": "integer", + "example": 9 + } + } + } + } +} \ No newline at end of file diff --git a/docs/swagger.yaml b/docs/swagger.yaml new file mode 100644 index 0000000..6b82000 --- /dev/null +++ b/docs/swagger.yaml @@ -0,0 +1,688 @@ +definitions: + learnsteam_learsteam-quiz-api_internal_models.LoginResponse: + properties: + refresh_token: + example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdXRob3JpemVkIjp0cnVlLCJleHAiOjE3MDc4OTcwMjcs + type: string + token: + example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdXRob3JpemVkIjp0cnVlLCJleHAiOjE3MDI3MTMwMjcsInN1 + type: string + user: + $ref: '#/definitions/learnsteam_learsteam-quiz-api_internal_models.User' + type: object + learnsteam_learsteam-quiz-api_internal_models.Program: + properties: + content: + example: 코스 설명 + type: string + course: + example: 코스 이름 + type: string + id: + example: ef74c59a-c707-4162-a52b-455906c81ec1 + type: string + publish_at: + example: "2023-11-10T00:00:00+09:00" + type: string + status: + example: "on" + type: string + subject: + example: 프로그램 제목 + type: string + tag: + items: + type: integer + type: array + type: object + learnsteam_learsteam-quiz-api_internal_models.ProgramListResponse: + properties: + data: + items: + $ref: '#/definitions/learnsteam_learsteam-quiz-api_internal_models.Program' + type: array + page: + example: 1 + type: integer + pageSize: + example: 10 + type: integer + total: + example: 999 + type: integer + totalPage: + example: 99 + type: integer + type: object + learnsteam_learsteam-quiz-api_internal_models.ProgramResponse: + properties: + content: + example: 코스 설명 + type: string + course: + example: 코스 이름 + type: string + id: + example: ef74c59a-c707-4162-a52b-455906c81ec1 + type: string + publish_at: + example: "2023-11-10T00:00:00+09:00" + type: string + status: + example: "on" + type: string + subject: + example: 프로그램 제목 + type: string + tag: + example: + - tag1 + - tag2 + items: + type: string + type: array + type: object + learnsteam_learsteam-quiz-api_internal_models.Quiz: + properties: + answer: + items: + type: integer + type: array + choice: + items: + type: integer + type: array + comment: + example: 퀴즈 해설 + type: string + hint: + example: 퀴즈 힌트 + type: string + id: + example: 1b066168-68c4-4b50-bc9a-b6c4fceaf378 + type: string + program_id: + example: 2036023a-fb56-4b6c-b3bb-c787c681ada6 + type: string + question: + example: 퀴즈 질문입니다. + type: string + quiz_type: + example: choice + type: string + sequence: + example: 5 + type: integer + type: object + learnsteam_learsteam-quiz-api_internal_models.QuizListResponse: + properties: + data: + items: + $ref: '#/definitions/learnsteam_learsteam-quiz-api_internal_models.Quiz' + type: array + page: + example: 1 + type: integer + pageSize: + example: 10 + type: integer + total: + example: 5 + type: integer + totalPage: + example: 1 + type: integer + type: object + learnsteam_learsteam-quiz-api_internal_models.QuizResponse: + properties: + answer: + items: + type: integer + type: array + choice: + items: + type: integer + type: array + comment: + example: 퀴즈 해설 + type: string + hint: + example: 퀴즈 힌트 + type: string + id: + example: 1b066168-68c4-4b50-bc9a-b6c4fceaf378 + type: string + program_id: + example: 2036023a-fb56-4b6c-b3bb-c787c681ada6 + type: string + question: + example: 퀴즈 질문입니다. + type: string + quiz_type: + example: check + type: string + sequence: + example: 5 + type: integer + type: object + learnsteam_learsteam-quiz-api_internal_models.RegisterResponse: + properties: + refresh_token: + example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdXRob3JpemVkIjp0cnVlLCJleHAiOjE3MDc4OTcwMjcs + type: string + token: + example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdXRob3JpemVkIjp0cnVlLCJleHAiOjE3MDI3MTMwMjcsInN1 + type: string + user: + $ref: '#/definitions/learnsteam_learsteam-quiz-api_internal_models.User' + type: object + learnsteam_learsteam-quiz-api_internal_models.User: + properties: + id: + example: 137c1683-2ad6-4201-b256-253828b61c49 + type: string + name: + example: 홍길동 + type: string + score: + example: 9999 + type: integer + username: + example: user0 + type: string + type: object + learnsteam_learsteam-quiz-api_internal_models.UserListResponse: + properties: + data: + items: + $ref: '#/definitions/learnsteam_learsteam-quiz-api_internal_models.User' + type: array + page: + example: 1 + type: integer + pageSize: + example: 10 + type: integer + total: + example: 90 + type: integer + totalPage: + example: 9 + type: integer + type: object +info: + contact: + email: sheen@jongyeob.com + name: Jay Sheen + description: Learnsteam Quiz 서비스 API + title: Learsteam Quiz API + version: "1.0" +paths: + /auth/login: + post: + consumes: + - application/json + description: username, password 를 입력하여 로그인 + parameters: + - description: username + in: body + name: username + required: true + schema: + type: string + - description: 비밀번호 + in: body + name: password + required: true + schema: + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/learnsteam_learsteam-quiz-api_internal_models.LoginResponse' + summary: 로그인 + /auth/register: + post: + consumes: + - application/json + description: username, name, password 를 입력하여 회원가입 + parameters: + - description: username + in: body + name: username + required: true + schema: + type: string + - description: 이름 + in: body + name: name + required: true + schema: + type: string + - description: 비밀번호 + in: body + name: password + required: true + schema: + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/learnsteam_learsteam-quiz-api_internal_models.RegisterResponse' + summary: 회원가입 + /program: + get: + consumes: + - application/json + description: 퀴즈 프로그램 목록을 가져옵니다. + parameters: + - description: 태그 + in: query + name: tag + type: string + - description: 검색어 + in: query + name: q + type: string + - description: 페이지 + in: query + name: page + type: integer + - description: 페이지 사이즈 + in: query + name: limit + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/learnsteam_learsteam-quiz-api_internal_models.ProgramListResponse' + security: + - ApiKeyAuth: [] + summary: 프로그램 목록 가져오기 + post: + consumes: + - application/json + description: 퀴즈 프로그램을 만듭니다. + parameters: + - description: 프로그램 제목 + in: body + name: subject + required: true + schema: + type: string + - description: 프로그램 코스 + in: body + name: course + required: true + schema: + type: string + - description: 프로그램 내용 + in: body + name: content + required: true + schema: + type: string + - description: 프로그램 태그 + in: body + name: tag + required: true + schema: + items: + type: string + type: array + - description: 프로그램 상태 on 또는 off + in: body + name: status + required: true + schema: + type: string + - description: 프로그램 발행 날짜 + in: body + name: publish_at + required: true + schema: + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/learnsteam_learsteam-quiz-api_internal_models.ProgramResponse' + summary: 퀴즈 프로그램 생성 + put: + consumes: + - application/json + description: 퀴즈 프로그램을 수정합니다. + parameters: + - description: 프로그램 제목 + in: body + name: subject + required: true + schema: + type: string + - description: 프로그램 코스 + in: body + name: course + required: true + schema: + type: string + - description: 프로그램 내용 + in: body + name: content + required: true + schema: + type: string + - description: 프로그램 태그 + in: body + name: tag + required: true + schema: + items: + type: string + type: array + - description: 프로그램 상태 on 또는 off + in: body + name: status + required: true + schema: + type: string + - description: 프로그램 발행 날짜 + in: body + name: publish_at + required: true + schema: + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/learnsteam_learsteam-quiz-api_internal_models.ProgramResponse' + summary: 퀴즈 프로그램 수정 + /program/{id}: + get: + consumes: + - application/json + description: ID로 퀴즈 프로그램을 가져옵니다. + parameters: + - description: 퀴즈 프로그램 ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/learnsteam_learsteam-quiz-api_internal_models.ProgramResponse' + "400": + description: Bad Request + summary: 퀴즈 프로그램 가져오기 + /quiz: + get: + consumes: + - application/json + description: 퀴즈 목록을 가져옵니다. + parameters: + - description: 프로그램 ID + in: query + name: program_id + required: true + type: string + - description: 검색어 + in: query + name: q + type: string + - description: 페이지 + in: query + name: page + type: integer + - description: 페이지 사이즈 + in: query + name: limit + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/learnsteam_learsteam-quiz-api_internal_models.QuizListResponse' + summary: 퀴즈 목록 가져오기 + post: + consumes: + - application/json + description: 퀴즈를 만듭니다. + parameters: + - description: 프로그램 ID + in: body + name: program_id + required: true + schema: + type: string + - description: 퀴즈 순서 + in: body + name: sequence + required: true + schema: + type: integer + - description: '퀴즈 타입 : choice, check, ox, input' + in: body + name: quiz_type + required: true + schema: + type: string + - description: 퀴즈 문제 + in: body + name: question + required: true + schema: + type: string + - description: '퀴즈 선택지 : [''선택1'',''선택2'',''선택3'', ''선택4'']' + in: body + name: choice + required: true + schema: + items: + type: string + type: array + - description: '퀴즈 정답 : [1,3]' + in: body + name: answer + required: true + schema: + items: + type: string + type: array + - description: 퀴즈 힌트 + in: body + name: hint + required: true + schema: + type: string + - description: 퀴즈 해설 + in: body + name: comment + required: true + schema: + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/learnsteam_learsteam-quiz-api_internal_models.QuizResponse' + summary: 퀴즈 생성 + put: + consumes: + - application/json + description: 퀴즈를 수정합니다. + parameters: + - description: 퀴즈 ID + in: path + name: id + required: true + type: string + - description: 프로그램 ID + in: body + name: program_id + required: true + schema: + type: string + - description: 퀴즈 순서 + in: body + name: sequence + required: true + schema: + type: integer + - description: '퀴즈 타입 : choice, check, ox, input' + in: body + name: quiz_type + required: true + schema: + type: string + - description: 퀴즈 문제 + in: body + name: question + required: true + schema: + type: string + - description: '퀴즈 선택지 : [''선택1'',''선택2'',''선택3'', ''선택4'']' + in: body + name: choice + required: true + schema: + items: + type: string + type: array + - description: '퀴즈 정답 : [1,3]' + in: body + name: answer + required: true + schema: + items: + type: string + type: array + - description: 퀴즈 힌트 + in: body + name: hint + required: true + schema: + type: string + - description: 퀴즈 해설 + in: body + name: comment + required: true + schema: + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/learnsteam_learsteam-quiz-api_internal_models.QuizResponse' + summary: 퀴즈 수정 + /quiz/{id}: + get: + consumes: + - application/json + description: ID로 퀴즈를 가져옵니다. + parameters: + - description: 퀴즈 ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/learnsteam_learsteam-quiz-api_internal_models.QuizResponse' + "400": + description: Bad Request + summary: 퀴즈 가져오기 + /token/refresh: + post: + consumes: + - application/json + description: AccessToken을 RefreshToken으로 갱신합니다. + parameters: + - description: RefreshToken + in: body + name: refresh_token + required: true + schema: + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/learnsteam_learsteam-quiz-api_internal_models.ProgramResponse' + summary: AccessToken Refresh + /user: + get: + consumes: + - application/json + description: 사용자 목록을 가져옵니다. + parameters: + - description: 검색어 + in: query + name: q + type: string + - description: 페이지 + in: query + name: page + type: integer + - description: 페이지 사이즈 + in: query + name: limit + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/learnsteam_learsteam-quiz-api_internal_models.UserListResponse' + summary: 사용자 목록 가져오기 + /user/{id}: + get: + consumes: + - application/json + description: ID로 사용자 정보를 가져옵니다. + parameters: + - description: 사용자 ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/learnsteam_learsteam-quiz-api_internal_models.User' + "400": + description: Bad Request + summary: 사용자 정보 가져오기 +swagger: "2.0" diff --git a/go.mod b/go.mod index 79a9066..2110cc8 100644 --- a/go.mod +++ b/go.mod @@ -1,46 +1,80 @@ -module studioj/boilerplate_go +module learnsteam/learsteam-quiz-api -go 1.20 +go 1.21 + +toolchain go1.21.3 require github.com/gin-gonic/gin v1.9.1 require ( + github.com/KyleBanks/depth v1.2.1 // indirect + github.com/PuerkitoBio/purell v1.2.1 // indirect + github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect github.com/aws/aws-lambda-go v1.17.0 // indirect + github.com/chenzhuoyu/iasm v0.9.1 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/go-openapi/jsonpointer v0.20.0 // indirect + github.com/go-openapi/jsonreference v0.20.2 // indirect + github.com/go-openapi/spec v0.20.9 // indirect + github.com/go-openapi/swag v0.22.4 // indirect github.com/go-sql-driver/mysql v1.7.0 // indirect + github.com/golang/protobuf v1.5.3 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/mattn/go-sqlite3 v1.14.17 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect + github.com/swaggo/files v1.0.1 // indirect + github.com/swaggo/gin-swagger v1.6.0 // indirect + github.com/swaggo/swag v1.16.2 // indirect + github.com/urfave/cli/v2 v2.25.7 // indirect + github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect + golang.org/x/tools v0.15.0 // indirect + google.golang.org/appengine v1.6.7 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect ) require ( github.com/apex/gateway v1.1.2 - github.com/bytedance/sonic v1.9.1 // indirect - github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect - github.com/gabriel-vasile/mimetype v1.4.2 // indirect + github.com/bytedance/sonic v1.10.2 // indirect + github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect + github.com/gabriel-vasile/mimetype v1.4.3 // indirect + github.com/gin-contrib/cors v1.4.0 github.com/gin-contrib/sse v0.1.0 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.14.0 // indirect + github.com/go-playground/validator/v10 v10.16.0 // indirect github.com/goccy/go-json v0.10.2 // indirect + github.com/golang-jwt/jwt v3.2.2+incompatible github.com/golang-jwt/jwt/v5 v5.0.0 github.com/google/uuid v1.3.1 github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/cpuid/v2 v2.2.4 // indirect + github.com/klauspost/cpuid/v2 v2.2.6 // indirect github.com/leodido/go-urn v1.2.4 // indirect - github.com/mattn/go-isatty v0.0.19 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/pelletier/go-toml/v2 v2.0.8 // indirect + github.com/pelletier/go-toml/v2 v2.1.0 // indirect github.com/stretchr/testify v1.8.4 // indirect + github.com/tj/assert v0.0.3 github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.11 // indirect - golang.org/x/arch v0.3.0 // indirect - golang.org/x/crypto v0.9.0 - golang.org/x/net v0.10.0 // indirect - golang.org/x/sys v0.8.0 // indirect - golang.org/x/text v0.9.0 // indirect - google.golang.org/protobuf v1.30.0 // indirect + golang.org/x/arch v0.6.0 // indirect + golang.org/x/crypto v0.15.0 + golang.org/x/net v0.18.0 // indirect + golang.org/x/oauth2 v0.14.0 + golang.org/x/sys v0.14.0 // indirect + golang.org/x/text v0.14.0 // indirect + google.golang.org/protobuf v1.31.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + gorm.io/datatypes v1.2.0 gorm.io/driver/mysql v1.5.2 + gorm.io/driver/sqlite v1.5.4 gorm.io/gorm v1.25.5 ) diff --git a/go.sum b/go.sum index aa292d7..9f71c1b 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,10 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= +github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= +github.com/PuerkitoBio/purell v1.2.1 h1:QsZ4TjvwiMpat6gBCBxEQI0rcS9ehtkKtSpiUnd9N28= +github.com/PuerkitoBio/purell v1.2.1/go.mod h1:ZwHcC/82TOaovDi//J/804umJFFmbOHPngi8iYYv/Eo= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/apex/gateway v1.1.2 h1:OWyLov8eaau8YhkYKkRuOAYqiUhpBJalBR1o+3FzX+8= github.com/apex/gateway v1.1.2/go.mod h1:AMTkVbz5u5Hvd6QOGhhg0JUrNgCcLVu3XNJOGntdoB4= github.com/aws/aws-lambda-go v1.17.0 h1:Ogihmi8BnpmCNktKAGpNwSiILNNING1MiosnKUfU8m0= @@ -6,35 +12,79 @@ github.com/aws/aws-lambda-go v1.17.0/go.mod h1:FEwgPLE6+8wcGBTe5cJN3JWurd1Ztm9zN github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= +github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM= +github.com/bytedance/sonic v1.10.2 h1:GQebETVBxYB7JGWJtLBi07OVzWwt+8dWA00gEVW2ZFE= +github.com/bytedance/sonic v1.10.2/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4= github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= +github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0= +github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA= +github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog= +github.com/chenzhuoyu/iasm v0.9.1 h1:tUHQJXo3NhBqw6s33wkGn9SP3bvrWLdlVIJ3hQBL7P0= +github.com/chenzhuoyu/iasm v0.9.1/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM= +github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= +github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= +github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= +github.com/gin-contrib/cors v1.4.0 h1:oJ6gwtUl3lqV0WEIwM/LxPF1QZ5qe2lGWdY2+bz7y0g= +github.com/gin-contrib/cors v1.4.0/go.mod h1:bs9pNM0x/UsmHPBWT2xZz9ROh8xYjYkiURUfmBoMlcs= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk= github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonpointer v0.20.0 h1:ESKJdU9ASRfaPNOPRx12IUyA1vn3R9GiE3KYD14BXdQ= +github.com/go-openapi/jsonpointer v0.20.0/go.mod h1:6PGzBjjIIumbLYysB73Klnms1mwnU4G3YHOECG3CedA= +github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/spec v0.20.9 h1:xnlYNQAwKd2VQRRfwTEI0DcK+2cbuvI/0c7jx3gA8/8= +github.com/go-openapi/spec v0.20.9/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU= +github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos= github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js= github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= +github.com/go-playground/validator/v10 v10.16.0 h1:x+plE831WK4vaKHO/jpgUGsvLKIqRRkz6M78GuJAfGE= +github.com/go-playground/validator/v10 v10.16.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc= github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= +github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= +github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE= github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -42,27 +92,59 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= +github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc= +github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM= +github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= +github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= +github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= @@ -78,41 +160,121 @@ github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE= +github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg= +github.com/swaggo/gin-swagger v1.6.0 h1:y8sxvQ3E20/RCyrXeFfg60r6H0Z+SwpTjMYsMm+zy8M= +github.com/swaggo/gin-swagger v1.6.0/go.mod h1:BG00cCEy294xtVpyIAHG6+e2Qzj/xKlRdOqDkvq0uzo= +github.com/swaggo/swag v1.16.2 h1:28Pp+8DkQoV+HLzLx8RGJZXNGKbFqnuvSbAAtoxiY04= +github.com/swaggo/swag v1.16.2/go.mod h1:6YzXnDcpr0767iOejs318CwYkCQqyGer6BizOg03f+E= github.com/tj/assert v0.0.3 h1:Df/BlaZ20mq6kuai7f5z2TvPFiwC3xaWJSDQNiIS3Rk= github.com/tj/assert v0.0.3/go.mod h1:Ne6X72Q+TB1AteidzQncjw9PabbMp4PBMZ1k+vd1Pvk= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M= +github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/urfave/cli/v2 v2.1.1/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= +github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs= +github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.6.0 h1:S0JTfE48HbRj80+4tbvZDYsJ3tGv6BUU3XxyZ7CirAc= +golang.org/x/arch v0.6.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= +golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA= +golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg= +golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= +golang.org/x/oauth2 v0.14.0 h1:P0Vrf/2538nmC0H+pEQ3MNFRRnVR7RlqyVw+bvm26z0= +golang.org/x/oauth2 v0.14.0/go.mod h1:lAtNWgaWfL4cm7j2OV8TxGi9Qb7ECORx8DktCY74OwM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= +golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.15.0 h1:zdAyfUGbYmuVokhzVmghFl2ZJh5QhcfebBgmVPFYA+8= +golang.org/x/tools v0.15.0/go.mod h1:hpksKq4dtpQWS1uQ61JkdqWM3LscIS6Slf+VVkm+wQk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/datatypes v1.2.0 h1:5YT+eokWdIxhJgWHdrb2zYUimyk0+TaFth+7a0ybzco= +gorm.io/datatypes v1.2.0/go.mod h1:o1dh0ZvjIjhH/bngTpypG6lVRJ5chTBxE09FH/71k04= gorm.io/driver/mysql v1.5.2 h1:QC2HRskSE75wBuOxe0+iCkyJZ+RqpudsQtqkp+IMuXs= gorm.io/driver/mysql v1.5.2/go.mod h1:pQLhh1Ut/WUAySdTHwBpBv6+JKcj+ua4ZFx1QQTBzb8= +gorm.io/driver/sqlite v1.5.4 h1:IqXwXi8M/ZlPzH/947tn5uik3aYQslP9BVveoax0nV0= +gorm.io/driver/sqlite v1.5.4/go.mod h1:qxAuCol+2r6PannQDpOP1FP6ag3mKi4esLnB/jHed+4= gorm.io/gorm v1.25.2-0.20230530020048-26663ab9bf55/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls= gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/go.work b/go.work new file mode 100644 index 0000000..767c526 --- /dev/null +++ b/go.work @@ -0,0 +1,3 @@ +go 1.21.3 + +use . diff --git a/go.work.sum b/go.work.sum new file mode 100644 index 0000000..a91659a --- /dev/null +++ b/go.work.sum @@ -0,0 +1,5 @@ +cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/ugorji/go v1.2.7 h1:qYhyWUUd6WbiM+C6JZAUkIJt/1WrjzNHY9+KCIjVqTo= diff --git a/internal/controllers/auth.go b/internal/controllers/auth.go index 1e6f5e3..5ee1d6c 100644 --- a/internal/controllers/auth.go +++ b/internal/controllers/auth.go @@ -3,8 +3,8 @@ package controllers import ( "net/http" - "studioj/boilerplate_go/internal/models" - "studioj/boilerplate_go/internal/services" + "learnsteam/learsteam-quiz-api/internal/models" + "learnsteam/learsteam-quiz-api/internal/services" "github.com/gin-gonic/gin" ) @@ -15,15 +15,30 @@ type AuthController interface { } type authController struct { - service services.AuthService + service services.AuthService + tokenService services.TokenService } -func NewAuthController(service services.AuthService) AuthController { +func NewAuthController(service services.AuthService, tokenService services.TokenService) AuthController { return &authController{ - service: service, + service: service, + tokenService: tokenService, } } +// Register +// +// @Summary 회원가입 +// @Description username, name, password 를 입력하여 회원가입 +// @Accept json +// @Produce json +// +// @Param username body string true "username" +// @Param name body string true "이름" +// @Param password body string true "비밀번호" +// +// @Success 200 {object} models.RegisterResponse +// @Router /auth/register [post] func (controller *authController) Register(c *gin.Context) { var params models.RegisterRequest if c.BindJSON(¶ms) != nil { @@ -37,7 +52,7 @@ func (controller *authController) Register(c *gin.Context) { return } - token, err := controller.service.CreateToken(user.ID) + token, err := controller.tokenService.Create(user.ID) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return @@ -45,23 +60,36 @@ func (controller *authController) Register(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"user": user, "token": token.Token, "refresh_token": token.RefreshToken}) } +// Login +// +// @Summary 로그인 +// @Description username, password 를 입력하여 로그인 +// @Accept json +// @Produce json +// +// @Param username body string true "username" +// @Param password body string true "비밀번호" +// +// @Success 200 {object} models.LoginResponse +// @Router /auth/login [post] func (controller *authController) Login(c *gin.Context) { - var params models.LoginRequest - if c.BindJSON(¶ms) != nil { + var request models.LoginRequest + if c.BindJSON(&request) != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"}) return } - user, err := controller.service.Login(¶ms) + user, err := controller.service.Login(&request) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } - token, err := controller.service.CreateToken(user.ID) + token, err := controller.tokenService.Create(user.ID) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } + c.JSON(http.StatusOK, gin.H{"user": user, "token": token.Token, "refresh_token": token.RefreshToken}) } diff --git a/internal/controllers/program.go b/internal/controllers/program.go new file mode 100644 index 0000000..48f744a --- /dev/null +++ b/internal/controllers/program.go @@ -0,0 +1,207 @@ +package controllers + +import ( + "fmt" + "net/http" + "strconv" + "time" + + "learnsteam/learsteam-quiz-api/internal/models" + "learnsteam/learsteam-quiz-api/internal/services" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +type ProgramController interface { + List(*gin.Context) + Find(*gin.Context) + Create(*gin.Context) + Update(*gin.Context) +} + +type programController struct { + service services.ProgramService +} + +func NewProgramController(service services.ProgramService) ProgramController { + return &programController{ + service: service, + } +} + +// Program List +// +// @Summary 프로그램 목록 가져오기 +// @Description 퀴즈 프로그램 목록을 가져옵니다. +// @Accept json +// @Produce json +// @Param tag query string false "태그" +// @Param q query string false "검색어" +// @Param page query int false "페이지" +// @Param limit query int false "페이지 사이즈" +// @Success 200 {object} models.ProgramListResponse +// @Security ApiKeyAuth +// @Router /program [get] +func (controller *programController) List(c *gin.Context) { + tag := c.DefaultQuery("tag", "") + q := c.DefaultQuery("q", "") + page, err := strconv.Atoi(c.DefaultQuery("page", "1")) + if err != nil { + fmt.Println("error : page") + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + limit, err := strconv.Atoi(c.DefaultQuery("limit", "10")) + if err != nil { + fmt.Println("error : limit") + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + result, err := controller.service.List(q, tag, page, limit) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + totalNumber, err := controller.service.Total(q, tag) + if err != nil { + fmt.Println("error : list") + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + totalPage := int64(totalNumber/int64(limit) + 1) + + response := models.ProgramListResponse{ + Data: *result, + Total: totalPage, + Page: page, + TotalPage: totalPage, + PageSize: limit, + } + + c.JSON(http.StatusOK, response) +} + +// Get program +// +// @Summary 퀴즈 프로그램 가져오기 +// @Description ID로 퀴즈 프로그램을 가져옵니다. +// @Accept json +// @Produce json +// @Param id path string true "퀴즈 프로그램 ID" +// @Success 200 {object} models.ProgramResponse +// @Failure 400 +// @Router /program/{id} [get] +func (controller *programController) Find(c *gin.Context) { + id := c.Param("id") + + result, err := controller.service.Find(id) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, result) +} + +// Create Program +// +// @Summary 퀴즈 프로그램 생성 +// @Description 퀴즈 프로그램을 만듭니다. +// @Accept json +// @Produce json +// @Param subject body string true "프로그램 제목" +// @Param course body string true "프로그램 코스" +// @Param content body string true "프로그램 내용" +// @Param tag body []string true "프로그램 태그" +// @Param status body string true "프로그램 상태 on 또는 off" +// @Param publish_at body string true "프로그램 발행 날짜" +// @Router /program [post] +// @Success 200 {object} models.ProgramResponse +func (controller *programController) Create(c *gin.Context) { + var request models.ProgramRequest + if err := c.ShouldBindJSON(&request); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + publish_at, err := time.Parse("2006-01-02", request.PublishAt) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + program := &models.Program{ + ID: uuid.NewString(), + Subject: request.Subject, + Course: request.Course, + Content: request.Content, + Tag: request.Tag, + Tags: request.Tag.String(), + PublishAt: publish_at, + Status: request.Status, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + fmt.Println("publish_at", program.PublishAt) + result, err := controller.service.Create(program) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, result) +} + +// Update Program +// +// @Summary 퀴즈 프로그램 수정 +// @Description 퀴즈 프로그램을 수정합니다. +// @Accept json +// @Produce json +// @Param subject body string true "프로그램 제목" +// @Param course body string true "프로그램 코스" +// @Param content body string true "프로그램 내용" +// @Param tag body []string true "프로그램 태그" +// @Param status body string true "프로그램 상태 on 또는 off" +// @Param publish_at body string true "프로그램 발행 날짜" +// @Router /program [put] +// @Success 200 {object} models.ProgramResponse +func (controller *programController) Update(c *gin.Context) { + id := c.Param("id") + var request models.ProgramRequest + if err := c.ShouldBindJSON(&request); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + program, err := controller.service.Find(id) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + publish_at, err := time.Parse("2006-01-02", request.PublishAt) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + program.Subject = request.Subject + program.Course = request.Course + program.Content = request.Content + program.Tag = request.Tag + program.Tags = request.Tag.String() + program.PublishAt = publish_at + program.Status = request.Status + program.UpdatedAt = time.Now() + + result, err := controller.service.Update(program) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, result) +} diff --git a/internal/controllers/quiz.go b/internal/controllers/quiz.go new file mode 100644 index 0000000..40e6d18 --- /dev/null +++ b/internal/controllers/quiz.go @@ -0,0 +1,193 @@ +package controllers + +import ( + "fmt" + "net/http" + "strconv" + "time" + + "learnsteam/learsteam-quiz-api/internal/models" + "learnsteam/learsteam-quiz-api/internal/services" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +type QuizController interface { + List(*gin.Context) + Find(*gin.Context) + Create(*gin.Context) + Update(*gin.Context) +} + +type quizController struct { + service services.QuizService +} + +func NewQuizController(service services.QuizService) QuizController { + return &quizController{ + service: service, + } +} + +// Quiz List +// +// @Summary 퀴즈 목록 가져오기 +// @Description 퀴즈 목록을 가져옵니다. +// @Accept json +// @Produce json +// @Param program_id query string true "프로그램 ID" +// @Param q query string false "검색어" +// @Param page query int false "페이지" +// @Param limit query int false "페이지 사이즈" +// @Success 200 {object} models.QuizListResponse +// @Router /quiz [get] +func (controller *quizController) List(c *gin.Context) { + program_id := c.DefaultQuery("program_id", "") + page, err := strconv.Atoi(c.DefaultQuery("page", "1")) + if err != nil { + fmt.Println("error : page") + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + limit, err := strconv.Atoi(c.DefaultQuery("limit", "10")) + if err != nil { + fmt.Println("error : limit") + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + result, err := controller.service.List(program_id, page, limit) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + totalNumber, err := controller.service.Total(program_id) + if err != nil { + fmt.Println("error : list") + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + totalPage := int64(totalNumber/int64(limit) + 1) + + c.JSON(http.StatusOK, gin.H{"data": result, "total": totalNumber, "page": page, "totalPage": totalPage, "pageSize": limit}) +} + +// Get Quiz +// +// @Summary 퀴즈 가져오기 +// @Description ID로 퀴즈를 가져옵니다. +// @Accept json +// @Produce json +// @Param id path string true "퀴즈 ID" +// @Success 200 {object} models.QuizResponse +// @Failure 400 +// @Router /quiz/{id} [get] +func (controller *quizController) Find(c *gin.Context) { + id := c.Param("id") + + result, err := controller.service.Find(id) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, result) +} + +// Create Quiz +// +// @Summary 퀴즈 생성 +// @Description 퀴즈를 만듭니다. +// @Accept json +// @Produce json +// @Param program_id body string true "프로그램 ID" +// @Param sequence body int true "퀴즈 순서" +// @Param quiz_type body string true "퀴즈 타입 : choice, check, ox, input" +// @Param question body string true "퀴즈 문제" +// @Param choice body []string true "퀴즈 선택지 : ['선택1','선택2','선택3', '선택4']" +// @Param answer body []string true "퀴즈 정답 : [1,3]" +// @Param hint body string true "퀴즈 힌트" +// @Param comment body string true "퀴즈 해설" +// @Success 200 {object} models.QuizResponse +// @Router /quiz [post] +func (controller *quizController) Create(c *gin.Context) { + var request models.QuizRequest + if err := c.ShouldBindJSON(&request); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + quiz := &models.Quiz{ + ID: uuid.NewString(), + ProgramID: request.ProgramID, + Sequence: request.Sequence, + QuizType: request.QuizType, + Question: request.Question, + Choice: request.Choice, + Answer: request.Answer, + Hint: request.Hint, + Comment: request.Comment, + UpdatedAt: time.Now(), + CreatedAt: time.Now(), + } + + result, err := controller.service.Create(quiz) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, result) +} + +// Update Quiz +// +// @Summary 퀴즈 수정 +// @Description 퀴즈를 수정합니다. +// @Accept json +// @Produce json +// @Param id path string true "퀴즈 ID" +// @Param program_id body string true "프로그램 ID" +// @Param sequence body int true "퀴즈 순서" +// @Param quiz_type body string true "퀴즈 타입 : choice, check, ox, input" +// @Param question body string true "퀴즈 문제" +// @Param choice body []string true "퀴즈 선택지 : ['선택1','선택2','선택3', '선택4']" +// @Param answer body []string true "퀴즈 정답 : [1,3]" +// @Param hint body string true "퀴즈 힌트" +// @Param comment body string true "퀴즈 해설" +// @Success 200 {object} models.QuizResponse +// @Router /quiz [put] +func (controller *quizController) Update(c *gin.Context) { + id := c.Param("id") + var request models.QuizRequest + if err := c.ShouldBindJSON(&request); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + quiz, err := controller.service.Find(id) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + quiz.ProgramID = request.ProgramID + quiz.Sequence = request.Sequence + quiz.QuizType = request.QuizType + quiz.Question = request.Question + quiz.Choice = request.Choice + quiz.Answer = request.Answer + quiz.Hint = request.Hint + quiz.Comment = request.Comment + quiz.UpdatedAt = time.Now() + + result, err := controller.service.Update(quiz) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, result) +} diff --git a/internal/controllers/swagger.go b/internal/controllers/swagger.go new file mode 100644 index 0000000..d8f7825 --- /dev/null +++ b/internal/controllers/swagger.go @@ -0,0 +1,22 @@ +package controllers + +import ( + "net/http" + + "github.com/gin-gonic/gin" +) + +type SwaggerController interface { + Get(*gin.Context) +} + +type swaggerController struct { +} + +func NewSwaggerController() SwaggerController { + return &swaggerController{} +} + +func (controller *swaggerController) Get(c *gin.Context) { + c.Redirect(http.StatusFound, "/swagger/index.html") +} diff --git a/internal/controllers/token.go b/internal/controllers/token.go index 0740c3e..9bfcabb 100644 --- a/internal/controllers/token.go +++ b/internal/controllers/token.go @@ -3,13 +3,14 @@ package controllers import ( "net/http" - "studioj/boilerplate_go/internal/services" + "learnsteam/learsteam-quiz-api/internal/models" + "learnsteam/learsteam-quiz-api/internal/services" "github.com/gin-gonic/gin" ) type TokenController interface { - Find(*gin.Context) + Refresh(*gin.Context) } type tokenController struct { @@ -22,41 +23,27 @@ func NewTokenController(service services.TokenService) TokenController { } } -func (controller *tokenController) Find(c *gin.Context) { - id := c.Param("id") - user_id := c.GetString("user_id") - - if user_id != id { - c.JSON(http.StatusBadRequest, gin.H{"error": "Wrong user"}) - return - } - - result, err := controller.service.Find(id) - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - c.JSON(http.StatusOK, result) -} - -func (controller *tokenController) List(c *gin.Context) { - id := c.Param("id") - user_id := c.GetString("user_id") - - if user_id != id { - c.JSON(http.StatusBadRequest, gin.H{"error": "Wrong user"}) - return - } - - result, err := controller.service.Find(id) - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - c.JSON(http.StatusOK, result) -} - +// Refresh Token +// +// @Summary AccessToken Refresh +// @Description AccessToken을 RefreshToken으로 갱신합니다. +// @Accept json +// @Produce json +// @Param refresh_token body string true "RefreshToken" +// @Router /token/refresh [post] +// @Success 200 {object} models.ProgramResponse func (controller *tokenController) Refresh(c *gin.Context) { -} \ No newline at end of file + var request models.RefreshTokenRequest + if err := c.ShouldBindJSON(&request); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + result, err := controller.service.Refresh(request.RefreshToken) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, result) +} diff --git a/internal/controllers/user.go b/internal/controllers/user.go index 1a645aa..ab740f6 100644 --- a/internal/controllers/user.go +++ b/internal/controllers/user.go @@ -3,13 +3,16 @@ package controllers import ( "fmt" "net/http" + "strconv" - "studioj/boilerplate_go/internal/services" + "learnsteam/learsteam-quiz-api/internal/models" + "learnsteam/learsteam-quiz-api/internal/services" "github.com/gin-gonic/gin" ) type UserController interface { + List(*gin.Context) Find(*gin.Context) } @@ -25,6 +28,71 @@ func NewUserController(service services.UserService, tokenService services.Token } } +// User List +// +// @Summary 사용자 목록 가져오기 +// @Description 사용자 목록을 가져옵니다. +// @Accept json +// @Produce json +// +// @Param q query string false "검색어" +// @Param page query int false "페이지" +// @Param limit query int false "페이지 사이즈" +// +// @Success 200 {object} models.UserListResponse +// @Router /user [get] +func (controller *userController) List(c *gin.Context) { + q := c.DefaultQuery("q", "") + page, err := strconv.Atoi(c.DefaultQuery("page", "1")) + if err != nil { + fmt.Println("error : page") + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + limit, err := strconv.Atoi(c.DefaultQuery("limit", "10")) + if err != nil { + fmt.Println("error : limit") + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + result, err := controller.service.List(q, page, limit) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + totalNumber, err := controller.service.Total(q) + if err != nil { + fmt.Println("error : list") + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + totalPage := int64(totalNumber/int64(limit) + 1) + + response := models.UserListResponse{ + Data: *result, + Total: totalPage, + Page: page, + TotalPage: totalPage, + PageSize: limit, + } + + c.JSON(http.StatusOK, response) + + c.JSON(http.StatusOK, response) +} + +// Get User +// +// @Summary 사용자 정보 가져오기 +// @Description ID로 사용자 정보를 가져옵니다. +// @Accept json +// @Produce json +// @Param id path string true "사용자 ID" +// @Success 200 {object} models.User +// @Failure 400 +// @Router /user/{id} [get] func (controller *userController) Find(c *gin.Context) { id := c.Param("id") user_id := c.GetString("sub") @@ -36,7 +104,7 @@ func (controller *userController) Find(c *gin.Context) { return } - result, err := controller.service.FindByID(id) + result, err := controller.service.Find(id) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return diff --git a/internal/database/database.go b/internal/database/database.go index a505f97..198769d 100644 --- a/internal/database/database.go +++ b/internal/database/database.go @@ -1,8 +1,9 @@ package database import ( - config "studioj/boilerplate_go/configs" - "studioj/boilerplate_go/internal/models" + "fmt" + config "learnsteam/learsteam-quiz-api/configs" + "learnsteam/learsteam-quiz-api/internal/models" "gorm.io/driver/mysql" "gorm.io/gorm" @@ -33,8 +34,12 @@ func GetDB() *gorm.DB { } func AutoMigrate() { + fmt.Println("AugoMigrate") DB.AutoMigrate( &models.User{}, &models.Token{}, + &models.Program{}, + &models.Quiz{}, + // &models.Course{}, ) } diff --git a/internal/helpers/cipher/cipher.go b/internal/helpers/cipher/cipher.go new file mode 100644 index 0000000..7aae840 --- /dev/null +++ b/internal/helpers/cipher/cipher.go @@ -0,0 +1,86 @@ +package cipher + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "encoding/base64" + "strings" +) + +type Crypto interface { + Encrypt(plainText string) (string, error) + Decrypt(cipherIvKey string) (string, error) +} + +type niceCrypto struct { + cipherKey string + cipherIvKey string +} + +func (c niceCrypto) Encrypt(plainText string) (string, error) { + if strings.TrimSpace(plainText) == "" { + return plainText, nil + } + + block, err := aes.NewCipher([]byte(c.cipherKey)) + if err != nil { + return "", err + } + + encrypter := cipher.NewCBCEncrypter(block, []byte(c.cipherIvKey)) + paddedPlainText := padPKCS7([]byte(plainText), encrypter.BlockSize()) + + cipherText := make([]byte, len(paddedPlainText)) + // CryptBlocks 함수에 데이터(paddedPlainText)와 암호화 될 데이터를 저장할 슬라이스(cipherText)를 넣으면 암호화가 된다. + encrypter.CryptBlocks(cipherText, paddedPlainText) + + return base64.StdEncoding.EncodeToString(cipherText), nil +} + +func (c niceCrypto) Decrypt(cipherText string) (string, error) { + if strings.TrimSpace(cipherText) == "" { + return cipherText, nil + } + + decodedCipherText, err := base64.StdEncoding.DecodeString(cipherText) + if err != nil { + return "", err + } + + block, err := aes.NewCipher([]byte(c.cipherKey)) + if err != nil { + return "", err + } + + decrypter := cipher.NewCBCDecrypter(block, []byte(c.cipherIvKey)) + plainText := make([]byte, len(decodedCipherText)) + + decrypter.CryptBlocks(plainText, decodedCipherText) + trimmedPlainText := trimPKCS5(plainText) + + return string(trimmedPlainText), nil +} + +func NewNiceCrypto(cipherKey, cipherIvKey string) (Crypto, error) { + if ck := len(cipherKey); ck != 32 { + return nil, aes.KeySizeError(ck) + } + + if cik := len(cipherIvKey); cik != 16 { + return nil, aes.KeySizeError(cik) + } + + return &niceCrypto{cipherKey, cipherIvKey}, nil +} + +func padPKCS7(plainText []byte, blockSize int) []byte { + padding := blockSize - len(plainText)%blockSize + padText := bytes.Repeat([]byte{byte(padding)}, padding) + return append(plainText, padText...) +} + +func trimPKCS5(text []byte) []byte { + padding := text[len(text)-1] + return text[:len(text)-int(padding)] +} diff --git a/internal/helpers/helpers.go b/internal/helpers/helpers.go index e7de302..8a7fea1 100644 --- a/internal/helpers/helpers.go +++ b/internal/helpers/helpers.go @@ -1,9 +1,7 @@ package helpers import ( - "math/rand" "os" - "strconv" "time" ) @@ -14,14 +12,6 @@ func InLambda() bool { return false } -func RandomPin() string { - min := 100000 - max := 999999 - r := rand.New(rand.NewSource(time.Now().UnixNano())) - pin := r.Intn(max-min) + min - return strconv.Itoa(pin) -} - func Datetime(timeString string) (time.Time, error) { layout := "2006-01-02T15:04:05.000Z" result, err := time.Parse(layout, timeString) diff --git a/internal/helpers/securityutil/securityutil.go b/internal/helpers/securityutil/securityutil.go new file mode 100644 index 0000000..cc1b8fa --- /dev/null +++ b/internal/helpers/securityutil/securityutil.go @@ -0,0 +1,150 @@ +package securityutil + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "crypto/sha256" + "encoding/base64" + "encoding/hex" + "fmt" + + "golang.org/x/crypto/bcrypt" +) + +func SHA256(data string) string { + hash := sha256.New() + hash.Write([]byte(data)) + return hex.EncodeToString(hash.Sum(nil)) +} + +func HashPassword(password string) (string, error) { + bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14) + return string(bytes), err +} + +func CheckPasswordHash(password string, hash string) bool { + err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) + return err == nil +} + +func AES256Encode(plaintext string, key string, iv string, blockSize int) string { + bKey := []byte(key) + bIV := []byte(iv) + bPlaintext := PKCS5Padding([]byte(plaintext), blockSize, len(plaintext)) + block, err := aes.NewCipher(bKey) + if err != nil { + panic(err) + } + ciphertext := make([]byte, len(bPlaintext)) + mode := cipher.NewCBCEncrypter(block, bIV) + mode.CryptBlocks(ciphertext, bPlaintext) + // return hex.EncodeToString(ciphertext) + fmt.Printf("hex.EncodeToString(ciphertext) : %s\n", hex.EncodeToString(ciphertext)) + fmt.Printf("base64.StdEncoding.EncodeToString(ciphertext) : %s\n", base64.StdEncoding.EncodeToString(ciphertext)) + fmt.Printf("base64.RawStdEncoding.EncodeToString(ciphertext) : %s\n", base64.RawStdEncoding.EncodeToString(ciphertext)) + return base64.StdEncoding.EncodeToString(ciphertext) +} + +func AES256Decode(cipherText string, encKey string, iv string) (decryptedString string) { + bKey := []byte(encKey) + bIV := []byte(iv) + // cipherTextDecoded, err := hex.DecodeString(cipherText) + cipherTextDecoded, err := base64.StdEncoding.DecodeString(cipherText) + if err != nil { + panic(err) + } + + block, err := aes.NewCipher(bKey) + if err != nil { + panic(err) + } + + mode := cipher.NewCBCDecrypter(block, bIV) + mode.CryptBlocks([]byte(cipherTextDecoded), []byte(cipherTextDecoded)) + return string(cipherTextDecoded) +} + +func PKCS5Padding(ciphertext []byte, blockSize int, after int) []byte { + padding := (blockSize - len(ciphertext)%blockSize) + padtext := bytes.Repeat([]byte{byte(padding)}, padding) + return append(ciphertext, padtext...) +} + +// func pkcs7Pad(b []byte, blocksize int) ([]byte, error) { +// if blocksize <= 0 { +// return nil, ErrInvalidBlockSize +// } +// if b == nil || len(b) == 0 { +// return nil, ErrInvalidPKCS7Data +// } +// n := blocksize - (len(b) % blocksize) +// pb := make([]byte, len(b)+n) +// copy(pb, b) +// copy(pb[len(b):], bytes.Repeat([]byte{byte(n)}, n)) +// return pb, nil +// } + +// // pkcs7Unpad validates and unpads data from the given bytes slice. +// // The returned value will be 1 to n bytes smaller depending on the +// // amount of padding, where n is the block size. +// func pkcs7Unpad(b []byte, blocksize int) ([]byte, error) { +// if blocksize <= 0 { +// return nil, ErrInvalidBlockSize +// } +// if b == nil || len(b) == 0 { +// return nil, ErrInvalidPKCS7Data +// } +// if len(b)%blocksize != 0 { +// return nil, ErrInvalidPKCS7Padding +// } +// c := b[len(b)-1] +// n := int(c) +// if n == 0 || n > len(b) { +// return nil, ErrInvalidPKCS7Padding +// } +// for i := 0; i < n; i++ { +// if b[len(b)-n+i] != c { +// return nil, ErrInvalidPKCS7Padding +// } +// } +// return b[:len(b)-n], nil +// } + +// // func Encrypt(b cipher.Block, plaintext []byte) []byte { +// // if mod := len(plaintext) % aes.BlockSize; mod != 0 { // 블록 크기의 배수가 되어야함 +// // padding := make([]byte, aes.BlockSize-mod) // 블록 크기에서 모자라는 부분을 +// // plaintext = append(plaintext, padding...) // 채워줌 +// // } + +// // ciphertext := make([]byte, aes.BlockSize+len(plaintext)) // 초기화 벡터 공간(aes.BlockSize)만큼 더 생성 +// // // iv := ciphertext[:aes.BlockSize] // 부분 슬라이스로 초기화 벡터 공간을 가져옴 +// // // if _, err := io.ReadFull(rand.Reader, iv); err != nil { // 랜덤 값을 초기화 벡터에 넣어줌 +// // // fmt.Println(err) +// // // return nil +// // // } +// // iv := []byte("0000000000000000") +// // mode := cipher.NewCBCEncrypter(b, iv) // 암호화 블록과 초기화 벡터를 넣어서 암호화 블록 모드 인스턴스 생성 +// // mode.CryptBlocks(ciphertext[aes.BlockSize:], plaintext) // 암호화 블록 모드 인스턴스로 +// // // 암호화 + +// // return ciphertext +// // } + +// // func Decrypt(b cipher.Block, ciphertext []byte) []byte { +// // if len(ciphertext)%aes.BlockSize != 0 { // 블록 크기의 배수가 아니면 리턴 +// // fmt.Println("암호화된 데이터의 길이는 블록 크기의 배수가 되어야합니다.") +// // return nil +// // } + +// // // iv := ciphertext[:aes.BlockSize] // 부분 슬라이스로 초기화 벡터 공간을 가져옴 +// // iv := []byte("0000000000000000") +// // ciphertext = ciphertext[aes.BlockSize:] // 부분 슬라이스로 암호화된 데이터를 가져옴 + +// // plaintext := make([]byte, len(ciphertext)) // 평문 데이터를 저장할 공간 생성 +// // mode := cipher.NewCBCDecrypter(b, iv) // 암호화 블록과 초기화 벡터를 넣어서 +// // // 복호화 블록 모드 인스턴스 생성 +// // mode.CryptBlocks(plaintext, ciphertext) // 복호화 블록 모드 인스턴스로 복호화 + +// // return plaintext +// // } diff --git a/internal/middleware/auth.go b/internal/middleware/auth.go index 480afa3..497a09c 100644 --- a/internal/middleware/auth.go +++ b/internal/middleware/auth.go @@ -6,7 +6,7 @@ import ( "net/http" "strings" - config "studioj/boilerplate_go/configs" + config "learnsteam/learsteam-quiz-api/configs" "github.com/gin-gonic/gin" "github.com/golang-jwt/jwt/v5" diff --git a/internal/models/auth.go b/internal/models/auth.go index 9be2fd1..23a055a 100644 --- a/internal/models/auth.go +++ b/internal/models/auth.go @@ -5,7 +5,27 @@ type LoginRequest struct { Password string `json:"password"` } +type LoginResponse struct { + User User `json:"user"` + Token string `json:"token" example:"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdXRob3JpemVkIjp0cnVlLCJleHAiOjE3MDI3MTMwMjcsInN1"` + RefreshToken string `json:"refresh_token" example:"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdXRob3JpemVkIjp0cnVlLCJleHAiOjE3MDc4OTcwMjcs"` +} + +// ID string `json:"id" example:"7f9329f5-2e36-4638-92d2-73064b7291a4"` +// Username string `json:"username" example:"user0"` +// Name string `json:"name" example:"홍길동"` +// Token string `json:"token" example:"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdXRob3JpemVkIjp0cnVlLCJleHAiOjE3MDI3MTMwMjcsInN1"` +// RefreshToken string `json:"refresh_token" example:"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdXRob3JpemVkIjp0cnVlLCJleHAiOjE3MDc4OTcwMjcs"` +// } + type RegisterRequest struct { + Name string `json:"name"` Username string `json:"username"` Password string `json:"password"` } + +type RegisterResponse struct { + User User `json:"user"` + Token string `json:"token" example:"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdXRob3JpemVkIjp0cnVlLCJleHAiOjE3MDI3MTMwMjcsInN1"` + RefreshToken string `json:"refresh_token" example:"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdXRob3JpemVkIjp0cnVlLCJleHAiOjE3MDc4OTcwMjcs"` +} diff --git a/internal/models/program.go b/internal/models/program.go new file mode 100644 index 0000000..c162a59 --- /dev/null +++ b/internal/models/program.go @@ -0,0 +1,47 @@ +package models + +import ( + "time" + + "gorm.io/datatypes" +) + +type Program struct { + ID string `json:"id" db:"id" example:"ef74c59a-c707-4162-a52b-455906c81ec1" gorm:"column:id;size:255;primary_key;"` + Course string `json:"course" db:"course" example:"코스 이름" gorm:"column:course;size:40;index;"` + Subject string `json:"subject" db:"subject" example:"프로그램 제목" gorm:"column:subject;size:255;index;"` + Content string `json:"content" db:"content" example:"코스 설명" gorm:"column:content;size:512;"` + Tag datatypes.JSON `json:"tag" db:"tag" gorm:"column:tag;"` + Tags string `json:"-" db:"tags" gorm:"column:tags;index;"` + Status string `json:"status" example:"on" gorm:"column:status;size:10;index;"` + PublishAt time.Time `json:"publish_at" example:"2023-11-10T00:00:00+09:00" gorm:"column:publish_at;index;"` + UpdatedAt time.Time `json:"-" gorm:"column:updated_at;index;"` + CreatedAt time.Time `json:"-" gorm:"column:created_at;index;"` +} + +type ProgramRequest struct { + Course string `json:"course" example:"코스 이름"` + Subject string `json:"subject" example:"프로그램 제목"` + Content string `json:"content" example:"코스 설명"` + Tag datatypes.JSON `json:"tag"` + Status string `json:"status" example:"on"` + PublishAt string `json:"publish_at" example:"2023-11-10T00:00:00+09:00"` +} + +type ProgramResponse struct { + ID string `json:"id" example:"ef74c59a-c707-4162-a52b-455906c81ec1"` + Course string `json:"course" example:"코스 이름"` + Subject string `json:"subject" example:"프로그램 제목"` + Content string `json:"content" example:"코스 설명"` + Tag []string `json:"tag" example:"tag1,tag2"` + Status string `json:"status" example:"on"` + PublishAt string `json:"publish_at" example:"2023-11-10T00:00:00+09:00"` +} + +type ProgramListResponse struct { + Data []Program `json:"data"` + Total int64 `json:"total" example:"999"` + Page int `json:"page" example:"1"` + TotalPage int64 `json:"totalPage" example:"99"` + PageSize int `json:"pageSize" example:"10"` +} diff --git a/internal/models/quiz.go b/internal/models/quiz.go new file mode 100644 index 0000000..f22fdff --- /dev/null +++ b/internal/models/quiz.go @@ -0,0 +1,52 @@ +package models + +import ( + "time" + + "gorm.io/datatypes" +) + +type Quiz struct { + ID string `json:"id" db:"id" example:"1b066168-68c4-4b50-bc9a-b6c4fceaf378" gorm:"column:id;size:255;primary_key;"` + ProgramID string `json:"program_id" db:"program_id" example:"2036023a-fb56-4b6c-b3bb-c787c681ada6" gorm:"column:program_id;size:255;index;"` + Sequence int `json:"sequence" db:"sequence" example:"5" gorm:"column:sequence;index;"` + QuizType string `json:"quiz_type" db:"quiz_type" example:"choice" gorm:"column:quiz_type;size:10;index;"` + Question string `json:"question" db:"question" example:"퀴즈 질문입니다." gorm:"column:question;size:512;"` + Choice datatypes.JSON `json:"choice" db:"choice" gorm:"column:choice;"` + Answer datatypes.JSON `json:"answer" db:"answer" gorm:"column:answer;"` + Hint string `json:"hint" db:"hint" example:"퀴즈 힌트" gorm:"column:answer;size:1024;"` + Comment string `json:"comment" db:"comment" example:"퀴즈 해설" gorm:"column:comment;size:1024;"` + UpdatedAt time.Time `json:"-" gorm:"column:updated_at;index;"` + CreatedAt time.Time `json:"-" gorm:"column:created_at;index;"` +} + +type QuizRequest struct { + ProgramID string `json:"program_id"` + Sequence int `json:"sequence"` + QuizType string `json:"quiz_type"` + Question string `json:"question"` + Choice datatypes.JSON `json:"choice"` + Answer datatypes.JSON `json:"answer"` + Hint string `json:"hint"` + Comment string `json:"comment"` +} + +type QuizResponse struct { + ID string `json:"id" example:"1b066168-68c4-4b50-bc9a-b6c4fceaf378"` + ProgramID string `json:"program_id" example:"2036023a-fb56-4b6c-b3bb-c787c681ada6"` + Sequence int `json:"sequence" example:"5" gorm:"column:sequence;index;"` + QuizType string `json:"quiz_type" example:"check"` + Question string `json:"question" example:"퀴즈 질문입니다."` + Choice datatypes.JSON `json:"choice"` + Answer datatypes.JSON `json:"answer"` + Hint string `json:"hint" example:"퀴즈 힌트"` + Comment string `json:"comment" example:"퀴즈 해설"` +} + +type QuizListResponse struct { + Data []Quiz `json:"data"` + Total int64 `json:"total" example:"5"` + Page int `json:"page" example:"1"` + TotalPage int64 `json:"totalPage" example:"1"` + PageSize int `json:"pageSize" example:"10"` +} diff --git a/internal/models/token.go b/internal/models/token.go index 7f6a487..7f3a0fe 100644 --- a/internal/models/token.go +++ b/internal/models/token.go @@ -2,15 +2,32 @@ package models import "time" +// type Token struct { +// ID string `json:"id" db:"id" gorm:"column:id;size:255;primary_key;"` +// UserID string `json:"user_id" db:"user_id" gorm:"column:user_id;size:255;index;"` +// Token string `json:"token" gorm:"column:token;size:255;index;"` +// RefreshToken string `json:"refresh_token" gorm:"column:token;size:255;index;"` +// Status string `json:"status" gorm:"column:status;size:10;index;"` +// ExpireAt time.Time `json:"expire_at" gorm:"column:expire_at;index;"` +// UpdatedAt time.Time `json:"updated_at" gorm:"column:updated_at;index;"` +// CreatedAt time.Time `json:"created_at" gorm:"column:created_at;index;"` +// } + type Token struct { - ID string `json:"id" db:"id" gorm:"column:id;size:255;primary_key;"` - UserID string `json:"user_id" db:"user_id" gorm:"column:user_id;size:255;index;"` - Token string `json:"token" gorm:"column:token;size:255;index;"` - RefreshToken string `json:"refresh_token" gorm:"column:token;size:255;index;"` - Status string `json:"status" gorm:"column:status;size:10;index;"` - ExpireAt time.Time `json:"expire_at" gorm:"column:expire_at;index;"` - UpdatedAt time.Time `json:"updated_at" gorm:"column:updated_at;index;"` - CreatedAt time.Time `json:"created_at" gorm:"column:created_at;index;"` + ID string `json:"-" db:"id" gorm:"primary_key"` + UserID string `json:"-" gorm:"index;"` + Token string `json:"token" gorm:"size:255;index;"` + RefreshToken string `json:"refreshToken" gorm:"size:255"` + + Expires int64 `json:"expires"` + RefreshExpires int64 `json:"refreshExpires"` + + UpdatedAt time.Time `json:"-" gorm:"type:DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP;index;->:false"` + CreatedAt time.Time `json:"-" gorm:"type:DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP;index;->:false;<-:create"` +} + +type RefreshTokenRequest struct { + RefreshToken string `json:"refresh_token" binding:"required"` } type TokenResponse struct { diff --git a/internal/models/user.go b/internal/models/user.go index b761187..2b386f3 100644 --- a/internal/models/user.go +++ b/internal/models/user.go @@ -1,9 +1,21 @@ package models +import "time" + type User struct { - ID string `json:"id" db:"id" gorm:"column:id;size:255;primary_key;"` - Username string `json:"username" db:"username" gorm:"column:username;size:50;uniqueIndex;"` - Score int32 `json:"score" db:"score" gorm:"column:score;"` - Money int32 `json:"money" db:"money" gorm:"column:money;"` - Password string `json:"-" db:"password" gorm:"column:password;size:255;not null;"` + ID string `json:"id" db:"id" example:"137c1683-2ad6-4201-b256-253828b61c49" gorm:"column:id;size:255;primary_key;"` + Username string `json:"username" db:"username" example:"user0" gorm:"column:username;size:50;uniqueIndex;"` + Name string `json:"name" db:"name" example:"홍길동" gorm:"column:name;size:50;"` + Score int32 `json:"score" db:"score" example:"9999" gorm:"column:score;"` + Password string `json:"-" db:"password" gorm:"column:password;size:255;not null;"` + UpdatedAt time.Time `json:"-" gorm:"column:updated_at;index;"` + CreatedAt time.Time `json:"-" gorm:"column:created_at;index;"` +} + +type UserListResponse struct { + Data []User `json:"data"` + Total int64 `json:"total" example:"90"` + Page int `json:"page" example:"1"` + TotalPage int64 `json:"totalPage" example:"9"` + PageSize int `json:"pageSize" example:"10"` } diff --git a/internal/repositories/program.go b/internal/repositories/program.go new file mode 100644 index 0000000..b17aea4 --- /dev/null +++ b/internal/repositories/program.go @@ -0,0 +1,90 @@ +package repositories + +import ( + "fmt" + "learnsteam/learsteam-quiz-api/internal/models" + + "gorm.io/gorm" +) + +type programRepository struct { + DB *gorm.DB +} + +func NewProgramRepository(db *gorm.DB) ProgramRepository { + return &programRepository{ + DB: db, + } +} + +type ProgramRepository interface { + List(string, string, int, int) (*[]models.Program, error) + Total(string, string) (int64, error) + + Find(string) (*models.Program, error) + Create(*models.Program) (*models.Program, error) + Update(*models.Program) (*models.Program, error) + Delete(string) error +} + +func (r *programRepository) List(q string, tag string, page int, limit int) (*[]models.Program, error) { + var programs *[]models.Program + var err error + offset := limit * (page - 1) + fmt.Println("q", q) + if q != "" { + fmt.Println(" 1 q", q) + err = r.DB.Offset(offset).Limit(limit).Order("created_at DESC").Where("subject LIKE ? OR course LIKE ? OR tags LIKE ?", "%"+q+"%", "%"+q+"%", "%"+q+"%").Find(&programs).Error + } else if tag != "" { + fmt.Println(" tag", tag) + err = r.DB.Offset(offset).Limit(limit).Order("created_at DESC").Where("tags LIKE ?", "%"+tag+"%").Find(&programs).Error + } else { + fmt.Println(" query") + err = r.DB.Offset(offset).Limit(limit).Order("created_at DESC").Find(&programs).Error + } + return programs, err +} + +func (r *programRepository) Total(q string, tag string) (int64, error) { + var total int64 + var err error + if q != "" { + err = r.DB.Model(&models.Program{}).Where("subject LIKE ? OR course LIKE ? OR tags LIKE ?", "%"+q+"%", "%"+q+"%", "%"+q+"%").Count(&total).Error + } else if tag != "" { + err = r.DB.Model(&models.Program{}).Where("tags LIKE ?", "%"+tag+"%").Count(&total).Error + } else { + err = r.DB.Model(&models.Program{}).Count(&total).Error + } + + return total, err +} + +func (r *programRepository) Find(id string) (*models.Program, error) { + var program *models.Program + err := r.DB.Where("id = ?", id).First(&program).Error + return program, err +} + +func (r *programRepository) Create(program *models.Program) (*models.Program, error) { + err := r.DB.Create(&program).Error + return program, err +} + +func (r *programRepository) Update(program *models.Program) (*models.Program, error) { + var row *models.Program + if err := r.DB.Where("id=?", program.ID).First(&row).Error; err != nil { + return nil, err + } + + err := r.DB.Model(&row).Select("*").Updates(&program).Error + return row, err +} + +func (r *programRepository) Delete(id string) error { + var program *models.Program + if err := r.DB.Where("id=?", id).First(&program).Error; err != nil { + return err + } + err := r.DB.Delete(&program).Error + return err +} diff --git a/internal/repositories/quiz.go b/internal/repositories/quiz.go new file mode 100644 index 0000000..1869ec1 --- /dev/null +++ b/internal/repositories/quiz.go @@ -0,0 +1,84 @@ +package repositories + +import ( + "fmt" + "learnsteam/learsteam-quiz-api/internal/models" + + "gorm.io/gorm" +) + +type quizRepository struct { + DB *gorm.DB +} + +func NewQuizRepository(db *gorm.DB) QuizRepository { + return &quizRepository{ + DB: db, + } +} + +type QuizRepository interface { + List(string, int, int) (*[]models.Quiz, error) + Total(string) (int64, error) + + Find(string) (*models.Quiz, error) + Create(*models.Quiz) (*models.Quiz, error) + Update(*models.Quiz) (*models.Quiz, error) + Delete(string) error +} + +func (r *quizRepository) List(program_id string, page int, limit int) (*[]models.Quiz, error) { + var quizzes *[]models.Quiz + var err error + offset := limit * (page - 1) + if program_id != "" { + fmt.Println("program_id", program_id) + err = r.DB.Offset(offset).Limit(limit).Order("sequence ASC").Where("program_id = ?", program_id).Find(&quizzes).Error + } else { + fmt.Println("program_id", "none") + err = r.DB.Offset(offset).Limit(limit).Find(&quizzes).Error + } + return quizzes, err +} + +func (r *quizRepository) Total(program_id string) (int64, error) { + var total int64 + var err error + if program_id != "" { + err = r.DB.Model(&models.Quiz{}).Where("program_id = ?", program_id).Count(&total).Error + } else { + err = r.DB.Model(&models.Quiz{}).Count(&total).Error + } + + return total, err +} + +func (r *quizRepository) Find(id string) (*models.Quiz, error) { + var quiz *models.Quiz + err := r.DB.Where("id = ?", id).First(&quiz).Error + return quiz, err +} + +func (r *quizRepository) Create(quiz *models.Quiz) (*models.Quiz, error) { + err := r.DB.Create(&quiz).Error + return quiz, err +} + +func (r *quizRepository) Update(quiz *models.Quiz) (*models.Quiz, error) { + var row *models.Quiz + if err := r.DB.Where("id=?", quiz.ID).First(&row).Error; err != nil { + return nil, err + } + + err := r.DB.Model(&row).Select("*").Updates(&quiz).Error + return row, err +} + +func (r *quizRepository) Delete(id string) error { + var quiz *models.Quiz + if err := r.DB.Where("id=?", id).First(&quiz).Error; err != nil { + return err + } + err := r.DB.Delete(&quiz).Error + return err +} diff --git a/internal/repositories/token.go b/internal/repositories/token.go index 4863085..4e759e9 100644 --- a/internal/repositories/token.go +++ b/internal/repositories/token.go @@ -1,8 +1,8 @@ package repositories import ( - config "studioj/boilerplate_go/configs" - "studioj/boilerplate_go/internal/models" + config "learnsteam/learsteam-quiz-api/configs" + "learnsteam/learsteam-quiz-api/internal/models" "github.com/golang-jwt/jwt/v5" "gorm.io/gorm" @@ -21,6 +21,7 @@ func NewTokenRepository(db *gorm.DB) TokenRepository { type TokenRepository interface { Generate(string, int64) (string, error) Find(string) (*models.Token, error) + FindByRefreshToken(string, string) (*models.Token, error) Create(*models.Token) (*models.Token, error) Update(*models.Token) (*models.Token, error) Delete(string) error @@ -43,6 +44,12 @@ func (r *tokenRepository) Find(id string) (*models.Token, error) { return token, err } +func (r *tokenRepository) FindByRefreshToken(user_id string, refreshToken string) (*models.Token, error) { + var token *models.Token + err := r.DB.Where("user_id = ? AND refresh_token = ?", user_id, refreshToken).First(&token).Error + return token, err +} + func (r *tokenRepository) Create(token *models.Token) (*models.Token, error) { err := r.DB.Create(&token).Error return token, err diff --git a/internal/repositories/user.go b/internal/repositories/user.go index 620a13e..2c5f300 100644 --- a/internal/repositories/user.go +++ b/internal/repositories/user.go @@ -1,7 +1,8 @@ package repositories import ( - "studioj/boilerplate_go/internal/models" + "fmt" + "learnsteam/learsteam-quiz-api/internal/models" "gorm.io/gorm" ) @@ -17,21 +18,42 @@ func NewUserRepository(db *gorm.DB) UserRepository { } type UserRepository interface { - List() (*[]models.User, error) - FindByID(string) (*models.User, error) + List(string, int, int) (*[]models.User, error) + Total(string) (int64, error) + Find(string) (*models.User, error) FindByUsername(string) (*models.User, error) Create(*models.User) (*models.User, error) Update(*models.User) (*models.User, error) Delete(string) error } -func (r *userRepository) List() (*[]models.User, error) { +func (r *userRepository) List(q string, page int, limit int) (*[]models.User, error) { var users *[]models.User - err := r.DB.Find(&users).Error + var err error + offset := limit * (page - 1) + fmt.Println("q", q) + if q != "" { + err = r.DB.Offset(offset).Limit(limit).Order("name ASC").Where("name LIKE ? OR username LIKE ?", "%"+q+"%", "%"+q+"%").Find(&users).Error + } else { + fmt.Println(" query") + err = r.DB.Offset(offset).Limit(limit).Order("created_at DESC").Find(&users).Error + } return users, err } -func (r *userRepository) FindByID(id string) (*models.User, error) { +func (r *userRepository) Total(q string) (int64, error) { + var total int64 + var err error + if q != "" { + err = r.DB.Model(&models.User{}).Where("name LIKE ? OR username LIKE ?", "%"+q+"%", "%"+q+"%").Count(&total).Error + } else { + err = r.DB.Model(&models.User{}).Count(&total).Error + } + + return total, err +} + +func (r *userRepository) Find(id string) (*models.User, error) { var user *models.User err := r.DB.Where("id = ?", id).First(&user).Error return user, err diff --git a/internal/routers/auth.go b/internal/routers/auth.go index 782563f..5218722 100644 --- a/internal/routers/auth.go +++ b/internal/routers/auth.go @@ -1,14 +1,19 @@ package routers import ( - "studioj/boilerplate_go/internal/controllers" - "studioj/boilerplate_go/internal/repositories" - "studioj/boilerplate_go/internal/services" + "learnsteam/learsteam-quiz-api/internal/controllers" + "learnsteam/learsteam-quiz-api/internal/repositories" + "learnsteam/learsteam-quiz-api/internal/services" "github.com/gin-gonic/gin" "gorm.io/gorm" ) +func InitAuthRouter(db *gorm.DB, router *gin.Engine) { + r := NewAuthRouter(db, router) + r.SetAuthRouter(db, router) +} + type AuthRouter interface { SetRouter(db *gorm.DB, router *gin.Engine) } @@ -23,17 +28,12 @@ type authRouter struct { router *gin.Engine } -func InitAuthRouter(db *gorm.DB, router *gin.Engine) { - r := NewAuthRouter(db, router) - r.SetAuthRouter(db, router) -} - func NewAuthRouter(db *gorm.DB, router *gin.Engine) *authRouter { userRepository := repositories.NewUserRepository(db) tokenRepository := repositories.NewTokenRepository(db) service := services.NewAuthService(userRepository, tokenRepository) tokenService := services.NewTokenService(tokenRepository) - controller := controllers.NewAuthController(service) + controller := controllers.NewAuthController(service, tokenService) return &authRouter{ db: db, diff --git a/internal/routers/program.go b/internal/routers/program.go new file mode 100644 index 0000000..45edae2 --- /dev/null +++ b/internal/routers/program.go @@ -0,0 +1,50 @@ +package routers + +import ( + "learnsteam/learsteam-quiz-api/internal/controllers" + "learnsteam/learsteam-quiz-api/internal/middleware" + "learnsteam/learsteam-quiz-api/internal/repositories" + "learnsteam/learsteam-quiz-api/internal/services" + + "github.com/gin-gonic/gin" + "gorm.io/gorm" +) + +func InitProgramRouter(db *gorm.DB, router *gin.Engine) { + r := NewProgramRouter(db, router) + r.SetProgramRouter(db, router) +} + +type ProgramRouter interface { + SetRouter(db *gorm.DB, router *gin.Engine) +} + +type programRouter struct { + db *gorm.DB + repository repositories.ProgramRepository + service services.ProgramService + controller controllers.ProgramController + router *gin.Engine +} + +func NewProgramRouter(db *gorm.DB, router *gin.Engine) *programRouter { + repository := repositories.NewProgramRepository(db) + service := services.NewProgramService(repository) + controller := controllers.NewProgramController(service) + + return &programRouter{ + db: db, + repository: repository, + service: service, + controller: controller, + router: router, + } +} + +func (r *programRouter) SetProgramRouter(db *gorm.DB, router *gin.Engine) { + group := router.Group("/program") + group.GET("", middleware.Auth(), r.controller.List) + group.GET("/:id", middleware.Auth(), r.controller.Find) + group.POST("", middleware.Auth(), r.controller.Create) + group.PUT("/:id", middleware.Auth(), r.controller.Update) +} diff --git a/internal/routers/quiz.go b/internal/routers/quiz.go new file mode 100644 index 0000000..80faf1c --- /dev/null +++ b/internal/routers/quiz.go @@ -0,0 +1,50 @@ +package routers + +import ( + "learnsteam/learsteam-quiz-api/internal/controllers" + "learnsteam/learsteam-quiz-api/internal/middleware" + "learnsteam/learsteam-quiz-api/internal/repositories" + "learnsteam/learsteam-quiz-api/internal/services" + + "github.com/gin-gonic/gin" + "gorm.io/gorm" +) + +func InitQuizRouter(db *gorm.DB, router *gin.Engine) { + r := NewQuizRouter(db, router) + r.SetQuizRouter(db, router) +} + +type QuizRouter interface { + SetRouter(db *gorm.DB, router *gin.Engine) +} + +type quizRouter struct { + db *gorm.DB + repository repositories.QuizRepository + service services.QuizService + controller controllers.QuizController + router *gin.Engine +} + +func NewQuizRouter(db *gorm.DB, router *gin.Engine) *quizRouter { + repository := repositories.NewQuizRepository(db) + service := services.NewQuizService(repository) + controller := controllers.NewQuizController(service) + + return &quizRouter{ + db: db, + repository: repository, + service: service, + controller: controller, + router: router, + } +} + +func (r *quizRouter) SetQuizRouter(db *gorm.DB, router *gin.Engine) { + group := router.Group("/quiz") + group.GET("", middleware.Auth(), r.controller.List) + group.GET("/:id", middleware.Auth(), r.controller.Find) + group.POST("", middleware.Auth(), r.controller.Create) + group.PUT("/:id", middleware.Auth(), r.controller.Update) +} diff --git a/internal/routers/router.go b/internal/routers/router.go index 7bb9faa..98dff02 100644 --- a/internal/routers/router.go +++ b/internal/routers/router.go @@ -1,9 +1,10 @@ package routers import ( - "github.com/gin-gonic/gin" + "learnsteam/learsteam-quiz-api/internal/database" - "studioj/boilerplate_go/internal/database" + "github.com/gin-contrib/cors" + "github.com/gin-gonic/gin" ) var Router *gin.Engine @@ -11,9 +12,19 @@ var Router *gin.Engine func Init() { gin.SetMode(gin.ReleaseMode) Router = gin.Default() + config := cors.DefaultConfig() + config.AllowOrigins = []string{"http://127.0.0.1:3000", "http://localhost:3000", "http://localhost:3030", "https://learnsteam-quiz.jongyeob.com"} + config.AllowMethods = []string{"GET", "POST", "PUT", "DELETE", "PATCH"} + config.AllowHeaders = []string{"Origin", "Content-Length", "Content-Type"} + Router.Use(cors.New(config)) + maindb := database.GetDB() InitAuthRouter(maindb, Router) InitTokenRouter(maindb, Router) InitUserRouter(maindb, Router) + InitProgramRouter(maindb, Router) + InitQuizRouter(maindb, Router) + + InitSwaggerRouter(Router) } diff --git a/internal/routers/swagger.go b/internal/routers/swagger.go new file mode 100644 index 0000000..fea8a43 --- /dev/null +++ b/internal/routers/swagger.go @@ -0,0 +1,37 @@ +package routers + +import ( + "learnsteam/learsteam-quiz-api/internal/controllers" + + "github.com/gin-gonic/gin" + + swaggerFiles "github.com/swaggo/files" + ginSwagger "github.com/swaggo/gin-swagger" +) + +func InitSwaggerRouter(router *gin.Engine) { + r := NewSwaggerRouter(router) + r.SetSwaggerRouter(router) +} + +type SwaggerRouter interface { + SetRouter(router *gin.Engine) +} + +type swaggerRouter struct { + router *gin.Engine + controller controllers.SwaggerController +} + +func NewSwaggerRouter(router *gin.Engine) *swaggerRouter { + controller := controllers.NewSwaggerController() + return &swaggerRouter{ + router: router, + controller: controller, + } +} + +func (r *swaggerRouter) SetSwaggerRouter(router *gin.Engine) { + group := router.Group("/swagger") + group.GET("*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) +} diff --git a/internal/routers/token.go b/internal/routers/token.go index 2cc42db..a0f8e87 100644 --- a/internal/routers/token.go +++ b/internal/routers/token.go @@ -1,14 +1,19 @@ package routers import ( - "studioj/boilerplate_go/internal/controllers" - "studioj/boilerplate_go/internal/repositories" - "studioj/boilerplate_go/internal/services" + "learnsteam/learsteam-quiz-api/internal/controllers" + "learnsteam/learsteam-quiz-api/internal/repositories" + "learnsteam/learsteam-quiz-api/internal/services" "github.com/gin-gonic/gin" "gorm.io/gorm" ) +func InitTokenRouter(db *gorm.DB, router *gin.Engine) { + r := NewTokenRouter(db, router) + r.SetTokenRouter(db, router) +} + type TokenRouter interface { SetRouter(db *gorm.DB, router *gin.Engine) } @@ -21,11 +26,6 @@ type tokenRouter struct { router *gin.Engine } -func InitTokenRouter(db *gorm.DB, router *gin.Engine) { - r := NewTokenRouter(db, router) - r.SetTokenRouter(db, router) -} - func NewTokenRouter(db *gorm.DB, router *gin.Engine) *tokenRouter { repository := repositories.NewTokenRepository(db) service := services.NewTokenService(repository) @@ -41,6 +41,6 @@ func NewTokenRouter(db *gorm.DB, router *gin.Engine) *tokenRouter { } func (r *tokenRouter) SetTokenRouter(db *gorm.DB, router *gin.Engine) { - // group := router.Group("/token") - // group.GET("refresh", middleware.Auth(), r.controller.Refresh) + group := router.Group("/token") + group.POST("refresh", r.controller.Refresh) } diff --git a/internal/routers/user.go b/internal/routers/user.go index 4aeea5c..d137914 100644 --- a/internal/routers/user.go +++ b/internal/routers/user.go @@ -1,15 +1,20 @@ package routers import ( - "studioj/boilerplate_go/internal/controllers" - "studioj/boilerplate_go/internal/middleware" - "studioj/boilerplate_go/internal/repositories" - "studioj/boilerplate_go/internal/services" + "learnsteam/learsteam-quiz-api/internal/controllers" + "learnsteam/learsteam-quiz-api/internal/middleware" + "learnsteam/learsteam-quiz-api/internal/repositories" + "learnsteam/learsteam-quiz-api/internal/services" "github.com/gin-gonic/gin" "gorm.io/gorm" ) +func InitUserRouter(db *gorm.DB, router *gin.Engine) { + r := NewUserRouter(db, router) + r.SetUserRouter(db, router) +} + type UserRouter interface { SetRouter(db *gorm.DB, router *gin.Engine) } @@ -22,11 +27,6 @@ type userRouter struct { router *gin.Engine } -func InitUserRouter(db *gorm.DB, router *gin.Engine) { - r := NewUserRouter(db, router) - r.SetUserRouter(db, router) -} - func NewUserRouter(db *gorm.DB, router *gin.Engine) *userRouter { repository := repositories.NewUserRepository(db) tokenRepository := repositories.NewTokenRepository(db) @@ -46,5 +46,6 @@ func NewUserRouter(db *gorm.DB, router *gin.Engine) *userRouter { func (r *userRouter) SetUserRouter(db *gorm.DB, router *gin.Engine) { group := router.Group("/user") + group.GET("", middleware.Auth(), r.controller.List) group.GET("/:id", middleware.Auth(), r.controller.Find) } diff --git a/internal/services/auth.go b/internal/services/auth.go index 5643b6a..e5ebd4f 100644 --- a/internal/services/auth.go +++ b/internal/services/auth.go @@ -2,9 +2,8 @@ package services import ( "errors" - "studioj/boilerplate_go/internal/models" - "studioj/boilerplate_go/internal/repositories" - "time" + "learnsteam/learsteam-quiz-api/internal/models" + "learnsteam/learsteam-quiz-api/internal/repositories" "github.com/google/uuid" "golang.org/x/crypto/bcrypt" @@ -18,7 +17,6 @@ type authService struct { type AuthService interface { Register(*models.RegisterRequest) (*models.User, error) Login(*models.LoginRequest) (*models.User, error) - CreateToken(string) (*models.Token, error) } func NewAuthService(userRepository repositories.UserRepository, tokenRepository repositories.TokenRepository) AuthService { @@ -38,6 +36,8 @@ func (s *authService) Register(request *models.RegisterRequest) (*models.User, e newUser := models.User{ ID: uuid.NewString(), Username: request.Username, + Name: request.Name, + Score: 0, Password: string(hash), } @@ -61,30 +61,3 @@ func (s *authService) Login(request *models.LoginRequest) (*models.User, error) } return user, nil } - -func (s *authService) CreateToken(user_id string) (*models.Token, error) { - tokenExpiredAt := time.Now().Add(time.Hour * 24 * 30) - accessToken, err := s.tokenRepository.Generate(user_id, tokenExpiredAt.Unix()) - if err != nil { - return nil, err - } - - refreshExpiredAt := time.Now().Add(time.Hour * 24 * 90) - refreshToken, err := s.tokenRepository.Generate(user_id, refreshExpiredAt.Unix()) - if err != nil { - return nil, err - } - - newToken := &models.Token{ - ID: uuid.NewString(), - UserID: user_id, - Token: accessToken, - RefreshToken: refreshToken, - Status: "valid", - ExpireAt: tokenExpiredAt, - } - - token, err := s.tokenRepository.Create(newToken) - - return token, err -} diff --git a/internal/services/program.go b/internal/services/program.go new file mode 100644 index 0000000..527ac7d --- /dev/null +++ b/internal/services/program.go @@ -0,0 +1,56 @@ +package services + +import ( + "learnsteam/learsteam-quiz-api/internal/models" + "learnsteam/learsteam-quiz-api/internal/repositories" +) + +type programService struct { + repository repositories.ProgramRepository +} + +type ProgramService interface { + List(string, string, int, int) (*[]models.Program, error) + Total(string, string) (int64, error) + + Find(string) (*models.Program, error) + Create(*models.Program) (*models.Program, error) + Update(*models.Program) (*models.Program, error) + Delete(string) error +} + +func NewProgramService(repository repositories.ProgramRepository) ProgramService { + return &programService{ + repository: repository, + } +} + +func (s *programService) List(q string, tag string, page int, limit int) (*[]models.Program, error) { + return s.repository.List(q, tag, page, limit) +} + +func (s *programService) Total(q string, tag string) (int64, error) { + return s.repository.Total(q, tag) +} + +func (s *programService) Find(id string) (*models.Program, error) { + return s.repository.Find(id) +} + +func (s *programService) Create(program *models.Program) (*models.Program, error) { + result, err := s.repository.Create(program) + + return result, err +} + +func (s *programService) Update(program *models.Program) (*models.Program, error) { + result, err := s.repository.Update(program) + + return result, err +} + +func (s *programService) Delete(id string) error { + err := s.repository.Delete(id) + + return err +} diff --git a/internal/services/quiz.go b/internal/services/quiz.go new file mode 100644 index 0000000..bafadf0 --- /dev/null +++ b/internal/services/quiz.go @@ -0,0 +1,56 @@ +package services + +import ( + "learnsteam/learsteam-quiz-api/internal/models" + "learnsteam/learsteam-quiz-api/internal/repositories" +) + +type quizService struct { + repository repositories.QuizRepository +} + +type QuizService interface { + List(string, int, int) (*[]models.Quiz, error) + Total(string) (int64, error) + + Find(string) (*models.Quiz, error) + Create(*models.Quiz) (*models.Quiz, error) + Update(*models.Quiz) (*models.Quiz, error) + Delete(string) error +} + +func NewQuizService(repository repositories.QuizRepository) QuizService { + return &quizService{ + repository: repository, + } +} + +func (s *quizService) List(program_id string, page int, limit int) (*[]models.Quiz, error) { + return s.repository.List(program_id, page, limit) +} + +func (s *quizService) Total(tag string) (int64, error) { + return s.repository.Total(tag) +} + +func (s *quizService) Find(id string) (*models.Quiz, error) { + return s.repository.Find(id) +} + +func (s *quizService) Create(quiz *models.Quiz) (*models.Quiz, error) { + result, err := s.repository.Create(quiz) + + return result, err +} + +func (s *quizService) Update(quiz *models.Quiz) (*models.Quiz, error) { + result, err := s.repository.Update(quiz) + + return result, err +} + +func (s *quizService) Delete(id string) error { + err := s.repository.Delete(id) + + return err +} diff --git a/internal/services/token.go b/internal/services/token.go index 9c812c7..3ce3eb6 100644 --- a/internal/services/token.go +++ b/internal/services/token.go @@ -1,14 +1,17 @@ package services import ( + "errors" "fmt" "strings" + "time" - config "studioj/boilerplate_go/configs" - "studioj/boilerplate_go/internal/models" - "studioj/boilerplate_go/internal/repositories" + config "learnsteam/learsteam-quiz-api/configs" + "learnsteam/learsteam-quiz-api/internal/models" + "learnsteam/learsteam-quiz-api/internal/repositories" "github.com/golang-jwt/jwt/v5" + "github.com/google/uuid" ) type tokenService struct { @@ -17,10 +20,12 @@ type tokenService struct { type TokenService interface { Find(string) (*models.Token, error) - Create(*models.Token) (*models.Token, error) + Create(string) (*models.Token, error) Update(*models.Token) (*models.Token, error) Delete(string) error + Refresh(string) (*models.Token, error) + Generate(string, int64) (string, error) Verify(tokenString string) (*jwt.Token, error) @@ -40,8 +45,31 @@ func (s *tokenService) Find(id string) (*models.Token, error) { return s.repository.Find(id) } -func (s *tokenService) Create(token *models.Token) (*models.Token, error) { - return s.repository.Create(token) +func (s *tokenService) Create(sub string) (*models.Token, error) { + expiredAt := time.Now().Add(time.Hour * 24 * 30) + accessToken, err := s.repository.Generate(sub, expiredAt.Unix()) + if err != nil { + return nil, err + } + + refreshExpiredAt := time.Now().Add(time.Hour * 24 * 90) + refreshToken, err := s.repository.Generate(sub, refreshExpiredAt.Unix()) + if err != nil { + return nil, err + } + + newToken := &models.Token{ + ID: uuid.NewString(), + UserID: sub, + Token: accessToken, + RefreshToken: refreshToken, + Expires: expiredAt.Unix(), + RefreshExpires: refreshExpiredAt.Unix(), + } + + token, err := s.repository.Create(newToken) + + return token, err } func (s *tokenService) Update(token *models.Token) (*models.Token, error) { @@ -113,3 +141,60 @@ func (s *tokenService) ValidToken(jwtToken *jwt.Token) (bool, error) { } return jwtToken.Valid, nil } + +func (s *tokenService) Refresh(refreshToken string) (*models.Token, error) { + fmt.Println("refresh_token", refreshToken) + + jwtToken, err := jwt.Parse(refreshToken, func(jwtToken *jwt.Token) (interface{}, error) { + if _, ok := jwtToken.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("unexpected signing method: %v", jwtToken.Header["alg"]) + } + return []byte(config.SECRET_KEY), nil + }) + + if err != nil { + fmt.Println("refresh token is not valid") + } else { + + fmt.Println("refresh token is valid") + } + + if err != nil { + fmt.Println("error", err.Error()) + return nil, err + } + + claims, ok := jwtToken.Claims.(jwt.MapClaims) + if !ok || !jwtToken.Valid { + return nil, errors.New("refresh token is invalid") + } + + sub := fmt.Sprintf("%s", claims["sub"]) + if err != nil { + return nil, errors.New("wrong user") + } + + token, err := s.repository.FindByRefreshToken(sub, refreshToken) + if err != nil { + return nil, errors.New("wrong token") + } + + expiredAt := time.Now().Add(time.Hour * 24 * 30) + accessToken, err := s.repository.Generate(sub, expiredAt.Unix()) + if err != nil { + return nil, err + } + + refreshExpiredAt := time.Now().Add(time.Hour * 24 * 90) + refreshToken, err = s.repository.Generate(sub, refreshExpiredAt.Unix()) + if err != nil { + return nil, err + } + + token.Token = accessToken + token.Expires = expiredAt.Unix() + token.RefreshToken = refreshToken + token.RefreshExpires = refreshExpiredAt.Unix() + + return s.repository.Update(token) +} diff --git a/internal/services/user.go b/internal/services/user.go index 443b37c..c59189f 100644 --- a/internal/services/user.go +++ b/internal/services/user.go @@ -1,8 +1,8 @@ package services import ( - "studioj/boilerplate_go/internal/models" - "studioj/boilerplate_go/internal/repositories" + "learnsteam/learsteam-quiz-api/internal/models" + "learnsteam/learsteam-quiz-api/internal/repositories" ) type userService struct { @@ -11,7 +11,9 @@ type userService struct { } type UserService interface { - FindByID(string) (*models.User, error) + List(string, int, int) (*[]models.User, error) + Total(string) (int64, error) + Find(string) (*models.User, error) FindByUsername(string) (*models.User, error) Create(*models.User) (*models.User, error) } @@ -23,8 +25,16 @@ func NewUserService(repository repositories.UserRepository, tokenRepository repo } } -func (s *userService) FindByID(id string) (*models.User, error) { - return s.repository.FindByID(id) +func (s *userService) List(q string, page int, limit int) (*[]models.User, error) { + return s.repository.List(q, page, limit) +} + +func (s *userService) Total(q string) (int64, error) { + return s.repository.Total(q) +} + +func (s *userService) Find(id string) (*models.User, error) { + return s.repository.Find(id) } func (s *userService) FindByUsername(username string) (*models.User, error) { diff --git a/test/program_test.go b/test/program_test.go new file mode 100644 index 0000000..ccd33cf --- /dev/null +++ b/test/program_test.go @@ -0,0 +1,190 @@ +package learsteam_quiz_test + +import ( + "encoding/json" + "learnsteam/learsteam-quiz-api/internal/controllers" + "learnsteam/learsteam-quiz-api/internal/models" + "learnsteam/learsteam-quiz-api/internal/repositories" + "learnsteam/learsteam-quiz-api/internal/services" + "os" + "strconv" + "testing" + "time" + + "github.com/google/uuid" + "github.com/stretchr/testify/suite" + "github.com/tj/assert" + "gorm.io/datatypes" + "gorm.io/driver/sqlite" + "gorm.io/gorm" + "gorm.io/gorm/schema" +) + +type ProgramTestSuite struct { + suite.Suite + db *gorm.DB + repository repositories.ProgramRepository + service services.ProgramService + controller controllers.ProgramController +} + +func (suite *ProgramTestSuite) SetupSuite() { + // err := os.Remove("test.db") + // if err != nil { + // suite.Fail("Failed to remove the test database file") + // } + + gorm_config := gorm.Config{NamingStrategy: schema.NamingStrategy{SingularTable: true}} + db, _ := gorm.Open(sqlite.Open("test.db"), &gorm_config) + db.AutoMigrate(&models.Program{}) + + repository := repositories.NewProgramRepository(db) + service := services.NewProgramService(repository) + controller := controllers.NewProgramController(service) + + suite.db = db + suite.service = service + suite.repository = repository + suite.controller = controller + + suite.CreateSampleData() +} + +func (suite *ProgramTestSuite) CreateSampleData() { + var programs []models.Program + for i := 1; i < 101; i++ { + t := []string{"tag" + strconv.Itoa(i), "taga", "tagb"} + var j datatypes.JSON + j, _ = json.Marshal(t) + p := models.Program{ + ID: strconv.Itoa(i) + "_id", + Subject: strconv.Itoa(i) + " subject", + Content: strconv.Itoa(i) + " content", + Tag: j, + Tags: j.String(), + Status: "on", + PublishAt: time.Now(), + UpdatedAt: time.Now(), + CreatedAt: time.Now(), + } + programs = append(programs, p) + } + + for _, program := range programs { + suite.db.Create(&program) + } +} + +func (suite *ProgramTestSuite) TearDownSuite() { + // DB 삭제 + suite.db.Migrator().DropTable(&models.Program{}) + err := os.Remove("test.db") + if err != nil { + suite.Fail("Failed to remove the test database file") + } +} + +func (suite *ProgramTestSuite) SetupTest() { + +} + +func (suite *ProgramTestSuite) TearDownTest() { + +} + +func TestProgramSuite(t *testing.T) { + suite.Run(t, new(ProgramTestSuite)) +} + +// 목록 테스트 : 1page, 10개 +func (suite *ProgramTestSuite) TestListProgramSuccess() { + programs, err := suite.repository.List("", "", 1, 10) + assert.NoError(suite.T(), err) + assert.NotNil(suite.T(), programs) + assert.Equal(suite.T(), 10, len(*programs)) + program0 := (*programs)[0] + assert.Equal(suite.T(), "100_id", program0.ID) + + program9 := (*programs)[9] + assert.Equal(suite.T(), "91_id", program9.ID) +} + +// 목록 테스트 : tag 검색 +func (suite *ProgramTestSuite) TestListProgramTagSearchSuccess() { + programs, err := suite.repository.List("", "tag9", 1, 10) + assert.NoError(suite.T(), err) + assert.NotNil(suite.T(), programs) + assert.Equal(suite.T(), 10, len(*programs)) +} + +// Total 테스트 +func (suite *ProgramTestSuite) TestTotalSuccess() { + total, err := suite.repository.Total("", "") + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), int64(100), total) +} + +// Total 테스트 : tag 검색 +func (suite *ProgramTestSuite) TestTotalTagSuccess() { + total, err := suite.repository.Total("", "tag2") + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), int64(11), total) +} + +// 수정 테스트 +func (suite *ProgramTestSuite) TestUpdateProgramSuccess() { + t := []string{"TAG"} + var j datatypes.JSON + j, _ = json.Marshal(t) + p := &models.Program{ + ID: uuid.NewString(), + Subject: "My Subject", + Content: "My Content", + Tag: j, + Tags: j.String(), + Status: "on", + PublishAt: time.Now(), + UpdatedAt: time.Now(), + CreatedAt: time.Now(), + } + + program, err := suite.repository.Create(p) + assert.NoError(suite.T(), err) + + program.Subject = "My Subject Updated" + program.Content = "My Content Updated" + program.Tag = j + program.Tags = j.String() + program.Status = "draft" + + updatedProgram, err := suite.repository.Update(program) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), program.Subject, updatedProgram.Subject) + assert.Equal(suite.T(), program.Content, updatedProgram.Content) + assert.Equal(suite.T(), program.Tag, updatedProgram.Tag) + assert.Equal(suite.T(), program.Status, updatedProgram.Status) +} + +// 삭제 테스트 +func (suite *ProgramTestSuite) TestDeleteProgramSuccess() { + t := []string{"TAG"} + var j datatypes.JSON + j, _ = json.Marshal(t) + p := &models.Program{ + ID: uuid.NewString(), + Subject: "My Subject", + Content: "My Content", + Tag: j, + Tags: j.String(), + Status: "on", + PublishAt: time.Now(), + UpdatedAt: time.Now(), + CreatedAt: time.Now(), + } + _, err := suite.repository.Create(p) + assert.NoError(suite.T(), err) + err = suite.repository.Delete(p.ID) + assert.NoError(suite.T(), err) + _, err = suite.repository.Find(p.ID) + assert.Error(suite.T(), err) +} diff --git a/test/quiz_test.go b/test/quiz_test.go new file mode 100644 index 0000000..fcd06f4 --- /dev/null +++ b/test/quiz_test.go @@ -0,0 +1,183 @@ +package learsteam_quiz_test + +import ( + "encoding/json" + "learnsteam/learsteam-quiz-api/internal/controllers" + "learnsteam/learsteam-quiz-api/internal/models" + "learnsteam/learsteam-quiz-api/internal/repositories" + "learnsteam/learsteam-quiz-api/internal/services" + "os" + "strconv" + "testing" + "time" + + "github.com/google/uuid" + "github.com/stretchr/testify/suite" + "github.com/tj/assert" + "gorm.io/datatypes" + "gorm.io/driver/sqlite" + "gorm.io/gorm" + "gorm.io/gorm/schema" +) + +type QuizTestSuite struct { + suite.Suite + db *gorm.DB + repository repositories.QuizRepository + service services.QuizService + controller controllers.QuizController +} + +func (suite *QuizTestSuite) SetupSuite() { + // err := os.Remove("test.db") + // if err != nil { + // suite.Fail("Failed to remove the test database file") + // } + + gorm_config := gorm.Config{NamingStrategy: schema.NamingStrategy{SingularTable: true}} + db, _ := gorm.Open(sqlite.Open("test.db"), &gorm_config) + db.AutoMigrate(&models.Quiz{}) + + repository := repositories.NewQuizRepository(db) + service := services.NewQuizService(repository) + controller := controllers.NewQuizController(service) + + suite.db = db + suite.service = service + suite.repository = repository + suite.controller = controller + + suite.CreateSampleData() +} + +func (suite *QuizTestSuite) CreateSampleData() { + var quizzes []models.Quiz + program_id := "1_id" + + c := []string{"1", "2", "3", "4"} + var j datatypes.JSON + j, _ = json.Marshal(c) + + for i := 1; i < 6; i++ { + q := models.Quiz{ + ID: strconv.Itoa(i), + ProgramID: program_id, + Sequence: i, + QuizType: "multiple", + Question: strconv.Itoa(i) + " question", + Choice: j, + Answer: j, + Hint: "힌트입니다.", + Comment: "설명입니다.", + UpdatedAt: time.Now(), + CreatedAt: time.Now(), + } + quizzes = append(quizzes, q) + } + for _, quiz := range quizzes { + suite.db.Create(&quiz) + } +} + +func (suite *QuizTestSuite) TearDownSuite() { + // DB 삭제 + suite.db.Migrator().DropTable(&models.Quiz{}) + err := os.Remove("test.db") + if err != nil { + suite.Fail("Failed to remove the test database file") + } +} + +func (suite *QuizTestSuite) SetupTest() { + suite.CreateSampleData() +} + +func (suite *QuizTestSuite) TearDownTest() { + +} + +func TestQuizSuite(t *testing.T) { + suite.Run(t, new(QuizTestSuite)) +} + +// 목록 테스트 +func (suite *QuizTestSuite) TestListQuizSuccess() { + quizzes, err := suite.repository.List("id_1", 1, 10) + assert.NoError(suite.T(), err) + assert.NotNil(suite.T(), quizzes) + assert.Equal(suite.T(), 5, len(*quizzes)) +} + +// Find 테스트 +func (suite *QuizTestSuite) TestFindQuizSuccess() { + quiz, err := suite.repository.Find("1") + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), "1", quiz.ID) +} + +// Find 테스트 +func (suite *QuizTestSuite) TestFindQuizFail() { + _, err := suite.repository.Find("x") + assert.Error(suite.T(), err) +} + +// 수정 테스트 +func (suite *QuizTestSuite) TestUpdateQuizFail() { + c := []string{"1", "2", "3", "4"} + var j datatypes.JSON + j, _ = json.Marshal(c) + q := &models.Quiz{ + ID: uuid.NewString(), + ProgramID: uuid.NewString(), + Sequence: 1, + QuizType: "multiple", + Question: "question", + Choice: j, + Answer: j, + Hint: "힌트입니다.", + Comment: "설명입니다.", + UpdatedAt: time.Now(), + CreatedAt: time.Now(), + } + + quiz, err := suite.repository.Create(q) + assert.NoError(suite.T(), err) + + quiz.QuizType = "check" + quiz.Choice = j + quiz.Answer = j + quiz.Hint = "힌트입니다!" + + updatedQuiz, err := suite.repository.Update(quiz) + assert.NoError(suite.T(), err) + + assert.Equal(suite.T(), updatedQuiz.QuizType, quiz.QuizType) + assert.Equal(suite.T(), updatedQuiz.Choice, quiz.Choice) + assert.Equal(suite.T(), updatedQuiz.Answer, quiz.Answer) +} + +// 삭제 테스트 +func (suite *QuizTestSuite) TestDeleteQuizSuccess() { + c := []string{"1", "2", "3", "4"} + var j datatypes.JSON + j, _ = json.Marshal(c) + q := &models.Quiz{ + ID: uuid.NewString(), + ProgramID: uuid.NewString(), + Sequence: 1, + QuizType: "multiple", + Question: "question", + Choice: j, + Answer: j, + Hint: "힌트입니다.", + Comment: "설명입니다.", + UpdatedAt: time.Now(), + CreatedAt: time.Now(), + } + _, err := suite.repository.Create(q) + assert.NoError(suite.T(), err) + err = suite.repository.Delete(q.ID) + assert.NoError(suite.T(), err) + _, err = suite.repository.Find(q.ID) + assert.Error(suite.T(), err) +} diff --git a/tests/token_test.go b/test/token_test.go similarity index 99% rename from tests/token_test.go rename to test/token_test.go index 149573a..4857201 100644 --- a/tests/token_test.go +++ b/test/token_test.go @@ -1,4 +1,4 @@ -package octet_test +package learsteam_quiz_test // type TokenTestSuite struct { // suite.Suite diff --git a/tests/test.db b/tests/test.db deleted file mode 100644 index 613ed74ffda90213935a594732ef2e4e1f64a4b9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 569344 zcmeEv2e=j0)%MJs-c?k@LJi)PK6J7dP8+MM~_{-W22Nu!4CII91S!-kI=)qiH=_w;LB zO3CEK|76za^0(#LnGNLaGaD`USM<0{|Iokgp%#5YY9XqfIqqjHtRaJ${RhpQRh`*? zQEmT4LuM{qJf}QwcJP0-xz*Z&8Kr7jG$KiXIAFTMO#!B ztxy%8+pNp5<*L|!rg+}+MQ=Z8%=lrGru82?YFhK92M;cHqK8<$M^6V&!Tya|DVz2ku$U||FdSfbGvZaw+e4a zGapjXMjN$w%gesY-=l#iBzhOoz`3UI8~@OM*Jl50JRrUN0~&wlOI&c4FI@ENAv0_H z&!4pbFS=;v;K7~a)jr;}X4l_m(?1Zca-6kBy%aq8iP~XWho% zt-IXlZ2VRD0BhBz%K+;lh%SHI>e^ifW60PzpmV35I#sKfE{&R7UX&|#s@A^=>r`ES z9o#vzb8@2hz=86zO;?<|yF70BnR4AOzbr4f|KFxH1)tP-A>n^m`xTxoTv1q7m|oBd ziNaHbs|v>y>V;v2wew%)pU7XHKPq3%Z<${+_eJip+@-lAa;4m6x!&1Nvkzx4$}Y|B zmi4l$Wj@Y4n7JUcB(qD#$*hw8FnwS8-1MR8>1ivya_WQBy{U6j2dAc`%v2%y_vBxb zXC)6zPDyIXY~r27U5PUiixWF0Hc6!8Z^!S5{~^9_d}4e^JP~^%c5CdE*uvO^*x*<+ z`fBu+=x?L*qhq5RMCHg!ksBi?Mdn1di40VJRGwF^Q;t{mQnpqG$luG)%Gb!p%CqE= zazE*Z(3Qi0$Y@ztb)%@-McwYNIYV`QsOfaLhPScm4OP|o>qW*h3|DovCTSi|ax~Xz zlI9MKjAcr)b@8-0d_k6Oh=OMGBvUud=F|4#NrtYvP12q`N!KmCNt(rzG~KbAq&;|& zs(VJ0v^y&S-7^KJ&K9J*mMus%p5!>HXp|~XvR&P3R;j|1EYB2TD)&HAEzv3^o@8p8 zsQ65tWN4lc_za$;>$d1AyYVE=Ac617lT_0XBG`o`X`V?U*qJA}mM-KpohLb#CP>qG zl5M%7_wK}#EXxrMJe4Pzwjny^6rN-_x{&x}o}{~)kkgJlNpp443wGd1s^^Heo74kI zHAMqYWJ#*0+2V2A^CWa}A@S{al4BTxG=V4CrY0UYo+nur&0*ts5}HK}gt0uy2uAR> zJW028QPLQmq*u^=wbVOGf#gjC{7Nn7(vtgqo z)Tuc=jo=DGl`QJL!+DZxQ%S>kl7qq7eB4$%$@Zvyx9owWI->V(!IMyL@wm--l4&@i z&uzw&4AT(GS z@gzr6g#=8VWb3ME6@w>P29b2W-liemP2&qP3=)9KlXSxpRoaAqnnqJ$2VanC8$wD& zmgIPjA*3{fC%G>5-HmyY<5}Wy8}&d^X?h;alWdJXt%G+>YtvBadh9@{F8=7~NqkS|D6Y0Rz5lT=+3tulZm*`A?_D)r|{u0cbkA5U@&YTv#* z3Dc#REY{&kmPPaB+B^v#4AH~a;z@>W3K6WylXQpH^nG}e=F*yPjUGslJM=MFg(q1aP2nr|KvHQbv(kzrsE670 zBjc6jwL#MvYU&-HN1eW>Ae=&BtwiM%D4(e4k|jiH*+0gZ*GOzAlgBP+gR7ws0)UGtm2cN)z3P>&1W z7rrWdTKKT=ZsERB`hP<=_Gd5)?mA%sDX`O=W$5I}$nzw)Y=yxk$CEURyn|Vu zq@q?$eLch4QcRERQt40c85CGKBh{w3;PBK}2*M5L6j|JS9@q{54Z8ww{D zW*0^m*3JKre=dJ*{a#!Sz%9Y{oUp@PA_JQpA*+a9_vPL$Sc_(vc z<`0>DGTUVaXCmpB(>JD1Oz)K*mF}1NCiP_M^3;*3Qfjl*>dB9j4d5 znv1*>xhrydWZ%g4k&Plz7GN2eH z8MdTnd6sUGg;#opXKC4buH>`(i1$( zvB<8CUlV@p8Fv{-WS!&>ylx}BPwr7)3Te>ZTh21=E zhvV38z`B)ZS&l2_Yw6EC3vOPL#GiPU5jX&(TX>dk(Pu=uIiwjy?KSetNH_6CVMhlw z_KhqH4wYb--N3Uj&S-30&$Aqjyvx#cJj>R|vnXB5vn<%d#b~^SXPGLw6r`(pmX1Cl z%DReYX|7G({z{&uy5xP4u3%Xf90XL>VZRPdmnX3-Y=g9D%$~@zTs`<2pTM)Qt(2a4JkLU}r*}M#XIa?T zBKp^{Jj+yRIP3dnjL7GM>`^=qu8P}7xRR8#UptZ z)-EK}BY2jr(w;f#aGqtlf#{duC@GQe~=u352 zsJiKIe#X8N+H8C%Ptve0t|`<*dLXIfhD6s{{ASJ_<9%YE#U74b7+VtCIcCKQ(f6Wv zN6(1v7o8a0I2wz*8o4=gGWPn9j;x2h{?94bD8E+f$}nXu`3w0``4ahXd4}xCy`&FW z^}qYTFvKPcEH}j9m$&GFrPAJcd2^oCT7b%%@ht4w5tEXP{fjH^C!u;_+irIPG%&(k~?O1&t`iE}l4mXPH(o%KGyx>=maz z(T`{84z1thzB~(i$Y{1%hi9qSzbtCMHp|jIEd2#*EuMwt8%bhKo&_5fO%i>0mTd-7 zU4v&?CRsq`-aHGkCrPZ%vkY6Mp0gUy(jBss%B%7$EY?X9y?B=Dk-b>PK1XIw)zzR{ zE3-xU>uZ`OR^nMMHqeV^EbuHR*v2X6d6wN07knK|}37&;*%~bn1%TlqgnfhSJ#!vBSR9#Hb;vK^_eKtQq zszWC;7P7HcS#V^k30(}(MVzZp$C>q zlZ5m=+XrDm6t5_K#}$RoCk$Cl$xGjcuo~NK@x{ibW5N1{XJP$MD%aOM3krZ}An7Zf zg-;OG{!5;v1>Z*Li;$&@s69#abG{&~cEv|l`iy14t3e+|=~JHN>Op__glFNX2HEpI z4r#+?)p1-$j2`J9d{LG`hf1Z7c$R6Bx+Hzbv!F4OAU@!$g|(9C4)60MjaFUK-}&dM zE}eUj-eXz#SkuOM>0O@HI&LJr(*q0V*F@$2#)5 zSq@Hs3)YJ~%eI1!^8(MpAzCWy`E=n0X}WThT)4h)d|_5$WT9{VoBWeF?SEvxl;1SJ zTJ9gY`*Y{!4$e&twEt7H3$x?18)oIqi<#>)$7g0`Mr77Wf0ceBeOda5^vv|o^s1?k zQupDk|3RrKDK(W&zLmT!c}j9Ya$ItQq?C9eab4oL#2$&^iM8Wj#vhAc5?>bIE$+rw ziG3KmFLqAs;Mmld7RyHe7QG{ST69r#LUd46iM$-S2`Bw$N4AdiSH4xAQm#;rQp(C^ zN^kiS`62lN`7n99Y|44*J-kr(ANh~VpY9MxF*KVzXL5+Afk;+x`b|E!yJU%F;&f26 zmE;gl1F;O7494==d|A*4NyRyfXK4ntrToY41B9L!oGX^kWQ+0_ptMLhgDnag6)n6% z91Y@mSd@?#TMltF5UcAM7`|H25J(Oojs_|V^Eg#L#L+-3Y)_;mRfwa3Sn#D#D~324 zh=n73V%*9hjs{|>7L9H>#L>`XVWzO@8!Cr58i?gO!TRhZu7Ti;qzOzuktel!V2GQ6 zo`*h9%hV7z1F=k#JeG2Zn}JwHu<=9=aWfFBW$Tth+ziBmjgurF;$|Qg90=5JLfj03 z<-&MI?-=4{2v~FyQx0)65D6zusdI(683GmthnOOcVB5!Snc)uSMuxw`E-)c3ML<#<6D1Nji1<)TA~vJU21 zi1$FF`XHWVYr%YaAkVV2psWLUmZ_1iSYFJtAbRS}`|~VpCZu_HKc1yw_o9%*zC24c zgV|;umSsbCCV4I5S?HwYfqj93mf(OSv(6~Dk^Iao`qxN z^p3mpESM39Rp(i{6SQNEXF+i!c~yB9rZ}p7r3aQuDu;~YdMnzWgfqoxd0a|tQ3fJ; z(8M&8XW3rxDW1^-3rFEa$Jvc%S-M6--IZsV*a#xZ+J$EsdeH56=2@-qPV#h~)l!Az zX*>(NAE+63Vp%Z6k@6)^Horku$<%eI4Qbw{3M;kbs7>JB^$gM<3S zB%Xz3y-=&)BSkD4`{dk_`niM8O9>=pVF_G-Y z@+>$mNM76WEXxX(6JvN5>}T|j+wd&I2^5aeJga4HlyQ=enI^Q>S9=s+l%~=~YI!7I zRLe>|f-MTWbb?u8IM0F;fnTrYX5tvf}#YNlq+JsqS7LJ)a$*PPPV~^Qu{!UXyZ0mJ^ zq?YwXv&(zU-m8Wm#_w3MChR$0A2&k${cQY$(Ubi`cA7YO|M8QiOq><`w$~K1GJ5vn z=~E|-nzqxV*<<#cH*5T!!xoQUtnRnn2zCDn+Qj`P958Rc@dwOPC+uJzF#dqqr7?5W zjss@T88OQq=1tgk`rIjVhL0Imo3?QJtl85oYl1V^oVNScc5(ZWizd#QI@{d0G<#OH z*{9s1>QcZ&w*!mC;KQ?gpYsDMHh&lM?7#TAb8@;V)$!xqy5ZI8wp+AHbx74HRf{E~ z45^k&c3soV3IdA#@}%l?m8uEjRP*x`E;i_PVp8~5zjsbfkz8wr>sCy+T+}L#RfH}8 zxngobgCa_f>(+Fws{Zoi8ie=sd!rug*bbK)qD_ib`oH?Ub8?d8S~peQs;Ne?Zd;|I zS#qsnS%+}7a@{p5*ieGq(Z4*odemPX9RGE6TCI1NTl06)uK(5Vi0Mmm)hgH&QmSi3 zYy&WgrtQ^=Wv^ruYX~~1R?NEVluN%nxdyYVFE@N!gd!0xH*8K}=ZWUOr{6m#$4Rc( z=BXJt;8etxn>zZdRw=r5t6Hp^cFk0sYTZ?TfpQJzS6?ozu)5)Lg@WCq+*`kQPL7dW z5pWMD4oXfD#p7#bRE%QT_3FM{>o`YYLk+F}^7?Bqzxr~;OxR<8G+~0`zH7~YPrr9g zj*?sv6S1UKJ)>xqD>me6Irwf>JX}guXzkb`to;Jl12n(-hm&D5^|=1n{5=Sm*R4OV z@O$UvNFY~F_3B2wRIFAUw`lq+u9D|eiZ$Ib%cfU@v(x?M^;eo-8**`jeFx!k!|z;w zrGGDe@0_fVTurA^g?XV`#C{Ba9x#Dm&n_Bn$-zX6jiDvymnT=6UmJ2kkLBip=I^kBySInY;xRzMImP*BPxmGP=cZ^|FYo4Q4&0n5eX?|_U4Muli zh6$G&wqOXONw?pj%#Iv@%UY}BlFIn||A!>Mkg`&CQufPK-`w8$HwtFz{p`b;XA7Ca zm8touQ`4KL>)G=%>D1`V6`5TMyC%QLZBsZR_d$Mlc>3?k-JR=~9h^HYzeRqv^y~Q( z(#NN7Dm<4tJY(c<&K`*3+ke^;XiK0ifwlzN5@<`HErI{O1O`PW%g`n(x>I+H)shLV zM^$S@w_5Sd+NDytR@Tdc2!R=72Qe|b}H4ZBGZQL)je+`pJ+N&Kh9LCi1_^`oCG!&u+U$Nb!_QH z6d$dEDhzHAYgNm%%B7;?s^y|tDSO3o1vRNvDq2;mBUgs!4f2UNl2gGZg-Wqx!hnNq z{BE%fExA}O>19i=s@1Yp-Owlc(FF8*)z_SL|E&#JDX^9yZb`{&0*7u2)URO(SqZVOQ#;v0C?A2F4t`uv;l=RctnQ@Bp`B4e*IjY^$zW!@Ht= ziZH5|d`|*e#<0)_uoVeA5&Qc@yRI7Ly5|<{5-N%f9hD;LTq#1-IM0Npu4t8hKCuo- z`6t!zng(8N4TAw zTE(uowW?RQ?6rKNUiK<#y$(GI4k&aAD50fty^bo^>hLe)SIu41C#sfFsn#qHI;w_O zsK9eoE>~^zM_sF^s$SFdN*|wCcFUeuMlV#$2G}LjESAkWdYx5)>?E-I9ryF%7rqX%5DNR;@bFgw48E>+KWkmWk5QC>VNZnJTL1maAsbDc7`;r`wRb zy1Gxa@iaW5jKNjKqj3hHh&=`6V#S92*S29Hb)D6GB8(O|*;v8PX2mbjABE@)#fpKK zuEKDv)yu0k>gefp%SEG9Yrfe7E;@8vjB6d{W^}>2rYf^{VZ_C{cmGACE?##oPipR?E6xH896{M$RWz zaZ(26pIQ-FMleZXzJX5@rXffN-37Ca?$xu6I{pObXQuJ!pWx3+`^2B(pi23~pW<;z z`oy2&LP+?;pJEe_`^2AOWc9PaAS=`~QD7xsrbh zk%QX)|DSOlApcZDu5JJS&(>dq`L*r;|5s%|NpbfmFCxm->>ce|JmjNnqM1o zZTtU!Ho4OL+Vt-Y-2aJs|aE>cP}mseMx0CeQq7x^$b>mOxtqZ3(m`(3U`30&NM91U8EH9xpF{ z>a3v;nO^f$8JwTu!0?a|YGa8{RM(*`?2LB7uz9wYpIVwXqN*D$-ks3JeaR zHtX6^6gQGU16#82YC{)3!EVY@R+CT3rc1f?qQpIsW+n}K5*`BD&+Z>ZUP#eqM zmiinGp*FjVXf5Oi5GU(4WT~RHuz5hd)B|)=BI8(y)hV98CQEHL7IMi`EP>TI zYS36J1p!!vrHaNvU}!O8tH-4k_r^Ju&WYNN4QJN=aqYIDvMjfL2{q7Sam zQbl8#LD1C!9F-bNMeMa^7g(nUsz0>+)G}gd^ z!f8G+U~?>0v{uVgmFkJQywNp=o;^Ob{CAVFANczJH04Fa|Bvn%?TDr#Z$@s7{64aG zWNc)Eh^)LQ;sTB+tdsvL|9FrUVAb45x%+bGz%DQ)r{>bxx3afoPsuJoT)>U8(abB2 ztN?Q}+ho>Fe~+vHSEr9j*V0?2`=mZgJuI>U*r}D0e^34;d1i8dWCiF*CKGQY{+#$- zVt!(5V*U6(0I_oIJ}{xQ0^5&eHFWli~W z`4Radd8xds?8qxiAF%QN@TQStmp>0-Hu@RjG*SpNB_!rBk^GHI%^s$tyT@rD7J~AK zDPBqOEPP%>7gUnnqd8Dnh~zFFnBa>-n0+cLB;+s^rP-7gM~U%eA-M@XG0L*AXNXSu zDiNNALx5uFC?PS2MZNGrq+zSbd|AjLNM%Vp3sJemx=r?D4tFOur)yh7WMv{=&|Uh( z>Jlb{C$)n|4#k{`kkGBrJ^$0-k)lgz*heZlv>Xz4m{>?#MsZ<6!VVJ)sb$1yltaP} z2dq{E@Q=Iq8o^RMmEsP|A#sO^GbI{5mafgY8O?6_R$sh7hL=-@h#0NafLKY

{KH5Y3J1^$uSaW)o_mkjTSS7Gl~{ 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

j_*(;93IL5V)GaRRpdia0P+O30y|tQUaF{xR}62 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