diff --git a/build.gradle b/build.gradle index b323788..73cc788 100644 --- a/build.gradle +++ b/build.gradle @@ -69,6 +69,8 @@ dependencies { implementation 'software.amazon.awssdk:s3:2.20.89' implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0' implementation 'org.springframework.boot:spring-boot-starter-actuator' + implementation 'org.flywaydb:flyway-core' + implementation 'org.flywaydb:flyway-mysql' compileOnly 'org.projectlombok:lombok' diff --git a/docs/FLYWAY_MIGRATION_TEST_GUIDE.md b/docs/FLYWAY_MIGRATION_TEST_GUIDE.md new file mode 100644 index 0000000..8c7a944 --- /dev/null +++ b/docs/FLYWAY_MIGRATION_TEST_GUIDE.md @@ -0,0 +1,268 @@ + +# Flyway 마이그레이션 리팩토링 가이드 + +## 📋 목차 +1. [변경 사항 개요](#변경-사항-개요) +2. [주요 변경 내용](#주요-변경-내용) +3. [데이터베이스 변경 방법](#데이터베이스-변경-방법) +5. [환경별 설정](#환경별-설정) + +--- + +## 변경 사항 개요 + +### 목적 +- **프로덕션 안정성 향상**: JPA의 `ddl-auto: update`를 제거하고 Flyway로 스키마 버전 관리 +- **스키마 버전 관리**: 모든 DDL/DML 변경을 SQL 마이그레이션 파일로 관리 +- **환경 분리**: 개발 환경과 운영 환경의 데이터를 명확히 분리 + +### 변경 전후 비교 + +| 항목 | 변경 전 | 변경 후 | +|------|---------|---------| +| 스키마 관리 | JPA `ddl-auto: update` | Flyway SQL 마이그레이션 | +| 초기 데이터 | Java `DataInitializer`, `CountryInitializer` | SQL `V2__insert_basic_data.sql`, `V3__insert_countries.sql` | +| 테스트 데이터 | Java 코드 | SQL `V4__test_user.sql` (개발 전용) | +| 스키마 검증 | 자동 생성 | `ddl-auto: validate` + Flyway 검증 | + +--- + +## 주요 변경 내용 + +### 1. 의존성 추가 (`build.gradle`) + +```gradle +dependencies { + implementation 'org.flywaydb:flyway-core' + runtimeOnly 'org.flywaydb:flyway-mysql' +} +``` + +### 2. JPA 설정 변경 (`application.yml`) + +**변경 전:** +```yaml +jpa: + hibernate: + ddl-auto: update # 자동 스키마 생성 +``` + +**변경 후:** +```yaml +jpa: + hibernate: + ddl-auto: validate # 스키마 검증만 수행 + properties: + hibernate: + physical_naming_strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl + +flyway: + enabled: true + baseline-on-migrate: false + validate-on-migrate: true + locations: + - classpath:db/migration + - classpath:db/migration-dev # 개발 환경 전용 +``` + +### 3. Flyway 마이그레이션 파일 구조 + +``` +src/main/resources/db/ +├── migration/ # 프로덕션 + 개발 환경 공통 +│ ├── V1__init.sql # DDL (테이블 생성) +│ ├── V2__insert_basic_data.sql # 기초 데이터 (테마, 감정, 날씨 등) +│ └── V3__insert_countries.sql # 국가 데이터 (245개) +└── migration-dev/ # 개발 환경 전용 + └── V4__test_user.sql # 테스트 사용자 데이터 +``` + +### 4. 엔티티 매핑 명시화 + +모든 엔티티에 명시적 매핑 추가: + +```java +@Entity +@Table(name = "Diary") // 테이블명 명시 +public class Diary { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) // ID 생성 전략 명시 + @Column(name = "diaryId") // 컬럼명 명시 (camelCase) + private Long id; + + @Lob + @Column(name = "content", nullable = false, length = 88) + private String content; // TINYTEXT로 매핑 +} +``` + +**변경된 엔티티 목록:** +- `User`, `Trip`, `Diary`, `DiaryImage` +- `TripTheme`, `BoardTheme`, `Emotion`, `Weather` +- `Country`, `VisitedCountry`, `Setting` +- `Board`, `BoardStickerMap`, `AlarmHistory` +- `Sticker` + +### 5. 데이터 초기화 클래스 비활성화 + +**변경 전:** +```java +@Component // 자동 실행 +public class DataInitializer implements CommandLineRunner { + // ... +} +``` + +**변경 후:** +```java +// @Component - 비활성화: Flyway 마이그레이션으로 대체됨 +public class DataInitializer implements CommandLineRunner { + // ... +} +``` + +**비활성화된 클래스:** +- `DataInitializer.java` +- `CountryInitializer.java` + +### 6. 컬럼명 및 테이블명 통일 + +- **컬럼명**: 모든 컬럼을 `camelCase`로 통일 (예: `userId`, `createdAt`) +- **테이블명**: 엔티티 클래스명과 일치하도록 변경 (예: `User`, `Diary`, `TripTheme`) + +--- + +## 데이터베이스 변경 방법 + +### 새로운 마이그레이션 파일 생성 + +1. **파일명 규칙**: `V{버전}__{설명}.sql` + - 예: `V5__add_user_email_column.sql` + - 버전은 순차적으로 증가 (V1, V2, V3...) + +2. **파일 위치:** + - 프로덕션 포함: `src/main/resources/db/migration/` + - 개발 전용: `src/main/resources/db/migration-dev/` + +3. **마이그레이션 작성 예시:** + +```sql +-- V5__add_user_email_column.sql +ALTER TABLE `User` +ADD COLUMN `email` VARCHAR(255) AFTER `kakaoId`; + +-- 인덱스 추가 +CREATE INDEX `idx_user_email` ON `User` (`email`); +``` + +### 마이그레이션 실행 + +**자동 실행:** +- 애플리케이션 시작 시 Flyway가 자동으로 미적용 마이그레이션 실행 + +**수동 실행 (선택사항):** +```bash +./gradlew flywayMigrate +``` + +### 마이그레이션 롤백 + +⚠️ **주의**: Flyway는 기본적으로 롤백을 지원하지 않습니다. + +**롤백이 필요한 경우:** +1. 새로운 마이그레이션 파일로 롤백 SQL 작성 + ```sql + -- V6__rollback_email_column.sql + ALTER TABLE `User` DROP COLUMN `email`; + ``` + +2. 또는 데이터베이스를 이전 상태로 복원 (백업 필요) + +### 이미 적용된 마이그레이션 수정 금지 + +❌ **절대 하지 말 것:** +- 이미 적용된 마이그레이션 파일(V1, V2, V3...) 수정 +- Checksum 불일치로 인한 오류 발생 + +✅ **올바른 방법:** +- 새로운 버전의 마이그레이션 파일로 변경사항 추가 + +--- + +## 환경별 설정 + +### 개발 환경 (`application.yml`) + +```yaml +spring: + flyway: + enabled: true + baseline-on-migrate: false + validate-on-migrate: true + locations: + - classpath:db/migration # 공통 마이그레이션 + - classpath:db/migration-dev # 개발 전용 (테스트 데이터) +``` + +**포함되는 마이그레이션:** +- V1: DDL (테이블 생성) +- V2: 기초 데이터 +- V3: 국가 데이터 +- V4: 테스트 사용자 (개발 전용) + +### 운영 환경 (`application-prod.yml`) + +```yaml +spring: + config: + activate: + on-profile: prod + + flyway: + enabled: true + baseline-on-migrate: true # 기존 DB에 Flyway 적용 시 필요 + validate-on-migrate: true + locations: classpath:db/migration # 개발 전용 제외 +``` + +**포함되는 마이그레이션:** +- V1: DDL (테이블 생성) +- V2: 기초 데이터 +- V3: 국가 데이터 +- ❌ V4: 테스트 사용자 (제외됨) + +### 프로파일 활성화 + +**로컬 개발:** +```bash +# 기본 프로파일 (application.yml 사용) +./gradlew bootRun +``` + +**운영 배포:** +```bash +# prod 프로파일 활성화 +java -jar app.jar --spring.profiles.active=prod +``` + +--- + +## 체크리스트 + +### 새로운 스키마 변경 시 + +- [ ] 마이그레이션 파일명이 `V{버전}__{설명}.sql` 형식인가? +- [ ] 버전 번호가 기존 마이그레이션보다 큰가? +- [ ] 개발 전용 데이터인가? → `migration-dev/` 사용 +- [ ] 엔티티에 `@Table`, `@Column` 매핑이 명시되어 있는가? +- [ ] `@GeneratedValue(strategy = GenerationType.IDENTITY)` 설정되어 있는가? +- [ ] 로컬에서 테스트 후 커밋했는가? + +### 배포 전 확인 + +- [ ] `application-prod.yml`에 `baseline-on-migrate: true` 설정되어 있는가? +- [ ] 운영 환경에서 테스트 데이터 마이그레이션이 제외되는가? +- [ ] 데이터베이스 백업을 수행했는가? + + +**마지막 업데이트**: 2026-02-03 diff --git a/src/main/java/zim/tave/memory/controller/AlarmController.java b/src/main/java/zim/tave/memory/controller/AlarmController.java index 92414c5..5a540c0 100644 --- a/src/main/java/zim/tave/memory/controller/AlarmController.java +++ b/src/main/java/zim/tave/memory/controller/AlarmController.java @@ -17,14 +17,12 @@ import org.springframework.web.bind.annotation.RestController; import zim.tave.memory.config.swagger.ApiErrorCodeExamples; import zim.tave.memory.domain.AlarmType; -import zim.tave.memory.domain.User; import zim.tave.memory.dto.response.AlarmSendResponseDto; import zim.tave.memory.global.common.ApiResponseDto; import zim.tave.memory.global.common.ResponseCode; import zim.tave.memory.global.common.exception.ErrorCode; import zim.tave.memory.service.AlarmService; -import java.util.Map; @RestController @RequiredArgsConstructor diff --git a/src/main/java/zim/tave/memory/domain/AlarmHistory.java b/src/main/java/zim/tave/memory/domain/AlarmHistory.java index 3805259..4570611 100644 --- a/src/main/java/zim/tave/memory/domain/AlarmHistory.java +++ b/src/main/java/zim/tave/memory/domain/AlarmHistory.java @@ -14,28 +14,28 @@ public class AlarmHistory { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "alarm_history_id") + @Column(name = "alarmHistoryId") private Long id; // 알림을 받을 사용자 - @Column(nullable = false) + @Column(name = "userId", nullable = false) private Long userId; // 어떤 여행에 대한 알림인지 - @Column(nullable = false) + @Column(name = "tripId", nullable = false) private Long tripId; // 알림 종류 @Enumerated(EnumType.STRING) - @Column(nullable = false, length = 50) + @Column(name = "alarmType", nullable = false, length = 50) private AlarmType alarmType; // 실제 발송 시각 - @Column(nullable = false) + @Column(name = "sentAt", nullable = false) private LocalDateTime sentAt; // 일 단위 조회 최적화를 위한 필드 (캡 계산용) - @Column(nullable = false) + @Column(name = "sentDate", nullable = false) private LocalDate sentDate; public static AlarmHistory create( diff --git a/src/main/java/zim/tave/memory/domain/Board.java b/src/main/java/zim/tave/memory/domain/Board.java index bb69fbc..5dfcded 100644 --- a/src/main/java/zim/tave/memory/domain/Board.java +++ b/src/main/java/zim/tave/memory/domain/Board.java @@ -18,14 +18,16 @@ public class Board { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "boardId") private Long boardId; - @Column(nullable = false, length = 100) + @Column(name = "title", nullable = false, length = 100) private String title; - @Column(nullable = false, updatable = false) + @Column(name = "createdAt", nullable = false, updatable = false) private OffsetDateTime createdAt; + @Column(name = "updatedAt") private OffsetDateTime updatedAt; @ManyToOne(fetch = FetchType.LAZY) diff --git a/src/main/java/zim/tave/memory/domain/BoardStickerMap.java b/src/main/java/zim/tave/memory/domain/BoardStickerMap.java index 51f2e12..29034df 100644 --- a/src/main/java/zim/tave/memory/domain/BoardStickerMap.java +++ b/src/main/java/zim/tave/memory/domain/BoardStickerMap.java @@ -14,6 +14,7 @@ public class BoardStickerMap { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "boardStickerId") private Long boardStickerId; @ManyToOne(fetch = FetchType.LAZY) @@ -24,12 +25,12 @@ public class BoardStickerMap { @JoinColumn(name = "stickerId", nullable = false) private Sticker sticker; - @Column(nullable = false) + @Column(name = "posX", nullable = false) private BigDecimal posX; - @Column(nullable = false) + @Column(name = "posY", nullable = false) private BigDecimal posY; - @Column(nullable = false) + @Column(name = "rotation", nullable = false) private BigDecimal rotation; } diff --git a/src/main/java/zim/tave/memory/domain/BoardTheme.java b/src/main/java/zim/tave/memory/domain/BoardTheme.java index b9b5649..3c5c62c 100644 --- a/src/main/java/zim/tave/memory/domain/BoardTheme.java +++ b/src/main/java/zim/tave/memory/domain/BoardTheme.java @@ -13,14 +13,15 @@ public class BoardTheme { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "boardThemeId") private Long boardThemeId; - @Column(nullable = false, length = 100) + @Column(name = "themeName", nullable = false, length = 100) private String themeName; - @Column(nullable = false, length = 255) + @Column(name = "thumbnailUrl", nullable = false, length = 255) private String thumbnailUrl; - @Column(nullable = false, length = 255) + @Column(name = "cardUrl", nullable = false, length = 255) private String cardUrl; } diff --git a/src/main/java/zim/tave/memory/domain/Country.java b/src/main/java/zim/tave/memory/domain/Country.java index ed1bd9f..1928dc1 100644 --- a/src/main/java/zim/tave/memory/domain/Country.java +++ b/src/main/java/zim/tave/memory/domain/Country.java @@ -1,8 +1,6 @@ package zim.tave.memory.domain; -import jakarta.persistence.Entity; -import jakarta.persistence.Id; -import jakarta.persistence.Column; +import jakarta.persistence.*; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @@ -14,8 +12,10 @@ public class Country { @Id + @Column(name = "countryCode") private String countryCode; // 예: "KR", "US" + @Column(name = "countryName") private String countryName; @Column(columnDefinition = "VARCHAR(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci") diff --git a/src/main/java/zim/tave/memory/domain/Diary.java b/src/main/java/zim/tave/memory/domain/Diary.java index 3283d15..9f025c2 100644 --- a/src/main/java/zim/tave/memory/domain/Diary.java +++ b/src/main/java/zim/tave/memory/domain/Diary.java @@ -14,7 +14,8 @@ @Getter @Setter public class Diary { - @Id @GeneratedValue + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "diaryId") private Long id; @@ -30,16 +31,17 @@ public class Diary { @JoinColumn(name = "countryId") private Country country; - @Column(nullable = false) + @Column(name = "city", nullable = false) private String city; - @Column(nullable = false) + @Column(name = "dateTime", nullable = false) private OffsetDateTime dateTime; @Lob - @Column(nullable = false, length = 88) + @Column(name = "content", nullable = false, length = 88) private String content; + @Column(name = "detailedLocation") private String detailedLocation; @ManyToOne(fetch = FetchType.LAZY) @@ -54,10 +56,10 @@ public class Diary { @BatchSize(size = 20) // lazy loading 시 20개씩 조회 private List diaryImages = new ArrayList<>(); - @Column(updatable = false) + @Column(name = "createdAt", updatable = false) private OffsetDateTime createdAt; - @Column(nullable = false) + @Column(name = "isStored", nullable = false) private Boolean isStored; @PrePersist diff --git a/src/main/java/zim/tave/memory/domain/DiaryImage.java b/src/main/java/zim/tave/memory/domain/DiaryImage.java index ba4a53b..6628d3a 100644 --- a/src/main/java/zim/tave/memory/domain/DiaryImage.java +++ b/src/main/java/zim/tave/memory/domain/DiaryImage.java @@ -6,11 +6,12 @@ import com.fasterxml.jackson.annotation.JsonCreator; @Entity +@Table(name = "DiaryImage") @Getter @Setter public class DiaryImage { @Id - @GeneratedValue + @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "DiaryImageId") private Long id; @@ -18,15 +19,17 @@ public class DiaryImage { @JoinColumn(name = "diaryId") private Diary diary; - @Column(nullable = false) + @Column(name = "imageUrl", nullable = false) private String imageUrl; @Enumerated(EnumType.STRING) - @Column(nullable = false) + @Column(name = "cameraType", nullable = false) private CameraType cameraType; // FRONT, BACK + @Column(name = "isRepresentative") private boolean isRepresentative; // 사용자가 선택한 대표사진 + @Column(name = "imageOrder") private int imageOrder; // 이미지 순서 (1, 2) public enum CameraType { diff --git a/src/main/java/zim/tave/memory/domain/Emotion.java b/src/main/java/zim/tave/memory/domain/Emotion.java index 03a0026..fd8a18e 100644 --- a/src/main/java/zim/tave/memory/domain/Emotion.java +++ b/src/main/java/zim/tave/memory/domain/Emotion.java @@ -10,11 +10,14 @@ @Getter @Setter public class Emotion { - @Id @GeneratedValue + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "emotion_id") private Long id; + @Column(name = "name") private String name; + @Column(name = "colorCode") private String colorCode; public Emotion(String name, String colorCode) { diff --git a/src/main/java/zim/tave/memory/domain/Sticker.java b/src/main/java/zim/tave/memory/domain/Sticker.java index 521b697..16cf907 100644 --- a/src/main/java/zim/tave/memory/domain/Sticker.java +++ b/src/main/java/zim/tave/memory/domain/Sticker.java @@ -13,11 +13,12 @@ public class Sticker { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "stickerId") private Long stickerId; - @Column(nullable = false) + @Column(name = "name", nullable = false) private String name; - @Column(nullable = false) + @Column(name = "imageUrl", nullable = false) private String imageUrl; } diff --git a/src/main/java/zim/tave/memory/domain/Trip.java b/src/main/java/zim/tave/memory/domain/Trip.java index 5d20091..46bc75e 100644 --- a/src/main/java/zim/tave/memory/domain/Trip.java +++ b/src/main/java/zim/tave/memory/domain/Trip.java @@ -15,24 +15,27 @@ @Getter @Setter public class Trip { - @Id @GeneratedValue + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "tripId") private Long id; - @Column(length = 14, nullable = false) + @Column(name = "tripName", length = 14, nullable = false) private String tripName; - @Column(length = 56) + @Column(name = "description", length = 56) private String description; - @Column(nullable = false) + @Column(name = "startDate", nullable = false) private LocalDate startDate; - @Column(nullable = false) + @Column(name = "endDate", nullable = false) private LocalDate endDate; + @Column(name = "isStored") private Boolean isStored = false; //보관 여부 확인 + @Column(name = "isPast") private Boolean isPast = false; //과거 여행 여부 확인 @JsonBackReference @@ -45,8 +48,10 @@ public class Trip { private TripTheme tripTheme; @Lob + @Column(name = "content") private String content; + @Column(name = "representativeImageUrl") private String representativeImageUrl; @JsonManagedReference diff --git a/src/main/java/zim/tave/memory/domain/TripTheme.java b/src/main/java/zim/tave/memory/domain/TripTheme.java index 2aebd40..3f6d6fa 100644 --- a/src/main/java/zim/tave/memory/domain/TripTheme.java +++ b/src/main/java/zim/tave/memory/domain/TripTheme.java @@ -14,13 +14,17 @@ @NoArgsConstructor public class TripTheme { - @Id @GeneratedValue + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + @Column(name = "themeName") private String themeName; + @Column(name = "sampleImageUrl") private String sampleImageUrl; // 사용자가 테마를 선택할 때 보여줄 샘플 이미지 + @Column(name = "cardImageUrl") private String cardImageUrl; // 실제 카드에 들어갈 이미지 @JsonManagedReference diff --git a/src/main/java/zim/tave/memory/domain/User.java b/src/main/java/zim/tave/memory/domain/User.java index cdaaf98..2fc6c18 100644 --- a/src/main/java/zim/tave/memory/domain/User.java +++ b/src/main/java/zim/tave/memory/domain/User.java @@ -19,31 +19,38 @@ public class User { private Long id; //카카오 로그인 - @Column(unique=true) + @Column(name = "kakaoId", unique=true) private String kakaoId; + @Column(name = "profileImageUrl") private String profileImageUrl; - @Column(name = "sur_name") + @Column(name = "surName") private String surName; - @Column(name = "first_name") + @Column(name = "firstName") private String firstName; - @Column(name = "korean_name") + @Column(name = "koreanName") private String koreanName; + @Column(name = "createdAt") private LocalDate createdAt; + @Column(name = "status") private boolean status; + @Column(name = "birth") private LocalDate birth; + @Column(name = "nationality") private String nationality; - @Column(nullable = false) + @Column(name = "isRegistered", nullable = false) private boolean isRegistered; // 회원가입 완료 여 (true = 가입 완료) //마이페이지 Statistics 정보 + @Column(name = "diaryCount") private Long diaryCount; //일기 수 + @Column(name = "visitedCountryCount") private Long visitedCountryCount; //방문한 나라 수 - @Column(length = 255) + @Column(name = "flags", length = 255) private String flags; //국기 @JsonManagedReference diff --git a/src/main/java/zim/tave/memory/domain/VisitedCountry.java b/src/main/java/zim/tave/memory/domain/VisitedCountry.java index 008e212..aefd38d 100644 --- a/src/main/java/zim/tave/memory/domain/VisitedCountry.java +++ b/src/main/java/zim/tave/memory/domain/VisitedCountry.java @@ -16,10 +16,13 @@ public class VisitedCountry { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "visitedCountryId") private Long visitedCountryId; + @Column(name = "color") private String color; + @Column(name = "createdAt") private OffsetDateTime createdAt; @ManyToOne(fetch = FetchType.LAZY) diff --git a/src/main/java/zim/tave/memory/domain/Weather.java b/src/main/java/zim/tave/memory/domain/Weather.java index 906affb..efa8e5f 100644 --- a/src/main/java/zim/tave/memory/domain/Weather.java +++ b/src/main/java/zim/tave/memory/domain/Weather.java @@ -8,10 +8,13 @@ @Getter @Setter public class Weather { - @Id @GeneratedValue + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "weather_id") private Long id; + @Column(name = "name") private String name; + @Column(name = "iconUrl") private String iconUrl; } diff --git a/src/main/java/zim/tave/memory/init/CountryInitializer.java b/src/main/java/zim/tave/memory/init/CountryInitializer.java deleted file mode 100644 index f4afbbf..0000000 --- a/src/main/java/zim/tave/memory/init/CountryInitializer.java +++ /dev/null @@ -1,18 +0,0 @@ -package zim.tave.memory.init; - -import lombok.RequiredArgsConstructor; -import org.springframework.boot.CommandLineRunner; -import org.springframework.stereotype.Component; -import zim.tave.memory.service.CountryService; - -@Component -@RequiredArgsConstructor -public class CountryInitializer implements CommandLineRunner { - - private final CountryService countryService; - - @Override - public void run(String... args) { - countryService.init(); - } -} diff --git a/src/main/java/zim/tave/memory/init/DataInitializer.java b/src/main/java/zim/tave/memory/init/DataInitializer.java deleted file mode 100644 index 1c114e1..0000000 --- a/src/main/java/zim/tave/memory/init/DataInitializer.java +++ /dev/null @@ -1,220 +0,0 @@ -package zim.tave.memory.init; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.boot.CommandLineRunner; -import org.springframework.stereotype.Component; -import org.springframework.transaction.annotation.Transactional; -import zim.tave.memory.domain.*; -import zim.tave.memory.repository.*; - -@Slf4j -@Component -@RequiredArgsConstructor -public class DataInitializer implements CommandLineRunner { - - private final TripThemeRepository tripThemeRepository; - private final EmotionRepository emotionRepository; - private final WeatherRepository weatherRepository; - private final UserRepository userRepository; - private final BoardThemeRepository boardThemeRepository; - private final StickerRepository stickerRepository; - - @Override - @Transactional - public void run(String... args) throws Exception { - log.info("기본 데이터 초기화 시작..."); - - // 테마 데이터 초기화 - initTripThemes(); - - // 감정 데이터 초기화 - initEmotions(); - - // 날씨 데이터 초기화 - initWeathers(); - - // 보드 데이터 초기화 - initBoardThemes(); - - // 보드 스티커 초기화 - initStickers(); - - // 테스트 사용자 데이터 초기화 - initTestUser(); - - log.info("기본 데이터 초기화 완료!"); - } - - private void initTripThemes() { - if (tripThemeRepository.count() == 0) { - log.info("여행 테마 데이터 생성 중..."); - - // 기본 테마 (ID 1) - TripTheme basicTheme = new TripTheme("기본", - "https://me-mory01.mooo.com/api/files?key=images/d69be93f-fef6-4a73-815f-ea6b93fd2d59_trip_thumb_default.png", - "https://me-mory01.mooo.com/api/files?key=images/6e56681d-9fc3-43d2-a803-0ef165c63c3c_trip_card_default.png"); - tripThemeRepository.save(basicTheme); - - // Grey 테마 (ID 2) - TripTheme greyTheme = new TripTheme("Grey", - "https://me-mory01.mooo.com/api/files?key=images/f0d5e1f7-2718-4dac-a780-d6f6e5f2d42a_trip_thumb_grey.png", - "https://me-mory01.mooo.com/api/files?key=images/462d91ad-b618-4449-805f-35f07187247b_trip_card_grey.png"); - tripThemeRepository.save(greyTheme); - - // 탑승권 테마 (ID 3) - TripTheme ticketTheme = new TripTheme("탑승권", - "https://me-mory01.mooo.com/api/files?key=images/cbcf48b8-8469-4e0f-b597-7ef27cc6190e_trip_thumb_boardingpass.png", - "https://me-mory01.mooo.com/api/files?key=images/3e637461-f9a2-477d-ae06-4245033174a9_trip_card_boardingpass.png"); - tripThemeRepository.save(ticketTheme); - - // 액자 테마 (ID 4) - TripTheme frameTheme = new TripTheme("액자", - "https://me-mory01.mooo.com/api/files?key=images/c3efb8f8-f4a8-41a1-9d29-e93bc9d2dd83_trip_thumb_frame.png", - "https://me-mory01.mooo.com/api/files?key=images/eade6dc1-c412-4924-af7f-832292b258c0_trip_card_frame.png"); - tripThemeRepository.save(frameTheme); - - // Beach 테마 (ID 5) - TripTheme beachTheme = new TripTheme("Beach", - "https://me-mory01.mooo.com/api/files?key=images/f692134b-0676-47e9-9782-778f7df9a23a_trip_thumb_beach.png", - "https://me-mory01.mooo.com/api/files?key=images/94abb8d5-5f56-4061-8e90-1df9c9107caf_trip_card_beach.png"); - tripThemeRepository.save(beachTheme); - - // Forest 테마 (ID 6) - TripTheme forestTheme = new TripTheme("Forest", - "https://me-mory01.mooo.com/api/files?key=images/6f40c3c9-313f-44e5-9624-3264991b2f9b_trip_thumb_forest.png", - "https://me-mory01.mooo.com/api/files?key=images/99589e53-9539-46eb-b702-644fab7a87e3_trip_card_forest.png"); - tripThemeRepository.save(forestTheme); - - log.info("여행 테마 데이터 생성 완료: {}개", tripThemeRepository.count()); - } - } - - private void initEmotions() { - if (emotionRepository.count() == 0) { - log.info("감정 데이터 생성 중..."); - - emotionRepository.save(new Emotion("기본", "#EEEEEE")); - emotionRepository.save(new Emotion("설렘", "#FDD7DE")); - emotionRepository.save(new Emotion("신기함", "#FFCB6B")); - emotionRepository.save(new Emotion("즐거움", "#FFE13E")); - emotionRepository.save(new Emotion("힐링", "#C1E8A0")); - emotionRepository.save(new Emotion("평온", "#D1E5D4")); - emotionRepository.save(new Emotion("뿌듯함", "#5ACFD5")); - emotionRepository.save(new Emotion("해방감", "#5EB6D9")); - emotionRepository.save(new Emotion("낯섦", "#634E72")); - emotionRepository.save(new Emotion("긴장됨", "#2C3E50")); - emotionRepository.save(new Emotion("외로움", "#A9A9B0")); - emotionRepository.save(new Emotion("아쉬움", "#866868")); - emotionRepository.save(new Emotion("벅참", "#800020")); - - - log.info("감정 데이터 생성 완료: {}개", emotionRepository.count()); - } - } - - private void initWeathers() { - if (weatherRepository.count() == 0) { - log.info("날씨 데이터 생성 중..."); - - Weather sunny = new Weather(); - sunny.setName("맑음"); - sunny.setIconUrl("https://me-mory01.mooo.com/api/files?key=images/7de47dfb-6f09-47d2-9268-2b8c49c2f9bd_weather_sunny.png"); - weatherRepository.save(sunny); - - Weather cloudy = new Weather(); - cloudy.setName("구름"); - cloudy.setIconUrl("https://me-mory01.mooo.com/api/files?key=images/6a8c4a38-0ccd-479c-ba70-7202d7535bf7_weather_cloudy.png"); - weatherRepository.save(cloudy); - - Weather rainy = new Weather(); - rainy.setName("비"); - rainy.setIconUrl("https://me-mory01.mooo.com/api/files?key=images/72a8e191-9b31-4646-ae48-cb31fb543de7_weather_rainy.png"); - weatherRepository.save(rainy); - - Weather windy = new Weather(); - windy.setName("바람"); - windy.setIconUrl("https://me-mory01.mooo.com/api/files?key=images/dae2b4e2-7ac4-45fe-bccc-be835218d8d4_weather_windy.png"); - weatherRepository.save(windy); - - Weather snowy = new Weather(); - snowy.setName("눈"); - snowy.setIconUrl("https://me-mory01.mooo.com/api/files?key=images/3b298957-360e-4ea4-86ea-bcd65fcd3e77_weather_snowy.png"); - weatherRepository.save(snowy); - - log.info("날씨 데이터 생성 완료: {}개", weatherRepository.count()); - } - } - - private void initBoardThemes() { - if (boardThemeRepository.count() == 0) { - log.info("보드 테마 데이터 생성 중..."); - - BoardTheme chalkboard = new BoardTheme(); - chalkboard.setThemeName("칠판"); - chalkboard.setThumbnailUrl("https://me-mory01.mooo.com/api/files?key=images/board_theme/chalkboard_thumb.png"); - chalkboard.setCardUrl("https://me-mory01.mooo.com/api/files?key=images/73212582-a940-4929-9c9b-f99ce68f3238_board_card_chalkboard.png"); - boardThemeRepository.save(chalkboard); - - BoardTheme tablecloth = new BoardTheme(); - tablecloth.setThemeName("식탁보"); - tablecloth.setThumbnailUrl("https://me-mory01.mooo.com/api/files?key=images/board_theme/tablecloth_thumb.png"); - tablecloth.setCardUrl("https://me-mory01.mooo.com/api/files?key=images/ea8230ad-a70a-4f87-acb4-648add56b1b8_board_card_tablecloth.png"); - boardThemeRepository.save(tablecloth); - - BoardTheme board = new BoardTheme(); - board.setThemeName("나무보드"); - board.setThumbnailUrl("https://me-mory01.mooo.com/api/files?key=images/board_theme/board_thumb.png"); - board.setCardUrl("https://me-mory01.mooo.com/api/files?key=images/ecee4553-72cc-4097-8d20-10a7ee889269_board_card_woodboard.png"); - boardThemeRepository.save(board); - - BoardTheme chess = new BoardTheme(); - chess.setThemeName("체스판"); - chess.setThumbnailUrl("https://me-mory01.mooo.com/api/files?key=images/board_theme/chess_thumb.png"); - chess.setCardUrl("https://me-mory01.mooo.com/api/files?key=images/e026afe7-1619-4ee5-b3e7-c2564d88447c_board_card_chess.png"); - boardThemeRepository.save(chess); - - log.info("보드 테마 데이터 생성 완료: {}개", boardThemeRepository.count()); - } - } - - private void initStickers() { - if (stickerRepository.count() == 0) { - log.info("스티커 데이터 생성 중..."); - - Sticker starSticker = new Sticker(); - starSticker.setName("Star"); - starSticker.setImageUrl("https://me-mory01.mooo.com/api/files?key=images/6d4c8b2f-00ec-41ae-bd57-9ce914e1c78d_sticker_star.png"); - - stickerRepository.save(starSticker); - - log.info("스티커 데이터 생성 완료: {}개", stickerRepository.count()); - } - } - - private void initTestUser() { - // 테스트용 사용자가 없으면 생성 - if (!userRepository.findByKakaoId("test_강지혜").isPresent()) { - log.info("테스트 사용자 데이터 생성 중..."); - - User testUser = new User(); - testUser.setKakaoId("test_강지혜"); - testUser.setSurName("KANG"); - testUser.setFirstName("JIHYE"); - testUser.setKoreanName("강지혜"); - testUser.setBirth(java.time.LocalDate.of(1995, 3, 15)); - testUser.setNationality("REPUBLIC OF KOREA"); - testUser.setCreatedAt(java.time.LocalDate.now()); - testUser.setStatus(true); - testUser.setProfileImageUrl("https://me-mory.mooo.com/api/files?key=images/7c24aa61-2d36-48fd-80a5-646ac518256d_IMG_3831.jpg"); - testUser.setDiaryCount(0L); - testUser.setVisitedCountryCount(0L); - testUser.setFlags("🇰🇷"); - userRepository.save(testUser); - - log.info("테스트 사용자 데이터 생성 완료"); - } else { - log.info("테스트 사용자가 이미 존재합니다."); - } - } -} diff --git a/src/main/java/zim/tave/memory/service/CountryService.java b/src/main/java/zim/tave/memory/service/CountryService.java index fb01ae2..1b30183 100644 --- a/src/main/java/zim/tave/memory/service/CountryService.java +++ b/src/main/java/zim/tave/memory/service/CountryService.java @@ -53,267 +53,4 @@ public void saveCountry(Country country) { } countryRepository.save(country); } - - @Transactional - public void init() { - log.info("CountryService.init() 실행됨"); - if (!countryRepository.findAll().isEmpty()) { - log.info("이미 Country 데이터가 존재함 → init 종료"); - return; - } - - - List countries = List.of( - new Country("GH", "가나", "🇬🇭"), - new Country("GA", "가봉", "🇬🇦"), - new Country("GY", "가이아나", "🇬🇾"), - new Country("GM", "감비아", "🇬🇲"), - new Country("GG", "건지", "🇬🇬"), - new Country("GP", "과들루프", "🇬🇵"), - new Country("GT", "과테말라", "🇬🇹"), - new Country("GU", "괌", "🇬🇺"), - new Country("GD", "그레나다", "🇬🇩"), - new Country("GR", "그리스", "🇬🇷"), - new Country("GL", "그린란드", "🇬🇱"), - new Country("GN", "기니", "🇬🇳"), - new Country("GW", "기니비사우", "🇬🇼"), - new Country("NA", "나미비아", "🇳🇦"), - new Country("NR", "나우루", "🇳🇷"), - new Country("NG", "나이지리아", "🇳🇬"), - new Country("AQ", "남극", "🇦🇶"), - new Country("SS", "남수단", "🇸🇸"), - new Country("ZA", "남아프리카공화국", "🇿🇦"), - new Country("NL", "네덜란드", "🇳🇱"), - new Country("NP", "네팔", "🇳🇵"), - new Country("NO", "노르웨이", "🇳🇴"), - new Country("NF", "노퍽섬", "🇳🇫"), - new Country("NZ", "뉴질랜드", "🇳🇿"), - new Country("NC", "뉴칼레도니아", "🇳🇨"), - new Country("NU", "니우에", "🇳🇺"), - new Country("NE", "니제르", "🇳🇪"), - new Country("NI", "니카라과", "🇳🇮"), - new Country("TW", "대만", "🇹🇼"), - new Country("KR", "대한민국", "🇰🇷"), - new Country("DK", "덴마크", "🇩🇰"), - new Country("DM", "도미니카", "🇩🇲"), - new Country("DO", "도미니카 공화국", "🇩🇴"), - new Country("DE", "독일", "🇩🇪"), - new Country("TL", "동티모르", "🇹🇱"), - new Country("LA", "라오스", "🇱🇦"), - new Country("LR", "라이베리아", "🇱🇷"), - new Country("LV", "라트비아", "🇱🇻"), - new Country("RU", "러시아", "🇷🇺"), - new Country("LB", "레바논", "🇱🇧"), - new Country("LS", "레소토", "🇱🇸"), - new Country("RE", "레위니옹", "🇷🇪"), - new Country("RO", "루마니아", "🇷🇴"), - new Country("LU", "룩셈부르크", "🇱🇺"), - new Country("RW", "르완다", "🇷🇼"), - new Country("LY", "리비아", "🇱🇾"), - new Country("LT", "리투아니아", "🇱🇹"), - new Country("LI", "리히텐슈타인", "🇱🇮"), - new Country("MG", "마다가스카르", "🇲🇬"), - new Country("MQ", "마르티니크", "🇲🇶"), - new Country("MH", "마셜제도", "🇲🇭"), - new Country("YT", "마요트", "🇾🇹"), - new Country("MO", "마카오", "🇲🇴"), - new Country("MW", "말라위", "🇲🇼"), - new Country("MY", "말레이시아", "🇲🇾"), - new Country("ML", "말리", "🇲🇱"), - new Country("IM", "맨섬", "🇮🇲"), - new Country("MX", "멕시코", "🇲🇽"), - new Country("MC", "모나코", "🇲🇨"), - new Country("MA", "모로코", "🇲🇦"), - new Country("MR", "모리타니", "🇲🇷"), - new Country("MZ", "모잠비크", "🇲🇿"), - new Country("ME", "몬테네그로", "🇲🇪"), - new Country("MS", "몬트세라트", "🇲🇸"), - new Country("MD", "몰도바", "🇲🇩"), - new Country("MV", "몰디브", "🇲🇻"), - new Country("MT", "몰타", "🇲🇹"), - new Country("MN", "몽골", "🇲🇳"), - new Country("US", "미국", "🇺🇸"), - new Country("UM", "미국령 군소 제도", "🇺🇲"), - new Country("VI", "미국령 버진아일랜드", "🇻🇮"), - new Country("MM", "미얀마", "🇲🇲"), - new Country("FM", "미크로네시아", "🇫🇲"), - new Country("VU", "바누아투", "🇻🇺"), - new Country("BH", "바레인", "🇧🇭"), - new Country("BB", "바베이도스", "🇧🇧"), - new Country("VA", "바티칸", "🇻🇦"), - new Country("BS", "바하마", "🇧🇸"), - new Country("BD", "방글라데시", "🇧🇩"), - new Country("BM", "버뮤다", "🇧🇲"), - new Country("BJ", "베냉", "🇧🇯"), - new Country("VE", "베네수엘라", "🇻🇪"), - new Country("VN", "베트남", "🇻🇳"), - new Country("BY", "벨라루스", "🇧🇾"), - new Country("BZ", "벨리즈", "🇧🇿"), - new Country("BQ", "보네르섬, 신트유스타티우스섬, 사바섬", "🇧🇶"), - new Country("BA", "보스니아 헤르체고비나", "🇧🇦"), - new Country("BW", "보츠와나", "🇧🇼"), - new Country("BO", "볼리비아", "🇧🇴"), - new Country("BI", "부룬디", "🇧🇮"), - new Country("BF", "부르키나파소", "🇧🇫"), - new Country("BV", "부베섬", "🇧🇻"), - new Country("BT", "부탄", "🇧🇹"), - new Country("MP", "북마리아나제도", "🇲🇵"), - new Country("MK", "북마케도니아", "🇲🇰"), - new Country("BG", "불가리아", "🇧🇬"), - new Country("BR", "브라질", "🇧🇷"), - new Country("BN", "브루나이", "🇧🇳"), - new Country("WS", "사모아", "🇼🇸"), - new Country("SA", "사우디아라비아", "🇸🇦"), - new Country("GS", "사우스조지아 사우스샌드위치 제도", "🇬🇸"), - new Country("SM", "산마리노", "🇸🇲"), - new Country("ST", "상투메 프린시페", "🇸🇹"), - new Country("MF", "생마르탱(프랑스령)", "🇲🇫"), - new Country("BL", "생바르텔레미", "🇧🇱"), - new Country("PM", "생피에르 미클롱", "🇵🇲"), - new Country("EH", "서사하라", "🇪🇭"), - new Country("SN", "세네갈", "🇸🇳"), - new Country("RS", "세르비아", "🇷🇸"), - new Country("SC", "세이셸", "🇸🇨"), - new Country("LC", "세인트루시아", "🇱🇨"), - new Country("VC", "세인트빈센트 그레나딘", "🇻🇨"), - new Country("KN", "세인트키츠 네비스", "🇰🇳"), - new Country("SH", "세인트헬레나", "🇸🇭"), - new Country("SO", "소말리아", "🇸🇴"), - new Country("SB", "솔로몬 제도", "🇸🇧"), - new Country("SD", "수단", "🇸🇩"), - new Country("SR", "수리남", "🇸🇷"), - new Country("LK", "스리랑카", "🇱🇰"), - new Country("SJ", "스발바르 얀마옌 제도", "🇸🇯"), - new Country("SE", "스웨덴", "🇸🇪"), - new Country("CH", "스위스", "🇨🇭"), - new Country("ES", "스페인", "🇪🇸"), - new Country("SK", "슬로바키아", "🇸🇰"), - new Country("SI", "슬로베니아", "🇸🇮"), - new Country("SY", "시리아", "🇸🇾"), - new Country("SL", "시에라리온", "🇸🇱"), - new Country("SX", "신트마르턴", "🇸🇽"), - new Country("SG", "싱가포르", "🇸🇬"), - new Country("AE", "아랍에미리트", "🇦🇪"), - new Country("AW", "아루바", "🇦🇼"), - new Country("AM", "아르메니아", "🇦🇲"), - new Country("AS", "아메리칸사모아", "🇦🇸"), - new Country("IS", "아이슬란드", "🇮🇸"), - new Country("HT", "아이티", "🇭🇹"), - new Country("IE", "아일랜드", "🇮🇪"), - new Country("AZ", "아제르바이잔", "🇦🇿"), - new Country("AF", "아프가니스탄", "🇦🇫"), - new Country("AD", "안도라", "🇦🇩"), - new Country("AG", "안티구아 바부다", "🇦🇬"), - new Country("AL", "알바니아", "🇦🇱"), - new Country("DZ", "알제리", "🇩🇿"), - new Country("AO", "앙골라", "🇦🇴"), - new Country("AI", "앵귈라", "🇦🇮"), - new Country("ER", "에리트레아", "🇪🇷"), - new Country("SZ", "에스와티니", "🇸🇿"), - new Country("EE", "에스토니아", "🇪🇪"), - new Country("EC", "에콰도르", "🇪🇨"), - new Country("ET", "에티오피아", "🇪🇹"), - new Country("SV", "엘살바도르", "🇸🇻"), - new Country("GB", "영국", "🇬🇧"), - new Country("VG", "영국령 버진아일랜드", "🇻🇬"), - new Country("IO", "영국령 인도양 지역", "🇮🇴"), - new Country("YE", "예멘", "🇾🇪"), - new Country("OM", "오만", "🇴🇲"), - new Country("AT", "오스트리아", "🇦🇹"), - new Country("HN", "온두라스", "🇭🇳"), - new Country("AX", "올란드 제도", "🇦🇽"), - new Country("JO", "요르단", "🇯🇴"), - new Country("UG", "우간다", "🇺🇬"), - new Country("UY", "우루과이", "🇺🇾"), - new Country("UZ", "우즈베키스탄", "🇺🇿"), - new Country("UA", "우크라이나", "🇺🇦"), - new Country("WF", "월리스 푸투나", "🇼🇫"), - new Country("IQ", "이라크", "🇮🇶"), - new Country("IR", "이란", "🇮🇷"), - new Country("IL", "이스라엘", "🇮🇱"), - new Country("EG", "이집트", "🇪🇬"), - new Country("IT", "이탈리아", "🇮🇹"), - new Country("IN", "인도", "🇮🇳"), - new Country("ID", "인도네시아", "🇮🇩"), - new Country("JP", "일본", "🇯🇵"), - new Country("JM", "자메이카", "🇯🇲"), - new Country("ZM", "잠비아", "🇿🇲"), - new Country("JE", "저지", "🇯🇪"), - new Country("GQ", "적도 기니", "🇬🇶"), - new Country("KP", "조선민주주의인민공화국", "🇰🇵"), - new Country("GE", "조지아", "🇬🇪"), - new Country("CN", "중국", "🇨🇳"), - new Country("CF", "중앙아프리카공화국", "🇨🇫"), - new Country("DJ", "지부티", "🇩🇯"), - new Country("GI", "지브롤터", "🇬🇮"), - new Country("ZW", "짐바브웨", "🇿🇼"), - new Country("TD", "차드", "🇹🇩"), - new Country("CZ", "체코", "🇨🇿"), - new Country("CL", "칠레", "🇨🇱"), - new Country("CM", "카메룬", "🇨🇲"), - new Country("CV", "카보베르데", "🇨🇻"), - new Country("KZ", "카자흐스탄", "🇰🇿"), - new Country("QA", "카타르", "🇶🇦"), - new Country("KH", "캄보디아", "🇰🇭"), - new Country("CA", "캐나다", "🇨🇦"), - new Country("KE", "케냐", "🇰🇪"), - new Country("KY", "케이맨 제도", "🇰🇾"), - new Country("KM", "코모로", "🇰🇲"), - new Country("CR", "코스타리카", "🇨🇷"), - new Country("CC", "코코스(킬링) 제도", "🇨🇨"), - new Country("CI", "코트디부아르", "🇨🇮"), - new Country("CO", "콜롬비아", "🇨🇴"), - new Country("CG", "콩고", "🇨🇬"), - new Country("CD", "콩고민주공화국", "🇨🇩"), - new Country("CU", "쿠바", "🇨🇺"), - new Country("KW", "쿠웨이트", "🇰🇼"), - new Country("CK", "쿡 제도", "🇨🇰"), - new Country("CW", "퀴라소", "🇨🇼"), - new Country("HR", "크로아티아", "🇭🇷"), - new Country("CX", "크리스마스섬", "🇨🇽"), - new Country("KG", "키르기스스탄", "🇰🇬"), - new Country("KI", "키리바시", "🇰🇮"), - new Country("CY", "키프로스", "🇨🇾"), - new Country("TJ", "타지키스탄", "🇹🇯"), - new Country("TZ", "탄자니아", "🇹🇿"), - new Country("TH", "태국", "🇹🇭"), - new Country("TC", "터크스 케이커스 제도", "🇹🇨"), - new Country("TR", "터키", "🇹🇷"), - new Country("TG", "토고", "🇹🇬"), - new Country("TK", "토켈라우", "🇹🇰"), - new Country("TO", "통가", "🇹🇴"), - new Country("TM", "투르크메니스탄", "🇹🇲"), - new Country("TV", "투발루", "🇹🇻"), - new Country("TN", "튀니지", "🇹🇳"), - new Country("TT", "트리니다드 토바고", "🇹🇹"), - new Country("PA", "파나마", "🇵🇦"), - new Country("PY", "파라과이", "🇵🇾"), - new Country("PK", "파키스탄", "🇵🇰"), - new Country("PG", "파푸아뉴기니", "🇵🇬"), - new Country("PW", "팔라우", "🇵🇼"), - new Country("PS", "팔레스타인", "🇵🇸"), - new Country("FO", "페로 제도", "🇫🇴"), - new Country("PE", "페루", "🇵🇪"), - new Country("PT", "포르투갈", "🇵🇹"), - new Country("FK", "포클랜드 제도", "🇫🇰"), - new Country("PL", "폴란드", "🇵🇱"), - new Country("PR", "푸에르토리코", "🇵🇷"), - new Country("FR", "프랑스", "🇫🇷"), - new Country("GF", "프랑스령 기아나", "🇬🇫"), - new Country("TF", "프랑스령 남부 지역", "🇹🇫"), - new Country("PF", "프랑스령 폴리네시아", "🇵🇫"), - new Country("FJ", "피지", "🇫🇯"), - new Country("FI", "핀란드", "🇫🇮"), - new Country("PH", "필리핀", "🇵🇭"), - new Country("PN", "핏케언 제도", "🇵🇳"), - new Country("HM", "허드 맥도널드 제도", "🇭🇲"), - new Country("HU", "헝가리", "🇭🇺"), - new Country("HK", "홍콩", "🇭🇰") - ); - countries.forEach(countryRepository::save); - countryRepository.flush(); // 강제 DB 반영 - - List check = countryRepository.findAll(); - log.info("실제 저장된 Country 수: {}", check.size()); - } -} +} \ No newline at end of file diff --git a/src/main/java/zim/tave/memory/service/S3Uploader.java b/src/main/java/zim/tave/memory/service/S3Uploader.java index e91f75e..8613fcf 100644 --- a/src/main/java/zim/tave/memory/service/S3Uploader.java +++ b/src/main/java/zim/tave/memory/service/S3Uploader.java @@ -6,7 +6,6 @@ import org.springframework.web.multipart.MultipartFile; import software.amazon.awssdk.core.sync.RequestBody; import software.amazon.awssdk.services.s3.S3Client; -import software.amazon.awssdk.services.s3.model.ObjectCannedACL; import software.amazon.awssdk.services.s3.model.PutObjectRequest; import zim.tave.memory.config.MemoryProperties; diff --git a/src/main/java/zim/tave/memory/service/SettingService.java b/src/main/java/zim/tave/memory/service/SettingService.java index eefe17c..adf09de 100644 --- a/src/main/java/zim/tave/memory/service/SettingService.java +++ b/src/main/java/zim/tave/memory/service/SettingService.java @@ -13,9 +13,6 @@ import zim.tave.memory.repository.UserRepository; import zim.tave.memory.repository.VisitedCountryRepository; -import java.util.List; -import java.util.stream.Collectors; - @Service @RequiredArgsConstructor public class SettingService { diff --git a/src/main/java/zim/tave/memory/util/EmojiValidator.java b/src/main/java/zim/tave/memory/util/EmojiValidator.java index 6aa35f9..0ae13e3 100644 --- a/src/main/java/zim/tave/memory/util/EmojiValidator.java +++ b/src/main/java/zim/tave/memory/util/EmojiValidator.java @@ -1,7 +1,5 @@ package zim.tave.memory.util; -import java.util.regex.Pattern; - public class EmojiValidator { private EmojiValidator() { diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index 2a261b0..e509da4 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -6,6 +6,13 @@ spring: activate: on-profile: prod + flyway: + enabled: true + baseline-on-migrate: true + validate-on-migrate: true + # 운영 환경: db/migration만 실행 (테스트 데이터 제외) + locations: classpath:db/migration + memory: base-url: https://voyame-studio.org diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index b9cfe59..92ca82a 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -23,11 +23,12 @@ spring: jpa: hibernate: - ddl-auto: update + ddl-auto: validate show-sql: true properties: hibernate: format_sql: true + physical_naming_strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl jdbc: time_zone: UTC timezone: @@ -39,6 +40,15 @@ spring: globally_quoted_identifiers: true globally_quoted_identifiers_skip_column_definitions: true + flyway: + enabled: true + baseline-on-migrate: false + validate-on-migrate: true + # 개발 환경: db/migration + db/migration-dev (테스트 데이터 포함) + locations: + - classpath:db/migration + - classpath:db/migration-dev + logging: level: org.hibernate.SQL: debug diff --git a/src/main/resources/db/migration-dev/V4__test_user.sql b/src/main/resources/db/migration-dev/V4__test_user.sql new file mode 100644 index 0000000..24fe2b3 --- /dev/null +++ b/src/main/resources/db/migration-dev/V4__test_user.sql @@ -0,0 +1,46 @@ +-- ============================================ +-- V4__test_user.sql +-- 개발 환경 전용 테스트 사용자 데이터 삽입 +-- 운영 환경에서는 실행되지 않음 (application-prod.yml에서 db/migration만 로드) +-- INSERT IGNORE를 사용하여 이미 데이터가 있으면 실행하지 않음 +-- ============================================ + +-- 테스트 사용자 삽입 (이미 존재하는 경우 무시) +INSERT IGNORE INTO `User` ( + `kakaoId`, + `profileImageUrl`, + `surName`, + `firstName`, + `koreanName`, + `birth`, + `nationality`, + `createdAt`, + `status`, + `isRegistered`, + `diaryCount`, + `visitedCountryCount`, + `flags` +) VALUES ( + 'test_강지혜', + 'https://me-mory.mooo.com/api/files?key=images/7c24aa61-2d36-48fd-80a5-646ac518256d_IMG_3831.jpg', + 'KANG', + 'JIHYE', + '강지혜', + '1995-03-15', + 'REPUBLIC OF KOREA', + CURRENT_DATE, + TRUE, + FALSE, + 0, + 0, + '🇰🇷' +); + +-- 테스트 사용자의 Setting 생성 (이미 존재하는 경우 무시) +INSERT IGNORE INTO `Setting` (`userId`, `alarm`) +SELECT u.`userId`, TRUE +FROM `User` u +WHERE u.`kakaoId` = 'test_강지혜' +AND NOT EXISTS ( + SELECT 1 FROM `Setting` s WHERE s.`userId` = u.`userId` +); diff --git a/src/main/resources/db/migration/V1__init.sql b/src/main/resources/db/migration/V1__init.sql new file mode 100644 index 0000000..a08b8e3 --- /dev/null +++ b/src/main/resources/db/migration/V1__init.sql @@ -0,0 +1,169 @@ +-- ============================================ +-- V1__init.sql +-- 초기 데이터베이스 스키마 생성 +-- ============================================ + +-- Country 테이블 (FK 없음, 가장 먼저 생성) +CREATE TABLE IF NOT EXISTS `Country` ( + `countryCode` VARCHAR(2) NOT NULL PRIMARY KEY, + `countryName` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + `emoji` VARCHAR(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- Emotion 테이블 (FK 없음) +CREATE TABLE IF NOT EXISTS `Emotion` ( + `emotion_id` BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + `name` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + `colorCode` VARCHAR(255) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- Weather 테이블 (FK 없음) +CREATE TABLE IF NOT EXISTS `Weather` ( + `weather_id` BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + `name` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + `iconUrl` VARCHAR(255) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- TripTheme 테이블 (FK 없음) +CREATE TABLE IF NOT EXISTS `TripTheme` ( + `id` BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + `themeName` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + `sampleImageUrl` VARCHAR(255), + `cardImageUrl` VARCHAR(255) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- BoardTheme 테이블 (FK 없음) +CREATE TABLE IF NOT EXISTS `BoardTheme` ( + `boardThemeId` BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + `themeName` VARCHAR(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `thumbnailUrl` VARCHAR(255) NOT NULL, + `cardUrl` VARCHAR(255) NOT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- Sticker 테이블 (FK 없음) +CREATE TABLE IF NOT EXISTS `Sticker` ( + `stickerId` BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + `name` VARCHAR(255) NOT NULL, + `imageUrl` VARCHAR(255) NOT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- User 테이블 (FK 없음) +CREATE TABLE IF NOT EXISTS `User` ( + `userId` BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + `kakaoId` VARCHAR(255) UNIQUE, + `profileImageUrl` VARCHAR(255), + `surName` VARCHAR(255), + `firstName` VARCHAR(255), + `koreanName` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + `createdAt` DATE DEFAULT (CURRENT_DATE), + `status` BOOLEAN DEFAULT FALSE, + `birth` DATE, + `nationality` VARCHAR(255), + `isRegistered` BOOLEAN NOT NULL DEFAULT FALSE, + `diaryCount` BIGINT, + `visitedCountryCount` BIGINT, + `flags` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- Setting 테이블 (FK: User) +CREATE TABLE IF NOT EXISTS `Setting` ( + `userId` BIGINT NOT NULL PRIMARY KEY, + `alarm` BOOLEAN NOT NULL DEFAULT TRUE, + CONSTRAINT `fk_setting_user` FOREIGN KEY (`userId`) REFERENCES `User` (`userId`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- Trip 테이블 (FK: User, TripTheme) +CREATE TABLE IF NOT EXISTS `Trip` ( + `tripId` BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + `tripName` VARCHAR(14) NOT NULL, + `description` VARCHAR(56), + `startDate` DATE NOT NULL, + `endDate` DATE NOT NULL, + `isStored` BOOLEAN DEFAULT FALSE, + `isPast` BOOLEAN DEFAULT FALSE, + `userId` BIGINT, + `tripThemeId` BIGINT, + `content` TINYTEXT, + `representativeImageUrl` VARCHAR(255), + CONSTRAINT `fk_trip_user` FOREIGN KEY (`userId`) REFERENCES `User` (`userId`) ON DELETE CASCADE, + CONSTRAINT `fk_trip_trip_theme` FOREIGN KEY (`tripThemeId`) REFERENCES `TripTheme` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- Diary 테이블 (FK: User, Trip, Country, Emotion, Weather) +CREATE TABLE IF NOT EXISTS `Diary` ( + `diaryId` BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + `userId` BIGINT, + `tripId` BIGINT, + `countryId` VARCHAR(2), + `city` VARCHAR(255) NOT NULL, + `dateTime` DATETIME(6) NOT NULL, + `content` TINYTEXT NOT NULL, + `detailedLocation` VARCHAR(255), + `emotionId` BIGINT, + `weatherId` BIGINT, + `createdAt` DATETIME(6) DEFAULT (UTC_TIMESTAMP()), + `isStored` BOOLEAN NOT NULL DEFAULT FALSE, + CONSTRAINT `fk_diary_user` FOREIGN KEY (`userId`) REFERENCES `User` (`userId`) ON DELETE CASCADE, + CONSTRAINT `fk_diary_trip` FOREIGN KEY (`tripId`) REFERENCES `Trip` (`tripId`) ON DELETE CASCADE, + CONSTRAINT `fk_diary_country` FOREIGN KEY (`countryId`) REFERENCES `Country` (`countryCode`), + CONSTRAINT `fk_diary_emotion` FOREIGN KEY (`emotionId`) REFERENCES `Emotion` (`emotion_id`), + CONSTRAINT `fk_diary_weather` FOREIGN KEY (`weatherId`) REFERENCES `Weather` (`weather_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- DiaryImage 테이블 (FK: Diary) +CREATE TABLE IF NOT EXISTS `DiaryImage` ( + `DiaryImageId` BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + `diaryId` BIGINT, + `imageUrl` VARCHAR(255) NOT NULL, + `cameraType` VARCHAR(255) NOT NULL, + `isRepresentative` BOOLEAN DEFAULT FALSE, + `imageOrder` INT, + CONSTRAINT `fk_diary_image_diary` FOREIGN KEY (`diaryId`) REFERENCES `Diary` (`diaryId`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- VisitedCountry 테이블 (FK: User, Country, Emotion) +CREATE TABLE IF NOT EXISTS `VisitedCountry` ( + `visitedCountryId` BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + `color` VARCHAR(255), + `createdAt` DATETIME(6) DEFAULT (UTC_TIMESTAMP()), + `userId` BIGINT, + `countryCode` VARCHAR(2), + `emotionId` BIGINT, + CONSTRAINT `fk_visited_country_user` FOREIGN KEY (`userId`) REFERENCES `User` (`userId`) ON DELETE CASCADE, + CONSTRAINT `fk_visited_country_country` FOREIGN KEY (`countryCode`) REFERENCES `Country` (`countryCode`), + CONSTRAINT `fk_visited_country_emotion` FOREIGN KEY (`emotionId`) REFERENCES `Emotion` (`emotion_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- Board 테이블 (FK: User, BoardTheme) +CREATE TABLE IF NOT EXISTS `Board` ( + `boardId` BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + `title` VARCHAR(100) NOT NULL, + `createdAt` DATETIME(6) NOT NULL DEFAULT (UTC_TIMESTAMP()), + `updatedAt` DATETIME(6), + `userId` BIGINT NOT NULL, + `boardThemeId` BIGINT NOT NULL, + CONSTRAINT `fk_board_user` FOREIGN KEY (`userId`) REFERENCES `User` (`userId`) ON DELETE CASCADE, + CONSTRAINT `fk_board_board_theme` FOREIGN KEY (`boardThemeId`) REFERENCES `BoardTheme` (`boardThemeId`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- BoardStickerMap 테이블 (FK: Board, Sticker) +CREATE TABLE IF NOT EXISTS `BoardStickerMap` ( + `boardStickerId` BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + `boardId` BIGINT NOT NULL, + `stickerId` BIGINT NOT NULL, + `posX` DECIMAL(19,2) NOT NULL, + `posY` DECIMAL(19,2) NOT NULL, + `rotation` DECIMAL(19,2) NOT NULL, + CONSTRAINT `fk_board_sticker_map_board` FOREIGN KEY (`boardId`) REFERENCES `Board` (`boardId`) ON DELETE CASCADE, + CONSTRAINT `fk_board_sticker_map_sticker` FOREIGN KEY (`stickerId`) REFERENCES `Sticker` (`stickerId`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- AlarmHistory 테이블 (FK 없음, 하지만 User, Trip 참조) +CREATE TABLE IF NOT EXISTS `AlarmHistory` ( + `alarmHistoryId` BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + `userId` BIGINT NOT NULL, + `tripId` BIGINT NOT NULL, + `alarmType` VARCHAR(50) NOT NULL, + `sentAt` DATETIME NOT NULL, + `sentDate` DATE NOT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/src/main/resources/db/migration/V2__insert_basic_data.sql b/src/main/resources/db/migration/V2__insert_basic_data.sql new file mode 100644 index 0000000..96c2ea0 --- /dev/null +++ b/src/main/resources/db/migration/V2__insert_basic_data.sql @@ -0,0 +1,48 @@ +-- ============================================ +-- V2__insert_basic_data.sql +-- 기초 데이터 삽입 (TripTheme, Emotion, Weather, BoardTheme, Sticker) +-- ============================================ + +-- TripTheme 데이터 삽입 +INSERT INTO `TripTheme` (`themeName`, `sampleImageUrl`, `cardImageUrl`) VALUES +('기본', 'https://me-mory01.mooo.com/api/files?key=images/d69be93f-fef6-4a73-815f-ea6b93fd2d59_trip_thumb_default.png', 'https://me-mory01.mooo.com/api/files?key=images/6e56681d-9fc3-43d2-a803-0ef165c63c3c_trip_card_default.png'), +('Grey', 'https://me-mory01.mooo.com/api/files?key=images/f0d5e1f7-2718-4dac-a780-d6f6e5f2d42a_trip_thumb_grey.png', 'https://me-mory01.mooo.com/api/files?key=images/462d91ad-b618-4449-805f-35f07187247b_trip_card_grey.png'), +('탑승권', 'https://me-mory01.mooo.com/api/files?key=images/cbcf48b8-8469-4e0f-b597-7ef27cc6190e_trip_thumb_boardingpass.png', 'https://me-mory01.mooo.com/api/files?key=images/3e637461-f9a2-477d-ae06-4245033174a9_trip_card_boardingpass.png'), +('액자', 'https://me-mory01.mooo.com/api/files?key=images/c3efb8f8-f4a8-41a1-9d29-e93bc9d2dd83_trip_thumb_frame.png', 'https://me-mory01.mooo.com/api/files?key=images/eade6dc1-c412-4924-af7f-832292b258c0_trip_card_frame.png'), +('Beach', 'https://me-mory01.mooo.com/api/files?key=images/f692134b-0676-47e9-9782-778f7df9a23a_trip_thumb_beach.png', 'https://me-mory01.mooo.com/api/files?key=images/94abb8d5-5f56-4061-8e90-1df9c9107caf_trip_card_beach.png'), +('Forest', 'https://me-mory01.mooo.com/api/files?key=images/6f40c3c9-313f-44e5-9624-3264991b2f9b_trip_thumb_forest.png', 'https://me-mory01.mooo.com/api/files?key=images/99589e53-9539-46eb-b702-644fab7a87e3_trip_card_forest.png'); + +-- Emotion 데이터 삽입 +INSERT INTO `Emotion` (`name`, `colorCode`) VALUES +('기본', '#EEEEEE'), +('설렘', '#FDD7DE'), +('신기함', '#FFCB6B'), +('즐거움', '#FFE13E'), +('힐링', '#C1E8A0'), +('평온', '#D1E5D4'), +('뿌듯함', '#5ACFD5'), +('해방감', '#5EB6D9'), +('낯섦', '#634E72'), +('긴장됨', '#2C3E50'), +('외로움', '#A9A9B0'), +('아쉬움', '#866868'), +('벅참', '#800020'); + +-- Weather 데이터 삽입 +INSERT INTO `Weather` (`name`, `iconUrl`) VALUES +('맑음', 'https://me-mory01.mooo.com/api/files?key=images/7de47dfb-6f09-47d2-9268-2b8c49c2f9bd_weather_sunny.png'), +('구름', 'https://me-mory01.mooo.com/api/files?key=images/6a8c4a38-0ccd-479c-ba70-7202d7535bf7_weather_cloudy.png'), +('비', 'https://me-mory01.mooo.com/api/files?key=images/72a8e191-9b31-4646-ae48-cb31fb543de7_weather_rainy.png'), +('바람', 'https://me-mory01.mooo.com/api/files?key=images/dae2b4e2-7ac4-45fe-bccc-be835218d8d4_weather_windy.png'), +('눈', 'https://me-mory01.mooo.com/api/files?key=images/3b298957-360e-4ea4-86ea-bcd65fcd3f77_weather_snowy.png'); + +-- BoardTheme 데이터 삽입 +INSERT INTO `BoardTheme` (`themeName`, `thumbnailUrl`, `cardUrl`) VALUES +('칠판', 'https://me-mory01.mooo.com/api/files?key=images/board_theme/chalkboard_thumb.png', 'https://me-mory01.mooo.com/api/files?key=images/73212582-a940-4929-9c9b-f99ce68f3238_board_card_chalkboard.png'), +('식탁보', 'https://me-mory01.mooo.com/api/files?key=images/board_theme/tablecloth_thumb.png', 'https://me-mory01.mooo.com/api/files?key=images/ea8230ad-a70a-4f87-acb4-648add56b1b8_board_card_tablecloth.png'), +('나무보드', 'https://me-mory01.mooo.com/api/files?key=images/board_theme/board_thumb.png', 'https://me-mory01.mooo.com/api/files?key=images/ecee4553-72cc-4097-8d20-10a7ee889269_board_card_woodboard.png'), +('체스판', 'https://me-mory01.mooo.com/api/files?key=images/board_theme/chess_thumb.png', 'https://me-mory01.mooo.com/api/files?key=images/e026afe7-1619-4ee5-b3e7-c2564d88447c_board_card_chess.png'); + +-- Sticker 데이터 삽입 +INSERT INTO `Sticker` (`name`, `imageUrl`) VALUES +('Star', 'https://me-mory01.mooo.com/api/files?key=images/6d4c8b2f-00ec-41ae-bd57-9ce914e1c78d_sticker_star.png'); diff --git a/src/main/resources/db/migration/V3__insert_countries.sql b/src/main/resources/db/migration/V3__insert_countries.sql new file mode 100644 index 0000000..991f14c --- /dev/null +++ b/src/main/resources/db/migration/V3__insert_countries.sql @@ -0,0 +1,251 @@ +-- ============================================ +-- V3__insert_countries.sql +-- Country 데이터 삽입 (245개 국가) +-- ============================================ + +INSERT INTO `Country` (`countryCode`, `countryName`, `emoji`) VALUES +('GH', '가나', '🇬🇭'), +('GA', '가봉', '🇬🇦'), +('GY', '가이아나', '🇬🇾'), +('GM', '감비아', '🇬🇲'), +('GG', '건지', '🇬🇬'), +('GP', '과들루프', '🇬🇵'), +('GT', '과테말라', '🇬🇹'), +('GU', '괌', '🇬🇺'), +('GD', '그레나다', '🇬🇩'), +('GR', '그리스', '🇬🇷'), +('GL', '그린란드', '🇬🇱'), +('GN', '기니', '🇬🇳'), +('GW', '기니비사우', '🇬🇼'), +('NA', '나미비아', '🇳🇦'), +('NR', '나우루', '🇳🇷'), +('NG', '나이지리아', '🇳🇬'), +('AQ', '남극', '🇦🇶'), +('SS', '남수단', '🇸🇸'), +('ZA', '남아프리카공화국', '🇿🇦'), +('NL', '네덜란드', '🇳🇱'), +('NP', '네팔', '🇳🇵'), +('NO', '노르웨이', '🇳🇴'), +('NF', '노퍽섬', '🇳🇫'), +('NZ', '뉴질랜드', '🇳🇿'), +('NC', '뉴칼레도니아', '🇳🇨'), +('NU', '니우에', '🇳🇺'), +('NE', '니제르', '🇳🇪'), +('NI', '니카라과', '🇳🇮'), +('TW', '대만', '🇹🇼'), +('KR', '대한민국', '🇰🇷'), +('DK', '덴마크', '🇩🇰'), +('DM', '도미니카', '🇩🇲'), +('DO', '도미니카 공화국', '🇩🇴'), +('DE', '독일', '🇩🇪'), +('TL', '동티모르', '🇹🇱'), +('LA', '라오스', '🇱🇦'), +('LR', '라이베리아', '🇱🇷'), +('LV', '라트비아', '🇱🇻'), +('RU', '러시아', '🇷🇺'), +('LB', '레바논', '🇱🇧'), +('LS', '레소토', '🇱🇸'), +('RE', '레위니옹', '🇷🇪'), +('RO', '루마니아', '🇷🇴'), +('LU', '룩셈부르크', '🇱🇺'), +('RW', '르완다', '🇷🇼'), +('LY', '리비아', '🇱🇾'), +('LT', '리투아니아', '🇱🇹'), +('LI', '리히텐슈타인', '🇱🇮'), +('MG', '마다가스카르', '🇲🇬'), +('MQ', '마르티니크', '🇲🇶'), +('MH', '마셜제도', '🇲🇭'), +('YT', '마요트', '🇾🇹'), +('MO', '마카오', '🇲🇴'), +('MW', '말라위', '🇲🇼'), +('MY', '말레이시아', '🇲🇾'), +('ML', '말리', '🇲🇱'), +('IM', '맨섬', '🇮🇲'), +('MX', '멕시코', '🇲🇽'), +('MC', '모나코', '🇲🇨'), +('MA', '모로코', '🇲🇦'), +('MR', '모리타니', '🇲🇷'), +('MZ', '모잠비크', '🇲🇿'), +('ME', '몬테네그로', '🇲🇪'), +('MS', '몬트세라트', '🇲🇸'), +('MD', '몰도바', '🇲🇩'), +('MV', '몰디브', '🇲🇻'), +('MT', '몰타', '🇲🇹'), +('MN', '몽골', '🇲🇳'), +('US', '미국', '🇺🇸'), +('UM', '미국령 군소 제도', '🇺🇲'), +('VI', '미국령 버진아일랜드', '🇻🇮'), +('MM', '미얀마', '🇲🇲'), +('FM', '미크로네시아', '🇫🇲'), +('VU', '바누아투', '🇻🇺'), +('BH', '바레인', '🇧🇭'), +('BB', '바베이도스', '🇧🇧'), +('VA', '바티칸', '🇻🇦'), +('BS', '바하마', '🇧🇸'), +('BD', '방글라데시', '🇧🇩'), +('BM', '버뮤다', '🇧🇲'), +('BJ', '베냉', '🇧🇯'), +('VE', '베네수엘라', '🇻🇪'), +('VN', '베트남', '🇻🇳'), +('BY', '벨라루스', '🇧🇾'), +('BZ', '벨리즈', '🇧🇿'), +('BQ', '보네르섬, 신트유스타티우스섬, 사바섬', '🇧🇶'), +('BA', '보스니아 헤르체고비나', '🇧🇦'), +('BW', '보츠와나', '🇧🇼'), +('BO', '볼리비아', '🇧🇴'), +('BI', '부룬디', '🇧🇮'), +('BF', '부르키나파소', '🇧🇫'), +('BV', '부베섬', '🇧🇻'), +('BT', '부탄', '🇧🇹'), +('MP', '북마리아나제도', '🇲🇵'), +('MK', '북마케도니아', '🇲🇰'), +('BG', '불가리아', '🇧🇬'), +('BR', '브라질', '🇧🇷'), +('BN', '브루나이', '🇧🇳'), +('WS', '사모아', '🇼🇸'), +('SA', '사우디아라비아', '🇸🇦'), +('GS', '사우스조지아 사우스샌드위치 제도', '🇬🇸'), +('SM', '산마리노', '🇸🇲'), +('ST', '상투메 프린시페', '🇸🇹'), +('MF', '생마르탱(프랑스령)', '🇲🇫'), +('BL', '생바르텔레미', '🇧🇱'), +('PM', '생피에르 미클롱', '🇵🇲'), +('EH', '서사하라', '🇪🇭'), +('SN', '세네갈', '🇸🇳'), +('RS', '세르비아', '🇷🇸'), +('SC', '세이셸', '🇸🇨'), +('LC', '세인트루시아', '🇱🇨'), +('VC', '세인트빈센트 그레나딘', '🇻🇨'), +('KN', '세인트키츠 네비스', '🇰🇳'), +('SH', '세인트헬레나', '🇸🇭'), +('SO', '소말리아', '🇸🇴'), +('SB', '솔로몬 제도', '🇸🇧'), +('SD', '수단', '🇸🇩'), +('SR', '수리남', '🇸🇷'), +('LK', '스리랑카', '🇱🇰'), +('SJ', '스발바르 얀마옌 제도', '🇸🇯'), +('SE', '스웨덴', '🇸🇪'), +('CH', '스위스', '🇨🇭'), +('ES', '스페인', '🇪🇸'), +('SK', '슬로바키아', '🇸🇰'), +('SI', '슬로베니아', '🇸🇮'), +('SY', '시리아', '🇸🇾'), +('SL', '시에라리온', '🇸🇱'), +('SX', '신트마르턴', '🇸🇽'), +('SG', '싱가포르', '🇸🇬'), +('AE', '아랍에미리트', '🇦🇪'), +('AW', '아루바', '🇦🇼'), +('AM', '아르메니아', '🇦🇲'), +('AS', '아메리칸사모아', '🇦🇸'), +('IS', '아이슬란드', '🇮🇸'), +('HT', '아이티', '🇭🇹'), +('IE', '아일랜드', '🇮🇪'), +('AZ', '아제르바이잔', '🇦🇿'), +('AF', '아프가니스탄', '🇦🇫'), +('AD', '안도라', '🇦🇩'), +('AG', '안티구아 바부다', '🇦🇬'), +('AL', '알바니아', '🇦🇱'), +('DZ', '알제리', '🇩🇿'), +('AO', '앙골라', '🇦🇴'), +('AI', '앵귈라', '🇦🇮'), +('ER', '에리트레아', '🇪🇷'), +('SZ', '에스와티니', '🇸🇿'), +('EE', '에스토니아', '🇪🇪'), +('EC', '에콰도르', '🇪🇨'), +('ET', '에티오피아', '🇪🇹'), +('SV', '엘살바도르', '🇸🇻'), +('GB', '영국', '🇬🇧'), +('VG', '영국령 버진아일랜드', '🇻🇬'), +('IO', '영국령 인도양 지역', '🇮🇴'), +('YE', '예멘', '🇾🇪'), +('OM', '오만', '🇴🇲'), +('AT', '오스트리아', '🇦🇹'), +('HN', '온두라스', '🇭🇳'), +('AX', '올란드 제도', '🇦🇽'), +('JO', '요르단', '🇯🇴'), +('UG', '우간다', '🇺🇬'), +('UY', '우루과이', '🇺🇾'), +('UZ', '우즈베키스탄', '🇺🇿'), +('UA', '우크라이나', '🇺🇦'), +('WF', '월리스 푸투나', '🇼🇫'), +('IQ', '이라크', '🇮🇶'), +('IR', '이란', '🇮🇷'), +('IL', '이스라엘', '🇮🇱'), +('EG', '이집트', '🇪🇬'), +('IT', '이탈리아', '🇮🇹'), +('IN', '인도', '🇮🇳'), +('ID', '인도네시아', '🇮🇩'), +('JP', '일본', '🇯🇵'), +('JM', '자메이카', '🇯🇲'), +('ZM', '잠비아', '🇿🇲'), +('JE', '저지', '🇯🇪'), +('GQ', '적도 기니', '🇬🇶'), +('KP', '조선민주주의인민공화국', '🇰🇵'), +('GE', '조지아', '🇬🇪'), +('CN', '중국', '🇨🇳'), +('CF', '중앙아프리카공화국', '🇨🇫'), +('DJ', '지부티', '🇩🇯'), +('GI', '지브롤터', '🇬🇮'), +('ZW', '짐바브웨', '🇿🇼'), +('TD', '차드', '🇹🇩'), +('CZ', '체코', '🇨🇿'), +('CL', '칠레', '🇨🇱'), +('CM', '카메룬', '🇨🇲'), +('CV', '카보베르데', '🇨🇻'), +('KZ', '카자흐스탄', '🇰🇿'), +('QA', '카타르', '🇶🇦'), +('KH', '캄보디아', '🇰🇭'), +('CA', '캐나다', '🇨🇦'), +('KE', '케냐', '🇰🇪'), +('KY', '케이맨 제도', '🇰🇾'), +('KM', '코모로', '🇰🇲'), +('CR', '코스타리카', '🇨🇷'), +('CC', '코코스(킬링) 제도', '🇨🇨'), +('CI', '코트디부아르', '🇨🇮'), +('CO', '콜롬비아', '🇨🇴'), +('CG', '콩고', '🇨🇬'), +('CD', '콩고민주공화국', '🇨🇩'), +('CU', '쿠바', '🇨🇺'), +('KW', '쿠웨이트', '🇰🇼'), +('CK', '쿡 제도', '🇨🇰'), +('CW', '퀴라소', '🇨🇼'), +('HR', '크로아티아', '🇭🇷'), +('CX', '크리스마스섬', '🇨🇽'), +('KG', '키르기스스탄', '🇰🇬'), +('KI', '키리바시', '🇰🇮'), +('CY', '키프로스', '🇨🇾'), +('TJ', '타지키스탄', '🇹🇯'), +('TZ', '탄자니아', '🇹🇿'), +('TH', '태국', '🇹🇭'), +('TC', '터크스 케이커스 제도', '🇹🇨'), +('TR', '터키', '🇹🇷'), +('TG', '토고', '🇹🇬'), +('TK', '토켈라우', '🇹🇰'), +('TO', '통가', '🇹🇴'), +('TM', '투르크메니스탄', '🇹🇲'), +('TV', '투발루', '🇹🇻'), +('TN', '튀니지', '🇹🇳'), +('TT', '트리니다드 토바고', '🇹🇹'), +('PA', '파나마', '🇵🇦'), +('PY', '파라과이', '🇵🇾'), +('PK', '파키스탄', '🇵🇰'), +('PG', '파푸아뉴기니', '🇵🇬'), +('PW', '팔라우', '🇵🇼'), +('PS', '팔레스타인', '🇵🇸'), +('FO', '페로 제도', '🇫🇴'), +('PE', '페루', '🇵🇪'), +('PT', '포르투갈', '🇵🇹'), +('FK', '포클랜드 제도', '🇫🇰'), +('PL', '폴란드', '🇵🇱'), +('PR', '푸에르토리코', '🇵🇷'), +('FR', '프랑스', '🇫🇷'), +('GF', '프랑스령 기아나', '🇬🇫'), +('TF', '프랑스령 남부 지역', '🇹🇫'), +('PF', '프랑스령 폴리네시아', '🇵🇫'), +('FJ', '피지', '🇫🇯'), +('FI', '핀란드', '🇫🇮'), +('PH', '필리핀', '🇵🇭'), +('PN', '핏케언 제도', '🇵🇳'), +('HM', '허드 맥도널드 제도', '🇭🇲'), +('HU', '헝가리', '🇭🇺'), +('HK', '홍콩', '🇭🇰'); diff --git a/src/test/java/zim/tave/memory/api/ApiTestBase.java b/src/test/java/zim/tave/memory/api/ApiTestBase.java index 43dd6c3..c184968 100644 --- a/src/test/java/zim/tave/memory/api/ApiTestBase.java +++ b/src/test/java/zim/tave/memory/api/ApiTestBase.java @@ -14,7 +14,6 @@ import java.time.LocalDate; import java.time.OffsetDateTime; -import java.time.ZoneOffset; /** * API 통합 테스트 베이스 클래스 diff --git a/src/test/java/zim/tave/memory/controller/CountryControllerTest.java b/src/test/java/zim/tave/memory/controller/CountryControllerTest.java index a7f1f1f..d23872c 100644 --- a/src/test/java/zim/tave/memory/controller/CountryControllerTest.java +++ b/src/test/java/zim/tave/memory/controller/CountryControllerTest.java @@ -41,7 +41,6 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.hamcrest.Matchers.containsString; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; diff --git a/src/test/java/zim/tave/memory/controller/JoinControllerTest.java b/src/test/java/zim/tave/memory/controller/JoinControllerTest.java index f6e9fbc..325e765 100644 --- a/src/test/java/zim/tave/memory/controller/JoinControllerTest.java +++ b/src/test/java/zim/tave/memory/controller/JoinControllerTest.java @@ -23,7 +23,6 @@ import java.time.LocalDate; -import static org.hamcrest.Matchers.containsString; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; diff --git a/src/test/java/zim/tave/memory/controller/LoginControllerTest.java b/src/test/java/zim/tave/memory/controller/LoginControllerTest.java index 6ce9d87..adbfd66 100644 --- a/src/test/java/zim/tave/memory/controller/LoginControllerTest.java +++ b/src/test/java/zim/tave/memory/controller/LoginControllerTest.java @@ -26,9 +26,12 @@ import zim.tave.memory.service.KakaoOAuthService; import zim.tave.memory.service.LoginService; +import jakarta.servlet.http.Cookie; import java.util.Optional; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.when; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; @@ -65,34 +68,33 @@ class LoginControllerTest { @DisplayName("카카오 로그인 (인가 코드 방식) - 성공") void 카카오_로그인_인가코드_성공() throws Exception { // given - String authCode = "test_auth_code"; + String authCode = "valid_auth_code"; String kakaoAccessToken = "kakao_access_token"; - LoginResponseDto response = new LoginResponseDto( - 1L, - true, - "4317757086", - "http://img1.kakaocdn.net/profile.jpeg", - "jwt_access_token", - "jwt_refresh_token" - ); + User user = new User(); + user.setId(1L); + user.setKakaoId("4317757086"); + user.setProfileImageUrl("http://img1.kakaocdn.net/profile.jpeg"); + user.setRegistered(true); + + String[] tokens = new String[]{"jwt_access_token", "jwt_refresh_token"}; + LoginResponseDto response = LoginResponseDto.from(user, tokens[0], true); when(kakaoOAuthService.getAccessToken(authCode)).thenReturn(kakaoAccessToken); - when(loginService.login(any(LoginRequestDto.class))).thenReturn(response); + when(loginService.loginAndGenerateTokens(any(LoginRequestDto.class))).thenReturn(tokens); + when(loginService.getUserByKakaoAccessToken(kakaoAccessToken)).thenReturn(user); // when & then mockMvc.perform( get("/api/auth/login/kakao") - .param("code", "valid_auth_code") + .param("code", authCode) ) .andExpect(status().isOk()) .andExpect(jsonPath("$.code") .value(ResponseCode.LOGIN_SUCCESS.getCode())) .andExpect(jsonPath("$.data.userId").value(1L)) .andExpect(jsonPath("$.data.accessToken") - .value("jwt_access_token")) - .andExpect(jsonPath("$.data.refreshToken") - .value("jwt_refresh_token")); + .value("jwt_access_token")); } @@ -103,17 +105,18 @@ class LoginControllerTest { // given String kakaoAccessToken = "valid_kakao_token"; - LoginResponseDto response = new LoginResponseDto( - 1L, - true, - "4317757086", - "http://img1.kakaocdn.net/profile.jpeg", - "jwt_access_token", - "jwt_refresh_token" - ); + User user = new User(); + user.setId(1L); + user.setKakaoId("4317757086"); + user.setProfileImageUrl("http://img1.kakaocdn.net/profile.jpeg"); + user.setRegistered(true); + + String[] tokens = new String[]{"jwt_access_token", "jwt_refresh_token"}; + LoginResponseDto response = LoginResponseDto.from(user, tokens[0], true); when(kakaoOAuthService.getAccessToken(any())).thenReturn(kakaoAccessToken); - when(loginService.login(any(LoginRequestDto.class))).thenReturn(response); + when(loginService.loginAndGenerateTokens(any(LoginRequestDto.class))).thenReturn(tokens); + when(loginService.getUserByKakaoAccessToken(kakaoAccessToken)).thenReturn(user); // when & then mockMvc.perform(get("/api/auth/login/kakao") @@ -121,8 +124,7 @@ class LoginControllerTest { .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(ResponseCode.LOGIN_SUCCESS.getCode())) .andExpect(jsonPath("$.data.userId").value(1L)) - .andExpect(jsonPath("$.data.accessToken").value("jwt_access_token")) - .andExpect(jsonPath("$.data.refreshToken").value("jwt_refresh_token")); + .andExpect(jsonPath("$.data.accessToken").value("jwt_access_token")); } @Test @@ -132,17 +134,18 @@ class LoginControllerTest { String authCode = "new_user_code"; String kakaoAccessToken = "new_user_kakao_token"; - LoginResponseDto response = new LoginResponseDto( - 10L, - false, // 앱 가입 미완료 - "9999999999", - "http://new-profile.jpg", - "new_access_token", - "new_refresh_token" - ); + User user = new User(); + user.setId(10L); + user.setKakaoId("9999999999"); + user.setProfileImageUrl("http://new-profile.jpg"); + user.setRegistered(false); + + String[] tokens = new String[]{"new_access_token", "new_refresh_token"}; + LoginResponseDto response = LoginResponseDto.from(user, tokens[0], false); when(kakaoOAuthService.getAccessToken(authCode)).thenReturn(kakaoAccessToken); - when(loginService.login(any(LoginRequestDto.class))).thenReturn(response); + when(loginService.loginAndGenerateTokens(any(LoginRequestDto.class))).thenReturn(tokens); + when(loginService.getUserByKakaoAccessToken(kakaoAccessToken)).thenReturn(user); // when & then mockMvc.perform(get("/api/auth/login/kakao") @@ -159,7 +162,7 @@ class LoginControllerTest { String authCode = "test_code"; when(kakaoOAuthService.getAccessToken(authCode)).thenReturn(""); - when(loginService.login(any())) + when(loginService.loginAndGenerateTokens(any())) .thenThrow(new CustomException(ErrorCode.KAKAO_TOKEN_MISSING)); // when & then @@ -194,15 +197,16 @@ class LoginControllerTest { user.setId(1L); user.setKakaoId("kakao123"); - when(jwtUtil.validateToken(refreshToken)).thenReturn(true); + doNothing().when(jwtUtil).validateTokenWithException(refreshToken); when(jwtUtil.isRefreshToken(refreshToken)).thenReturn(true); when(jwtUtil.getUserIdFromToken(refreshToken)).thenReturn(1L); when(userRepository.findById(1L)).thenReturn(Optional.of(user)); when(jwtUtil.generateAccessToken(1L, "kakao123")).thenReturn("new_access_token"); + when(jwtUtil.generateRefreshToken(1L)).thenReturn("new_refresh_token"); // when & then mockMvc.perform(post("/api/auth/refresh") - .header("Authorization", "Bearer " + refreshToken)) + .cookie(new Cookie("refreshToken", refreshToken))) .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(ResponseCode.TOKEN_REFRESH_SUCCESS.getCode())) .andExpect(jsonPath("$.data.accessToken").value("new_access_token")); @@ -214,13 +218,14 @@ class LoginControllerTest { // given String invalidRefreshToken = "invalid_refresh_token"; - when(jwtUtil.validateToken(invalidRefreshToken)).thenReturn(false); + doThrow(new CustomException(ErrorCode.INVALID_TOKEN)) + .when(jwtUtil).validateTokenWithException(invalidRefreshToken); // when & then mockMvc.perform(post("/api/auth/refresh") - .header("Authorization", "Bearer " + invalidRefreshToken)) + .cookie(new Cookie("refreshToken", invalidRefreshToken))) .andExpect(status().isUnauthorized()) - .andExpect(jsonPath("$.code").value(500)); // GlobalExceptionHandler가 500을 반환 + .andExpect(jsonPath("$.code").value(ResponseCode.INVALID_TOKEN.getCode())); } @@ -230,23 +235,23 @@ class LoginControllerTest { // given String accessToken = "access_token_not_refresh"; - when(jwtUtil.validateToken(accessToken)).thenReturn(true); + doNothing().when(jwtUtil).validateTokenWithException(accessToken); when(jwtUtil.isRefreshToken(accessToken)).thenReturn(false); // Access Token // when & then mockMvc.perform(post("/api/auth/refresh") - .header("Authorization", "Bearer " + accessToken)) + .cookie(new Cookie("refreshToken", accessToken))) .andExpect(status().isUnauthorized()) - .andExpect(jsonPath("$.code").value(500)); // GlobalExceptionHandler가 500을 반환 + .andExpect(jsonPath("$.code").value(ResponseCode.INVALID_REFRESH_TOKEN.getCode())); } @Test - @DisplayName("토큰 갱신 실패 - Authorization 헤더 누락") + @DisplayName("토큰 갱신 실패 - 쿠키 누락") void 토큰_갱신_실패_헤더누락() throws Exception { // when & then mockMvc.perform(post("/api/auth/refresh")) - .andExpect(status().isInternalServerError()) - .andExpect(jsonPath("$.code").value(500)); + .andExpect(status().isUnauthorized()) + .andExpect(jsonPath("$.code").value(ResponseCode.INVALID_REFRESH_TOKEN.getCode())); } @Test diff --git a/src/test/java/zim/tave/memory/controller/TimelineControllerTest.java b/src/test/java/zim/tave/memory/controller/TimelineControllerTest.java index a3057da..10000a2 100644 --- a/src/test/java/zim/tave/memory/controller/TimelineControllerTest.java +++ b/src/test/java/zim/tave/memory/controller/TimelineControllerTest.java @@ -38,7 +38,6 @@ import static org.mockito.BDDMockito.willThrow; import static org.mockito.Mockito.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; -import static org.hamcrest.Matchers.containsString; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; diff --git a/src/test/java/zim/tave/memory/integration/ImageUploadIntegrationTest.java b/src/test/java/zim/tave/memory/integration/ImageUploadIntegrationTest.java index 7b6402c..5cd70ba 100644 --- a/src/test/java/zim/tave/memory/integration/ImageUploadIntegrationTest.java +++ b/src/test/java/zim/tave/memory/integration/ImageUploadIntegrationTest.java @@ -2,7 +2,6 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import static org.assertj.core.api.Assertions.*; diff --git a/src/test/java/zim/tave/memory/integration/IntegrationTestBase.java b/src/test/java/zim/tave/memory/integration/IntegrationTestBase.java index 6b8ee9e..215ac2f 100644 --- a/src/test/java/zim/tave/memory/integration/IntegrationTestBase.java +++ b/src/test/java/zim/tave/memory/integration/IntegrationTestBase.java @@ -8,7 +8,6 @@ import java.time.LocalDate; import java.time.OffsetDateTime; -import java.time.ZoneOffset; /** * 통합 테스트 베이스 클래스 diff --git a/src/test/java/zim/tave/memory/integration/MyPageIntegrationTest.java b/src/test/java/zim/tave/memory/integration/MyPageIntegrationTest.java index 2a42958..efa4504 100644 --- a/src/test/java/zim/tave/memory/integration/MyPageIntegrationTest.java +++ b/src/test/java/zim/tave/memory/integration/MyPageIntegrationTest.java @@ -4,7 +4,6 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import zim.tave.memory.domain.*; -import zim.tave.memory.dto.request.CreateDiaryRequest; import zim.tave.memory.dto.response.MyPageResponseDto; import zim.tave.memory.dto.response.VisitedCountryListResponseDto; import zim.tave.memory.service.MyPageService; @@ -14,7 +13,6 @@ import java.time.LocalDate; import java.time.OffsetDateTime; import java.time.ZoneOffset; -import java.util.Arrays; import static org.assertj.core.api.Assertions.assertThat; diff --git a/src/test/java/zim/tave/memory/service/AlarmPolicyServiceTest.java b/src/test/java/zim/tave/memory/service/AlarmPolicyServiceTest.java index 6c3a47e..8320b3e 100644 --- a/src/test/java/zim/tave/memory/service/AlarmPolicyServiceTest.java +++ b/src/test/java/zim/tave/memory/service/AlarmPolicyServiceTest.java @@ -1,7 +1,6 @@ package zim.tave.memory.service; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; @@ -13,8 +12,10 @@ import zim.tave.memory.repository.AlarmHistoryRepository; import zim.tave.memory.repository.DiaryRepository; +import java.time.Clock; import java.time.LocalDate; import java.time.LocalDateTime; +import java.time.ZoneId; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.*; @@ -29,6 +30,9 @@ public class AlarmPolicyServiceTest { @Mock private DiaryRepository diaryRepository; + @Mock + private Clock clock; + @InjectMocks private AlarmPolicyService alarmPolicyService; @@ -47,6 +51,14 @@ void setUp() { // totalTripDays = 12 (inclusive), serviceSlotLimit=floor(12*0.25)=3, maxIncompleteSlot=floor(3*0.4)=1 trip.setStartDate(LocalDate.of(2026, 1, 1)); trip.setEndDate(LocalDate.of(2026, 1, 12)); + + // Clock 모킹 설정 - 현재 시간을 고정된 값으로 설정 + Clock fixedClock = Clock.fixed( + LocalDateTime.of(2026, 1, 15, 12, 0).atZone(ZoneId.systemDefault()).toInstant(), + ZoneId.systemDefault() + ); + when(clock.getZone()).thenReturn(ZoneId.systemDefault()); + when(clock.instant()).thenReturn(fixedClock.instant()); } @Test diff --git a/src/test/java/zim/tave/memory/service/AlarmServiceTest.java b/src/test/java/zim/tave/memory/service/AlarmServiceTest.java index 8b478a9..5643131 100644 --- a/src/test/java/zim/tave/memory/service/AlarmServiceTest.java +++ b/src/test/java/zim/tave/memory/service/AlarmServiceTest.java @@ -17,7 +17,10 @@ import zim.tave.memory.repository.AlarmHistoryRepository; import zim.tave.memory.repository.UserRepository; +import java.time.Clock; import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneId; import java.util.Optional; import static org.assertj.core.api.Assertions.assertThat; @@ -41,6 +44,9 @@ public class AlarmServiceTest { @Mock private UserRepository userRepository; + @Mock + private Clock clock; + @InjectMocks private AlarmService alarmService; @@ -176,6 +182,14 @@ class DecideAlarmType { User user = userWithSetting(userId, true); Trip trip = trip(tripId, LocalDate.now().minusDays(1), LocalDate.now().plusDays(1)); + // Clock 모킹 설정 + Clock fixedClock = Clock.fixed( + LocalDateTime.of(2026, 1, 15, 12, 0).atZone(ZoneId.systemDefault()).toInstant(), + ZoneId.systemDefault() + ); + when(clock.getZone()).thenReturn(ZoneId.systemDefault()); + when(clock.instant()).thenReturn(fixedClock.instant()); + when(userRepository.findById(userId)).thenReturn(Optional.of(user)); when(tripService.findCurrentOngoingTrip(userId)).thenReturn(trip); when(alarmPolicyService.decide(user, trip)).thenReturn(AlarmType.DIARY_REMIND); diff --git a/src/test/java/zim/tave/memory/service/CountryServiceTest.java b/src/test/java/zim/tave/memory/service/CountryServiceTest.java index cff8d90..9bbc3b6 100644 --- a/src/test/java/zim/tave/memory/service/CountryServiceTest.java +++ b/src/test/java/zim/tave/memory/service/CountryServiceTest.java @@ -4,7 +4,6 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.annotation.Rollback; import org.springframework.transaction.annotation.Transactional; import zim.tave.memory.domain.Country; import zim.tave.memory.repository.CountryRepository; @@ -28,32 +27,8 @@ void clearDB() { countryRepository.findAll().forEach(c -> countryRepository.delete(c)); } - @Test - @Transactional - @Rollback(false) - void init_빈_DB에_정상작동_확인() { - // when - countryService.init(); - // then - List result = countryRepository.findAll(); - assertThat(result).isNotEmpty(); - assertThat(result).anyMatch(c -> c.getCountryCode().equals("KR")); - assertThat(result).anyMatch(c -> c.getCountryCode().equals("US")); - } - - @Test - void init_중복저장_방지_확인() { - // given - countryService.init(); - int firstCount = countryRepository.findAll().size(); - - // when - countryService.init(); // 두 번째 실행 - int secondCount = countryRepository.findAll().size(); - - // then - assertThat(secondCount).isEqualTo(firstCount); // 중복 저장 안 됐는지 확인 - } + // init() 메서드는 Flyway 마이그레이션으로 대체되었으므로 테스트 제거 + // Flyway가 V3__insert_countries.sql을 통해 국가 데이터를 관리합니다. @Test void searchCountryByKeyword() { diff --git a/src/test/java/zim/tave/memory/service/JoinServiceTest.java b/src/test/java/zim/tave/memory/service/JoinServiceTest.java index 86ca849..c184842 100644 --- a/src/test/java/zim/tave/memory/service/JoinServiceTest.java +++ b/src/test/java/zim/tave/memory/service/JoinServiceTest.java @@ -19,7 +19,6 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; -import static zim.tave.memory.service.SettingServiceTest.*; @ExtendWith(MockitoExtension.class) public class JoinServiceTest { diff --git a/src/test/java/zim/tave/memory/service/LoginServiceTest.java b/src/test/java/zim/tave/memory/service/LoginServiceTest.java index b3473e9..782723b 100644 --- a/src/test/java/zim/tave/memory/service/LoginServiceTest.java +++ b/src/test/java/zim/tave/memory/service/LoginServiceTest.java @@ -46,7 +46,7 @@ public class LoginServiceTest { request.setAccessToken(null); // when & then - assertThatThrownBy(() -> loginService.login(request)) + assertThatThrownBy(() -> loginService.loginAndGenerateTokens(request)) .isInstanceOf(CustomException.class) .hasMessageContaining(ErrorCode.KAKAO_TOKEN_MISSING.getMessage()); } @@ -54,8 +54,9 @@ public class LoginServiceTest { @Test void 토큰_누락시_예외() { LoginRequestDto request = new LoginRequestDto(""); + request.setAccessToken(""); - assertThatThrownBy(() -> loginService.login(request)) + assertThatThrownBy(() -> loginService.loginAndGenerateTokens(request)) .isInstanceOf(CustomException.class) .hasMessageContaining(ErrorCode.KAKAO_TOKEN_MISSING.getMessage()); } @@ -64,7 +65,8 @@ public class LoginServiceTest { @DisplayName("기존 회원 로그인 - Access Token과 Refresh Token 발급") void 기존회원_로그인() { // given - LoginRequestDto request = new LoginRequestDto("valid_kakao_token"); + LoginRequestDto request = new LoginRequestDto(); + request.setAccessToken("valid_kakao_token"); KakaoUserInfo kakaoInfo = new KakaoUserInfo("kakao123", "http://img.jpg"); User user = new User(); @@ -78,14 +80,13 @@ public class LoginServiceTest { when(jwtUtil.generateRefreshToken(anyLong())).thenReturn("refresh_token_456"); // when - LoginResponseDto response = loginService.login(request); + String[] tokens = loginService.loginAndGenerateTokens(request); // then - assertThat(response.isRegistered()).isTrue(); - assertThat(response.getUserId()).isEqualTo(1L); - assertThat(response.getKakaoId()).isEqualTo("kakao123"); - assertThat(response.getAccessToken()).isEqualTo("access_token_123"); - assertThat(response.getRefreshToken()).isEqualTo("refresh_token_456"); + assertThat(tokens).isNotNull(); + assertThat(tokens.length).isEqualTo(2); + assertThat(tokens[0]).isEqualTo("access_token_123"); + assertThat(tokens[1]).isEqualTo("refresh_token_456"); verify(jwtUtil, times(1)).generateAccessToken(1L, "kakao123"); verify(jwtUtil, times(1)).generateRefreshToken(1L); @@ -95,7 +96,8 @@ public class LoginServiceTest { @DisplayName("신규 회원 로그인 - 카카오 로그인만 완료, 앱 가입 미완료") void 신규회원_로그인() { // given - LoginRequestDto request = new LoginRequestDto("valid_kakao_token"); + LoginRequestDto request = new LoginRequestDto(); + request.setAccessToken("valid_kakao_token"); KakaoUserInfo kakaoInfo = new KakaoUserInfo("kakao999", "http://profile.jpg"); @@ -111,14 +113,13 @@ public class LoginServiceTest { }); // when - LoginResponseDto response = loginService.login(request); + String[] tokens = loginService.loginAndGenerateTokens(request); // then - assertThat(response.isRegistered()).isFalse(); // 앱 가입 미완료 - assertThat(response.getUserId()).isEqualTo(10L); - assertThat(response.getKakaoId()).isEqualTo("kakao999"); - assertThat(response.getAccessToken()).isEqualTo("new_access_token"); - assertThat(response.getRefreshToken()).isEqualTo("new_refresh_token"); + assertThat(tokens).isNotNull(); + assertThat(tokens.length).isEqualTo(2); + assertThat(tokens[0]).isEqualTo("new_access_token"); + assertThat(tokens[1]).isEqualTo("new_refresh_token"); verify(userRepository, times(1)).save(any(User.class)); verify(jwtUtil, times(1)).generateAccessToken(10L, "kakao999"); @@ -173,13 +174,14 @@ public class LoginServiceTest { @DisplayName("카카오 API 호출 실패 시 예외 전파") void 카카오_API_실패() { // given - LoginRequestDto request = new LoginRequestDto("invalid_kakao_token"); + LoginRequestDto request = new LoginRequestDto(); + request.setAccessToken("invalid_kakao_token"); when(kakaoApiClient.getKakaoUserInfo("invalid_kakao_token")) .thenThrow(new CustomException(ErrorCode.KAKAO_INVALID_TOKEN)); // when & then - assertThatThrownBy(() -> loginService.login(request)) + assertThatThrownBy(() -> loginService.loginAndGenerateTokens(request)) .isInstanceOf(CustomException.class) .hasMessageContaining(ErrorCode.KAKAO_INVALID_TOKEN.getMessage()); } diff --git a/src/test/java/zim/tave/memory/service/MyPageServiceTest.java b/src/test/java/zim/tave/memory/service/MyPageServiceTest.java index b3e577a..6a1a43d 100644 --- a/src/test/java/zim/tave/memory/service/MyPageServiceTest.java +++ b/src/test/java/zim/tave/memory/service/MyPageServiceTest.java @@ -5,7 +5,6 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.beans.factory.annotation.Autowired; import zim.tave.memory.domain.Country; import zim.tave.memory.domain.User; import zim.tave.memory.domain.VisitedCountry; diff --git a/src/test/java/zim/tave/memory/service/VisitedCountryServiceTest.java b/src/test/java/zim/tave/memory/service/VisitedCountryServiceTest.java index d215052..86d0f5a 100644 --- a/src/test/java/zim/tave/memory/service/VisitedCountryServiceTest.java +++ b/src/test/java/zim/tave/memory/service/VisitedCountryServiceTest.java @@ -32,7 +32,7 @@ public class VisitedCountryServiceTest { void testRegisterVisitedCountry_SuccessAndUpdate() { // given: 테스트용 데이터 생성 User user = createTestUser("testKakao1"); - Country country = createTestCountry("KR1", "대한민국1", "🇰🇷"); + Country country = createTestCountry("KR", "대한민국1", "🇰🇷"); Emotion emotion1 = createTestEmotion("행복1", "#FFD700"); Emotion emotion2 = createTestEmotion("슬픔1", "#0000FF"); @@ -50,7 +50,7 @@ void testRegisterVisitedCountry_SuccessAndUpdate() { // first는 첫 번째 등록 결과이므로 "행복1"이 맞음 assertThat(first.getEmotionName()).isEqualTo("행복1"); assertThat(updated.getEmotionName()).isEqualTo("슬픔1"); - assertThat(visitedList.get(0).getCountryCode()).isEqualTo("KR1"); + assertThat(visitedList.get(0).getCountryCode()).isEqualTo("KR"); assertThat(visitedList.get(0).getEmotionName()).isEqualTo("슬픔1"); assertThat(visitedList.get(0).getColor()).isEqualTo("#0000FF"); } @@ -59,7 +59,7 @@ void testRegisterVisitedCountry_SuccessAndUpdate() { void testRegisterVisitedCountry_ReturnsSavedEntity() { // given User user = createTestUser("testKakaoNew"); - Country country = createTestCountry("KRNEW", "대한민국NEW", "🇰🇷"); + Country country = createTestCountry("US", "미국", "🇺🇸"); Emotion emotion = createTestEmotion("설렘", "#FDD7DE"); // when @@ -68,7 +68,7 @@ void testRegisterVisitedCountry_ReturnsSavedEntity() { // then assertThat(result.getVisitedCountryId()).isNotNull(); assertThat(result.getUserId()).isEqualTo(user.getId()); - assertThat(result.getCountryCode()).isEqualTo("KRNEW"); + assertThat(result.getCountryCode()).isEqualTo("US"); assertThat(result.getEmotionName()).isEqualTo("설렘"); assertThat(result.getColor()).isEqualTo("#FDD7DE"); } @@ -77,7 +77,7 @@ void testRegisterVisitedCountry_ReturnsSavedEntity() { void testAlreadyVisited() { // given: 테스트용 데이터 생성 User user = createTestUser("testKakao2"); - Country country = createTestCountry("KR2", "대한민국2", "🇰🇷"); + Country country = createTestCountry("JP", "일본", "🇯🇵"); Emotion emotion = createTestEmotion("행복2", "#FFD700"); // when & then: 방문 여부 확인 @@ -94,7 +94,7 @@ void testAlreadyVisited() { void testGetVisitedCountries() { // given: 테스트용 데이터 생성 User user = createTestUser("testKakao8"); - Country country = createTestCountry("KR8", "대한민국8", "🇰🇷"); + Country country = createTestCountry("CN", "중국", "🇨🇳"); Emotion emotion = createTestEmotion("행복8", "#FFD700"); // 방문 국가 등록