diff --git a/build.gradle.kts b/build.gradle.kts index 010ad46..4b715ef 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -74,6 +74,7 @@ project(":user-api") { project(":front") { dependencies { implementation("org.springframework.boot:spring-boot-starter-thymeleaf") + implementation("nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect") } } diff --git a/front/src/main/kotlin/com/sns/front/controller/HomeController.kt b/front/src/main/kotlin/com/sns/front/controller/HomeController.kt deleted file mode 100644 index 1c2e175..0000000 --- a/front/src/main/kotlin/com/sns/front/controller/HomeController.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.sns.front.controller - -import org.springframework.stereotype.Controller -import org.springframework.web.bind.annotation.GetMapping - -/** - * @author Hyounglin Jun - */ -@Controller -class HomeController { - - @GetMapping("/") - fun home(): String { - return "home/home" - } - -} diff --git a/front/src/main/kotlin/com/sns/front/controller/UserController.kt b/front/src/main/kotlin/com/sns/front/controller/UserController.kt new file mode 100644 index 0000000..086bc14 --- /dev/null +++ b/front/src/main/kotlin/com/sns/front/controller/UserController.kt @@ -0,0 +1,31 @@ +package com.sns.front.controller + +import org.springframework.stereotype.Controller +import org.springframework.web.bind.annotation.GetMapping + +/** + * User 관련된 페이지 모음 + * @author Hyounglin Jun + */ +@Controller +class UserController { + @GetMapping("/home") + fun home(): String { + return "pages/home" + } + + @GetMapping("/login") + fun login(): String { + return "pages/login" + } + + @GetMapping("/register") + fun register(): String { + return "pages/register" + } + + @GetMapping("/profile") + fun profile(): String { + return "pages/profile" + } +} diff --git a/front/src/main/resources/templates/fragments/footer.html b/front/src/main/resources/templates/fragments/footer.html new file mode 100644 index 0000000..1b70639 --- /dev/null +++ b/front/src/main/resources/templates/fragments/footer.html @@ -0,0 +1,12 @@ + + + + + + diff --git a/front/src/main/resources/templates/fragments/head.html b/front/src/main/resources/templates/fragments/head.html new file mode 100644 index 0000000..a8b31f1 --- /dev/null +++ b/front/src/main/resources/templates/fragments/head.html @@ -0,0 +1,13 @@ + + + + + + + DDD Juniors + + + + + + diff --git a/front/src/main/resources/templates/fragments/header.html b/front/src/main/resources/templates/fragments/header.html new file mode 100644 index 0000000..3fbada0 --- /dev/null +++ b/front/src/main/resources/templates/fragments/header.html @@ -0,0 +1,60 @@ + + + + + + diff --git a/front/src/main/resources/templates/home/home.html b/front/src/main/resources/templates/home/home.html deleted file mode 100644 index 40efd2f..0000000 --- a/front/src/main/resources/templates/home/home.html +++ /dev/null @@ -1,11 +0,0 @@ - - - - - Title - Hello world! - - - - - diff --git a/front/src/main/resources/templates/layouts/layout.html b/front/src/main/resources/templates/layouts/layout.html new file mode 100644 index 0000000..ea91b45 --- /dev/null +++ b/front/src/main/resources/templates/layouts/layout.html @@ -0,0 +1,17 @@ + + + + + +
+ +
+ +
+ + +
+ + diff --git a/front/src/main/resources/templates/pages/home.html b/front/src/main/resources/templates/pages/home.html new file mode 100644 index 0000000..0dc61c7 --- /dev/null +++ b/front/src/main/resources/templates/pages/home.html @@ -0,0 +1,11 @@ + + + + +
+ Home 입니다 +
+
+ diff --git a/front/src/main/resources/templates/pages/login.html b/front/src/main/resources/templates/pages/login.html new file mode 100644 index 0000000..d3446a4 --- /dev/null +++ b/front/src/main/resources/templates/pages/login.html @@ -0,0 +1,34 @@ + + + + +
+

+ + + + + + + +

+
+
+

+ + + + +

+
+
+

+ +

+
+
+ diff --git a/front/src/main/resources/templates/pages/profile.html b/front/src/main/resources/templates/pages/profile.html new file mode 100644 index 0000000..ce2b345 --- /dev/null +++ b/front/src/main/resources/templates/pages/profile.html @@ -0,0 +1,31 @@ + + + + +
+
+
+
+
+ Placeholder image +
+
+
+

John Smith

+

@johnsmith

+
+
+ +
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. + Phasellus nec iaculis mauris. @bulmaio. + #css #responsive +
+ +
+
+
+
+ diff --git a/front/src/main/resources/templates/pages/register.html b/front/src/main/resources/templates/pages/register.html new file mode 100644 index 0000000..043ea95 --- /dev/null +++ b/front/src/main/resources/templates/pages/register.html @@ -0,0 +1,92 @@ + + + + +
+ +
+ +
+
+ +
+ +
+ + + + + + + +
+

This username is available

+
+ +
+ +
+ + + + + + + +
+

This email is invalid

+
+ +
+ +
+
+ +
+
+
+ +
+ +
+ +
+
+ +
+
+ +
+
+ +
+
+ + +
+
+ +
+
+ +
+
+ +
+
+
+ diff --git a/user-api/src/main/kotlin/com/sns/user/component/user/application/ProfileCommandService.kt b/user-api/src/main/kotlin/com/sns/user/component/user/application/ProfileCommandService.kt new file mode 100644 index 0000000..b02e3bf --- /dev/null +++ b/user-api/src/main/kotlin/com/sns/user/component/user/application/ProfileCommandService.kt @@ -0,0 +1,23 @@ +package com.sns.user.component.user.application + +import com.sns.user.component.user.domains.Profile +import com.sns.user.component.user.repositories.ProfileRepository +import org.springframework.stereotype.Service + +/** + * @author Hyounglin Jun + */ +@Service +class ProfileCommandService( + val profileRepository: ProfileRepository, +) { + fun create( + userId: String, + ): Profile { + + val profile = Profile.create(userId) + + profileRepository.save(profile) + return profile + } +} diff --git a/user-api/src/main/kotlin/com/sns/user/component/user/application/ProfileQueryService.kt b/user-api/src/main/kotlin/com/sns/user/component/user/application/ProfileQueryService.kt new file mode 100644 index 0000000..11da03f --- /dev/null +++ b/user-api/src/main/kotlin/com/sns/user/component/user/application/ProfileQueryService.kt @@ -0,0 +1,16 @@ +package com.sns.user.component.user.application + +import com.sns.user.component.user.domains.Profile +import com.sns.user.component.user.repositories.ProfileRepository +import org.springframework.stereotype.Service +import java.util.* + +/** + * @author Hyounglin Jun + */ +@Service +class ProfileQueryService( + val profileRepository: ProfileRepository, +) { + fun getById(userId: String): Optional = profileRepository.findById(userId) +} diff --git a/user-api/src/main/kotlin/com/sns/user/component/user/domains/Profile.kt b/user-api/src/main/kotlin/com/sns/user/component/user/domains/Profile.kt new file mode 100644 index 0000000..008953f --- /dev/null +++ b/user-api/src/main/kotlin/com/sns/user/component/user/domains/Profile.kt @@ -0,0 +1,75 @@ +package com.sns.user.component.user.domains + +import org.hibernate.validator.constraints.URL +import org.springframework.data.annotation.Id +import org.springframework.data.annotation.LastModifiedDate +import org.springframework.data.annotation.Transient +import org.springframework.data.domain.Persistable +import org.springframework.data.relational.core.mapping.MappedCollection +import java.time.Instant +import javax.validation.constraints.Max +import javax.validation.constraints.NotNull +import javax.validation.constraints.Size + +/** + * 사용자 프로필 도메인 객체(VO) + * User 객체와 1:1 관계 + * @author Hyounglin Jun + */ +data class Profile( + @Id + @NotNull + @Max(50) + val userId: String, + + @Max(50) + val nickName: String?, + + @Max(100) + @URL + val iconImageUrl: String?, // TODO URL 객체로? + + @Max(200) + val intro: String?, // 소개, 약력 + + @Size(max = 5) + @MappedCollection + val hobbies: List?, // 취미 목록 + + @LastModifiedDate + var updatedAt: Instant = Instant.MIN, +) : Persistable { + companion object { + fun create( + userId: String, + nickName: String? = null, + iconImageUrl: String? = null, + intro: String? = null, + hobbies: List? = null, + ): Profile { + return Profile( + userId = userId, + nickName = nickName, + iconImageUrl = iconImageUrl, + intro = intro, + hobbies = hobbies?.map { e -> Hobby(e) }?.toList(), + ).apply { new = true } + } + } + + @Transient + private var new: Boolean = false + + override fun isNew() = new + override fun getId() = this.userId + + fun getServiceIconImageUrl(): String { + return if (iconImageUrl.isNullOrBlank()) PROFILE_DEFAULT_ICON_IMAGE_URL else iconImageUrl + } +} + +data class Hobby( + val name: String, +) + +const val PROFILE_DEFAULT_ICON_IMAGE_URL: String = "https://ssl.pstatic.net/static/kin/09renewal/avatar/200x200_m_gray/public.png" diff --git a/user-api/src/main/kotlin/com/sns/user/component/user/domains/User.kt b/user-api/src/main/kotlin/com/sns/user/component/user/domains/User.kt index 7e7f7bc..a17a405 100644 --- a/user-api/src/main/kotlin/com/sns/user/component/user/domains/User.kt +++ b/user-api/src/main/kotlin/com/sns/user/component/user/domains/User.kt @@ -4,10 +4,6 @@ import com.sns.commons.DomainEvent import com.sns.user.component.user.dtos.FriendRequestedEvent import com.sns.user.component.user.events.UserStatusChangedEvent import com.sns.user.core.exceptions.AlreadyExistException -import java.sql.ResultSet -import java.time.Instant -import javax.validation.constraints.Max -import javax.validation.constraints.NotBlank import org.springframework.data.annotation.CreatedDate import org.springframework.data.annotation.Id import org.springframework.data.annotation.LastModifiedDate @@ -15,6 +11,10 @@ import org.springframework.data.annotation.Transient import org.springframework.data.domain.Persistable import org.springframework.data.relational.core.mapping.MappedCollection import org.springframework.jdbc.core.RowMapper +import java.sql.ResultSet +import java.time.Instant +import javax.validation.constraints.Max +import javax.validation.constraints.NotBlank data class User( @Id @@ -44,7 +44,7 @@ data class User( var updatedAt: Instant = Instant.MIN, @NotBlank - var status: Status = Status.ACTIVATED + var status: Status = Status.ACTIVATED, ) : Persistable { @Transient private var new: Boolean = false @@ -136,3 +136,11 @@ enum class Status { if (status == this) throw AlreadyExistException() } } + +data class UserId( + // TODO 적용 예정. + private val id: String, // email +) { + public fun getEmailAddress() = this.id + public fun getId() = this.id +} diff --git a/user-api/src/main/kotlin/com/sns/user/component/user/repositories/ProfileRepository.kt b/user-api/src/main/kotlin/com/sns/user/component/user/repositories/ProfileRepository.kt new file mode 100644 index 0000000..5f722e4 --- /dev/null +++ b/user-api/src/main/kotlin/com/sns/user/component/user/repositories/ProfileRepository.kt @@ -0,0 +1,11 @@ +package com.sns.user.component.user.repositories + +import com.sns.user.component.user.domains.Profile +import org.springframework.data.repository.CrudRepository +import org.springframework.stereotype.Repository + +/** + * @author Hyounglin Jun + */ +@Repository +interface ProfileRepository : CrudRepository diff --git a/user-api/src/main/kotlin/com/sns/user/core/config/db/converter/JdbcConfiguration.kt b/user-api/src/main/kotlin/com/sns/user/core/config/db/converter/JdbcConfiguration.kt new file mode 100644 index 0000000..faf3a1c --- /dev/null +++ b/user-api/src/main/kotlin/com/sns/user/core/config/db/converter/JdbcConfiguration.kt @@ -0,0 +1,35 @@ +package com.sns.user.core.config.db.converter + +import com.sns.user.component.user.domains.UserId +import org.springframework.context.annotation.Configuration +import org.springframework.core.convert.converter.Converter +import org.springframework.data.convert.ReadingConverter +import org.springframework.data.convert.WritingConverter +import org.springframework.data.jdbc.core.convert.JdbcCustomConversions +import org.springframework.data.jdbc.repository.config.AbstractJdbcConfiguration + +/** + * @author Hyounglin Jun + */ +@Configuration +class JdbcConfiguration : AbstractJdbcConfiguration() { + override fun jdbcCustomConversions(): JdbcCustomConversions { + return JdbcCustomConversions( + listOf(UserIdToStringConverter(), StringToUserIdConverter()), + ) + } +} + +@WritingConverter +class UserIdToStringConverter : Converter { + override fun convert(source: UserId): String? { + return source.getId() + } +} + +@ReadingConverter +class StringToUserIdConverter : Converter { + override fun convert(source: String): UserId { + return UserId(source) + } +} diff --git a/user-api/src/main/kotlin/com/sns/user/core/exceptions/AlreadyExistException.kt b/user-api/src/main/kotlin/com/sns/user/core/exceptions/AlreadyExistException.kt index e0c2102..13f9dcf 100644 --- a/user-api/src/main/kotlin/com/sns/user/core/exceptions/AlreadyExistException.kt +++ b/user-api/src/main/kotlin/com/sns/user/core/exceptions/AlreadyExistException.kt @@ -4,5 +4,4 @@ import org.springframework.http.HttpStatus import org.springframework.web.bind.annotation.ResponseStatus @ResponseStatus(HttpStatus.CONFLICT) -class AlreadyExistException(msg: String = "이미 존재합니다.") : RuntimeException(msg) { -} +class AlreadyExistException(msg: String = "이미 존재합니다.") : RuntimeException(msg) diff --git a/user-api/src/main/kotlin/com/sns/user/core/exceptions/NoAuthorityException.kt b/user-api/src/main/kotlin/com/sns/user/core/exceptions/NoAuthorityException.kt index 830bda4..c95c502 100644 --- a/user-api/src/main/kotlin/com/sns/user/core/exceptions/NoAuthorityException.kt +++ b/user-api/src/main/kotlin/com/sns/user/core/exceptions/NoAuthorityException.kt @@ -4,5 +4,4 @@ import org.springframework.http.HttpStatus import org.springframework.web.bind.annotation.ResponseStatus @ResponseStatus(HttpStatus.UNAUTHORIZED) -class NoAuthorityException(msg: String? = "권한이 없습니다") : RuntimeException(msg) { -} +class NoAuthorityException(msg: String? = "권한이 없습니다") : RuntimeException(msg) diff --git a/user-api/src/main/kotlin/com/sns/user/core/exceptions/NotExistException.kt b/user-api/src/main/kotlin/com/sns/user/core/exceptions/NotExistException.kt new file mode 100644 index 0000000..62ecab6 --- /dev/null +++ b/user-api/src/main/kotlin/com/sns/user/core/exceptions/NotExistException.kt @@ -0,0 +1,10 @@ +package com.sns.user.core.exceptions + +import org.springframework.http.HttpStatus +import org.springframework.web.bind.annotation.ResponseStatus + +/** + * @author Hyounglin Jun + */ +@ResponseStatus(HttpStatus.NOT_FOUND) +class NotExistException(msg: String = "해당 데이터가 없습니다.") : RuntimeException(msg) diff --git a/user-api/src/main/kotlin/com/sns/user/endpoints/user/ProfileController.kt b/user-api/src/main/kotlin/com/sns/user/endpoints/user/ProfileController.kt new file mode 100644 index 0000000..e40ed1c --- /dev/null +++ b/user-api/src/main/kotlin/com/sns/user/endpoints/user/ProfileController.kt @@ -0,0 +1,41 @@ +package com.sns.user.endpoints.user + +import com.sns.user.component.user.application.ProfileQueryService +import com.sns.user.core.config.SwaggerTag +import com.sns.user.core.exceptions.NotExistException +import com.sns.user.endpoints.user.responses.ProfileResponse +import io.swagger.annotations.ApiOperation +import io.swagger.v3.oas.annotations.responses.ApiResponse +import io.swagger.v3.oas.annotations.responses.ApiResponses +import io.swagger.v3.oas.annotations.tags.Tag +import org.springframework.http.HttpStatus +import org.springframework.validation.annotation.Validated +import org.springframework.web.bind.annotation.* +import javax.validation.constraints.Email + +/** + * @author Hyounglin Jun + */ +@Validated +@RestController +@Tag(name = SwaggerTag.SIGN_UP) +@RequestMapping("/api") +class ProfileController( + val profileQueryService: ProfileQueryService, +) { + @ApiOperation("프로필 조회") + @ApiResponses( + value = [ + ApiResponse(description = "성공", responseCode = "202"), + ApiResponse(description = "해당 유저가 없음", responseCode = "409"), + ], + ) + @ResponseStatus(HttpStatus.OK) + @GetMapping("/v1/profiles/{userId}") + fun getProfile(@Email @PathVariable userId: String): ProfileResponse { + return ProfileResponse( + profileQueryService.getById(userId) + .orElseThrow { NotExistException() }, + ) + } +} diff --git a/user-api/src/main/kotlin/com/sns/user/endpoints/user/SignUpController.kt b/user-api/src/main/kotlin/com/sns/user/endpoints/user/SignUpController.kt index 0198817..20830fc 100644 --- a/user-api/src/main/kotlin/com/sns/user/endpoints/user/SignUpController.kt +++ b/user-api/src/main/kotlin/com/sns/user/endpoints/user/SignUpController.kt @@ -3,6 +3,7 @@ package com.sns.user.endpoints.user import com.sns.commons.utils.ifTrue import com.sns.user.component.authcode.application.AuthCodeCommandService import com.sns.user.component.authcode.domain.Purpose +import com.sns.user.component.user.application.ProfileCommandService import com.sns.user.component.user.application.UserCommandService import com.sns.user.component.user.application.UserQueryService import com.sns.user.core.config.SwaggerTag @@ -15,19 +16,12 @@ import io.swagger.v3.oas.annotations.media.Schema import io.swagger.v3.oas.annotations.responses.ApiResponse import io.swagger.v3.oas.annotations.responses.ApiResponses import io.swagger.v3.oas.annotations.tags.Tag -import javax.validation.constraints.Email import org.springframework.http.HttpStatus import org.springframework.stereotype.Component import org.springframework.transaction.annotation.Transactional import org.springframework.validation.annotation.Validated -import org.springframework.web.bind.annotation.GetMapping -import org.springframework.web.bind.annotation.PathVariable -import org.springframework.web.bind.annotation.PostMapping -import org.springframework.web.bind.annotation.PutMapping -import org.springframework.web.bind.annotation.RequestBody -import org.springframework.web.bind.annotation.RequestMapping -import org.springframework.web.bind.annotation.ResponseStatus -import org.springframework.web.bind.annotation.RestController +import org.springframework.web.bind.annotation.* +import javax.validation.constraints.Email @Validated @RestController @@ -88,11 +82,15 @@ class SignUpController( @Component class SignUpAggregator( val authCodeCommandService: AuthCodeCommandService, - val userCommandService: UserCommandService + val userCommandService: UserCommandService, + val profileCommandService: ProfileCommandService, ) { @Transactional fun verifyAuthentication(userId: String, code: String): Boolean = authCodeCommandService.verify(userId, Purpose.SIGN_UP, code) - .ifTrue { userCommandService.activate(userId) } ?: false + .ifTrue { + userCommandService.activate(userId) + profileCommandService.create(userId) + } ?: false } diff --git a/user-api/src/main/kotlin/com/sns/user/endpoints/user/responses/ProfileResponse.kt b/user-api/src/main/kotlin/com/sns/user/endpoints/user/responses/ProfileResponse.kt new file mode 100644 index 0000000..b74cdfa --- /dev/null +++ b/user-api/src/main/kotlin/com/sns/user/endpoints/user/responses/ProfileResponse.kt @@ -0,0 +1,23 @@ +package com.sns.user.endpoints.user.responses + +import com.sns.user.component.user.domains.Profile + +/** + * @author Hyounglin Jun + */ + +data class ProfileResponse( + val userId: String, + val nickName: String?, + val iconImageUrl: String, + val intro: String?, + val hobbies: List?, +) { + constructor(profile: Profile) : this( + userId = profile.userId, + nickName = profile.nickName, + iconImageUrl = profile.getServiceIconImageUrl(), + intro = profile.intro, + hobbies = profile.hobbies?.map { e -> e.name }?.toList(), + ) +} diff --git a/user-api/src/test/kotlin/com/sns/user/AssertThatExtensions.kt b/user-api/src/test/kotlin/com/sns/user/AssertThatExtensions.kt index 1ae499c..b6520d8 100644 --- a/user-api/src/test/kotlin/com/sns/user/AssertThatExtensions.kt +++ b/user-api/src/test/kotlin/com/sns/user/AssertThatExtensions.kt @@ -1,14 +1,15 @@ package com.sns.user +import org.assertj.core.api.Assertions.assertThat import java.util.* import java.util.function.Consumer import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.IterableAssert import org.assertj.core.api.ObjectAssert -infix fun T.isEqualTo(other: T) = assertThat(this).isEqualTo(other) +infix fun T?.isEqualTo(other: T?) = assertThat(this).isEqualTo(other) -infix fun T.isNotEqualTo(other: T) = assertThat(this).isNotEqualTo(other) +infix fun T?.isNotEqualTo(other: T?) = assertThat(this).isNotEqualTo(other) infix fun T.satisfies(requirements: Consumer) = assertThat(this).satisfies(requirements) diff --git a/user-api/src/test/kotlin/com/sns/user/component/user/repositories/ProfileRepositoryTest.kt b/user-api/src/test/kotlin/com/sns/user/component/user/repositories/ProfileRepositoryTest.kt new file mode 100644 index 0000000..555072b --- /dev/null +++ b/user-api/src/test/kotlin/com/sns/user/component/user/repositories/ProfileRepositoryTest.kt @@ -0,0 +1,44 @@ +package com.sns.user.component.user.repositories + +import com.sns.user.component.user.domains.Hobby +import com.sns.user.component.user.domains.Profile +import com.sns.user.hasValueSatisfying +import com.sns.user.isEqualTo +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest + +/** + * @author Hyounglin Jun + */ +@SpringBootTest +internal class ProfileRepositoryTest { + @Autowired + lateinit var profileRepository: ProfileRepository + + @Test + fun save() { + val id = "test@gmail.com" + val nickName = "닉네임" + val inputHobbies = listOf("밥먹기", "운동하기") + val outputHobbies = listOf(Hobby("밥먹기"), Hobby("운동하기")) + val profile = Profile.create( + userId = id, nickName = nickName, + hobbies = inputHobbies, + ) + + profileRepository.save(profile) + + profileRepository.findById(id) hasValueSatisfying { savedUser -> + savedUser.userId isEqualTo id + savedUser.nickName isEqualTo nickName + savedUser.iconImageUrl isEqualTo null + savedUser.intro isEqualTo null + savedUser.hobbies isEqualTo outputHobbies + } + } +} + + + + diff --git a/user-api/src/test/resources/db/users/schema.sql b/user-api/src/test/resources/db/users/schema.sql index 4cd9574..5bec763 100644 --- a/user-api/src/test/resources/db/users/schema.sql +++ b/user-api/src/test/resources/db/users/schema.sql @@ -10,6 +10,23 @@ CREATE TABLE IF NOT EXISTS `user` updated_at DATETIME NOT NULL COMMENT '마지막 수정 시간' ); +DROP TABLE IF EXISTS `profile`; +CREATE TABLE IF NOT EXISTS `profile` ( + user_id VARCHAR(50) NOT NULL PRIMARY KEY COMMENT '아이디 (이메일)', + nick_name VARCHAR(50) COMMENT '닉네임', + icon_image_url VARCHAR(100) COMMENT '아이콘 이미지 URL', + intro VARCHAR(200) COMMENT '소개, 약력', + hobbies JSON COMMENT '취미 목록', + updated_at DATETIME NOT NULL COMMENT '마지막 수정 시간' +); + +DROP TABLE IF EXISTS `hobby`; +CREATE TABLE IF NOT EXISTS `hobby` ( + profile_key VARCHAR(50) NOT NULL COMMENT '리스트 순서 번호', + profile VARCHAR(50) NOT NULL COMMENT 'profile의 id', + name VARCHAR(20) NOT NULL COMMENT '취미 이름' +); + DROP TABLE IF EXISTS `auth_code`; CREATE TABLE IF NOT EXISTS `auth_code` (