diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 88dcfc35..3653a42a 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -24,9 +24,6 @@ jobs:
# Set up Docker Buildx
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- with:
- install: true
- driver: docker
# Build and push Docker image
- name: Build and push
diff --git a/src/main/generated/learningFlow/learningFlow_BE/domain/QEmailVerificationToken.java b/src/main/generated/learningFlow/learningFlow_BE/domain/QEmailVerificationToken.java
index 39ab4679..a1dd4c84 100644
--- a/src/main/generated/learningFlow/learningFlow_BE/domain/QEmailVerificationToken.java
+++ b/src/main/generated/learningFlow/learningFlow_BE/domain/QEmailVerificationToken.java
@@ -7,6 +7,7 @@
import com.querydsl.core.types.PathMetadata;
import javax.annotation.processing.Generated;
import com.querydsl.core.types.Path;
+import com.querydsl.core.types.dsl.PathInits;
/**
@@ -17,6 +18,8 @@ public class QEmailVerificationToken extends EntityPathBase updatedAt = _super.updatedAt;
+ public final QUser user;
+
public final BooleanPath verified = createBoolean("verified");
public QEmailVerificationToken(String variable) {
- super(EmailVerificationToken.class, forVariable(variable));
+ this(EmailVerificationToken.class, forVariable(variable), INITS);
}
public QEmailVerificationToken(Path extends EmailVerificationToken> path) {
- super(path.getType(), path.getMetadata());
+ this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS));
}
public QEmailVerificationToken(PathMetadata metadata) {
- super(EmailVerificationToken.class, metadata);
+ this(metadata, PathInits.getFor(metadata, INITS));
+ }
+
+ public QEmailVerificationToken(PathMetadata metadata, PathInits inits) {
+ this(EmailVerificationToken.class, metadata, inits);
+ }
+
+ public QEmailVerificationToken(Class extends EmailVerificationToken> type, PathMetadata metadata, PathInits inits) {
+ super(type, metadata, inits);
+ this.user = inits.isInitialized("user") ? new QUser(forProperty("user")) : null;
}
}
diff --git a/src/main/generated/learningFlow/learningFlow_BE/domain/QUser.java b/src/main/generated/learningFlow/learningFlow_BE/domain/QUser.java
index a772f919..aa6ef6fb 100644
--- a/src/main/generated/learningFlow/learningFlow_BE/domain/QUser.java
+++ b/src/main/generated/learningFlow/learningFlow_BE/domain/QUser.java
@@ -22,6 +22,8 @@ public class QUser extends EntityPathBase {
public final QBaseEntity _super = new QBaseEntity(this);
+ public final StringPath bannerImgUrl = createString("bannerImgUrl");
+
public final ListPath> bookmarkedCollectionIds = this.>createList("bookmarkedCollectionIds", Long.class, NumberPath.class, PathInits.DIRECT2);
//inherited
diff --git a/src/main/generated/learningFlow/learningFlow_BE/domain/QUserEpisodeProgress.java b/src/main/generated/learningFlow/learningFlow_BE/domain/QUserEpisodeProgress.java
index 6b9e370c..e7aba947 100644
--- a/src/main/generated/learningFlow/learningFlow_BE/domain/QUserEpisodeProgress.java
+++ b/src/main/generated/learningFlow/learningFlow_BE/domain/QUserEpisodeProgress.java
@@ -31,6 +31,8 @@ public class QUserEpisodeProgress extends EntityPathBase {
public final NumberPath episodeNumber = createNumber("episodeNumber", Integer.class);
+ public final BooleanPath isComplete = createBoolean("isComplete");
+
public final EnumPath resourceType = createEnum("resourceType", learningFlow.learningFlow_BE.domain.enums.ResourceType.class);
public final NumberPath totalProgress = createNumber("totalProgress", Integer.class);
diff --git a/src/main/java/learningFlow/learningFlow_BE/LearningFlowBeApplication.java b/src/main/java/learningFlow/learningFlow_BE/LearningFlowBeApplication.java
index 3d9123a8..0bd7a76b 100644
--- a/src/main/java/learningFlow/learningFlow_BE/LearningFlowBeApplication.java
+++ b/src/main/java/learningFlow/learningFlow_BE/LearningFlowBeApplication.java
@@ -2,7 +2,6 @@
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
-import org.springframework.context.annotation.Import;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.scheduling.annotation.EnableScheduling;
diff --git a/src/main/java/learningFlow/learningFlow_BE/apiPayload/code/status/ErrorStatus.java b/src/main/java/learningFlow/learningFlow_BE/apiPayload/code/status/ErrorStatus.java
index 503babf1..7dd2dde5 100644
--- a/src/main/java/learningFlow/learningFlow_BE/apiPayload/code/status/ErrorStatus.java
+++ b/src/main/java/learningFlow/learningFlow_BE/apiPayload/code/status/ErrorStatus.java
@@ -21,9 +21,24 @@ public enum ErrorStatus implements BaseErrorCode {
// 멤버 관려 에러
+ EMAIL_ALREADY_EXISTS(HttpStatus.BAD_REQUEST,"EMAIL4001" ,"이미 동일한 이메일로 생성된 계정이 존재합니다."),
+ EMAIL_VERIFICATION_IN_PROGRESS(HttpStatus.BAD_REQUEST, "EMAIL4002", "이미 진행 중인 이메일 인증이 있습니다. 이메일을 확인해주세요."),
+ EMAIL_CHANGE_SAME_AS_CURRENT(HttpStatus.BAD_REQUEST, "EMAIL4004", "기존과 동일한 이메일로는 변경하실 수 없습니다."),
+ GOOGLE_USER_CANNOT_CHANGE_EMAIL(HttpStatus.BAD_REQUEST,"EMAIL4005","구글 로그인 유저는 이메일 변경을 하실 수 없습니다."),
+ EMAIL_CODE_INVALID(HttpStatus.BAD_REQUEST, "EMAIL4006", "유효하지 않은 이메일 인증 코드입니다."),
+ EMAIL_CODE_EXPIRED(HttpStatus.BAD_REQUEST,"EMAIL4007","만료된 이메일 인증 코드입니다. 이메일 인증을 다시 요청해주세요."),
+
USER_NOT_FOUND(HttpStatus.BAD_REQUEST, "USER4001", "사용자를 찾을 수 없습니다."),
NICKNAME_NOT_EXIST(HttpStatus.BAD_REQUEST, "USER4002", "닉네임은 필수 입니다."),
+ // 비밀번호 관련 에러 추가
+ INVALID_PASSWORD(HttpStatus.BAD_REQUEST, "PASSWORD4001", "유효하지 않은 비밀번호입니다."),
+ PASSWORD_CURRENT_MISMATCH(HttpStatus.BAD_REQUEST, "PASSWORD4002", "현재 비밀번호가 일치하지 않습니다."),
+ PASSWORD_SAME_AS_CURRENT(HttpStatus.BAD_REQUEST, "PASSWORD4003", "새 비밀번호는 현재 비밀번호와 달라야 합니다."),
+ PASSWORD_RESET_CODE_INVALID(HttpStatus.BAD_REQUEST, "PASSWORD4004", "유효하지 않은 비밀번호 재설정 코드입니다."),
+ PASSWORD_RESET_CODE_EXPIRED(HttpStatus.BAD_REQUEST, "PASSWORD4005", "만료된 비밀번호 재설정 코드입니다. 비밀번호 재설정을 다시 요청해주세요."),
+
+
//Resources 관련 에어
RESOURCES_NOT_FOUND(HttpStatus.NOT_FOUND,"RESOURCE4001","강의 에피소드를 찾을 수 없습니다."),
QUANTITY_IS_NULL(HttpStatus.BAD_REQUEST, "RESOURCE4002", "분량이 존재하지 않습니다"),
@@ -41,10 +56,6 @@ public enum ErrorStatus implements BaseErrorCode {
// 예시,,,
ARTICLE_NOT_FOUND(HttpStatus.NOT_FOUND, "ARTICLE4001", "게시글이 없습니다."),
-
- EMAIL_ALREADY_EXISTS(HttpStatus.BAD_REQUEST,"EMAIL4001" ,"이미 동일한 이메일로 생성된 계정이 존재합니다."),
- INVALID_PASSWORD(HttpStatus.BAD_REQUEST, "PASSWORD4001", "유효하지 않은 비밀번호입니다."),
-
//이미지
IMAGE_FORMAT_BADREQUEST(HttpStatus.BAD_REQUEST,"COMMON400","이미지 파일만 업로드할 수 있습니다."),
IMAGE_UPLOAD_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "COMMON5001", "이미지 업로드에 실패했습니다. 다시 시도해주세요."),
diff --git a/src/main/java/learningFlow/learningFlow_BE/config/CorsConfig.java b/src/main/java/learningFlow/learningFlow_BE/config/CorsConfig.java
index 8f1b0568..080ca919 100644
--- a/src/main/java/learningFlow/learningFlow_BE/config/CorsConfig.java
+++ b/src/main/java/learningFlow/learningFlow_BE/config/CorsConfig.java
@@ -1,3 +1,4 @@
+/*
package learningFlow.learningFlow_BE.config;
//Spring Security까지 CORS 적용 목적
@@ -7,6 +8,7 @@
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
+import java.util.Arrays;
import java.util.List;
@Configuration
@@ -16,12 +18,28 @@ public class CorsConfig {
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
+
+ config.setAllowedOrigins(Arrays.asList(
+ "http://localhost:3000",
+ "http://localhost:8081",
+ "http://onboarding.p-e.kr:8080",
+ "http://54.180.118.227",
+ "https://accounts.google.com"
+ ));
+
+ config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"));
+ config.setAllowedHeaders(Arrays.asList("*"));
config.setAllowCredentials(true);
- config.setAllowedOrigins(List.of("http://localhost:3000", "http://localhost:8081")); // ✅ Swagger 포함
- config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"));
- config.setAllowedHeaders(List.of("*"));
- config.setMaxAge(3600L);
+ config.setExposedHeaders(Arrays.asList(
+ "Authorization",
+ "Refresh-Token",
+ "Access-Control-Allow-Origin",
+ "Access-Control-Allow-Credentials"
+ ));
+ config.setMaxAge(86400L);
+
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
}
+*/
diff --git a/src/main/java/learningFlow/learningFlow_BE/config/WebConfig.java b/src/main/java/learningFlow/learningFlow_BE/config/WebConfig.java
index d94dacbe..b5040d3d 100644
--- a/src/main/java/learningFlow/learningFlow_BE/config/WebConfig.java
+++ b/src/main/java/learningFlow/learningFlow_BE/config/WebConfig.java
@@ -1,3 +1,4 @@
+/*
package learningFlow.learningFlow_BE.config;
import org.springframework.context.annotation.Configuration;
@@ -10,11 +11,19 @@ public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
- .allowedOrigins("http://localhost:3000", "http://localhost:8080", "http://onboarding.p-e.kr:8080", "http://54.180.118.227")
-// .allowedOrigins("http://localhost:3000") // 프론트엔드 주소
+ .allowedOriginPatterns("*") // 개발 중일 때만 사용
+ // 또는 특정 출처만 허용
+ .allowedOrigins(
+ "http://localhost:3000",
+ "http://localhost:8081",
+ "http://onboarding.p-e.kr:8080",
+ "http://54.180.118.227",
+ "https://accounts.google.com"
+ )
.allowedMethods("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(86400L);
}
}
+*/
diff --git a/src/main/java/learningFlow/learningFlow_BE/config/security/SecurityConfig.java b/src/main/java/learningFlow/learningFlow_BE/config/security/SecurityConfig.java
index daff5cd2..1e23f7df 100644
--- a/src/main/java/learningFlow/learningFlow_BE/config/security/SecurityConfig.java
+++ b/src/main/java/learningFlow/learningFlow_BE/config/security/SecurityConfig.java
@@ -53,10 +53,16 @@ public SecurityFilterChain filterChain(HttpSecurity http, JwtAuthenticationFilte
"/search/**",
"/",
"/collections/{collectionId:[\\d]+}",
- "/image/upload" //이미지 업로드는 허용
+ "/image/upload", //이미지 업로드는 허용
+ "/favicon.ico",
+ "/register",
+ "/register/complete",
+ "/login",
+ "/login/google",
+ "/oauth2/**",
+ "/login/oauth2/**",
+ "/user/change-email"
).permitAll()
- .requestMatchers(
- "/register", "/register/complete", "/login", "/login/google", "/oauth2/**").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.requestMatchers("/user/**", "/resources/**", "/collections/{collectionId:[\\d]+}/likes", "/logout/**").authenticated()
.anyRequest().permitAll()
@@ -72,7 +78,6 @@ public SecurityFilterChain filterChain(HttpSecurity http, JwtAuthenticationFilte
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
.exceptionHandling(exception ->
exception.authenticationEntryPoint(authenticationEntryPoint));
-
return http.build();
}
@@ -89,10 +94,37 @@ public PasswordEncoder passwordEncoder() {
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
- configuration.setAllowedOrigins(List.of("http://localhost:3000")); // 프론트엔드 주소
+
+ configuration.setAllowedOrigins(Arrays.asList(
+ "http://localhost:3000",
+ "http://localhost:8081",
+ "http://onboarding.p-e.kr:8080",
+ "http://54.180.118.227",
+ "https://accounts.google.com"
+ ));
+
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"));
- configuration.setAllowedHeaders(List.of("*"));
+ configuration.setAllowedHeaders(Arrays.asList(
+ "Authorization",
+ "Refresh-Token",
+ "Access-Control-Allow-Origin",
+ "Access-Control-Allow-Credentials",
+ "Content-Type",
+ "Accept",
+ "Origin",
+ "X-Requested-With"
+ ));
configuration.setAllowCredentials(true);
+ configuration.setExposedHeaders(Arrays.asList(
+ "Authorization",
+ "Refresh-Token",
+ "Access-Control-Allow-Origin",
+ "Access-Control-Allow-Credentials",
+ "Content-Type",
+ "Accept",
+ "Origin",
+ "X-Requested-With"
+ ));
configuration.setMaxAge(86400L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
diff --git a/src/main/java/learningFlow/learningFlow_BE/converter/CollectionConverter.java b/src/main/java/learningFlow/learningFlow_BE/converter/CollectionConverter.java
index 870f6f0b..35a5fea8 100644
--- a/src/main/java/learningFlow/learningFlow_BE/converter/CollectionConverter.java
+++ b/src/main/java/learningFlow/learningFlow_BE/converter/CollectionConverter.java
@@ -47,12 +47,12 @@ public static SearchRequestDTO.SearchConditionDTO toSearchConditionDTO(
public static CollectionResponseDTO.SearchResultDTO toSearchResultDTO(
List collections,
- Long lastId,
boolean hasNext,
int totalPages,
int currentPage,
User currentUser,
- Map learningInfoMap
+ Map learningInfoMap,
+ int totalCount
) {
List list = collections.stream()
.map(collection -> toCollectionPreviewDTO(
@@ -63,10 +63,10 @@ public static CollectionResponseDTO.SearchResultDTO toSearchResultDTO(
return CollectionResponseDTO.SearchResultDTO.builder()
.searchResults(list)
- .lastId(lastId)
.hasNext(hasNext)
.currentPage(currentPage)
.totalPages(totalPages)
+ .totalCount(totalCount)
.build();
}
@@ -82,6 +82,7 @@ public static CollectionResponseDTO.CollectionPreviewDTO toCollectionPreviewDTO(
return CollectionResponseDTO.CollectionPreviewDTO.builder()
.collectionId(collection.getId())
+ .imageUrl(collection.getCollectionImgUrl())
.interestField(collection.getInterestField())
.title(collection.getTitle())
.creator(collection.getCreator())
diff --git a/src/main/java/learningFlow/learningFlow_BE/converter/ResourceConverter.java b/src/main/java/learningFlow/learningFlow_BE/converter/ResourceConverter.java
index d57c8197..49a0d5a3 100644
--- a/src/main/java/learningFlow/learningFlow_BE/converter/ResourceConverter.java
+++ b/src/main/java/learningFlow/learningFlow_BE/converter/ResourceConverter.java
@@ -2,12 +2,10 @@
import learningFlow.learningFlow_BE.domain.*;
import learningFlow.learningFlow_BE.domain.Collection;
-import learningFlow.learningFlow_BE.web.dto.collection.CollectionResponseDTO;
import learningFlow.learningFlow_BE.web.dto.resource.ResourceRequestDTO;
import learningFlow.learningFlow_BE.web.dto.resource.ResourceResponseDTO;
import learningFlow.learningFlow_BE.domain.CollectionEpisode;
import learningFlow.learningFlow_BE.domain.UserCollection;
-import learningFlow.learningFlow_BE.web.dto.home.HomeResponseDTO;
import java.util.*;
public class ResourceConverter {
@@ -23,10 +21,15 @@ public static ResourceResponseDTO.ResourceUrlDTO watchEpisode(Collection collect
.urlTitle(resource.getTitle())
.progress(userProgress.getCurrentProgress())
.memoContents(memoContents)
- .episodeInformationList(episodeInformationList(collection))
+ .episodeInformationList(episodeInformationList(collection,userProgress))
.build();
}
- public static ResourceResponseDTO.ResourceBlogUrlDTO watchBlogEpisode(Collection collection, UserEpisodeProgress userProgress, String pageResource, String resourceTitle, Optional memo){
+ public static ResourceResponseDTO.ResourceBlogUrlDTO watchBlogEpisode(
+ Collection collection,
+ UserEpisodeProgress userProgress,
+ String pageResource,
+ String resourceTitle,
+ Optional memo){
String memoContents = "작성하신 글의 첫 줄은 노트의 제목이 됩니다, 최대 2,000자까지 입력하실 수 있어요";
if (memo.isPresent())
memoContents = memo.get().getContents();
@@ -38,17 +41,20 @@ public static ResourceResponseDTO.ResourceBlogUrlDTO watchBlogEpisode(Collection
.urlTitle(resourceTitle)
.progress(userProgress.getCurrentProgress())
.memoContents(memoContents)
- .episodeInformationList(episodeInformationList(collection))
+ .episodeInformationList(episodeInformationList(collection, userProgress))
.build();
}
- public static List episodeInformationList(Collection collection) {
+ public static List episodeInformationList(
+ Collection collection, UserEpisodeProgress userEpisodeProgress
+ ) {
List episodeInformationList = new ArrayList<>();
for (CollectionEpisode episode : collection.getEpisodes()) {
episodeInformationList.add(new ResourceResponseDTO.episodeInformation(
episode.getEpisodeNumber(),
- episode.getResource().getTitle()
+ episode.getResource().getTitle(),
+ userEpisodeProgress.getIsComplete()
));
}
episodeInformationList.sort(Comparator.comparingInt(ResourceResponseDTO.episodeInformation::getEpisodeNumber));
@@ -62,6 +68,12 @@ public static ResourceResponseDTO.ProgressResponseDTO toSaveProgressResponse(Res
.build();
}
+ public static ResourceResponseDTO.changeEpisodeIsCompleteDTO toChangeEpisodeIsCompleteDTO(Boolean isComplete){
+ return ResourceResponseDTO.changeEpisodeIsCompleteDTO.builder()
+ .isComplete(isComplete)
+ .build();
+ }
+
public static ResourceResponseDTO.SearchResultResourceDTO convertToResourceDTO(CollectionEpisode episode) {
return ResourceResponseDTO.SearchResultResourceDTO.builder()
.resourceId(episode.getId())
diff --git a/src/main/java/learningFlow/learningFlow_BE/converter/UserConverter.java b/src/main/java/learningFlow/learningFlow_BE/converter/UserConverter.java
index 9af1797a..5420ff52 100644
--- a/src/main/java/learningFlow/learningFlow_BE/converter/UserConverter.java
+++ b/src/main/java/learningFlow/learningFlow_BE/converter/UserConverter.java
@@ -19,6 +19,7 @@ public static UserResponseDTO.UserLoginResponseDTO toUserLoginResponseDTO(User u
.name(user.getName())
.role(user.getRole())
.socialType(user.getSocialType())
+ .profileImgUrl(user.getProfileImgUrl())
.build();
}
@@ -30,6 +31,7 @@ public static UserInfoDTO convertToUserInfoDTO(User user) {
.interestFields(user.getInterestFields())
.preferType(user.getPreferType())
.profileImgUrl(user.getProfileImgUrl())
+ .bannerImgUrl(user.getBannerImgUrl())
.build();
}
@@ -53,6 +55,7 @@ private static UserResponseDTO.UserPreviewDTO convertToUserPreviewDTO(User user)
.email(user.getEmail())
.job(user.getJob().getDescription())
.profileImgUrl(user.getProfileImgUrl())
+ .bannerImgUrl(user.getBannerImgUrl())
.build();
}
}
diff --git a/src/main/java/learningFlow/learningFlow_BE/domain/EmailVerificationToken.java b/src/main/java/learningFlow/learningFlow_BE/domain/EmailVerificationToken.java
index 3ec808fc..3f357989 100644
--- a/src/main/java/learningFlow/learningFlow_BE/domain/EmailVerificationToken.java
+++ b/src/main/java/learningFlow/learningFlow_BE/domain/EmailVerificationToken.java
@@ -1,9 +1,6 @@
package learningFlow.learningFlow_BE.domain;
-import jakarta.persistence.Column;
-import jakarta.persistence.Entity;
-import jakarta.persistence.Id;
-import jakarta.persistence.Table;
+import jakarta.persistence.*;
import lombok.*;
import java.time.LocalDateTime;
@@ -26,6 +23,10 @@ public class EmailVerificationToken extends BaseEntity {
@Column(name = "password", nullable = false)
private String password;
+ @OneToOne(fetch = FetchType.LAZY) // optional = true 제거
+ @JoinColumn(name = "user_id", nullable = true)
+ private User user;
+
@Column(name = "expiry_date", nullable = false)
private LocalDateTime expiryDate;
diff --git a/src/main/java/learningFlow/learningFlow_BE/domain/User.java b/src/main/java/learningFlow/learningFlow_BE/domain/User.java
index 3949c594..1835c891 100644
--- a/src/main/java/learningFlow/learningFlow_BE/domain/User.java
+++ b/src/main/java/learningFlow/learningFlow_BE/domain/User.java
@@ -62,6 +62,9 @@ public class User extends BaseEntity {
@Column(nullable = true)
private String profileImgUrl;
+
+ @Column(nullable = true)
+ private String bannerImgUrl;
// @ManyToOne(fetch = FetchType.LAZY)
// @JoinColumn(name = "image_id")
// private Image image;
@@ -121,6 +124,10 @@ public void changePassword(String newEncodedPassword) {
this.pw = newEncodedPassword;
}
+ public void changeEmail(String newEmail) {
+ this.email = newEmail;
+ }
+
public void updateName(String name) {
this.name = name;
}
diff --git a/src/main/java/learningFlow/learningFlow_BE/domain/UserCollection.java b/src/main/java/learningFlow/learningFlow_BE/domain/UserCollection.java
index d3208ce5..8fef140a 100644
--- a/src/main/java/learningFlow/learningFlow_BE/domain/UserCollection.java
+++ b/src/main/java/learningFlow/learningFlow_BE/domain/UserCollection.java
@@ -31,7 +31,7 @@ public class UserCollection extends BaseEntity{
private User user;
@Column(name = "user_collection_status", nullable = false)
- private Integer userCollectionStatus;
+ private Integer userCollectionStatus; // 가장 최신에 수강한 강의 저장
@Column(name = "completed_time", nullable = false)
private LocalDate completedTime;
diff --git a/src/main/java/learningFlow/learningFlow_BE/domain/UserEpisodeProgress.java b/src/main/java/learningFlow/learningFlow_BE/domain/UserEpisodeProgress.java
index 9e33dbd6..0767b2be 100644
--- a/src/main/java/learningFlow/learningFlow_BE/domain/UserEpisodeProgress.java
+++ b/src/main/java/learningFlow/learningFlow_BE/domain/UserEpisodeProgress.java
@@ -24,9 +24,16 @@ public class UserEpisodeProgress extends BaseEntity {
@Setter
private Integer currentProgress;
- @Column(nullable = false)
private Integer totalProgress;
+ @Column(nullable = false)
+ private Boolean isComplete = false;
+
@Column(nullable = false)
private ResourceType resourceType;
+
+ public Boolean setIsComplete(Boolean isComplete){
+ this.isComplete = isComplete;
+ return this.isComplete;
+ }
}
diff --git a/src/main/java/learningFlow/learningFlow_BE/repository/collection/CollectionRepositoryCustom.java b/src/main/java/learningFlow/learningFlow_BE/repository/collection/CollectionRepositoryCustom.java
index 432c81a5..0ebb6103 100644
--- a/src/main/java/learningFlow/learningFlow_BE/repository/collection/CollectionRepositoryCustom.java
+++ b/src/main/java/learningFlow/learningFlow_BE/repository/collection/CollectionRepositoryCustom.java
@@ -9,9 +9,8 @@
import java.util.List;
public interface CollectionRepositoryCustom {
- List searchCollections(SearchRequestDTO.SearchConditionDTO condition, Long lastId, Pageable pageable);
+ List searchCollections(SearchRequestDTO.SearchConditionDTO condition, Pageable pageable);
Integer getTotalCount(SearchRequestDTO.SearchConditionDTO condition);
- List searchNextPage(SearchRequestDTO.SearchConditionDTO condition, Collection lastCollection, Pageable pageable);
Integer getCountGreaterThanBookmark(Integer bookmarkCount, Long lastId, SearchRequestDTO.SearchConditionDTO condition);
List findTopBookmarkedCollections(int limit);
List findByInterestFieldAndPreferType(List interestFields,
diff --git a/src/main/java/learningFlow/learningFlow_BE/repository/collection/CollectionRepositoryImpl.java b/src/main/java/learningFlow/learningFlow_BE/repository/collection/CollectionRepositoryImpl.java
index fc9ab29a..8a3159e8 100644
--- a/src/main/java/learningFlow/learningFlow_BE/repository/collection/CollectionRepositoryImpl.java
+++ b/src/main/java/learningFlow/learningFlow_BE/repository/collection/CollectionRepositoryImpl.java
@@ -1,8 +1,8 @@
package learningFlow.learningFlow_BE.repository.collection;
-import com.querydsl.core.types.ExpressionUtils;
import com.querydsl.core.types.OrderSpecifier;
import com.querydsl.core.types.dsl.BooleanExpression;
+import com.querydsl.core.types.dsl.Expressions;
import com.querydsl.jpa.impl.JPAQueryFactory;
import learningFlow.learningFlow_BE.apiPayload.code.status.ErrorStatus;
import learningFlow.learningFlow_BE.apiPayload.exception.handler.CollectionHandler;
@@ -28,10 +28,13 @@ public class CollectionRepositoryImpl implements CollectionRepositoryCustom {
private final QCollection collection = QCollection.collection;
@Override
- public List searchCollections(SearchRequestDTO.SearchConditionDTO condition, Long lastId, Pageable pageable) {
+ public List searchCollections(SearchRequestDTO.SearchConditionDTO condition, Pageable pageable) {
+
+ int skip = pageable.getPageNumber() * pageable.getPageSize();
BooleanExpression searchConditions = createSearchConditions(condition);
+/*
if (lastId == 0L) {
return jpaQueryFactory
.select(episode.collection)
@@ -53,6 +56,16 @@ public List searchCollections(SearchRequestDTO.SearchConditionDTO co
}
return searchNextPage(condition, lastCollection, pageable);
+*/
+ return jpaQueryFactory
+ .select(episode.collection)
+ .from(episode)
+ .where(searchConditions)
+ .groupBy(episode.collection.id)
+ .orderBy(createOrderSpecifier(condition.getSortType()))
+ .offset(skip) // ✅ added: 시작 위치 설정
+ .limit(pageable.getPageSize())
+ .fetch();
}
private OrderSpecifier>[] createOrderSpecifier(Integer sortType) { // 반환 타입을 배열로 변경
@@ -66,7 +79,7 @@ private OrderSpecifier>[] createOrderSpecifier(Integer sortType) { // 반환
}
private BooleanExpression createSearchConditions(SearchRequestDTO.SearchConditionDTO condition) {
- return (BooleanExpression) ExpressionUtils.allOf(
+ return Expressions.allOf(
createDynamicKeyword(condition.getKeyword()),
createDynamicInterestFields(condition.getInterestFields()),
createDynamicPreferMediaType(condition.getPreferMediaType()),
@@ -86,21 +99,6 @@ public Integer getTotalCount(SearchRequestDTO.SearchConditionDTO condition) {
return count != null ? count.intValue() : 0;
}
- @Override
- public List searchNextPage(SearchRequestDTO.SearchConditionDTO condition, Collection lastCollection, Pageable pageable) {
- BooleanExpression searchConditions = createSearchConditions(condition);
- BooleanExpression cursorCondition = createCursorCondition(condition.getSortType(), lastCollection);
-
- return jpaQueryFactory
- .select(episode.collection)
- .from(episode)
- .where(searchConditions, cursorCondition)
- .groupBy(episode.collection.id)
- .orderBy(createOrderSpecifier(condition.getSortType()))
- .limit(pageable.getPageSize())
- .fetch();
- }
-
private BooleanExpression createCursorCondition(Integer sortType, Collection lastCollection) {
if (sortType == null || sortType == 0) {
return episode.collection.id.lt(lastCollection.getId());
diff --git a/src/main/java/learningFlow/learningFlow_BE/security/handler/OAuth2LoginSuccessHandler.java b/src/main/java/learningFlow/learningFlow_BE/security/handler/OAuth2LoginSuccessHandler.java
index dc3c8ff6..ccca6b8e 100644
--- a/src/main/java/learningFlow/learningFlow_BE/security/handler/OAuth2LoginSuccessHandler.java
+++ b/src/main/java/learningFlow/learningFlow_BE/security/handler/OAuth2LoginSuccessHandler.java
@@ -10,6 +10,8 @@
import learningFlow.learningFlow_BE.web.dto.user.UserResponseDTO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.HttpStatus;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
@@ -26,6 +28,9 @@ public class OAuth2LoginSuccessHandler implements AuthenticationSuccessHandler {
private final JwtTokenProvider jwtTokenProvider;
+ @Value("${app.frontend-url}")
+ private String frontendUrl;
+
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException {
@@ -34,7 +39,7 @@ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletRespo
// Principal 타입 확인, 첫 로그인인 경우 회원가입으로 이동
if (authentication.getPrincipal() instanceof OAuth2UserTemp oAuth2UserTemp) {
String temporaryToken = jwtTokenProvider.createTemporaryToken(oAuth2UserTemp);
- String redirectUrl = "/oauth2/additional-info?token=" + temporaryToken;
+ String redirectUrl = frontendUrl + "/oauth2/additional-info?oauth2RegistrationCode=" + temporaryToken;
response.sendRedirect(redirectUrl);
return;
}
@@ -48,16 +53,21 @@ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletRespo
//Access Token 생성
String accessToken = jwtTokenProvider.createAccessToken(authentication);
log.info("Access 토큰 발급 : {}", accessToken);
- response.addHeader("Authorization", "Bearer " + accessToken);
+ response.setHeader("Authorization", "Bearer " + accessToken);
String refreshToken = jwtTokenProvider.createRefreshToken(authentication);
log.info("자동 로그인 활성화, Refresh Token 발급 : {}", refreshToken);
- response.addHeader("Refresh-Token", refreshToken);
+ response.setHeader("Refresh-Token", refreshToken);
// 헤더 설정 확인 로깅
log.info("Authorization Header: {}", response.getHeader("Authorization"));
log.info("Refresh-Token Header: {}", response.getHeader("Refresh-Token"));
+ response.setHeader("Access-Control-Allow-Origin", request.getHeader("Origin"));
+ response.setHeader("Access-Control-Allow-Credentials", "true");
+ response.setHeader("Access-Control-Expose-Headers", "Authorization, Refresh-Token");
+
+/*
UserResponseDTO.UserLoginResponseDTO loginResponse =
toUserLoginResponseDTO(principalDetails.getUser());
@@ -65,5 +75,7 @@ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletRespo
response.setCharacterEncoding("UTF-8");
String jsonResponse = new ObjectMapper().writeValueAsString(ApiResponse.onSuccess(loginResponse));
response.getWriter().write(jsonResponse);
+*/
+ response.setStatus(HttpStatus.OK.value());
}
}
diff --git a/src/main/java/learningFlow/learningFlow_BE/security/jwt/JwtAuthenticationFilter.java b/src/main/java/learningFlow/learningFlow_BE/security/jwt/JwtAuthenticationFilter.java
index 3673a330..589868cd 100644
--- a/src/main/java/learningFlow/learningFlow_BE/security/jwt/JwtAuthenticationFilter.java
+++ b/src/main/java/learningFlow/learningFlow_BE/security/jwt/JwtAuthenticationFilter.java
@@ -131,14 +131,14 @@ private boolean isPermitAllUrl(String requestURI) {
requestURI.startsWith("/search") ||
requestURI.equals("/reset-password") ||
requestURI.matches("/collections/\\d+") ||
+ requestURI.contains("/user/change-email") ||
requestURI.startsWith("/user/imgUpload"); // 이미지 업로드는 인증 없이 허용
}
-
@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
String path = request.getRequestURI();
- boolean shouldSkip = path.equals("/image/upload");
+ boolean shouldSkip = path.equals("/image/upload") || path.equals("/favicon.ico");
log.info("🛑 [JwtAuthenticationFilter] shouldNotFilter 실행: path={}, shouldSkip={}", path, shouldSkip);
return shouldSkip;
}
diff --git a/src/main/java/learningFlow/learningFlow_BE/service/auth/common/UserVerificationEmailService.java b/src/main/java/learningFlow/learningFlow_BE/service/auth/common/UserVerificationEmailService.java
index 05c4a4e2..9e0bb04f 100644
--- a/src/main/java/learningFlow/learningFlow_BE/service/auth/common/UserVerificationEmailService.java
+++ b/src/main/java/learningFlow/learningFlow_BE/service/auth/common/UserVerificationEmailService.java
@@ -16,7 +16,7 @@ public class UserVerificationEmailService {
private final JavaMailSender emailSender;
- @Value("${app.url}")
+ @Value("${app.frontend-url}")
private String baseUrl;
public void sendVerificationEmail(String email, String token) {
@@ -73,7 +73,7 @@ public void sendVerificationEmail(String email, String token) {
버튼을 누르면 자동으로 인증 후 추가 정보 입력 페이지로 이동합니다.
-
이메일 인증하기
@@ -101,7 +101,89 @@ public void sendVerificationEmail(String email, String token) {
}
}
- public void sendPasswordResetEmail(String email, String token) {
+ public void sendEmailResetEmail(String email, String token) {
+ try {
+ MimeMessage message = emailSender.createMimeMessage();
+ MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
+
+ helper.setTo(email);
+ helper.setSubject("[OnBoarding] 이메일 인증");
+
+ String htmlContent = """
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 
+
+
+
+
+ 이메일을 변경하기 위해 메일을 인증해주세요
+
+
+
+ 안녕하세요, OnBoarding입니다.
+ 이메일 변경을 완료하기 위해 메일을 인증해주세요.
+ 버튼을 누르면 자동으로 인증 후 이메일 변경이 완료됩니다.
+
+
+
+ 이메일 인증하기
+
+
+
+ 이 메일은 24시간 동안 유효합니다.
+ 본인이 요청하지 않은 경우, 이 메일을 무시해주세요.
+
+
+
+ |
+
+
+
+
+ """.formatted(baseUrl, token);
+
+ helper.setText(htmlContent, true);
+ emailSender.send(message);
+ log.info("이메일 인증 메일 발송 완료: {}", email);
+ } catch (MessagingException e) {
+ log.error("이메일 발송 실패: {}", e.getMessage());
+ throw new RuntimeException("이메일 발송에 실패했습니다.");
+ }
+ }
+
+ public void sendPasswordResetEmail(String email, String passwordResetCode) {
try {
MimeMessage message = emailSender.createMimeMessage();
@@ -156,7 +238,7 @@ public void sendPasswordResetEmail(String email, String token) {
버튼을 누르면 자동으로 인증 후 비밀번호 재설정 페이지로 이동합니다.
-
이메일 인증하기
@@ -173,7 +255,7 @@ public void sendPasswordResetEmail(String email, String token) {