diff --git a/build.gradle b/build.gradle index 327e810..a0bc022 100644 --- a/build.gradle +++ b/build.gradle @@ -4,6 +4,8 @@ plugins { id 'io.spring.dependency-management' version '1.1.7' id "org.sonarqube" version "6.2.0.5505" id 'jacoco' + id 'org.flywaydb.flyway' version '11.10.0' + } group = 'org.Runimo' @@ -44,6 +46,8 @@ dependencies { runtimeOnly 'com.h2database:h2' runtimeOnly 'com.mysql:mysql-connector-j' + implementation 'org.flywaydb:flyway-core' + implementation 'org.flywaydb:flyway-mysql' testImplementation 'io.rest-assured:rest-assured:5.4.0' diff --git a/src/main/java/org/runimo/runimo/auth/domain/SignupToken.java b/src/main/java/org/runimo/runimo/auth/domain/SignupToken.java index 7461634..b6b3ca6 100644 --- a/src/main/java/org/runimo/runimo/auth/domain/SignupToken.java +++ b/src/main/java/org/runimo/runimo/auth/domain/SignupToken.java @@ -6,11 +6,14 @@ import jakarta.persistence.Enumerated; import jakarta.persistence.Id; import jakarta.persistence.Table; +import jakarta.persistence.Version; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import org.runimo.runimo.auth.exceptions.SignUpException; import org.runimo.runimo.common.CreatedAuditEntity; import org.runimo.runimo.user.domain.SocialProvider; +import org.runimo.runimo.user.enums.UserHttpResponseCode; @Table(name = "signup_token") @Entity @@ -32,6 +35,13 @@ public class SignupToken extends CreatedAuditEntity { @Enumerated(EnumType.STRING) private SocialProvider socialProvider; + @Version + @Column(name = "version") + private Long version; + + @Column(name = "used", nullable = false) + private Boolean used = false; + @Builder public SignupToken(String token, String providerId, String refreshToken, SocialProvider socialProvider) { @@ -40,4 +50,11 @@ public SignupToken(String token, String providerId, String refreshToken, this.refreshToken = refreshToken; this.socialProvider = socialProvider; } + + public void markAsUsed() { + if (this.used) { + throw new SignUpException(UserHttpResponseCode.SIGNIN_FAIL_ALREADY_EXIST); + } + this.used = true; + } } diff --git a/src/main/java/org/runimo/runimo/auth/repository/SignupTokenRepository.java b/src/main/java/org/runimo/runimo/auth/repository/SignupTokenRepository.java index 405b76a..117a143 100644 --- a/src/main/java/org/runimo/runimo/auth/repository/SignupTokenRepository.java +++ b/src/main/java/org/runimo/runimo/auth/repository/SignupTokenRepository.java @@ -9,7 +9,9 @@ public interface SignupTokenRepository extends JpaRepository { @Query( - "SELECT st FROM SignupToken st WHERE st.token = :token AND st.createdAt > :createdAtAfter" + "SELECT st FROM SignupToken st WHERE st.token = :token " + + "AND st.createdAt > :createdAtAfter " + + "AND st.used = false" ) Optional findByIdAndCreatedAtAfter(String token, LocalDateTime createdAtAfter); diff --git a/src/main/java/org/runimo/runimo/auth/service/SignUpUsecaseImpl.java b/src/main/java/org/runimo/runimo/auth/service/SignUpUsecaseImpl.java index 13bd8e8..0970d67 100644 --- a/src/main/java/org/runimo/runimo/auth/service/SignUpUsecaseImpl.java +++ b/src/main/java/org/runimo/runimo/auth/service/SignUpUsecaseImpl.java @@ -1,13 +1,9 @@ package org.runimo.runimo.auth.service; -import java.time.LocalDateTime; import lombok.RequiredArgsConstructor; import org.runimo.runimo.auth.domain.SignupToken; -import org.runimo.runimo.auth.exceptions.SignUpException; -import org.runimo.runimo.auth.jwt.JwtResolver; import org.runimo.runimo.auth.jwt.JwtTokenFactory; import org.runimo.runimo.auth.jwt.SignupTokenPayload; -import org.runimo.runimo.auth.repository.SignupTokenRepository; import org.runimo.runimo.auth.service.dto.SignupUserResponse; import org.runimo.runimo.auth.service.dto.UserSignupCommand; import org.runimo.runimo.external.FileStorageService; @@ -17,7 +13,6 @@ import org.runimo.runimo.user.domain.AppleUserToken; import org.runimo.runimo.user.domain.SocialProvider; import org.runimo.runimo.user.domain.User; -import org.runimo.runimo.user.enums.UserHttpResponseCode; import org.runimo.runimo.user.repository.AppleUserTokenRepository; import org.runimo.runimo.user.service.UserRegisterService; import org.runimo.runimo.user.service.dto.command.DeviceTokenDto; @@ -29,21 +24,19 @@ @RequiredArgsConstructor public class SignUpUsecaseImpl implements SignUpUsecase { - private static final int REGISTER_CUTOFF_MIN = 10; private final UserRegisterService userRegisterService; private final FileStorageService fileStorageService; private final EggGrantService eggGrantService; private final JwtTokenFactory jwtTokenFactory; - private final SignupTokenRepository signupTokenRepository; private final AppleUserTokenRepository appleUserTokenRepository; - private final JwtResolver jwtResolver; + private final SignupTokenService signupTokenService; @Override @Transactional public SignupUserResponse register(UserSignupCommand command) { // 1. 토큰 검증 - SignupTokenPayload payload = jwtResolver.getSignupTokenPayload(command.registerToken()); - SignupToken signupToken = findUnExpiredSignupToken(payload.token()); + SignupTokenPayload payload = signupTokenService.extractPayload(command.registerToken()); + SignupToken signupToken = signupTokenService.findUnExpiredToken(payload.token()); // 2. 유저생성 userRegisterService.validateExistingUser(payload.providerId(), payload.socialProvider()); String imgUrl = fileStorageService.storeFile(command.profileImage()); @@ -57,22 +50,11 @@ public SignupUserResponse register(UserSignupCommand command) { Egg grantedEgg = eggGrantService.grantGreetingEggToUser(savedUser); EggType eggType = grantedEgg.getEggType(); - removeSignupToken(payload.token()); + signupTokenService.invalidateSignupToken(signupToken); return new SignupUserResponse(savedUser, jwtTokenFactory.generateTokenPair(savedUser), grantedEgg, eggType.getCode()); } - private void removeSignupToken(String token) { - signupTokenRepository.deleteByToken(token); - } - - private SignupToken findUnExpiredSignupToken(String token) { - LocalDateTime cutOffTime = LocalDateTime.now().minusMinutes(REGISTER_CUTOFF_MIN); - return signupTokenRepository. - findByIdAndCreatedAtAfter(token, cutOffTime) - .orElseThrow(() -> new SignUpException(UserHttpResponseCode.TOKEN_INVALID)); - } - private void createAppleUserToken(Long userId, SignupToken signupToken) { AppleUserToken appleUserToken = new AppleUserToken( userId, diff --git a/src/main/java/org/runimo/runimo/auth/service/SignupTokenService.java b/src/main/java/org/runimo/runimo/auth/service/SignupTokenService.java new file mode 100644 index 0000000..08a80f3 --- /dev/null +++ b/src/main/java/org/runimo/runimo/auth/service/SignupTokenService.java @@ -0,0 +1,42 @@ +package org.runimo.runimo.auth.service; + + +import java.time.LocalDateTime; +import lombok.RequiredArgsConstructor; +import org.runimo.runimo.auth.domain.SignupToken; +import org.runimo.runimo.auth.exceptions.SignUpException; +import org.runimo.runimo.auth.jwt.JwtResolver; +import org.runimo.runimo.auth.jwt.SignupTokenPayload; +import org.runimo.runimo.auth.repository.SignupTokenRepository; +import org.runimo.runimo.user.enums.UserHttpResponseCode; +import org.springframework.dao.OptimisticLockingFailureException; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class SignupTokenService { + + private static final int REGISTER_CUTOFF_MIN = 10; + private final JwtResolver jwtResolver; + private final SignupTokenRepository signupTokenRepository; + + public SignupTokenPayload extractPayload(String token) { + return jwtResolver.getSignupTokenPayload(token); + } + + public SignupToken findUnExpiredToken(String token) { + LocalDateTime cutOffTime = LocalDateTime.now().minusMinutes(REGISTER_CUTOFF_MIN); + return signupTokenRepository. + findByIdAndCreatedAtAfter(token, cutOffTime) + .orElseThrow(() -> new SignUpException(UserHttpResponseCode.TOKEN_INVALID)); + } + + public void invalidateSignupToken(SignupToken signupToken) { + try { + signupToken.markAsUsed(); + signupTokenRepository.save(signupToken); + } catch (OptimisticLockingFailureException e) { + throw new SignUpException(UserHttpResponseCode.SIGNIN_FAIL_ALREADY_EXIST); + } + } +} diff --git a/src/main/resources/db/migration/V1__Create_baseline.sql b/src/main/resources/db/migration/V1__Create_baseline.sql new file mode 100644 index 0000000..d3af4a6 --- /dev/null +++ b/src/main/resources/db/migration/V1__Create_baseline.sql @@ -0,0 +1,27 @@ +-- V1__Create_baseline.sql +-- Production baseline - existing schema as of 2025-06-24 +-- This migration represents the current state of the production database +-- All existing tables are already present, this is just a baseline marker + +-- Flyway baseline marker for existing production database +-- No actual schema changes in this file as tables already exist + +-- Existing tables: +-- - users +-- - user_token +-- - user_love_point +-- - oauth_account +-- - apple_user_token +-- - signup_token +-- - running_record +-- - item +-- - egg_type +-- - item_activity +-- - user_item +-- - incubating_egg +-- - runimo_definition +-- - runimo +-- - user_refresh_token + +-- This baseline allows us to start versioned migrations from V2 onwards +SELECT 'Baseline migration - existing schema marked as V1' as message; diff --git a/src/main/resources/db/migration/V20250702__add_version_column_signup_token.sql b/src/main/resources/db/migration/V20250702__add_version_column_signup_token.sql new file mode 100644 index 0000000..93a3598 --- /dev/null +++ b/src/main/resources/db/migration/V20250702__add_version_column_signup_token.sql @@ -0,0 +1,10 @@ +ALTER TABLE `signup_token` + ADD COLUMN `version` BIGINT NOT NULL DEFAULT 0; + +ALTER TABLE `signup_token` + ADD COLUMN `used` BOOLEAN NOT NULL DEFAULT FALSE; + +-- 기존 토큰들은 사용되지 않은 것으로 처리 +UPDATE `signup_token` +SET `used` = FALSE, + `version` = 0; \ No newline at end of file diff --git a/src/test/java/org/runimo/runimo/auth/controller/AuthAcceptanceTest.java b/src/test/java/org/runimo/runimo/auth/controller/AuthAcceptanceTest.java index e4a4faf..cef14ba 100644 --- a/src/test/java/org/runimo/runimo/auth/controller/AuthAcceptanceTest.java +++ b/src/test/java/org/runimo/runimo/auth/controller/AuthAcceptanceTest.java @@ -153,7 +153,8 @@ void tearDown() { } @Test - void 중복_유저_회원가입_409응답() throws JsonProcessingException { + @DisplayName("중복된 유저를 회원가입 시도하면 401응답 - 동일한 토큰") + void 중복_유저_회원가입_401응답() throws JsonProcessingException { AuthSignupRequest request = new AuthSignupRequest(token, "username", Gender.UNKNOWN); given() @@ -176,6 +177,44 @@ void tearDown() { ); token = jwtTokenFactory.generateSignupTemporalToken("provider-id", SocialProvider.KAKAO, "valid-token"); + request = new AuthSignupRequest(token, "username", Gender.UNKNOWN); + + given() + .contentType(ContentType.MULTIPART) + .multiPart("request", objectMapper.writeValueAsString(request)) + .when() + .post("/api/v1/auth/signup") + .then() + .statusCode(HttpStatus.UNAUTHORIZED.value()); + + signupTokenRepository.delete(signupToken); + } + + @Test + @DisplayName("중복된 유저를 회원가입 시도하면 409응답 - 동일한 토큰") + void 중복_유저_회원가입_409응답_다른토큰() throws JsonProcessingException { + AuthSignupRequest request = new AuthSignupRequest(token, "username", Gender.UNKNOWN); + + given() + .contentType(ContentType.MULTIPART) + .multiPart("request", objectMapper.writeValueAsString(request)) + .when() + .post("/api/v1/auth/signup") + .then() + .statusCode(HttpStatus.CREATED.value()) + .log().all() + .body("payload.nickname", equalTo("username")) + .body("payload.token_pair.access_token", notNullValue()) + .body("payload.token_pair.refresh_token", notNullValue()); + + SignupToken signupToken = new SignupToken( + "valid-token-2", + "provider-id", + "refresh-token", + SocialProvider.KAKAO + ); + token = jwtTokenFactory.generateSignupTemporalToken("provider-id", SocialProvider.KAKAO, + "valid-token-2"); signupTokenRepository.save(signupToken); request = new AuthSignupRequest(token, "username", Gender.UNKNOWN); diff --git a/src/test/java/org/runimo/runimo/auth/domain/SignupTokenTest.java b/src/test/java/org/runimo/runimo/auth/domain/SignupTokenTest.java new file mode 100644 index 0000000..d86b571 --- /dev/null +++ b/src/test/java/org/runimo/runimo/auth/domain/SignupTokenTest.java @@ -0,0 +1,25 @@ +package org.runimo.runimo.auth.domain; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; +import org.runimo.runimo.auth.exceptions.SignUpException; +import org.runimo.runimo.user.domain.SocialProvider; + +class SignupTokenTest { + + @Test + void should_fail_when_token_is_marked() { + // given + SignupToken token = SignupToken.builder() + .token("dummy.token.value") + .refreshToken("dummy.refresh.token.value") + .socialProvider(SocialProvider.KAKAO) + .build(); + token.markAsUsed(); + + // when & then + assertThrows(SignUpException.class, token::markAsUsed); + } + +} \ No newline at end of file diff --git a/src/test/java/org/runimo/runimo/auth/service/SignUpUsecaseTest.java b/src/test/java/org/runimo/runimo/auth/service/SignUpUsecaseTest.java index c9aadce..415d49c 100644 --- a/src/test/java/org/runimo/runimo/auth/service/SignUpUsecaseTest.java +++ b/src/test/java/org/runimo/runimo/auth/service/SignUpUsecaseTest.java @@ -8,17 +8,14 @@ import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.when; -import java.util.Optional; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.runimo.runimo.auth.domain.SignupToken; import org.runimo.runimo.auth.exceptions.SignUpException; -import org.runimo.runimo.auth.jwt.JwtResolver; import org.runimo.runimo.auth.jwt.JwtTokenFactory; import org.runimo.runimo.auth.jwt.SignupTokenPayload; -import org.runimo.runimo.auth.repository.SignupTokenRepository; import org.runimo.runimo.auth.service.dto.SignupUserResponse; import org.runimo.runimo.auth.service.dto.UserSignupCommand; import org.runimo.runimo.external.FileStorageService; @@ -43,11 +40,9 @@ class SignUpUsecaseTest { @Mock private JwtTokenFactory jwtTokenFactory; @Mock - private SignupTokenRepository signupTokenRepository; - @Mock private AppleUserTokenRepository appleUserTokenRepository; @Mock - private JwtResolver jwtResolver; + private SignupTokenService signupTokenService; private SignUpUsecase sut; @@ -59,9 +54,8 @@ void setUp() { fileStorageService, eggGrantService, jwtTokenFactory, - signupTokenRepository, appleUserTokenRepository, - jwtResolver + signupTokenService ); } @@ -72,10 +66,10 @@ void setUp() { SignupTokenPayload payload = new SignupTokenPayload(registerToken, "socialId123", SocialProvider.KAKAO); - when(jwtResolver.getSignupTokenPayload(registerToken)).thenReturn(payload); - when(signupTokenRepository.findByIdAndCreatedAtAfter(eq(registerToken), any())) - .thenReturn(Optional.of(new SignupToken( - registerToken, "refresh", "refresh", SocialProvider.KAKAO))); + when(signupTokenService.extractPayload(registerToken)).thenReturn(payload); + when(signupTokenService.findUnExpiredToken(eq(registerToken))) + .thenReturn(new SignupToken( + registerToken, "refresh", "refresh", SocialProvider.KAKAO)); when(userRegisterService.registerUser(any())).thenReturn(UserFixtures.getUserWithId(1L)); when(eggGrantService.grantGreetingEggToUser(any())).thenReturn( EggFixtures.createDefaultEgg()); @@ -101,10 +95,10 @@ void setUp() { SignupTokenPayload payload = new SignupTokenPayload(registerToken, "socialId123", SocialProvider.KAKAO); - when(jwtResolver.getSignupTokenPayload(registerToken)).thenReturn(payload); - when(signupTokenRepository.findByIdAndCreatedAtAfter(eq(registerToken), any())) - .thenReturn(Optional.of(new SignupToken( - registerToken, "refresh", "refresh", SocialProvider.KAKAO))); + when(signupTokenService.extractPayload(registerToken)).thenReturn(payload); + when(signupTokenService.findUnExpiredToken(eq(registerToken))) + .thenReturn(new SignupToken( + registerToken, "refresh", "refresh", SocialProvider.KAKAO)); doThrow(new org.runimo.runimo.auth.exceptions.SignUpException( UserHttpResponseCode.SIGNIN_FAIL_ALREADY_EXIST)) diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml index 57ed44d..50f808a 100644 --- a/src/test/resources/application.yml +++ b/src/test/resources/application.yml @@ -25,18 +25,26 @@ spring: url: jdbc:h2:mem:testdb;MODE=MYSQL;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE username: ${DB_USERNAME} password: ${DB_PASSWORD} + + flyway: + enabled: true + locations: classpath:db/migration/h2 + baseline-on-migrate: true + clean-disabled: false + jpa: hibernate: ddl-auto: none open-in-view: false show-sql: true - defer-datasource-initialization: true + defer-datasource-initialization: false + jackson: property-naming-strategy: SNAKE_CASE sql: init: - mode: always + mode: never schema-locations: classpath*:sql/schema.sql data-locations: diff --git a/src/test/resources/db/migration/h2/V1__Create_baseline.sql b/src/test/resources/db/migration/h2/V1__Create_baseline.sql new file mode 100644 index 0000000..3aeccf2 --- /dev/null +++ b/src/test/resources/db/migration/h2/V1__Create_baseline.sql @@ -0,0 +1,266 @@ +-- V1__Create_baseline.sql +SET FOREIGN_KEY_CHECKS = 0; + +DROP TABLE IF EXISTS egg_type; +DROP TABLE IF EXISTS signup_token; +DROP TABLE IF EXISTS apple_user_token; +DROP TABLE IF EXISTS user_token; +DROP TABLE IF EXISTS oauth_account; +DROP TABLE IF EXISTS running_record; +DROP TABLE IF EXISTS user_item; +DROP TABLE IF EXISTS incubator; +DROP TABLE IF EXISTS runimo; +DROP TABLE IF EXISTS item_activity; +DROP TABLE IF EXISTS runimo_definition; +DROP TABLE IF EXISTS item; +DROP TABLE IF EXISTS user_refresh_token; +DROP TABLE IF EXISTS users; +DROP TABLE IF EXISTS user_love_point; +DROP TABLE IF EXISTS incubating_egg; + +SET FOREIGN_KEY_CHECKS = 1; + +CREATE TABLE `users` +( + `id` BIGINT PRIMARY KEY AUTO_INCREMENT, + `public_id` VARCHAR(255), + `nickname` VARCHAR(255), + `img_url` VARCHAR(255), + `total_distance_in_meters` BIGINT NOT NULL DEFAULT 0, + `total_time_in_seconds` BIGINT NOT NULL DEFAULT 0, + `main_runimo_id` BIGINT, + `gender` VARCHAR(24), + `role` VARCHAR(24) NOT NULL DEFAULT 'USER', + `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + `deleted_at` TIMESTAMP NULL +); + +CREATE TABLE `user_token` +( + `id` BIGINT AUTO_INCREMENT PRIMARY KEY NOT NULL, + `user_id` BIGINT NOT NULL, + `device_token` VARCHAR(255) NOT NULL, + `platform` ENUM ('FCM', 'APNS') NOT NULL DEFAULT 'APNS', + `notification_allowed` BOOLEAN NOT NULL DEFAULT TRUE, + `last_used_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `deleted_at` TIMESTAMP NULL, + + FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE +); + +CREATE TABLE `user_love_point` +( + `id` BIGINT PRIMARY KEY AUTO_INCREMENT, + `user_id` BIGINT NOT NULL, + `amount` BIGINT NOT NULL DEFAULT 0, + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `deleted_at` TIMESTAMP NULL +); + +CREATE TABLE `oauth_account` +( + `id` BIGINT PRIMARY KEY AUTO_INCREMENT, + `user_id` BIGINT NOT NULL, + `provider` VARCHAR(255), + `provider_id` VARCHAR(255), + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `deleted_at` TIMESTAMP NULL, + FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE +); + + +CREATE TABLE `apple_user_token` +( + `id` BIGINT PRIMARY KEY AUTO_INCREMENT, + `user_id` BIGINT NOT NULL, + `refresh_token` VARCHAR(255) NOT NULL, + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `deleted_at` TIMESTAMP NULL, + FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE +); + +CREATE TABLE `signup_token` +( + `token` VARCHAR(255) PRIMARY KEY NOT NULL UNIQUE, + `provider_id` VARCHAR(255) NOT NULL, + `refresh_token` VARCHAR(255), + `provider` VARCHAR(255), + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + + +CREATE TABLE `running_record` +( + `id` BIGINT PRIMARY KEY AUTO_INCREMENT, + `user_id` BIGINT NOT NULL, + `record_public_id` VARCHAR(255) NOT NULL, + `title` VARCHAR(255), + `description` VARCHAR(255), + `img_url` VARCHAR(255), + `started_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + `end_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + `total_distance` BIGINT, + `total_time_in_seconds` BIGINT, + `pace_in_milli_seconds` BIGINT, + `is_rewarded` BOOLEAN, + `pace_per_km` VARCHAR(10000), + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `deleted_at` TIMESTAMP NULL +); + +CREATE TABLE `item` +( + `id` BIGINT PRIMARY KEY AUTO_INCREMENT, + `name` VARCHAR(255) NOT NULL, + `item_code` VARCHAR(255) NOT NULL, + `description` VARCHAR(255), + `item_type` VARCHAR(255) NOT NULL, + `img_url` VARCHAR(255), + `dtype` VARCHAR(255), + `egg_type_id` BIGINT, + `hatch_require_amount` BIGINT, + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `deleted_at` TIMESTAMP NULL +); + +CREATE TABLE `egg_type` +( + `id` BIGINT PRIMARY KEY AUTO_INCREMENT, + `name` VARCHAR(64) NOT NULL, + `code` VARCHAR(64) NOT NULL, + `required_distance_in_meters` BIGINT, + `level` INTEGER, + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `deleted_at` TIMESTAMP NULL + +); + +CREATE TABLE `item_activity` +( + `id` BIGINT PRIMARY KEY AUTO_INCREMENT, + `activity_user_id` BIGINT NOT NULL, + `activity_item_id` BIGINT NOT NULL, + `activity_event_type` VARCHAR(255) NOT NULL, + `quantity` BIGINT, + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `deleted_at` TIMESTAMP NULL +); + +CREATE TABLE `user_item` +( + `id` BIGINT PRIMARY KEY AUTO_INCREMENT, + `user_id` BIGINT NOT NULL, + `item_id` BIGINT NOT NULL, + `quantity` BIGINT NOT NULL, + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `deleted_at` TIMESTAMP NULL +); + +CREATE TABLE `incubating_egg` +( + `id` BIGINT PRIMARY KEY AUTO_INCREMENT, + `user_id` BIGINT NOT NULL, + `egg_id` BIGINT NOT NULL, + `current_love_point_amount` BIGINT, + `hatch_require_amount` BIGINT, + `egg_status` VARCHAR(255), + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `deleted_at` TIMESTAMP NULL +); + +CREATE TABLE `runimo_definition` +( + `id` BIGINT PRIMARY KEY AUTO_INCREMENT, + `name` VARCHAR(255), + `code` VARCHAR(255), + `description` VARCHAR(255), + `img_url` varchar(255), + `egg_type_id` BIGINT NOT NULL, + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `deleted_at` TIMESTAMP NULL +); + +CREATE TABLE `runimo` +( + `id` BIGINT PRIMARY KEY AUTO_INCREMENT, + `user_id` BIGINT NOT NULL, + `runimo_definition_id` BIGINT NOT NULL, + `total_run_count` BIGINT NOT NULL DEFAULT 0, + `total_distance_in_meters` BIGINT NOT NULL DEFAULT 0, + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `deleted_at` TIMESTAMP NULL +); + +CREATE TABLE `user_refresh_token` +( + `id` BIGINT PRIMARY KEY AUTO_INCREMENT, + `user_id` BIGINT NOT NULL UNIQUE, + `refresh_token` TEXT NOT NULL, + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); + +ALTER TABLE `user_token` + ADD FOREIGN KEY (`user_id`) REFERENCES `users` (`id`); + +ALTER TABLE `oauth_account` + ADD FOREIGN KEY (`user_id`) REFERENCES `users` (`id`); + +-- insert static data + +INSERT INTO egg_type (id, name, code, required_distance_in_meters, level, created_at, updated_at) +VALUES (1, '마당', 'A100', 0, 1, NOW(), NOW()), + (2, '숲', 'A101', 30000, 2, NOW(), NOW()); + + +INSERT INTO runimo_definition (id, name, code, description, img_url, egg_type_id, created_at, + updated_at) +VALUES (1, '강아지', 'R-101', '마당-강아지예여', 'http://dummy1', 1, NOW(), NOW()), + (2, '고양이', 'R-102', '마당-고양이예여', 'http://dummy1', 1, NOW(), NOW()), + (3, '토끼', 'R-103', '마당-토끼예여', 'http://dummy1', 1, NOW(), NOW()), + (4, '오리', 'R-104', '마당-오리예여', 'http://dummy1', 1, NOW(), NOW()), + (5, '늑대 강아지', 'R-105', '늑대 강아지예여', 'http://dummy2', 2, NOW(), NOW()), + (6, '숲 고양이', 'R-106', '숲 고양이예여', 'http://dummy2', 2, NOW(), NOW()), + (7, '나뭇잎 토끼', 'R-107', '나뭇잎 토끼예여', 'http://dummy2', 2, NOW(), NOW()), + (8, '숲 오리', 'R-108', '숲 오리예여', 'http://dummy2', 2, NOW(), NOW()); + +INSERT INTO item (id, name, item_code, description, item_type, img_url, dtype, egg_type_id, + hatch_require_amount, created_at, updated_at) +VALUES (1, '마당', 'A100', '기본 알', 'USABLE', 'https://example.com/images/egg1.png', 'EGG', 1, + 100, NOW(), NOW()), + (2, '숲', 'A101', '두번째 단계 알', 'USABLE', 'https://example.com/images/egg2.png', 'EGG', + 2, 30000, NOW(), NOW()); + +-- Existing tables: +-- - users +-- - user_token +-- - user_love_point +-- - oauth_account +-- - apple_user_token +-- - signup_token +-- - running_record +-- - item +-- - egg_type +-- - item_activity +-- - user_item +-- - incubating_egg +-- - runimo_definition +-- - runimo +-- - user_refresh_token + +-- This baseline allows us to start versioned migrations from V2 onwards +SELECT 'Baseline migration - existing schema marked as V1' as message; diff --git a/src/test/resources/db/migration/h2/V20250702__add_version_column_signup_token.sql b/src/test/resources/db/migration/h2/V20250702__add_version_column_signup_token.sql new file mode 100644 index 0000000..93a3598 --- /dev/null +++ b/src/test/resources/db/migration/h2/V20250702__add_version_column_signup_token.sql @@ -0,0 +1,10 @@ +ALTER TABLE `signup_token` + ADD COLUMN `version` BIGINT NOT NULL DEFAULT 0; + +ALTER TABLE `signup_token` + ADD COLUMN `used` BOOLEAN NOT NULL DEFAULT FALSE; + +-- 기존 토큰들은 사용되지 않은 것으로 처리 +UPDATE `signup_token` +SET `used` = FALSE, + `version` = 0; \ No newline at end of file