diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..fbefbe6 --- /dev/null +++ b/.env.example @@ -0,0 +1,33 @@ +# Application +APP_BASE_URL=http://localhost:8080 +APP_FRONTEND_BASE_URL=http://localhost:8080 +APP_CORS_ALLOWED_ORIGINS=http://localhost:8080 + +# Database +DB_URL=jdbc:mysql://localhost:3306/loginmodule?serverTimezone=Asia/Seoul&characterEncoding=UTF-8 +DB_USERNAME=root +DB_PASSWORD=change-me +DB_DRIVER_CLASS_NAME=com.mysql.cj.jdbc.Driver +JPA_DDL_AUTO=update +JPA_SHOW_SQL=true + +# JWT +# Must be at least 32 bytes. +JWT_SECRET=change-me-to-at-least-32-bytes-secret +JWT_ACCESS_EXP_MIN=60 +JWT_REFRESH_EXP_DAY=30 + +# OAuth redirect base +# Default: ${APP_FRONTEND_BASE_URL}/oauth/callback +# OAUTH_REDIRECT_BASE_URI=http://localhost:8080/oauth/callback + +# Google Developer Console +GOOGLE_CLIENT_ID=change-me +GOOGLE_CLIENT_SECRET=change-me +# GOOGLE_REDIRECT_URI=http://localhost:8080/oauth/callback/google + +# Meta for Developers / Facebook Login +FACEBOOK_CLIENT_ID=change-me +FACEBOOK_CLIENT_SECRET=change-me +FACEBOOK_GRAPH_API_VERSION=v21.0 +# FACEBOOK_REDIRECT_URI=http://localhost:8080/oauth/callback/facebook \ No newline at end of file diff --git a/.gitignore b/.gitignore index c2065bc..ab06448 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,6 @@ out/ ### VS Code ### .vscode/ + +### .env ### +.env \ No newline at end of file diff --git a/src/main/java/com/daehanforeigner/capstone/domain/audio_file/entity/AudioFile.java b/src/main/java/com/daehanforeigner/capstone/domain/audio_file/entity/AudioFile.java new file mode 100644 index 0000000..f69c3a8 --- /dev/null +++ b/src/main/java/com/daehanforeigner/capstone/domain/audio_file/entity/AudioFile.java @@ -0,0 +1,34 @@ +package com.daehanforeigner.capstone.domain.audio_file.entity; + +import com.daehanforeigner.capstone.domain.pronunciation_attempt.PronunciationAttempt; +import com.daehanforeigner.capstone.global.entity.GlobalEntity; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Table(name = "audio_file") +public class AudioFile extends GlobalEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "audio_file_id") + private Long audioFileId; + + @ManyToOne + @JoinColumn(name = "attempt_id") + private PronunciationAttempt attempt; + + @Column(name = "file_url") // 오디오 파일 URL + private String fileUrl; + + @Column(name = "file_size") // 오디오 파일 크기 + private int fileSize; + + @Column(name = "duration_ms") // 오디오 파일 길이 (밀리초 단위) + private int durationMs; +} diff --git a/src/main/java/com/daehanforeigner/capstone/domain/content_category/CategoryType.java b/src/main/java/com/daehanforeigner/capstone/domain/content_category/CategoryType.java new file mode 100644 index 0000000..71989c5 --- /dev/null +++ b/src/main/java/com/daehanforeigner/capstone/domain/content_category/CategoryType.java @@ -0,0 +1,6 @@ +package com.daehanforeigner.capstone.domain.content_category; + +public enum CategoryType { + BASIC, // 기본 학습 + CULTURE // 문화 학습 +} diff --git a/src/main/java/com/daehanforeigner/capstone/domain/content_category/ContentCategory.java b/src/main/java/com/daehanforeigner/capstone/domain/content_category/ContentCategory.java new file mode 100644 index 0000000..ba05bc9 --- /dev/null +++ b/src/main/java/com/daehanforeigner/capstone/domain/content_category/ContentCategory.java @@ -0,0 +1,30 @@ +package com.daehanforeigner.capstone.domain.content_category; + +import com.daehanforeigner.capstone.global.entity.GlobalEntity; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Table(name = "content_category") +public class ContentCategory extends GlobalEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "category_id") + private Long categoryId; + + @Enumerated(EnumType.STRING) + @Column(name = "type") + private CategoryType type; + + @Column(name = "name") // 카테고리 이름 ex) 학습, 드라마 대사, K문화, 신조어 등 + private String name; + + @Column(name = "description") // 카테고리 설명 + private String description; +} diff --git a/src/main/java/com/daehanforeigner/capstone/domain/feedback/entity/Feedback.java b/src/main/java/com/daehanforeigner/capstone/domain/feedback/entity/Feedback.java new file mode 100644 index 0000000..6e724b5 --- /dev/null +++ b/src/main/java/com/daehanforeigner/capstone/domain/feedback/entity/Feedback.java @@ -0,0 +1,33 @@ +package com.daehanforeigner.capstone.domain.feedback.entity; + +import com.daehanforeigner.capstone.domain.pronunciation_attempt.PronunciationAttempt; +import com.daehanforeigner.capstone.domain.user.entity.NativeLangauge; +import com.daehanforeigner.capstone.global.entity.GlobalEntity; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Table(name = "feedback") +public class Feedback extends GlobalEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "feedback_id") + private Long feedbackId; + + @ManyToOne + @JoinColumn(name = "attempt_id") + private PronunciationAttempt attempt; + + @Column(name = "content") // 피드백 내용 + private String content; + + @Enumerated(EnumType.STRING) + @Column(name = "language") // 피드백 언어 + private NativeLangauge language; +} diff --git a/src/main/java/com/daehanforeigner/capstone/domain/game_field_table/entity/GameFieldTable.java b/src/main/java/com/daehanforeigner/capstone/domain/game_field_table/entity/GameFieldTable.java new file mode 100644 index 0000000..1dc4545 --- /dev/null +++ b/src/main/java/com/daehanforeigner/capstone/domain/game_field_table/entity/GameFieldTable.java @@ -0,0 +1,30 @@ +package com.daehanforeigner.capstone.domain.game_field_table.entity; + +import com.daehanforeigner.capstone.domain.game_result.entity.GameResult; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Table(name = "game_field_table") +public class GameFieldTable { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "game_sentence_id") // 게임 문장 ID + private Long gameSentenceId; + + @ManyToOne + @JoinColumn(name = "game_result_id") // 게임 결과 ID + private GameResult gameResult; + + @Column(name = "is_sentence") // 문장 여부 + private boolean isSentence; + + @Column(name = "game_field") // 내용 + private String gameField; +} diff --git a/src/main/java/com/daehanforeigner/capstone/domain/game_result/entity/GameResult.java b/src/main/java/com/daehanforeigner/capstone/domain/game_result/entity/GameResult.java new file mode 100644 index 0000000..6694c06 --- /dev/null +++ b/src/main/java/com/daehanforeigner/capstone/domain/game_result/entity/GameResult.java @@ -0,0 +1,40 @@ +package com.daehanforeigner.capstone.domain.game_result.entity; + +import com.daehanforeigner.capstone.domain.content_category.ContentCategory; +import com.daehanforeigner.capstone.domain.user.entity.User; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Entity +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Table(name = "game_result") +public class GameResult { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "game_result_id") + private Long gameResultId; + + @ManyToOne + @JoinColumn(name = "user_id") + private User user; + + @ManyToOne + @JoinColumn(name = "category_id") + private ContentCategory category; + + @Column(name = "score") + private int score; + + @Column(name = "is_passed") + private boolean isPassed; + + @Column(name = "played_at") + private LocalDateTime playedAt; +} diff --git a/src/main/java/com/daehanforeigner/capstone/domain/learning_content/ContentType.java b/src/main/java/com/daehanforeigner/capstone/domain/learning_content/ContentType.java new file mode 100644 index 0000000..590f436 --- /dev/null +++ b/src/main/java/com/daehanforeigner/capstone/domain/learning_content/ContentType.java @@ -0,0 +1,7 @@ +package com.daehanforeigner.capstone.domain.learning_content; + +public enum ContentType { + SYLLABLE, // 음절 + WORD, // 단어 + SENTENCE // 문장 +} diff --git a/src/main/java/com/daehanforeigner/capstone/domain/learning_content/Difficulty.java b/src/main/java/com/daehanforeigner/capstone/domain/learning_content/Difficulty.java new file mode 100644 index 0000000..ca405f5 --- /dev/null +++ b/src/main/java/com/daehanforeigner/capstone/domain/learning_content/Difficulty.java @@ -0,0 +1,7 @@ +package com.daehanforeigner.capstone.domain.learning_content; + +public enum Difficulty { + BEGINNER, // 초급 + INTERMEDIATE, // 중급 + ADVANCED // 고급 +} diff --git a/src/main/java/com/daehanforeigner/capstone/domain/learning_content/LearningContent.java b/src/main/java/com/daehanforeigner/capstone/domain/learning_content/LearningContent.java new file mode 100644 index 0000000..cc7610b --- /dev/null +++ b/src/main/java/com/daehanforeigner/capstone/domain/learning_content/LearningContent.java @@ -0,0 +1,44 @@ +package com.daehanforeigner.capstone.domain.learning_content; + +import com.daehanforeigner.capstone.domain.content_category.ContentCategory; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Table(name = "learning_content") +public class LearningContent { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "content_id") + private Long contentId; + + @ManyToOne + @JoinColumn(name = "category_id") + private ContentCategory contentCategory; + + @Enumerated(EnumType.STRING) + @Column(name = "content_type") // 음절, 단어, 문장 + private ContentType contentType; + + @Enumerated(EnumType.STRING) + @Column(name = "difficulty") // 초급, 중급, 고급 + private Difficulty difficulty; + + @Column(name = "text") // 학습 텍스트 + private String text; + + @Column(name = "meaning") // 의미 + private String meaning; + + @Column(name = "example_sentence") // 예문 + private String exampleSentence; + + @Column(name = "pronunciation_guide") // 발음 가이드 + private String pronunciationGuide; +} diff --git a/src/main/java/com/daehanforeigner/capstone/domain/phoneme_score/entity/PhonemeScore.java b/src/main/java/com/daehanforeigner/capstone/domain/phoneme_score/entity/PhonemeScore.java new file mode 100644 index 0000000..bf6f4b6 --- /dev/null +++ b/src/main/java/com/daehanforeigner/capstone/domain/phoneme_score/entity/PhonemeScore.java @@ -0,0 +1,31 @@ +package com.daehanforeigner.capstone.domain.phoneme_score.entity; + +import com.daehanforeigner.capstone.domain.pronunciation_attempt.PronunciationAttempt; +import com.daehanforeigner.capstone.global.entity.GlobalEntity; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Table(name = "phoneme_score") +public class PhonemeScore extends GlobalEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "phoneme_score_id") + private Long phonemeScoreId; + + @ManyToOne + @JoinColumn(name = "attempt_id") + private PronunciationAttempt attempt; + + @Column(name = "phoneme") // 음소 + private String phoneme; + + @Column(name = "score") // 점수 + private int score; +} diff --git a/src/main/java/com/daehanforeigner/capstone/domain/pronunciation_attempt/PronunciationAttempt.java b/src/main/java/com/daehanforeigner/capstone/domain/pronunciation_attempt/PronunciationAttempt.java new file mode 100644 index 0000000..697bfcc --- /dev/null +++ b/src/main/java/com/daehanforeigner/capstone/domain/pronunciation_attempt/PronunciationAttempt.java @@ -0,0 +1,47 @@ +package com.daehanforeigner.capstone.domain.pronunciation_attempt; + +import com.daehanforeigner.capstone.domain.learning_content.LearningContent; +import com.daehanforeigner.capstone.domain.user.entity.User; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Table(name = "pronunciation_attempt") +public class PronunciationAttempt { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "attempt_id") + private Long attemptId; + + @ManyToOne + @JoinColumn(name = "user_id") + private User user; + + @ManyToOne + @JoinColumn(name = "content_id") + private LearningContent learningContent; + + @Column(name = "recognized_text") // STT 인식 결과 + private String recognizedText; + + @Column(name = "voice_score") // 발음 점수 + private int voiceScore; + + @Column(name = "lip_score") // 입모양 점수 + private int lipScore; + + @Column(name = "accuracy") // 정확도 (음성 70%, 입모양 30%) + private double accuracy; + + @Column(name = "is_passed") // 합격 여부 (85% 이상 합격) + private boolean isPassed; + + @Column(name = "user_pitch_data", columnDefinition = "json") // 사용자의 피치 데이터 (JSON 형식) + private String userPitchData; +} diff --git a/src/main/java/com/daehanforeigner/capstone/domain/ranking/entity/Ranking.java b/src/main/java/com/daehanforeigner/capstone/domain/ranking/entity/Ranking.java new file mode 100644 index 0000000..9b778da --- /dev/null +++ b/src/main/java/com/daehanforeigner/capstone/domain/ranking/entity/Ranking.java @@ -0,0 +1,31 @@ +package com.daehanforeigner.capstone.domain.ranking.entity; + +import com.daehanforeigner.capstone.domain.user.entity.User; +import com.daehanforeigner.capstone.global.entity.GlobalEntity; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Table(name = "ranking") +public class Ranking extends GlobalEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "rank_id") + private Long rankId; + + @ManyToOne + @JoinColumn(name = "user_id") + private User user; + + @Column(name = "best_score") + private int bestScore; + + @Column(name = "spoken_word_count") // 말한 단어 수 + private int spokenWordCount; +} diff --git a/src/main/java/com/daehanforeigner/capstone/domain/social_account/entity/Provider.java b/src/main/java/com/daehanforeigner/capstone/domain/social_account/entity/Provider.java new file mode 100644 index 0000000..fe4c049 --- /dev/null +++ b/src/main/java/com/daehanforeigner/capstone/domain/social_account/entity/Provider.java @@ -0,0 +1,6 @@ +package com.daehanforeigner.capstone.domain.social_account.entity; + +public enum Provider { + GOOGLE, + FACEBOOK +} diff --git a/src/main/java/com/daehanforeigner/capstone/domain/social_account/entity/SocialAccount.java b/src/main/java/com/daehanforeigner/capstone/domain/social_account/entity/SocialAccount.java new file mode 100644 index 0000000..b19df75 --- /dev/null +++ b/src/main/java/com/daehanforeigner/capstone/domain/social_account/entity/SocialAccount.java @@ -0,0 +1,29 @@ +package com.daehanforeigner.capstone.domain.social_account.entity; + +import com.daehanforeigner.capstone.domain.user.entity.User; +import com.daehanforeigner.capstone.global.entity.GlobalEntity; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Table(name = "social_account") +public class SocialAccount extends GlobalEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "social_id") + private Long socialId; + + @ManyToOne + @JoinColumn(name = "user_id") + private User user; + + @Enumerated(EnumType.STRING) + @Column(name = "provider") + private Provider provider; +} diff --git a/src/main/java/com/daehanforeigner/capstone/domain/standard_pronunciation/entity/StandardPronunciation.java b/src/main/java/com/daehanforeigner/capstone/domain/standard_pronunciation/entity/StandardPronunciation.java new file mode 100644 index 0000000..bff4120 --- /dev/null +++ b/src/main/java/com/daehanforeigner/capstone/domain/standard_pronunciation/entity/StandardPronunciation.java @@ -0,0 +1,34 @@ +package com.daehanforeigner.capstone.domain.standard_pronunciation.entity; + +import com.daehanforeigner.capstone.domain.learning_content.LearningContent; +import com.daehanforeigner.capstone.global.entity.GlobalEntity; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Table(name = "standard_pronunciation") +public class StandardPronunciation extends GlobalEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "pronunciation_id") + private Long pronunciationId; + + @ManyToOne + @JoinColumn(name = "content_id") + private LearningContent learningContent; + + @Column(name = "native_pitch_data", columnDefinition = "json") // 피치 데이터 + private String nativePitchData; + + @Column(name = "lip_sequence", columnDefinition = "json") // 입술 움직임 시퀀스 + private String lipSequence; + + @Column(name = "answer_audio_url") // 정답 오디오 URL + private String answerAudioUrl; +} diff --git a/src/main/java/com/daehanforeigner/capstone/domain/user/entity/NativeLanguage.java b/src/main/java/com/daehanforeigner/capstone/domain/user/entity/NativeLanguage.java new file mode 100644 index 0000000..e49f571 --- /dev/null +++ b/src/main/java/com/daehanforeigner/capstone/domain/user/entity/NativeLanguage.java @@ -0,0 +1,8 @@ +package com.daehanforeigner.capstone.domain.user.entity; + +public enum NativeLanguage { + KR, // 한국어 + EN, // 영어 + JP, // 일본어 + CN, // 중국어 +} diff --git a/src/main/java/com/daehanforeigner/capstone/domain/user/entity/Role.java b/src/main/java/com/daehanforeigner/capstone/domain/user/entity/Role.java new file mode 100644 index 0000000..4a9cbc2 --- /dev/null +++ b/src/main/java/com/daehanforeigner/capstone/domain/user/entity/Role.java @@ -0,0 +1,6 @@ +package com.daehanforeigner.capstone.domain.user.entity; + +public enum Role { + USER, + ADMIN +} diff --git a/src/main/java/com/daehanforeigner/capstone/domain/user/entity/Status.java b/src/main/java/com/daehanforeigner/capstone/domain/user/entity/Status.java new file mode 100644 index 0000000..f7dd5f2 --- /dev/null +++ b/src/main/java/com/daehanforeigner/capstone/domain/user/entity/Status.java @@ -0,0 +1,6 @@ +package com.daehanforeigner.capstone.domain.user.entity; + +public enum Status { + ACTIVE, // 활성 + WITHDRAWN // 탈퇴 +} diff --git a/src/main/java/com/daehanforeigner/capstone/domain/user/entity/User.java b/src/main/java/com/daehanforeigner/capstone/domain/user/entity/User.java new file mode 100644 index 0000000..284fbce --- /dev/null +++ b/src/main/java/com/daehanforeigner/capstone/domain/user/entity/User.java @@ -0,0 +1,47 @@ +package com.daehanforeigner.capstone.domain.user.entity; + +import com.daehanforeigner.capstone.global.entity.GlobalEntity; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Table(name = "user") +public class User extends GlobalEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "user_id") // PK 유저 ID + private Long userId; + + @Column(name = "email") // 이메일 + private String email; + + @Column(name = "password") // 비밀번호 (소셜은 null) + private String password; + + @Column(name = "nickname") // 닉네임 + private String nickname; + + @Enumerated(EnumType.STRING) + @Column(name = "native_language") // 모국어 + private NativeLangauge nativeLanguage; + + @Column(name = "profile_image_url") // 프로필 이미지 URL + private String profileImageUrl; + + @Enumerated(EnumType.STRING) + @Column(name = "role") // 권한 (USER, ADMIN) + private Role role; + + @Enumerated(EnumType.STRING) + @Column(name = "status") // 상태 (ACTIVE, WITHDRAWN) + private Status status; + + @Column(name = "refresh_token") // 리프레시 토큰 + private String refreshToken; +} diff --git a/src/main/java/com/daehanforeigner/capstone/domain/wrong_answer/entity/WrongAnswer.java b/src/main/java/com/daehanforeigner/capstone/domain/wrong_answer/entity/WrongAnswer.java new file mode 100644 index 0000000..ea56c32 --- /dev/null +++ b/src/main/java/com/daehanforeigner/capstone/domain/wrong_answer/entity/WrongAnswer.java @@ -0,0 +1,42 @@ +package com.daehanforeigner.capstone.domain.wrong_answer.entity; + +import com.daehanforeigner.capstone.domain.learning_content.LearningContent; +import com.daehanforeigner.capstone.domain.user.entity.User; +import com.daehanforeigner.capstone.global.entity.GlobalEntity; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Entity +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Table(name = "wrong_answer") +public class WrongAnswer extends GlobalEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "wrong_id") + private Long wrongId; + + @ManyToOne + @JoinColumn(name = "user_id") + private User user; + + @ManyToOne + @JoinColumn(name = "content_id") + private LearningContent learningContent; + + @Column(name = "wrong_count") // 틀린 횟수 + private int wrongCount; + + @Column(name = "is_solved") // 해결 여부 + private boolean isSolved; + + @Column(name = "last_attempted_at") // 마지막 시도 날짜 + private LocalDateTime lastAttemptedAt; +} + diff --git a/src/main/java/com/daehanforeigner/capstone/global/config/SecurityConfig.java b/src/main/java/com/daehanforeigner/capstone/global/config/SecurityConfig.java new file mode 100644 index 0000000..7793581 --- /dev/null +++ b/src/main/java/com/daehanforeigner/capstone/global/config/SecurityConfig.java @@ -0,0 +1,53 @@ +package com.daehanforeigner.capstone.global.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +import java.util.List; + +@Configuration +@EnableWebSecurity +public class SecurityConfig { + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + // CORS 활성화 (아래 corsConfigurationSource 사용) + .cors(cors -> cors.configurationSource(corsConfigurationSource())) + // REST API + JWT 예정이므로 CSRF 비활성화 + .csrf(AbstractHttpConfigurer::disable) + // 세션 사용 안 함 (무상태) + .sessionManagement(session -> + session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + // 인가 규칙 (지금은 뼈대 → 우선 전체 허용, 이후 좁혀나가기) + .authorizeHttpRequests(auth -> auth + .anyRequest().permitAll() + ); + + return http.build(); + } + + @Bean + public CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration config = new CorsConfiguration(); + config.setAllowedOrigins(List.of( + "http://localhost:3000", // 프론트 개발 서버 (필요시 수정) + "http://localhost:8080" + )); + config.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")); + config.setAllowedHeaders(List.of("*")); + config.setAllowCredentials(true); // 쿠키/인증정보 허용 + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", config); + return source; + } +} \ No newline at end of file diff --git a/src/main/java/com/daehanforeigner/capstone/global/entity/GlobalEntity.java b/src/main/java/com/daehanforeigner/capstone/global/entity/GlobalEntity.java new file mode 100644 index 0000000..73282b5 --- /dev/null +++ b/src/main/java/com/daehanforeigner/capstone/global/entity/GlobalEntity.java @@ -0,0 +1,28 @@ +package com.daehanforeigner.capstone.global.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.MappedSuperclass; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.SuperBuilder; + +import java.time.LocalDateTime; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@SuperBuilder +@MappedSuperclass +@EntityListeners(GlobalEntityListener.class) +public class GlobalEntity { + + @Column(name = "created_at", nullable = false) + private LocalDateTime createdAt; + + @Column(name = "updated_at", nullable = false) + private LocalDateTime updatedAt; +} diff --git a/src/main/java/com/daehanforeigner/capstone/global/entity/GlobalEntityListener.java b/src/main/java/com/daehanforeigner/capstone/global/entity/GlobalEntityListener.java new file mode 100644 index 0000000..be58cbe --- /dev/null +++ b/src/main/java/com/daehanforeigner/capstone/global/entity/GlobalEntityListener.java @@ -0,0 +1,25 @@ +package com.daehanforeigner.capstone.global.entity; + +import jakarta.persistence.PrePersist; +import jakarta.persistence.PreUpdate; + +import java.time.LocalDateTime; + +public class GlobalEntityListener { + + @PrePersist + public void prePersist(GlobalEntity entity) { + // 생성일과 수정일을 현재 시간으로 설정해서 저장 + LocalDateTime now = LocalDateTime.now(); + + entity.setCreatedAt(now); + + entity.setUpdatedAt(now); + } + + @PreUpdate + public void preUpdate(GlobalEntity entity) { + // 수정일을 현재 시간으로 설정해서 저장 + entity.setUpdatedAt(LocalDateTime.now()); + } +} diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml new file mode 100644 index 0000000..2b29a1b --- /dev/null +++ b/src/main/resources/application-local.yml @@ -0,0 +1,18 @@ +# 로컬 환경에서 사용하는 설정 +spring: + # 로컬 DB 접속 정보 (MySQL 사용) + datasource: + url: ${DB_URL} + username: ${DB_USERNAME} + password: ${DB_PASSWORD} + driver-class-name: com.mysql.cj.jdbc.Driver + + jpa: + hibernate: + ddl-auto: update + show-sql: true + +logging: + level: + org.springframework.security: DEBUG + org.hibernate.SQL: DEBUG \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 11458d8..d7d9d83 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,3 +1,45 @@ spring: - application: - name: capstone + profiles: + active: local + + # 소셜 로그인 (구글, 페이스북 로그인) 설정 + security: + oauth2: + client: + registration: + google: + client-id: ${GOOGLE_CLIENT_ID} + client-secret: ${GOOGLE_CLIENT_SECRET} + scope: + - email + - profile + facebook: + client-id: ${FACEBOOK_CLIENT_ID} + client-secret: ${FACEBOOK_CLIENT_SECRET} + scope: + - email + - public_profile + + jpa: + hibernate: + ddl-auto: validate + properties: + hibernate: + format_sql: true + +custom: + # JWT 설정 + jwt: + secret-key: ${JWT_SECRET} + access-exp-min: ${JWT_ACCESS_EXP_MIN:60} # 토큰 유효시간 : 60분 + refresh-exp-day: ${JWT_REFRESH_EXP_DAY:30} # RT 유효시간 : 30일 + +# Swagger 설정 +springdoc: + swagger-ui: + path: /swagger-ui.html + tags-sorter: alpha # API 그룹 알파벳 순으로 정렬 + operations-sorter: alpha # 각 그룹 안의 API를 알파벳 순으로 정렬 + api-docs: + default-consumes-media-type: application/json # 요청 기본 타입을 JSON으로 설정 + default-produces-media-type: application/json # 응답 기본 타입을 JSON으로 설정 \ No newline at end of file