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 613ed74..0000000 Binary files a/tests/test.db and /dev/null differ