diff --git a/.github/workflows/work.yml b/.github/workflows/work.yml index 78eefc3..8a4f79e 100644 --- a/.github/workflows/work.yml +++ b/.github/workflows/work.yml @@ -71,4 +71,4 @@ jobs: docker rm trip_app || true docker pull ${{ secrets.DOCKER_REPO }}/trip docker run --name=trip_app --restart unless-stopped \ - -p 80:8080 -e TZ=Asia/Seoul -d ${{ secrets.DOCKER_REPO }}/trip + -p 8080:8080 -e TZ=Asia/Seoul -d ${{ secrets.DOCKER_REPO }}/trip diff --git a/build.gradle b/build.gradle index 915bfee..986cb45 100644 --- a/build.gradle +++ b/build.gradle @@ -26,34 +26,37 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-web' - testImplementation 'org.springframework.boot:spring-boot-starter-test' - testImplementation 'org.springframework.security:spring-security-test' + implementation 'org.springframework.boot:spring-boot-starter-mail' + + // aws + implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE' + implementation 'com.amazonaws:aws-java-sdk-s3:1.12.429' + implementation 'javax.xml.bind:jaxb-api:2.4.0-b180830.0359' + + // tika + implementation 'org.apache.tika:tika-core:2.9.1' + + // Swagger + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0' - //Querydsl 추가 implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta' annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta" annotationProcessor "jakarta.annotation:jakarta.annotation-api" annotationProcessor "jakarta.persistence:jakarta.persistence-api" + // Jwt + implementation group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.5' + runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.5' + runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.5' - // mysql runtimeOnly 'com.mysql:mysql-connector-j' - - // h2 runtimeOnly 'com.h2database:h2' - // lombok compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.projectlombok:lombok' - - // Jwt - implementation group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.5' - runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.5' - runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.5' - - // Swagger - implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.springframework.security:spring-security-test' } diff --git a/src/main/java/com/api/trip/common/auditing/config/AuditingConfig.java b/src/main/java/com/api/trip/common/auditing/config/AuditingConfig.java new file mode 100644 index 0000000..bdd7310 --- /dev/null +++ b/src/main/java/com/api/trip/common/auditing/config/AuditingConfig.java @@ -0,0 +1,17 @@ +package com.api.trip.common.auditing.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; + +@Configuration +@EnableJpaAuditing +public class AuditingConfig { + + // TODO: 회원가입 후 유저 정보를 가져와서 넣어주는 방법을 모르겠습니다... + /** + @Bean + public AuditorAware auditorAware() { + return () -> Optional.of("user1"); + } + */ +} diff --git a/src/main/java/com/api/trip/common/auditing/entity/BaseTimeEntity.java b/src/main/java/com/api/trip/common/auditing/entity/BaseTimeEntity.java new file mode 100644 index 0000000..5f4016b --- /dev/null +++ b/src/main/java/com/api/trip/common/auditing/entity/BaseTimeEntity.java @@ -0,0 +1,28 @@ +package com.api.trip.common.auditing.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.MappedSuperclass; +import lombok.Getter; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import java.time.LocalDateTime; + +@Getter +@MappedSuperclass +@EntityListeners(AuditingEntityListener.class) +public class BaseTimeEntity { + + @CreatedDate + @Column(nullable = false, updatable = false) + private LocalDateTime createdAt; + + @LastModifiedDate + @Column(nullable = false) + private LocalDateTime modifiedAt; + + private LocalDateTime deletedAt; + +} diff --git a/src/main/java/com/api/trip/common/security/SecurityConfig.java b/src/main/java/com/api/trip/common/security/config/SecurityConfig.java similarity index 92% rename from src/main/java/com/api/trip/common/security/SecurityConfig.java rename to src/main/java/com/api/trip/common/security/config/SecurityConfig.java index 8146dbb..342dbae 100644 --- a/src/main/java/com/api/trip/common/security/SecurityConfig.java +++ b/src/main/java/com/api/trip/common/security/config/SecurityConfig.java @@ -1,7 +1,8 @@ -package com.api.trip.common.security; +package com.api.trip.common.security.config; +import com.api.trip.common.security.jwt.JwtTokenFilter; +import com.api.trip.common.security.jwt.JwtTokenProvider; import lombok.RequiredArgsConstructor; -import org.springframework.boot.autoconfigure.security.servlet.PathRequest; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; diff --git a/src/main/java/com/api/trip/common/security/JwtToken.java b/src/main/java/com/api/trip/common/security/jwt/JwtToken.java similarity index 82% rename from src/main/java/com/api/trip/common/security/JwtToken.java rename to src/main/java/com/api/trip/common/security/jwt/JwtToken.java index 7437fe7..6d14b7b 100644 --- a/src/main/java/com/api/trip/common/security/JwtToken.java +++ b/src/main/java/com/api/trip/common/security/jwt/JwtToken.java @@ -1,4 +1,4 @@ -package com.api.trip.common.security; +package com.api.trip.common.security.jwt; import lombok.AllArgsConstructor; diff --git a/src/main/java/com/api/trip/common/security/JwtTokenFilter.java b/src/main/java/com/api/trip/common/security/jwt/JwtTokenFilter.java similarity index 89% rename from src/main/java/com/api/trip/common/security/JwtTokenFilter.java rename to src/main/java/com/api/trip/common/security/jwt/JwtTokenFilter.java index 72fffe1..1d88834 100644 --- a/src/main/java/com/api/trip/common/security/JwtTokenFilter.java +++ b/src/main/java/com/api/trip/common/security/jwt/JwtTokenFilter.java @@ -1,4 +1,4 @@ -package com.api.trip.common.security; +package com.api.trip.common.security.jwt; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; @@ -6,7 +6,6 @@ import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.http.HttpHeaders; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.filter.OncePerRequestFilter; @@ -42,7 +41,11 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse @Override protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException { - String[] excludePath = {"/api/members/join", "/api/members/login"}; + String[] excludePath = { + "/api/members/join", "/api/members/login", + "/api/members/send-email", "/api/members/auth-email" + }; + String path = request.getRequestURI(); return Arrays.stream(excludePath).anyMatch(path::startsWith); } diff --git a/src/main/java/com/api/trip/common/security/JwtTokenProvider.java b/src/main/java/com/api/trip/common/security/jwt/JwtTokenProvider.java similarity index 92% rename from src/main/java/com/api/trip/common/security/JwtTokenProvider.java rename to src/main/java/com/api/trip/common/security/jwt/JwtTokenProvider.java index d6e829b..5f288ac 100644 --- a/src/main/java/com/api/trip/common/security/JwtTokenProvider.java +++ b/src/main/java/com/api/trip/common/security/jwt/JwtTokenProvider.java @@ -1,10 +1,10 @@ -package com.api.trip.common.security; +package com.api.trip.common.security.jwt; -import com.api.trip.common.security.dto.AuthenticationMember; -import com.api.trip.domain.member.model.Member; -import com.api.trip.domain.member.repository.MemberRepository; -import io.jsonwebtoken.*; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.io.Decoders; import io.jsonwebtoken.security.Keys; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/com/api/trip/common/security/MemberSecurityService.java b/src/main/java/com/api/trip/common/security/service/MemberSecurityService.java similarity index 96% rename from src/main/java/com/api/trip/common/security/MemberSecurityService.java rename to src/main/java/com/api/trip/common/security/service/MemberSecurityService.java index 6e8ecb3..a9eb390 100644 --- a/src/main/java/com/api/trip/common/security/MemberSecurityService.java +++ b/src/main/java/com/api/trip/common/security/service/MemberSecurityService.java @@ -1,4 +1,4 @@ -package com.api.trip.common.security; +package com.api.trip.common.security.service; import com.api.trip.common.security.dto.AuthenticationMember; import com.api.trip.domain.member.model.Member; diff --git a/src/main/java/com/api/trip/common/security/util/SecurityUtils.java b/src/main/java/com/api/trip/common/security/util/SecurityUtils.java new file mode 100644 index 0000000..0e1d970 --- /dev/null +++ b/src/main/java/com/api/trip/common/security/util/SecurityUtils.java @@ -0,0 +1,15 @@ +package com.api.trip.common.security.util; + +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; + +public class SecurityUtils { + public static String getCurrentUsername() { + final Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication == null || authentication.getName() == null) { + throw new RuntimeException("인증 정보가 없습니다!"); + } + return authentication.getName(); + } + +} diff --git a/src/main/java/com/api/trip/domain/article/controller/ArticleController.java b/src/main/java/com/api/trip/domain/article/controller/ArticleController.java new file mode 100644 index 0000000..0bcb45d --- /dev/null +++ b/src/main/java/com/api/trip/domain/article/controller/ArticleController.java @@ -0,0 +1,58 @@ +package com.api.trip.domain.article.controller; + +import com.api.trip.domain.article.controller.dto.*; +import com.api.trip.domain.article.service.ArticleService; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.data.web.PageableDefault; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/articles") +@RequiredArgsConstructor +public class ArticleController { + + private final ArticleService articleService; + + @PostMapping + public ResponseEntity createArticle(@RequestBody CreateArticleRequest request) { + String email = SecurityContextHolder.getContext().getAuthentication().getName(); + return ResponseEntity.ok(articleService.createArticle(request, email)); + } + + @PatchMapping("/{articleId}") + public ResponseEntity updateArticle(@PathVariable Long articleId, @RequestBody UpdateArticleRequest request) { + String email = SecurityContextHolder.getContext().getAuthentication().getName(); + articleService.updateArticle(articleId, request, email); + return ResponseEntity.ok().build(); + } + + @DeleteMapping("/{articleId}") + public ResponseEntity deleteArticle(@PathVariable Long articleId) { + String email = SecurityContextHolder.getContext().getAuthentication().getName(); + articleService.deleteArticle(articleId, email); + return ResponseEntity.ok().build(); + } + + @GetMapping("/{articleId}") + public ResponseEntity readArticle(@PathVariable Long articleId) { + String email = SecurityContextHolder.getContext().getAuthentication().getName(); + return ResponseEntity.ok(articleService.readArticle(articleId, email)); + } + + @GetMapping + public ResponseEntity getArticles( + @PageableDefault(size = 8) Pageable pageable, + @RequestParam(value = "filter", required = false) String filter + ) { + return ResponseEntity.ok(articleService.getArticles(pageable, filter)); + } + + @GetMapping("/me") + public ResponseEntity getMyArticles() { + String email = SecurityContextHolder.getContext().getAuthentication().getName(); + return ResponseEntity.ok(articleService.getMyArticles(email)); + } +} diff --git a/src/main/java/com/api/trip/domain/article/controller/dto/CreateArticleRequest.java b/src/main/java/com/api/trip/domain/article/controller/dto/CreateArticleRequest.java new file mode 100644 index 0000000..da050a4 --- /dev/null +++ b/src/main/java/com/api/trip/domain/article/controller/dto/CreateArticleRequest.java @@ -0,0 +1,20 @@ +package com.api.trip.domain.article.controller.dto; + +import com.api.trip.domain.article.model.Article; +import com.api.trip.domain.member.model.Member; +import lombok.Getter; + +@Getter +public class CreateArticleRequest { + + private String title; + private String content; + + public Article toEntity(Member writer) { + return Article.builder() + .writer(writer) + .title(title) + .content(content) + .build(); + } +} diff --git a/src/main/java/com/api/trip/domain/article/controller/dto/GetArticlesResponse.java b/src/main/java/com/api/trip/domain/article/controller/dto/GetArticlesResponse.java new file mode 100644 index 0000000..d4c86f6 --- /dev/null +++ b/src/main/java/com/api/trip/domain/article/controller/dto/GetArticlesResponse.java @@ -0,0 +1,66 @@ +package com.api.trip.domain.article.controller.dto; + +import com.api.trip.domain.article.model.Article; +import com.api.trip.domain.member.model.Member; +import lombok.Builder; +import lombok.Getter; +import org.springframework.data.domain.Page; + +import java.time.LocalDateTime; +import java.util.List; + +@Getter +@Builder +public class GetArticlesResponse { + + private int totalPages; + private long totalElements; + private int page; + private boolean hasNext; + private boolean hasPrevious; + private int requestSize; + private int resultSize; + private List result; + + public static GetArticlesResponse of(Page
articlePage) { + Page articleDtoPage = articlePage.map(ArticleDto::of); + return builder() + .totalPages(articleDtoPage.getTotalPages()) + .totalElements(articleDtoPage.getTotalElements()) + .page(articleDtoPage.getNumber() + 1) + .hasNext(articleDtoPage.hasNext()) + .hasPrevious(articleDtoPage.hasPrevious()) + .requestSize(articleDtoPage.getSize()) + .resultSize(articleDtoPage.getNumberOfElements()) + .result(articleDtoPage.getContent()) + .build(); + } + + @Getter + @Builder + private static class ArticleDto { + + private Long articleId; + private String title; + private Long writerId; + private String writerNickname; + private String writerRole; + private long viewCount; + private long likeCount; + private LocalDateTime createdAt; + + private static ArticleDto of(Article article) { + Member writer = article.getWriter(); + return builder() + .articleId(article.getId()) + .title(article.getTitle()) + .writerId(writer.getId()) + .writerNickname(writer.getNickname()) + .writerRole(writer.getRole().name()) + .viewCount(article.getViewCount()) + .likeCount(article.getLikeCount()) + .createdAt(article.getCreatedAt()) + .build(); + } + } +} diff --git a/src/main/java/com/api/trip/domain/article/controller/dto/GetMyArticlesResponse.java b/src/main/java/com/api/trip/domain/article/controller/dto/GetMyArticlesResponse.java new file mode 100644 index 0000000..b47627a --- /dev/null +++ b/src/main/java/com/api/trip/domain/article/controller/dto/GetMyArticlesResponse.java @@ -0,0 +1,49 @@ +package com.api.trip.domain.article.controller.dto; + +import com.api.trip.domain.article.model.Article; +import com.api.trip.domain.member.model.Member; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +import java.time.LocalDateTime; +import java.util.List; + +@Getter +@AllArgsConstructor +public class GetMyArticlesResponse { + + private List articles; + + public static GetMyArticlesResponse of(List
articles) { + List articleDtos = articles.stream() + .map(ArticleDto::of) + .toList(); + + return new GetMyArticlesResponse(articleDtos); + } + + @Getter + @Builder + private static class ArticleDto { + + private Long articleId; + private String title; + private Long writerId; + private String writerNickname; + private String writerRole; + private LocalDateTime createdAt; + + private static ArticleDto of(Article article) { + Member writer = article.getWriter(); + return builder() + .articleId(article.getId()) + .title(article.getTitle()) + .writerId(writer.getId()) + .writerNickname(writer.getNickname()) + .writerRole(writer.getRole().name()) + .createdAt(article.getCreatedAt()) + .build(); + } + } +} diff --git a/src/main/java/com/api/trip/domain/article/controller/dto/ReadArticleResponse.java b/src/main/java/com/api/trip/domain/article/controller/dto/ReadArticleResponse.java new file mode 100644 index 0000000..f853c14 --- /dev/null +++ b/src/main/java/com/api/trip/domain/article/controller/dto/ReadArticleResponse.java @@ -0,0 +1,43 @@ +package com.api.trip.domain.article.controller.dto; + +import com.api.trip.domain.article.model.Article; +import com.api.trip.domain.interestarticle.model.InterestArticle; +import com.api.trip.domain.member.model.Member; +import lombok.Builder; +import lombok.Getter; + +import java.time.LocalDateTime; + +@Getter +@Builder +public class ReadArticleResponse { + + private Long articleId; + private String title; + private Long writerId; + private String writerNickname; + private String writerRole; + private String writerProfileImg; + private String content; + private long viewCount; + private long likeCount; + private LocalDateTime createdAt; + private Long interestArticleId; + + public static ReadArticleResponse of(Article article, InterestArticle interestArticle) { + Member writer = article.getWriter(); + return builder() + .articleId(article.getId()) + .title(article.getTitle()) + .writerId(writer.getId()) + .writerNickname(writer.getNickname()) + .writerProfileImg(writer.getProfileImg()) + .writerRole(writer.getRole().name()) + .content(article.getContent()) + .viewCount(article.getViewCount() + 1) + .likeCount(article.getLikeCount()) + .createdAt(article.getCreatedAt()) + .interestArticleId(interestArticle != null ? interestArticle.getId() : null) + .build(); + } +} diff --git a/src/main/java/com/api/trip/domain/article/controller/dto/UpdateArticleRequest.java b/src/main/java/com/api/trip/domain/article/controller/dto/UpdateArticleRequest.java new file mode 100644 index 0000000..1eceb9d --- /dev/null +++ b/src/main/java/com/api/trip/domain/article/controller/dto/UpdateArticleRequest.java @@ -0,0 +1,10 @@ +package com.api.trip.domain.article.controller.dto; + +import lombok.Getter; + +@Getter +public class UpdateArticleRequest { + + private String title; + private String content; +} diff --git a/src/main/java/com/api/trip/domain/article/model/Article.java b/src/main/java/com/api/trip/domain/article/model/Article.java new file mode 100644 index 0000000..c017dd9 --- /dev/null +++ b/src/main/java/com/api/trip/domain/article/model/Article.java @@ -0,0 +1,46 @@ +package com.api.trip.domain.article.model; + +import com.api.trip.common.auditing.entity.BaseTimeEntity; +import com.api.trip.domain.member.model.Member; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +public class Article extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Member writer; + + @Column(nullable = false) + private String title; + + @Column(nullable = false) + private String content; + + private long viewCount; + + private long likeCount; + + @Builder + private Article(Member writer, String title, String content, long viewCount, long likeCount) { + this.writer = writer; + this.title = title; + this.content = content; + this.viewCount = viewCount; + this.likeCount = likeCount; + } + + public void modify(String title, String content) { + this.title = title; + this.content = content; + } +} diff --git a/src/main/java/com/api/trip/domain/article/repository/ArticleRepository.java b/src/main/java/com/api/trip/domain/article/repository/ArticleRepository.java new file mode 100644 index 0000000..5dbb977 --- /dev/null +++ b/src/main/java/com/api/trip/domain/article/repository/ArticleRepository.java @@ -0,0 +1,27 @@ +package com.api.trip.domain.article.repository; + +import com.api.trip.domain.article.model.Article; +import com.api.trip.domain.member.model.Member; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; + +public interface ArticleRepository extends JpaRepository, ArticleRepositoryCustom { + + List
findAllByWriterOrderByIdDesc(Member writer); + + @Modifying + @Query("UPDATE Article a SET a.viewCount = a.viewCount + 1 WHERE a = :article") + void increaseViewCount(@Param("article") Article article); + + @Modifying + @Query("UPDATE Article a SET a.likeCount = a.likeCount + 1 WHERE a = :article") + void increaseLikeCount(@Param("article") Article article); + + @Modifying + @Query("UPDATE Article a SET a.likeCount = a.likeCount - 1 WHERE a = :article") + void decreaseLikeCount(@Param("article") Article article); +} diff --git a/src/main/java/com/api/trip/domain/article/repository/ArticleRepositoryCustom.java b/src/main/java/com/api/trip/domain/article/repository/ArticleRepositoryCustom.java new file mode 100644 index 0000000..fbdbe4f --- /dev/null +++ b/src/main/java/com/api/trip/domain/article/repository/ArticleRepositoryCustom.java @@ -0,0 +1,14 @@ +package com.api.trip.domain.article.repository; + +import com.api.trip.domain.article.model.Article; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import java.util.Optional; + +public interface ArticleRepositoryCustom { + + Optional
findArticle(Long articleId); + + Page
findArticles(Pageable pageable, String filter); +} diff --git a/src/main/java/com/api/trip/domain/article/repository/ArticleRepositoryCustomImpl.java b/src/main/java/com/api/trip/domain/article/repository/ArticleRepositoryCustomImpl.java new file mode 100644 index 0000000..aff3711 --- /dev/null +++ b/src/main/java/com/api/trip/domain/article/repository/ArticleRepositoryCustomImpl.java @@ -0,0 +1,81 @@ +package com.api.trip.domain.article.repository; + +import com.api.trip.domain.article.model.Article; +import com.api.trip.domain.member.model.MemberRole; +import com.querydsl.core.types.Order; +import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.persistence.EntityManager; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import static com.api.trip.domain.article.model.QArticle.article; +import static com.api.trip.domain.member.model.QMember.member; + +public class ArticleRepositoryCustomImpl implements ArticleRepositoryCustom { + + private final JPAQueryFactory jpaQueryFactory; + + public ArticleRepositoryCustomImpl(EntityManager em) { + this.jpaQueryFactory = new JPAQueryFactory(em); + } + + @Override + public Optional
findArticle(Long articleId) { + return Optional.ofNullable( + jpaQueryFactory + .selectFrom(article) + .innerJoin(article.writer, member).fetchJoin() + .where(article.id.eq(articleId)) + .fetchOne() + ); + } + + @Override + public Page
findArticles(Pageable pageable, String filter) { + List
content = jpaQueryFactory + .selectFrom(article) + .innerJoin(article.writer, member).fetchJoin() + .where(eqFilter(filter)) + .orderBy(getOrderSpecifiers(pageable)) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + Long total = jpaQueryFactory + .select(article.count()) + .from(article) + .innerJoin(article.writer, member) + .where(eqFilter(filter)) + .fetchOne(); + + return new PageImpl<>(content, pageable, total); + } + + private BooleanExpression eqFilter(String filter) { + for (MemberRole role : MemberRole.values()) { + if (role.name().equals(filter)) { + return member.role.eq(role); + } + } + return null; + } + + private OrderSpecifier[] getOrderSpecifiers(Pageable pageable) { + List> orderSpecifierList = new ArrayList<>(); + for (Sort.Order order : pageable.getSort()) { + if ("popular".equals(order.getProperty())) { + orderSpecifierList.add(new OrderSpecifier<>(Order.DESC, article.likeCount)); + } + } + orderSpecifierList.add(new OrderSpecifier<>(Order.DESC, article.id)); + return orderSpecifierList.toArray(OrderSpecifier[]::new); + } +} diff --git a/src/main/java/com/api/trip/domain/article/service/ArticleService.java b/src/main/java/com/api/trip/domain/article/service/ArticleService.java new file mode 100644 index 0000000..24509d8 --- /dev/null +++ b/src/main/java/com/api/trip/domain/article/service/ArticleService.java @@ -0,0 +1,87 @@ +package com.api.trip.domain.article.service; + +import com.api.trip.domain.article.controller.dto.*; +import com.api.trip.domain.article.model.Article; +import com.api.trip.domain.article.repository.ArticleRepository; +import com.api.trip.domain.interestarticle.model.InterestArticle; +import com.api.trip.domain.interestarticle.repository.InterestArticleRepository; +import com.api.trip.domain.member.model.Member; +import com.api.trip.domain.member.repository.MemberRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Objects; + +@Service +@Transactional +@RequiredArgsConstructor +public class ArticleService { + + private final ArticleRepository articleRepository; + private final MemberRepository memberRepository; + private final InterestArticleRepository interestArticleRepository; + + public Long createArticle(CreateArticleRequest request, String email) { + Member member = memberRepository.findByEmail(email).orElseThrow(); + + Article article = request.toEntity(member); + + return articleRepository.save(article).getId(); + } + + public void updateArticle(Long articleId, UpdateArticleRequest request, String email) { + Member member = memberRepository.findByEmail(email).orElseThrow(); + + Article article = articleRepository.findById(articleId).orElseThrow(); + if (article.getWriter() != member) { + throw new RuntimeException("수정 권한이 없습니다."); + } + + article.modify(request.getTitle(), request.getContent()); + } + + public void deleteArticle(Long articleId, String email) { + Member member = memberRepository.findByEmail(email).orElseThrow(); + + Article article = articleRepository.findById(articleId).orElseThrow(); + if (article.getWriter() != member) { + throw new RuntimeException("삭제 권한이 없습니다."); + } + + articleRepository.delete(article); + } + + public ReadArticleResponse readArticle(Long articleId, String email) { + Article article = articleRepository.findArticle(articleId).orElseThrow(); + + articleRepository.increaseViewCount(article); + + InterestArticle interestArticle = null; + if (!Objects.equals(email, "anonymousUser")) { + Member member = memberRepository.findByEmail(email).orElseThrow(); + interestArticle = interestArticleRepository.findByMemberAndArticle(member, article); + } + + return ReadArticleResponse.of(article, interestArticle); + } + + @Transactional(readOnly = true) + public GetArticlesResponse getArticles(Pageable pageable, String filter) { + Page
articlePage = articleRepository.findArticles(pageable, filter); + + return GetArticlesResponse.of(articlePage); + } + + @Transactional(readOnly = true) + public GetMyArticlesResponse getMyArticles(String email) { + Member member = memberRepository.findByEmail(email).orElseThrow(); + + List
articles = articleRepository.findAllByWriterOrderByIdDesc(member); + + return GetMyArticlesResponse.of(articles); + } +} diff --git a/src/main/java/com/api/trip/domain/aws/config/AmazonS3Config.java b/src/main/java/com/api/trip/domain/aws/config/AmazonS3Config.java new file mode 100644 index 0000000..37b2f18 --- /dev/null +++ b/src/main/java/com/api/trip/domain/aws/config/AmazonS3Config.java @@ -0,0 +1,33 @@ +package com.api.trip.domain.aws.config; + +import com.amazonaws.auth.AWSStaticCredentialsProvider; +import com.amazonaws.auth.BasicAWSCredentials; +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.AmazonS3ClientBuilder; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class AmazonS3Config { + + @Value("${cloud.aws.credentials.access-key}") + private String accessKey; + + @Value("${cloud.aws.credentials.secret-key}") + private String secretKey; + + @Value("${cloud.aws.region.static}") + private String region; + + @Bean + public AmazonS3 amazonS3Client() { + BasicAWSCredentials awsCredentials = new BasicAWSCredentials(accessKey, secretKey); + + return AmazonS3ClientBuilder + .standard() + .withRegion(region) + .withCredentials(new AWSStaticCredentialsProvider(awsCredentials)) + .build(); + } +} diff --git a/src/main/java/com/api/trip/domain/aws/service/AmazonS3Service.java b/src/main/java/com/api/trip/domain/aws/service/AmazonS3Service.java new file mode 100644 index 0000000..7d8f6be --- /dev/null +++ b/src/main/java/com/api/trip/domain/aws/service/AmazonS3Service.java @@ -0,0 +1,65 @@ +package com.api.trip.domain.aws.service; + +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.model.ObjectMetadata; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Optional; +import java.util.UUID; + +@Service +@Slf4j +@RequiredArgsConstructor +public class AmazonS3Service { + + private final AmazonS3 amazonS3; + + @Value("${cloud.aws.s3.bucket}") + private String bucket; + + @Getter + @Value("${cloud.aws.default-image}") + private String defaultProfileImg; + + public String upload(MultipartFile profileImg) { + + // 파일 확장자 분리 (.png, .jpg, .gif) + String ext = Optional.ofNullable(profileImg.getOriginalFilename()) + .filter(f -> f.contains(".")) + .map(f -> f.split("\\.")[1]) + .orElse(""); + + // 파일 이름은 UUID.ext 형식으로 업로드 + String fileName = "%s.%s".formatted(UUID.randomUUID(), ext); + String formatDate = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy_MM_dd")); + String s3location = bucket + "/members/" + formatDate; + + ObjectMetadata metadata = new ObjectMetadata(); + metadata.setContentLength(profileImg.getSize()); + metadata.setContentType(profileImg.getContentType()); + + try { + log.debug("uploading aws s3.. upload path: {}", s3location + fileName); + amazonS3.putObject(s3location, fileName, profileImg.getInputStream(), metadata); + } catch (IOException e) { + // TODO: add exception + throw new RuntimeException("Fail AWS S3 Upload.. {}", e); + } + + return String.valueOf(amazonS3.getUrl(s3location, fileName)); + } + + // TODO: 회원 정보 수정 구현시 필요 + public void delete(String key) { + amazonS3.deleteObject(bucket, key); + } + +} diff --git a/src/main/java/com/api/trip/domain/aws/util/MultipartFileUtils.java b/src/main/java/com/api/trip/domain/aws/util/MultipartFileUtils.java new file mode 100644 index 0000000..1a83db1 --- /dev/null +++ b/src/main/java/com/api/trip/domain/aws/util/MultipartFileUtils.java @@ -0,0 +1,18 @@ +package com.api.trip.domain.aws.util; + +import org.apache.tika.Tika; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; + +public class MultipartFileUtils { + + final static String[] PERMISSION_PROFILE_FILE_MIME_TYPE = {"image/jpeg", "image/png", "image/gif"}; + + // 파일 변조 여부 검사 + public static boolean isPermission(InputStream inputStream) throws IOException { + String mimeType = new Tika().detect(inputStream); + return Arrays.stream(PERMISSION_PROFILE_FILE_MIME_TYPE).anyMatch(type -> type.equalsIgnoreCase(mimeType)); + } +} diff --git a/src/main/java/com/api/trip/domain/comment/controller/CommentController.java b/src/main/java/com/api/trip/domain/comment/controller/CommentController.java new file mode 100644 index 0000000..6bdca89 --- /dev/null +++ b/src/main/java/com/api/trip/domain/comment/controller/CommentController.java @@ -0,0 +1,50 @@ +package com.api.trip.domain.comment.controller; + +import com.api.trip.domain.comment.controller.dto.CreateCommentRequest; +import com.api.trip.domain.comment.controller.dto.GetCommentsResponse; +import com.api.trip.domain.comment.controller.dto.GetMyCommentsResponse; +import com.api.trip.domain.comment.controller.dto.UpdateCommentRequest; +import com.api.trip.domain.comment.service.CommentService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/comments") +@RequiredArgsConstructor +public class CommentController { + + private final CommentService commentService; + + @PostMapping + public ResponseEntity createComment(@RequestBody CreateCommentRequest request) { + String email = SecurityContextHolder.getContext().getAuthentication().getName(); + return ResponseEntity.ok(commentService.createComment(request, email)); + } + + @PatchMapping("/{commentId}") + public ResponseEntity updateComment(@PathVariable Long commentId, @RequestBody UpdateCommentRequest request) { + String email = SecurityContextHolder.getContext().getAuthentication().getName(); + commentService.updateComment(commentId, request, email); + return ResponseEntity.ok().build(); + } + + @DeleteMapping("/{commentId}") + public ResponseEntity deleteComment(@PathVariable Long commentId) { + String email = SecurityContextHolder.getContext().getAuthentication().getName(); + commentService.deleteComment(commentId, email); + return ResponseEntity.ok().build(); + } + + @GetMapping + public ResponseEntity getComments(Long articleId) { + return ResponseEntity.ok(commentService.getComments(articleId)); + } + + @GetMapping("/me") + public ResponseEntity getMyComments() { + String email = SecurityContextHolder.getContext().getAuthentication().getName(); + return ResponseEntity.ok(commentService.getMyComments(email)); + } +} diff --git a/src/main/java/com/api/trip/domain/comment/controller/dto/CreateCommentRequest.java b/src/main/java/com/api/trip/domain/comment/controller/dto/CreateCommentRequest.java new file mode 100644 index 0000000..55408d3 --- /dev/null +++ b/src/main/java/com/api/trip/domain/comment/controller/dto/CreateCommentRequest.java @@ -0,0 +1,23 @@ +package com.api.trip.domain.comment.controller.dto; + +import com.api.trip.domain.article.model.Article; +import com.api.trip.domain.comment.model.Comment; +import com.api.trip.domain.member.model.Member; +import lombok.Getter; + +@Getter +public class CreateCommentRequest { + + private Long articleId; + private Long parentId; + private String content; + + public Comment toEntity(Member writer, Article article, Comment parent) { + return Comment.builder() + .writer(writer) + .article(article) + .content(content) + .parent(parent) + .build(); + } +} diff --git a/src/main/java/com/api/trip/domain/comment/controller/dto/GetCommentsResponse.java b/src/main/java/com/api/trip/domain/comment/controller/dto/GetCommentsResponse.java new file mode 100644 index 0000000..d615b8d --- /dev/null +++ b/src/main/java/com/api/trip/domain/comment/controller/dto/GetCommentsResponse.java @@ -0,0 +1,70 @@ +package com.api.trip.domain.comment.controller.dto; + +import com.api.trip.domain.comment.model.Comment; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Getter +@AllArgsConstructor +public class GetCommentsResponse { + + private List comments; + + public static GetCommentsResponse of(List comments) { + List commentDtos = comments.stream() + .map(CommentDto::of) + .toList(); + + Map> groupByParentId = commentDtos.stream() + .collect(Collectors.groupingBy(commentDto -> commentDto.getParentId() != null ? commentDto.getParentId() : 0)); + + commentDtos = commentDtos.stream() + .filter(commentDto -> { + if (commentDto.getParentId() == null) { + commentDto.setChildren(groupByParentId.get(commentDto.getCommentId())); + return true; + } + return false; + }) + .toList(); + + return new GetCommentsResponse(commentDtos); + } + + @Getter + @Builder + private static class CommentDto { + + private Long commentId; + private Long writerId; + private String writerNickname; + private String writerProfileImg; + private Long articleId; + private String content; + private Long parentId; + private LocalDateTime createdAt; + + @Setter + private List children; + + private static CommentDto of(Comment comment) { + return builder() + .commentId(comment.getId()) + .writerId(comment.getWriter().getId()) + .writerNickname(comment.getWriter().getNickname()) + .writerProfileImg(comment.getWriter().getProfileImg()) + .articleId(comment.getArticle().getId()) + .content(comment.getContent()) + .parentId(comment.getParent() != null ? comment.getParent().getId() : null) + .createdAt(comment.getCreatedAt()) + .build(); + } + } +} diff --git a/src/main/java/com/api/trip/domain/comment/controller/dto/GetMyCommentsResponse.java b/src/main/java/com/api/trip/domain/comment/controller/dto/GetMyCommentsResponse.java new file mode 100644 index 0000000..7469b6b --- /dev/null +++ b/src/main/java/com/api/trip/domain/comment/controller/dto/GetMyCommentsResponse.java @@ -0,0 +1,49 @@ +package com.api.trip.domain.comment.controller.dto; + +import com.api.trip.domain.comment.model.Comment; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +import java.time.LocalDateTime; +import java.util.List; + +@Getter +@AllArgsConstructor +public class GetMyCommentsResponse { + + private List comments; + + public static GetMyCommentsResponse of(List comments) { + List commentDtos = comments.stream() + .map(CommentDto::of) + .toList(); + + return new GetMyCommentsResponse(commentDtos); + } + + @Getter + @Builder + private static class CommentDto { + + private Long commentId; + private Long writerId; + private String writerNickname; + private Long articleId; + private String content; + private Long parentId; + private LocalDateTime createdAt; + + private static CommentDto of(Comment comment) { + return builder() + .commentId(comment.getId()) + .writerId(comment.getWriter().getId()) + .writerNickname(comment.getWriter().getNickname()) + .articleId(comment.getArticle().getId()) + .content(comment.getContent()) + .parentId(comment.getParent() != null ? comment.getParent().getId() : null) + .createdAt(comment.getCreatedAt()) + .build(); + } + } +} diff --git a/src/main/java/com/api/trip/domain/comment/controller/dto/UpdateCommentRequest.java b/src/main/java/com/api/trip/domain/comment/controller/dto/UpdateCommentRequest.java new file mode 100644 index 0000000..879ca39 --- /dev/null +++ b/src/main/java/com/api/trip/domain/comment/controller/dto/UpdateCommentRequest.java @@ -0,0 +1,9 @@ +package com.api.trip.domain.comment.controller.dto; + +import lombok.Getter; + +@Getter +public class UpdateCommentRequest { + + private String content; +} diff --git a/src/main/java/com/api/trip/domain/comment/model/Comment.java b/src/main/java/com/api/trip/domain/comment/model/Comment.java new file mode 100644 index 0000000..1a2b535 --- /dev/null +++ b/src/main/java/com/api/trip/domain/comment/model/Comment.java @@ -0,0 +1,44 @@ +package com.api.trip.domain.comment.model; + +import com.api.trip.common.auditing.entity.BaseTimeEntity; +import com.api.trip.domain.article.model.Article; +import com.api.trip.domain.member.model.Member; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +public class Comment extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Member writer; + + @ManyToOne(fetch = FetchType.LAZY) + private Article article; + + @Column(nullable = false) + private String content; + + @ManyToOne(fetch = FetchType.LAZY) + private Comment parent; + + @Builder + private Comment(Member writer, Article article, String content, Comment parent) { + this.writer = writer; + this.article = article; + this.content = content; + this.parent = parent; + } + + public void modify(String content) { + this.content = content; + } +} diff --git a/src/main/java/com/api/trip/domain/comment/repository/CommentRepository.java b/src/main/java/com/api/trip/domain/comment/repository/CommentRepository.java new file mode 100644 index 0000000..6a9eaaf --- /dev/null +++ b/src/main/java/com/api/trip/domain/comment/repository/CommentRepository.java @@ -0,0 +1,12 @@ +package com.api.trip.domain.comment.repository; + +import com.api.trip.domain.comment.model.Comment; +import com.api.trip.domain.member.model.Member; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface CommentRepository extends JpaRepository, CommentRepositoryCustom { + + List findAllByWriterOrderByIdDesc(Member writer); +} diff --git a/src/main/java/com/api/trip/domain/comment/repository/CommentRepositoryCustom.java b/src/main/java/com/api/trip/domain/comment/repository/CommentRepositoryCustom.java new file mode 100644 index 0000000..da423e7 --- /dev/null +++ b/src/main/java/com/api/trip/domain/comment/repository/CommentRepositoryCustom.java @@ -0,0 +1,11 @@ +package com.api.trip.domain.comment.repository; + +import com.api.trip.domain.article.model.Article; +import com.api.trip.domain.comment.model.Comment; + +import java.util.List; + +public interface CommentRepositoryCustom { + + List findComments(Article article); +} diff --git a/src/main/java/com/api/trip/domain/comment/repository/CommentRepositoryCustomImpl.java b/src/main/java/com/api/trip/domain/comment/repository/CommentRepositoryCustomImpl.java new file mode 100644 index 0000000..e261c9f --- /dev/null +++ b/src/main/java/com/api/trip/domain/comment/repository/CommentRepositoryCustomImpl.java @@ -0,0 +1,29 @@ +package com.api.trip.domain.comment.repository; + +import com.api.trip.domain.article.model.Article; +import com.api.trip.domain.comment.model.Comment; +import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.persistence.EntityManager; + +import java.util.List; + +import static com.api.trip.domain.comment.model.QComment.comment; +import static com.api.trip.domain.member.model.QMember.member; + +public class CommentRepositoryCustomImpl implements CommentRepositoryCustom { + + private final JPAQueryFactory jpaQueryFactory; + + public CommentRepositoryCustomImpl(EntityManager em) { + this.jpaQueryFactory = new JPAQueryFactory(em); + } + + @Override + public List findComments(Article article) { + return jpaQueryFactory + .selectFrom(comment) + .innerJoin(comment.writer, member).fetchJoin() + .where(comment.article.eq(article)) + .fetch(); + } +} diff --git a/src/main/java/com/api/trip/domain/comment/service/CommentService.java b/src/main/java/com/api/trip/domain/comment/service/CommentService.java new file mode 100644 index 0000000..239ae54 --- /dev/null +++ b/src/main/java/com/api/trip/domain/comment/service/CommentService.java @@ -0,0 +1,85 @@ +package com.api.trip.domain.comment.service; + +import com.api.trip.domain.article.model.Article; +import com.api.trip.domain.article.repository.ArticleRepository; +import com.api.trip.domain.comment.controller.dto.CreateCommentRequest; +import com.api.trip.domain.comment.controller.dto.GetCommentsResponse; +import com.api.trip.domain.comment.controller.dto.GetMyCommentsResponse; +import com.api.trip.domain.comment.controller.dto.UpdateCommentRequest; +import com.api.trip.domain.comment.model.Comment; +import com.api.trip.domain.comment.repository.CommentRepository; +import com.api.trip.domain.member.model.Member; +import com.api.trip.domain.member.repository.MemberRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@Transactional +@RequiredArgsConstructor +public class CommentService { + + private final CommentRepository commentRepository; + private final ArticleRepository articleRepository; + private final MemberRepository memberRepository; + + public Long createComment(CreateCommentRequest request, String email) { + Member member = memberRepository.findByEmail(email).orElseThrow(); + + Article article = articleRepository.findById(request.getArticleId()).orElseThrow(); + + Comment parent = null; + if (request.getParentId() != null) { + parent = commentRepository.findById(request.getParentId()).orElseThrow(); + if (parent.getParent() != null || parent.getArticle() != article) { + throw new RuntimeException("잘못된 요청입니다."); + } + } + + Comment comment = request.toEntity(member, article, parent); + + return commentRepository.save(comment).getId(); + } + + public void updateComment(Long commentId, UpdateCommentRequest request, String email) { + Member member = memberRepository.findByEmail(email).orElseThrow(); + + Comment comment = commentRepository.findById(commentId).orElseThrow(); + if (comment.getWriter() != member) { + throw new RuntimeException("수정 권한이 없습니다."); + } + + comment.modify(request.getContent()); + } + + public void deleteComment(Long commentId, String email) { + Member member = memberRepository.findByEmail(email).orElseThrow(); + + Comment comment = commentRepository.findById(commentId).orElseThrow(); + if (comment.getWriter() != member) { + throw new RuntimeException("삭제 권한이 없습니다."); + } + + commentRepository.delete(comment); + } + + @Transactional(readOnly = true) + public GetCommentsResponse getComments(Long articleId) { + Article article = articleRepository.findById(articleId).orElseThrow(); + + List comments = commentRepository.findComments(article); + + return GetCommentsResponse.of(comments); + } + + @Transactional(readOnly = true) + public GetMyCommentsResponse getMyComments(String email) { + Member member = memberRepository.findByEmail(email).orElseThrow(); + + List comments = commentRepository.findAllByWriterOrderByIdDesc(member); + + return GetMyCommentsResponse.of(comments); + } +} diff --git a/src/main/java/com/api/trip/domain/email/model/EmailAuth.java b/src/main/java/com/api/trip/domain/email/model/EmailAuth.java new file mode 100644 index 0000000..ee617b1 --- /dev/null +++ b/src/main/java/com/api/trip/domain/email/model/EmailAuth.java @@ -0,0 +1,46 @@ +package com.api.trip.domain.email.model; + +import com.api.trip.common.auditing.entity.BaseTimeEntity; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class EmailAuth extends BaseTimeEntity { + + private static final Long MAX_EXPIRE_TIME = 5L; // 링크 유효 기간 5분 + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String email; // 가입 이메일 + + private String authToken; // 인증 토큰 (UUID) + + private boolean expired; // 토큰 사용 여부 (인증 완료: true, 인증 미완료: false) + + private LocalDateTime expireDate; // 인증 토큰 만료일 (발급 날짜 + 5분) + + @Builder + public EmailAuth(String email, String authToken, boolean expired) { + this.email = email; + this.authToken = authToken; + this.expired = expired; + this.expireDate = LocalDateTime.now().plusMinutes(MAX_EXPIRE_TIME); + } + + public void useToken() { + this.expired = true; + } + +} diff --git a/src/main/java/com/api/trip/domain/email/repository/EmailAuthRepository.java b/src/main/java/com/api/trip/domain/email/repository/EmailAuthRepository.java new file mode 100644 index 0000000..0bcb4a1 --- /dev/null +++ b/src/main/java/com/api/trip/domain/email/repository/EmailAuthRepository.java @@ -0,0 +1,12 @@ +package com.api.trip.domain.email.repository; + +import com.api.trip.domain.email.model.EmailAuth; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface EmailAuthRepository extends JpaRepository, EmailAuthRepositoryCustom { + + Optional findByEmail(String email); + +} diff --git a/src/main/java/com/api/trip/domain/email/repository/EmailAuthRepositoryCustom.java b/src/main/java/com/api/trip/domain/email/repository/EmailAuthRepositoryCustom.java new file mode 100644 index 0000000..aa6be06 --- /dev/null +++ b/src/main/java/com/api/trip/domain/email/repository/EmailAuthRepositoryCustom.java @@ -0,0 +1,11 @@ +package com.api.trip.domain.email.repository; + +import com.api.trip.domain.email.model.EmailAuth; + +import java.time.LocalDateTime; +import java.util.Optional; + +public interface EmailAuthRepositoryCustom { + Optional findValidAuthByEmail(String email, String authToken, LocalDateTime now); + +} diff --git a/src/main/java/com/api/trip/domain/email/repository/EmailAuthRepositoryCustomImpl.java b/src/main/java/com/api/trip/domain/email/repository/EmailAuthRepositoryCustomImpl.java new file mode 100644 index 0000000..3d0936c --- /dev/null +++ b/src/main/java/com/api/trip/domain/email/repository/EmailAuthRepositoryCustomImpl.java @@ -0,0 +1,32 @@ +package com.api.trip.domain.email.repository; + +import com.api.trip.domain.email.model.EmailAuth; +import com.api.trip.domain.email.model.QEmailAuth; +import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.persistence.EntityManager; + +import java.time.LocalDateTime; +import java.util.Optional; + +public class EmailAuthRepositoryCustomImpl implements EmailAuthRepositoryCustom{ + + private final JPAQueryFactory jpaQueryFactory; + + public EmailAuthRepositoryCustomImpl(EntityManager em) { + this.jpaQueryFactory = new JPAQueryFactory(em); + } + + @Override + public Optional findValidAuthByEmail(String email, String authToken, LocalDateTime now) { + + EmailAuth emailAuth = jpaQueryFactory + .selectFrom(QEmailAuth.emailAuth) + .where(QEmailAuth.emailAuth.email.eq(email), + QEmailAuth.emailAuth.authToken.eq(authToken), + QEmailAuth.emailAuth.expireDate.goe(now), + QEmailAuth.emailAuth.expired.eq(false)) + .fetchFirst(); + + return Optional.ofNullable(emailAuth); + } +} diff --git a/src/main/java/com/api/trip/domain/email/service/EmailService.java b/src/main/java/com/api/trip/domain/email/service/EmailService.java new file mode 100644 index 0000000..b40dbf6 --- /dev/null +++ b/src/main/java/com/api/trip/domain/email/service/EmailService.java @@ -0,0 +1,114 @@ +package com.api.trip.domain.email.service; + +import com.api.trip.domain.email.model.EmailAuth; +import com.api.trip.domain.email.repository.EmailAuthRepository; +import com.api.trip.domain.member.controller.dto.EmailResponse; +import com.api.trip.domain.member.controller.dto.FindPasswordRequest; +import com.api.trip.domain.member.model.Member; +import com.api.trip.domain.member.service.MemberService; +import jakarta.mail.MessagingException; +import jakarta.mail.internet.MimeMessage; +import lombok.RequiredArgsConstructor; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.scheduling.annotation.Async; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.security.SecureRandom; +import java.time.LocalDateTime; +import java.util.UUID; + +@Service +@EnableAsync +@Transactional +@RequiredArgsConstructor +public class EmailService { + + private final MemberService memberService; + private final JavaMailSender javaMailSender; + private final EmailAuthRepository emailAuthRepository; + + @Async + public void send(String email, String authToken) { + // TODO: 개발용, 서버용 링크 분리 예정 + String text = "http://localhost:8080/api/members/auth-email/%s/%s" .formatted(email, authToken); + + MimeMessage message = javaMailSender.createMimeMessage(); + + try { + message.setSubject("[TRIP TRIP] 회원가입 인증 링크 발급"); + message.setRecipients(MimeMessage.RecipientType.TO, email); + message.setText(text, "UTF-8", "HTML"); + } catch (MessagingException e) { + throw new RuntimeException(e); + } + + javaMailSender.send(message); + } + + @Async + public void sendNewPassword(FindPasswordRequest findPasswordRequest) { + String email = findPasswordRequest.getEmail(); + + if (email == null || email.isEmpty()) { + throw new RuntimeException("이메일 정보가 없습니다!"); + } + + // 가입 회원 여부 검사 + Member member = memberService.getMemberByEmail(email); + + String newPassword = getRandomPassword(); + String text = "회원님의 임시 비밀번호는 %s 입니다. 로그인 후에 비밀번호를 변경해주세요.".formatted(newPassword); + + MimeMessage message = javaMailSender.createMimeMessage(); + try { + message.setSubject("[TRIP TRIP] 임시 비밀번호 발급"); + message.setRecipients(MimeMessage.RecipientType.TO, email); + message.setText(text); + } catch (MessagingException e) { + throw new RuntimeException(e); + } + + javaMailSender.send(message); + memberService.changePassword(email, newPassword); // 새 비밀번호로 업데이트 + } + + // 인증 메일 검증 + public EmailResponse authEmail(String email, String authToken) { + EmailAuth emailAuth = emailAuthRepository.findValidAuthByEmail(email, authToken, LocalDateTime.now()) + .orElseThrow(() -> new RuntimeException("토큰 정보가 일치하지 않습니다!")); + + emailAuth.useToken(); // 토큰 사용 -> 만료 + return EmailResponse.of(emailAuth.isExpired()); + } + + public String createEmailAuth(String email) { + EmailAuth emailAuth = EmailAuth.builder() + .email(email) + .authToken(UUID.randomUUID().toString()) + .expired(false) + .build(); + + return emailAuthRepository.save(emailAuth).getAuthToken(); + } + + // TODO: 랜덤 비밀번호 생성 -> 임시로 8자리 랜덤 문자열 반환 + private String getRandomPassword() { + char[] charSet = new char[] { + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', + 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z' + }; + + SecureRandom random = new SecureRandom(); + StringBuilder sb = new StringBuilder(); + + int rndAllCharactersLength = charSet.length; + for (int i = 0; i < 8; i++) { + sb.append(charSet[random.nextInt(rndAllCharactersLength)]); + } + + return sb.toString(); + } + +} diff --git a/src/main/java/com/api/trip/domain/interestarticle/controller/InterestArticleController.java b/src/main/java/com/api/trip/domain/interestarticle/controller/InterestArticleController.java new file mode 100644 index 0000000..a46bbb9 --- /dev/null +++ b/src/main/java/com/api/trip/domain/interestarticle/controller/InterestArticleController.java @@ -0,0 +1,29 @@ +package com.api.trip.domain.interestarticle.controller; + +import com.api.trip.domain.interestarticle.controller.dto.CreateInterestArticleRequest; +import com.api.trip.domain.interestarticle.service.InterestArticleService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/interest-articles") +@RequiredArgsConstructor +public class InterestArticleController { + + private final InterestArticleService interestArticleService; + + @PostMapping + public ResponseEntity createInterestArticle(@RequestBody CreateInterestArticleRequest request) { + String email = SecurityContextHolder.getContext().getAuthentication().getName(); + return ResponseEntity.ok(interestArticleService.createInterestArticle(request, email)); + } + + @DeleteMapping("/{interestArticleId}") + public ResponseEntity deleteInterestArticle(@PathVariable Long interestArticleId) { + String email = SecurityContextHolder.getContext().getAuthentication().getName(); + interestArticleService.deleteInterestArticle(interestArticleId, email); + return ResponseEntity.ok().build(); + } +} diff --git a/src/main/java/com/api/trip/domain/interestarticle/controller/dto/CreateInterestArticleRequest.java b/src/main/java/com/api/trip/domain/interestarticle/controller/dto/CreateInterestArticleRequest.java new file mode 100644 index 0000000..ed21e3d --- /dev/null +++ b/src/main/java/com/api/trip/domain/interestarticle/controller/dto/CreateInterestArticleRequest.java @@ -0,0 +1,19 @@ +package com.api.trip.domain.interestarticle.controller.dto; + +import com.api.trip.domain.article.model.Article; +import com.api.trip.domain.interestarticle.model.InterestArticle; +import com.api.trip.domain.member.model.Member; +import lombok.Getter; + +@Getter +public class CreateInterestArticleRequest { + + private Long articleId; + + public InterestArticle toEntity(Member member, Article article) { + return InterestArticle.builder() + .member(member) + .article(article) + .build(); + } +} diff --git a/src/main/java/com/api/trip/domain/interestarticle/model/InterestArticle.java b/src/main/java/com/api/trip/domain/interestarticle/model/InterestArticle.java new file mode 100644 index 0000000..5cd6066 --- /dev/null +++ b/src/main/java/com/api/trip/domain/interestarticle/model/InterestArticle.java @@ -0,0 +1,33 @@ +package com.api.trip.domain.interestarticle.model; + +import com.api.trip.common.auditing.entity.BaseTimeEntity; +import com.api.trip.domain.article.model.Article; +import com.api.trip.domain.member.model.Member; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Table(uniqueConstraints = {@UniqueConstraint(columnNames = {"member_id", "article_id"})}) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +public class InterestArticle extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Member member; + + @ManyToOne(fetch = FetchType.LAZY) + private Article article; + + @Builder + private InterestArticle(Member member, Article article) { + this.member = member; + this.article = article; + } +} diff --git a/src/main/java/com/api/trip/domain/interestarticle/repository/InterestArticleRepository.java b/src/main/java/com/api/trip/domain/interestarticle/repository/InterestArticleRepository.java new file mode 100644 index 0000000..04bb2ed --- /dev/null +++ b/src/main/java/com/api/trip/domain/interestarticle/repository/InterestArticleRepository.java @@ -0,0 +1,11 @@ +package com.api.trip.domain.interestarticle.repository; + +import com.api.trip.domain.article.model.Article; +import com.api.trip.domain.interestarticle.model.InterestArticle; +import com.api.trip.domain.member.model.Member; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface InterestArticleRepository extends JpaRepository { + + InterestArticle findByMemberAndArticle(Member member, Article article); +} diff --git a/src/main/java/com/api/trip/domain/interestarticle/service/InterestArticleService.java b/src/main/java/com/api/trip/domain/interestarticle/service/InterestArticleService.java new file mode 100644 index 0000000..93265c1 --- /dev/null +++ b/src/main/java/com/api/trip/domain/interestarticle/service/InterestArticleService.java @@ -0,0 +1,52 @@ +package com.api.trip.domain.interestarticle.service; + +import com.api.trip.domain.article.model.Article; +import com.api.trip.domain.article.repository.ArticleRepository; +import com.api.trip.domain.interestarticle.controller.dto.CreateInterestArticleRequest; +import com.api.trip.domain.interestarticle.model.InterestArticle; +import com.api.trip.domain.interestarticle.repository.InterestArticleRepository; +import com.api.trip.domain.member.model.Member; +import com.api.trip.domain.member.repository.MemberRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional +@RequiredArgsConstructor +public class InterestArticleService { + + private final InterestArticleRepository interestArticleRepository; + private final ArticleRepository articleRepository; + private final MemberRepository memberRepository; + + public Long createInterestArticle(CreateInterestArticleRequest request, String email) { + Member member = memberRepository.findByEmail(email).orElseThrow(); + + Article article = articleRepository.findById(request.getArticleId()).orElseThrow(); + + InterestArticle interestArticle = interestArticleRepository.findByMemberAndArticle(member, article); + if (interestArticle != null) { + throw new RuntimeException("잘못된 요청입니다."); + } + + interestArticle = request.toEntity(member, article); + + articleRepository.increaseLikeCount(interestArticle.getArticle()); + + return interestArticleRepository.save(interestArticle).getId(); + } + + public void deleteInterestArticle(Long interestArticleId, String email) { + Member member = memberRepository.findByEmail(email).orElseThrow(); + + InterestArticle interestArticle = interestArticleRepository.findById(interestArticleId).orElseThrow(); + if (interestArticle.getMember() != member) { + throw new RuntimeException("삭제 권한이 없습니다."); + } + + articleRepository.decreaseLikeCount(interestArticle.getArticle()); + + interestArticleRepository.delete(interestArticle); + } +} diff --git a/src/main/java/com/api/trip/domain/interestitem/InterestItem.java b/src/main/java/com/api/trip/domain/interestitem/InterestItem.java new file mode 100644 index 0000000..7c08783 --- /dev/null +++ b/src/main/java/com/api/trip/domain/interestitem/InterestItem.java @@ -0,0 +1,34 @@ +package com.api.trip.domain.interestitem; + +import com.api.trip.common.auditing.entity.BaseTimeEntity; +import com.api.trip.domain.item.model.Item; +import com.api.trip.domain.member.model.Member; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +public class InterestItem extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id") + private Member member; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "item_id") + private Item item; + + @Builder + private InterestItem(Member member, Item item) { + this.member = member; + this.item = item; + } +} diff --git a/src/main/java/com/api/trip/domain/interestitem/controller/InterestItemController.java b/src/main/java/com/api/trip/domain/interestitem/controller/InterestItemController.java new file mode 100644 index 0000000..77e3722 --- /dev/null +++ b/src/main/java/com/api/trip/domain/interestitem/controller/InterestItemController.java @@ -0,0 +1,43 @@ +package com.api.trip.domain.interestitem.controller; + +import com.api.trip.domain.interestitem.controller.dto.CreateInterestItemRequest; +import com.api.trip.domain.interestitem.service.InterestItemService; +import com.api.trip.domain.item.controller.dto.CreateItemRequest; +import com.api.trip.domain.item.controller.dto.GetItemResponse; +import com.api.trip.domain.item.controller.dto.GetItemsResponse; +import com.api.trip.domain.item.service.ItemService; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.data.web.PageableDefault; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/interest-items") +@RequiredArgsConstructor +public class InterestItemController { + + private final InterestItemService interestItemService; + + + @PostMapping + public ResponseEntity createInterestItem(@RequestBody CreateInterestItemRequest itemRequest) { + + interestItemService.createInterestItem(itemRequest); + return ResponseEntity.ok().build(); + } + + @GetMapping + public ResponseEntity getInterestItems( + @PageableDefault(size = 8) Pageable pageable + ) { + return ResponseEntity.ok(interestItemService.getInterestItems(pageable)); + } + + + @DeleteMapping("/{ItemId}") + public ResponseEntity cancelInterestItem(@PathVariable Long ItemId) { + interestItemService.cancelInterestItem(ItemId); + return ResponseEntity.ok().build(); + } +} diff --git a/src/main/java/com/api/trip/domain/interestitem/controller/dto/CreateInterestItemRequest.java b/src/main/java/com/api/trip/domain/interestitem/controller/dto/CreateInterestItemRequest.java new file mode 100644 index 0000000..7a7629d --- /dev/null +++ b/src/main/java/com/api/trip/domain/interestitem/controller/dto/CreateInterestItemRequest.java @@ -0,0 +1,12 @@ +package com.api.trip.domain.interestitem.controller.dto; + +import com.api.trip.domain.interestitem.InterestItem; +import com.api.trip.domain.member.model.Member; +import lombok.Getter; + +@Getter +public class CreateInterestItemRequest { + + private long itemId; + +} diff --git a/src/main/java/com/api/trip/domain/interestitem/controller/dto/GetInterestItemsResponse.java b/src/main/java/com/api/trip/domain/interestitem/controller/dto/GetInterestItemsResponse.java new file mode 100644 index 0000000..ad91aae --- /dev/null +++ b/src/main/java/com/api/trip/domain/interestitem/controller/dto/GetInterestItemsResponse.java @@ -0,0 +1,33 @@ +package com.api.trip.domain.interestitem.controller.dto; + +import com.api.trip.domain.interestitem.InterestItem; +import com.api.trip.domain.item.controller.dto.GetItemResponse; +import com.api.trip.domain.item.controller.dto.GetItemsResponse; +import com.api.trip.domain.item.model.Item; +import lombok.Builder; +import lombok.Getter; +import org.springframework.data.domain.Page; + +import java.util.List; +import java.util.stream.Collectors; + +@Getter +@Builder +public class GetInterestItemsResponse { + + private long totalCount; + private int currentPage; + private int totalPage; + private List itemList; + + public static GetItemsResponse of(Page page) + { + return GetItemsResponse.builder() + .currentPage(page.getNumber()) + .totalCount(page.getTotalElements()) + .totalPage(page.getTotalPages()) + .itemList(page.getContent().stream().map(InterestItem::getItem).map(GetItemResponse::of).collect(Collectors.toList())) + .build(); + } + +} diff --git a/src/main/java/com/api/trip/domain/interestitem/repository/InterestItemRepository.java b/src/main/java/com/api/trip/domain/interestitem/repository/InterestItemRepository.java new file mode 100644 index 0000000..9641209 --- /dev/null +++ b/src/main/java/com/api/trip/domain/interestitem/repository/InterestItemRepository.java @@ -0,0 +1,15 @@ +package com.api.trip.domain.interestitem.repository; + +import com.api.trip.domain.interestitem.InterestItem; +import com.api.trip.domain.item.model.Item; +import com.api.trip.domain.member.model.Member; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface InterestItemRepository extends JpaRepository { + + Page findByMember(Member member, Pageable pageable); + + InterestItem findByItem_Id(Long itemId); +} diff --git a/src/main/java/com/api/trip/domain/interestitem/service/InterestItemService.java b/src/main/java/com/api/trip/domain/interestitem/service/InterestItemService.java new file mode 100644 index 0000000..d33c785 --- /dev/null +++ b/src/main/java/com/api/trip/domain/interestitem/service/InterestItemService.java @@ -0,0 +1,53 @@ +package com.api.trip.domain.interestitem.service; + +import com.api.trip.domain.interestitem.InterestItem; +import com.api.trip.domain.interestitem.controller.dto.CreateInterestItemRequest; +import com.api.trip.domain.interestitem.controller.dto.GetInterestItemsResponse; +import com.api.trip.domain.interestitem.repository.InterestItemRepository; +import com.api.trip.domain.item.controller.dto.CreateItemRequest; +import com.api.trip.domain.item.controller.dto.GetItemsResponse; +import com.api.trip.domain.item.model.Item; +import com.api.trip.domain.item.service.ItemService; +import com.api.trip.domain.member.model.Member; +import com.api.trip.domain.member.service.MemberService; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.stream.Collectors; + +@Service +@Transactional +@RequiredArgsConstructor +public class InterestItemService { + + private final MemberService memberService; + private final ItemService itemService; + private final InterestItemRepository interestItemRepository; + + public void createInterestItem(CreateInterestItemRequest itemRequest) { + Member member = memberService.getAuthenticationMember(); + Item item = itemService.getItem(itemRequest.getItemId()); + + InterestItem interestItem = InterestItem.builder() + .item(item) + .member(member).build(); + + interestItemRepository.save(interestItem); + + } + + public GetItemsResponse getInterestItems(Pageable pageable) { + Member member = memberService.getAuthenticationMember(); + Page page = interestItemRepository.findByMember(member, pageable); + + return GetInterestItemsResponse.of(page); + } + + public void cancelInterestItem(Long itemId) { + InterestItem interestItem = interestItemRepository.findByItem_Id(itemId); + interestItemRepository.delete(interestItem); + } +} diff --git a/src/main/java/com/api/trip/domain/item/controller/ItemController.java b/src/main/java/com/api/trip/domain/item/controller/ItemController.java new file mode 100644 index 0000000..1e115f1 --- /dev/null +++ b/src/main/java/com/api/trip/domain/item/controller/ItemController.java @@ -0,0 +1,44 @@ +package com.api.trip.domain.item.controller; +import com.api.trip.domain.item.controller.dto.CreateItemRequest; +import com.api.trip.domain.item.controller.dto.GetItemResponse; +import com.api.trip.domain.item.controller.dto.GetItemsResponse; +import com.api.trip.domain.item.service.ItemService; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.data.web.PageableDefault; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/items") +@RequiredArgsConstructor +public class ItemController { + + private final ItemService itemService; + + @PostMapping + public ResponseEntity createItem(@RequestBody CreateItemRequest itemRequest) { + + return ResponseEntity.ok(itemService.createItem(itemRequest)); + } + @GetMapping("/{ItemId}") + public ResponseEntity getItem(@PathVariable Long ItemId) { + return ResponseEntity.ok(itemService.getItemDetail(ItemId)); + } + + @GetMapping + public ResponseEntity getItems( + @PageableDefault(size = 8) Pageable pageable + ) { + return ResponseEntity.ok(itemService.getItemsDetail(pageable)); + } + + + @DeleteMapping("/{ItemId}") + public ResponseEntity deleteItem(@PathVariable Long ItemId) { + itemService.deleteItem(ItemId); + return ResponseEntity.ok().build(); + } + +} diff --git a/src/main/java/com/api/trip/domain/item/controller/dto/CreateItemRequest.java b/src/main/java/com/api/trip/domain/item/controller/dto/CreateItemRequest.java new file mode 100644 index 0000000..d81f1eb --- /dev/null +++ b/src/main/java/com/api/trip/domain/item/controller/dto/CreateItemRequest.java @@ -0,0 +1,36 @@ +package com.api.trip.domain.item.controller.dto; + +import com.api.trip.domain.item.model.Item; +import com.api.trip.domain.member.model.Member; +import jakarta.persistence.Column; +import lombok.Getter; + +@Getter +public class CreateItemRequest { + + + private Long productId; + + private String title; + + private String shopName; + + private String buyUrl; + + private long maxPrice; + + private long minPrice; + + public Item toEntity(Member writer){ + return Item.builder() + .productId(productId) + .title(title) + .shopName(shopName) + .buyUrl(buyUrl) + .maxPrice(maxPrice) + .minPrice(minPrice) + .writer(writer) + .build(); + + } +} diff --git a/src/main/java/com/api/trip/domain/item/controller/dto/GetItemResponse.java b/src/main/java/com/api/trip/domain/item/controller/dto/GetItemResponse.java new file mode 100644 index 0000000..1541c4a --- /dev/null +++ b/src/main/java/com/api/trip/domain/item/controller/dto/GetItemResponse.java @@ -0,0 +1,40 @@ +package com.api.trip.domain.item.controller.dto; + +import com.api.trip.domain.item.model.Item; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class GetItemResponse { + + private Long id; + + private Long productId; + + private String title; + + private String shopName; + + private String buyUrl; + + private long maxPrice; + + private long minPrice; + + private String writerNickname; + + public static GetItemResponse of(Item item){ + return GetItemResponse.builder() + .id(item.getId()) + .productId(item.getProductId()) + .title(item.getTitle()) + .shopName(item.getShopName()) + .buyUrl(item.getBuyUrl()) + .maxPrice(item.getMaxPrice()) + .minPrice(item.getMinPrice()) + .writerNickname(item.getWriter().getNickname()) + .build(); + + } +} diff --git a/src/main/java/com/api/trip/domain/item/controller/dto/GetItemsResponse.java b/src/main/java/com/api/trip/domain/item/controller/dto/GetItemsResponse.java new file mode 100644 index 0000000..9d13fcf --- /dev/null +++ b/src/main/java/com/api/trip/domain/item/controller/dto/GetItemsResponse.java @@ -0,0 +1,30 @@ +package com.api.trip.domain.item.controller.dto; + +import com.api.trip.domain.item.model.Item; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.data.domain.Page; + +import java.util.List; +import java.util.stream.Collectors; + +@Getter +@Builder +public class GetItemsResponse { + + private long totalCount; + private int currentPage; + private int totalPage; + private List itemList; + + public static GetItemsResponse of(Page page) + { + return GetItemsResponse.builder() + .currentPage(page.getNumber()) + .totalCount(page.getTotalElements()) + .totalPage(page.getTotalPages()) + .itemList(page.getContent().stream().map(GetItemResponse::of).collect(Collectors.toList())) + .build(); + } +} diff --git a/src/main/java/com/api/trip/domain/item/model/Item.java b/src/main/java/com/api/trip/domain/item/model/Item.java new file mode 100644 index 0000000..7e808a3 --- /dev/null +++ b/src/main/java/com/api/trip/domain/item/model/Item.java @@ -0,0 +1,68 @@ +package com.api.trip.domain.item.model; + +import com.api.trip.common.auditing.entity.BaseTimeEntity; +import com.api.trip.domain.member.model.Member; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +public class Item extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private Long productId; + + @Column(nullable = false) + private String title; + + @Column(nullable = false) + private String shopName; + + @Column(nullable = false) + private String buyUrl; + + @Column(nullable = false) + private long maxPrice; + + @Column(nullable = false) + private long minPrice; + + @Column(nullable = false) + private long viewCount; + + @Column + private boolean isDeleted; + + @ManyToOne(fetch = FetchType.LAZY) + private Member writer; + + + @Builder + private Item(Long productId, String title, String shopName, String buyUrl, long maxPrice, long minPrice, Member writer) { + this.productId = productId; + this.title = title; + this.shopName = shopName; + this.buyUrl = buyUrl; + this.maxPrice = maxPrice; + this.minPrice = minPrice; + this.viewCount = 0; + this.writer = writer; + } + + public void increaseViewCount() { + this.viewCount++; + } + public void delete(){ + if(this.isDeleted == true) + new RuntimeException("이미 삭제된 아이템입니다."); + this.isDeleted = true; + } +} \ No newline at end of file diff --git a/src/main/java/com/api/trip/domain/item/repository/ItemRepository.java b/src/main/java/com/api/trip/domain/item/repository/ItemRepository.java new file mode 100644 index 0000000..34fd1dc --- /dev/null +++ b/src/main/java/com/api/trip/domain/item/repository/ItemRepository.java @@ -0,0 +1,7 @@ +package com.api.trip.domain.item.repository; + +import com.api.trip.domain.item.model.Item; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ItemRepository extends JpaRepository, ItemRepositoryCustom { +} diff --git a/src/main/java/com/api/trip/domain/item/repository/ItemRepositoryCustom.java b/src/main/java/com/api/trip/domain/item/repository/ItemRepositoryCustom.java new file mode 100644 index 0000000..015c942 --- /dev/null +++ b/src/main/java/com/api/trip/domain/item/repository/ItemRepositoryCustom.java @@ -0,0 +1,10 @@ +package com.api.trip.domain.item.repository; + +import com.api.trip.domain.article.model.Article; +import com.api.trip.domain.item.model.Item; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +public interface ItemRepositoryCustom { + Page findItems(Pageable pageable); +} diff --git a/src/main/java/com/api/trip/domain/item/repository/ItemRepositoryCustomImpl.java b/src/main/java/com/api/trip/domain/item/repository/ItemRepositoryCustomImpl.java new file mode 100644 index 0000000..c8051f3 --- /dev/null +++ b/src/main/java/com/api/trip/domain/item/repository/ItemRepositoryCustomImpl.java @@ -0,0 +1,49 @@ +package com.api.trip.domain.item.repository; + +import com.api.trip.domain.item.model.Item; +import com.api.trip.domain.member.model.QMember; +import com.querydsl.jpa.impl.JPAQuery; +import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.persistence.EntityManager; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.support.PageableExecutionUtils; + +import java.util.List; + +import static com.api.trip.domain.item.model.QItem.item; + +public class ItemRepositoryCustomImpl implements ItemRepositoryCustom{ + + private final JPAQueryFactory jpaQueryFactory; + + public ItemRepositoryCustomImpl(EntityManager em) { + this.jpaQueryFactory = new JPAQueryFactory(em); + } + + @Override + public Page findItems(Pageable pageable) { + + + List result = jpaQueryFactory.selectFrom(item) + .join(item.writer).fetchJoin() + .where( + item.isDeleted.eq(false) + ) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + //.orderBy() + .fetch(); + + JPAQuery countQuery = jpaQueryFactory.select(item.count()) + .from(item) + .where( + item.isDeleted.eq(false) + ); + + return PageableExecutionUtils.getPage(result, pageable, countQuery::fetchOne); + } + + +} diff --git a/src/main/java/com/api/trip/domain/item/service/ItemService.java b/src/main/java/com/api/trip/domain/item/service/ItemService.java new file mode 100644 index 0000000..c18be09 --- /dev/null +++ b/src/main/java/com/api/trip/domain/item/service/ItemService.java @@ -0,0 +1,66 @@ +package com.api.trip.domain.item.service; + + +import com.api.trip.domain.item.controller.dto.CreateItemRequest; +import com.api.trip.domain.item.controller.dto.GetItemResponse; +import com.api.trip.domain.item.controller.dto.GetItemsResponse; +import com.api.trip.domain.item.model.Item; +import com.api.trip.domain.item.repository.ItemRepository; +import com.api.trip.domain.member.model.Member; +import com.api.trip.domain.member.service.MemberService; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@Transactional +@RequiredArgsConstructor +public class ItemService { + + private final MemberService memberService; + private final ItemRepository itemRepository; + + public Long createItem(CreateItemRequest itemRequest) { + Member member = memberService.getAuthenticationMember(); + Item item = itemRequest.toEntity(member); + + return itemRepository.save(item).getId(); + } + + @Transactional(readOnly = true) + public Item getItem(Long itemId){ + return itemRepository.findById(itemId).orElseThrow(); + } + + @Transactional(readOnly = true) + public GetItemResponse getItemDetail(Long ItemId) { + Item item = itemRepository.findById(ItemId).orElseThrow(); + + item.increaseViewCount(); + return GetItemResponse.of(item); + } + + @Transactional(readOnly = true) + public GetItemsResponse getItemsDetail(Pageable pageable) { + Page itemPage = itemRepository.findItems(pageable); + itemPage.getContent(); + + return GetItemsResponse.of(itemPage); + } + + + public void deleteItem(Long ItemId) { + Member member = memberService.getAuthenticationMember(); + + Item item = itemRepository.findById(ItemId).orElseThrow(); + if (item.getWriter() != member) { + throw new RuntimeException("삭제 권한이 없습니다."); + } + item.delete(); + } + +} diff --git a/src/main/java/com/api/trip/domain/member/controller/MemberController.java b/src/main/java/com/api/trip/domain/member/controller/MemberController.java index ccafff3..e21aaa3 100644 --- a/src/main/java/com/api/trip/domain/member/controller/MemberController.java +++ b/src/main/java/com/api/trip/domain/member/controller/MemberController.java @@ -1,19 +1,24 @@ package com.api.trip.domain.member.controller; -import com.api.trip.common.security.dto.AuthenticationMember; -import com.api.trip.domain.member.controller.dto.JoinRequest; -import com.api.trip.domain.member.controller.dto.LoginRequest; -import com.api.trip.domain.member.controller.dto.LoginResponse; +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.AmazonS3Client; +import com.amazonaws.services.s3.model.ObjectMetadata; +import com.api.trip.domain.email.service.EmailService; +import com.api.trip.domain.member.controller.dto.*; import com.api.trip.domain.member.service.MemberService; -import lombok.Getter; import lombok.RequiredArgsConstructor; -import org.springframework.data.jpa.repository.Query; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; -import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; -import java.security.Principal; +import java.io.IOException; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Optional; +import java.util.UUID; @RestController @RequestMapping("/api/members") @@ -21,23 +26,45 @@ public class MemberController { private final MemberService memberService; + private final EmailService emailService; - @PostMapping("/join") - public ResponseEntity joinMember(@RequestBody JoinRequest joinRequest) { - memberService.join(joinRequest); + @PostMapping(value = "/join", consumes = {MediaType.APPLICATION_JSON_VALUE, MediaType.MULTIPART_FORM_DATA_VALUE}) + public ResponseEntity join(@RequestPart JoinRequest joinRequest, @RequestPart(required = false) MultipartFile profileImg) throws IOException { + + memberService.join(joinRequest, profileImg); return ResponseEntity.ok().build(); } @PostMapping("/login") - public ResponseEntity loginMember(@RequestBody LoginRequest loginRequest) { + public ResponseEntity login(@RequestBody LoginRequest loginRequest) { LoginResponse loginResponse = memberService.login(loginRequest); return ResponseEntity.ok().body(loginResponse); } - // 이메일 값 확인을 위한 테스트 메서드 입니다. - @GetMapping("/info") - public ResponseEntity info() { - String email = SecurityContextHolder.getContext().getAuthentication().getName(); - return ResponseEntity.ok().body(email); + // 인증 메일 전송 + // TODO: 리팩토링 예정.. + @PostMapping("/send-email/{email}") + public void sendAuthEmail(@PathVariable String email) { + emailService.send(email, emailService.createEmailAuth(email)); + } + + @GetMapping("/auth-email/{email}/{authToken}") + public ResponseEntity emailAndAuthToken(@PathVariable String email, @PathVariable String authToken) { + return ResponseEntity.ok().body(emailService.authEmail(email, authToken)); + } + + @PreAuthorize("isAnonymous()") + @PostMapping("/find/password") + public ResponseEntity sendNewPassword(@RequestBody FindPasswordRequest findPasswordRequest) { + emailService.sendNewPassword(findPasswordRequest); + return ResponseEntity.ok().build(); } + + @PreAuthorize("isAuthenticated()") + @DeleteMapping("/me") + public ResponseEntity deleteMember(@RequestBody DeleteRequest deleteRequest) { + memberService.deleteMember(deleteRequest); + return ResponseEntity.ok().build(); + } + } diff --git a/src/main/java/com/api/trip/domain/member/controller/dto/DeleteRequest.java b/src/main/java/com/api/trip/domain/member/controller/dto/DeleteRequest.java new file mode 100644 index 0000000..db23e25 --- /dev/null +++ b/src/main/java/com/api/trip/domain/member/controller/dto/DeleteRequest.java @@ -0,0 +1,14 @@ +package com.api.trip.domain.member.controller.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class DeleteRequest { + + private String password; + +} diff --git a/src/main/java/com/api/trip/domain/member/controller/dto/EmailResponse.java b/src/main/java/com/api/trip/domain/member/controller/dto/EmailResponse.java new file mode 100644 index 0000000..db68daf --- /dev/null +++ b/src/main/java/com/api/trip/domain/member/controller/dto/EmailResponse.java @@ -0,0 +1,19 @@ +package com.api.trip.domain.member.controller.dto; + +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class EmailResponse { + + private String message; + private boolean authEmail; + + public static EmailResponse of(boolean authEmail) { + return EmailResponse.builder() + .message("이메일 인증이 완료 되었습니다!") + .authEmail(authEmail) + .build(); + } +} diff --git a/src/main/java/com/api/trip/domain/member/controller/dto/FindPasswordRequest.java b/src/main/java/com/api/trip/domain/member/controller/dto/FindPasswordRequest.java new file mode 100644 index 0000000..0bd424f --- /dev/null +++ b/src/main/java/com/api/trip/domain/member/controller/dto/FindPasswordRequest.java @@ -0,0 +1,9 @@ +package com.api.trip.domain.member.controller.dto; + +import lombok.Getter; + +@Getter +public class FindPasswordRequest { + + private String email; +} diff --git a/src/main/java/com/api/trip/domain/member/controller/dto/JoinRequest.java b/src/main/java/com/api/trip/domain/member/controller/dto/JoinRequest.java index 255e3b7..5fde0eb 100644 --- a/src/main/java/com/api/trip/domain/member/controller/dto/JoinRequest.java +++ b/src/main/java/com/api/trip/domain/member/controller/dto/JoinRequest.java @@ -2,7 +2,6 @@ import com.api.trip.domain.member.model.Member; import lombok.Getter; -import lombok.NoArgsConstructor; @Getter public class JoinRequest { @@ -10,4 +9,12 @@ public class JoinRequest { private String email; private String password; private String nickname; + + public static Member of(JoinRequest joinRequest, String password){ + return Member.builder() + .email(joinRequest.getEmail()) + .nickname(joinRequest.getNickname()) + .password(password) + .build(); + } } diff --git a/src/main/java/com/api/trip/domain/member/controller/dto/LoginResponse.java b/src/main/java/com/api/trip/domain/member/controller/dto/LoginResponse.java index 0c37761..0943176 100644 --- a/src/main/java/com/api/trip/domain/member/controller/dto/LoginResponse.java +++ b/src/main/java/com/api/trip/domain/member/controller/dto/LoginResponse.java @@ -1,6 +1,6 @@ package com.api.trip.domain.member.controller.dto; -import com.api.trip.common.security.JwtToken; +import com.api.trip.common.security.jwt.JwtToken; import lombok.Builder; import lombok.Getter; diff --git a/src/main/java/com/api/trip/domain/member/model/Member.java b/src/main/java/com/api/trip/domain/member/model/Member.java index bd121c0..4eb8e2b 100644 --- a/src/main/java/com/api/trip/domain/member/model/Member.java +++ b/src/main/java/com/api/trip/domain/member/model/Member.java @@ -1,16 +1,21 @@ package com.api.trip.domain.member.model; +import com.api.trip.common.auditing.entity.BaseTimeEntity; import com.api.trip.domain.member.controller.dto.JoinRequest; import jakarta.persistence.*; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.SQLRestriction; @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) -public class Member { +@SQLDelete(sql = "UPDATE member SET deleted_at = NOW() WHERE id = ?") +@SQLRestriction("deleted_at IS NULL") +public class Member extends BaseTimeEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -25,23 +30,39 @@ public class Member { @Column(nullable = false) private String password; + @Column(nullable = false) // 기본 이미지가 무조건 들어갈 예정. + private String profileImg; + @Column(nullable = false) @Enumerated(EnumType.STRING) private MemberRole role; + private boolean emailAuth; + @Builder - private Member(String email, String nickname, String password){ + private Member(String email, String password, String nickname, String profileImg){ this.email = email; - this.nickname = nickname; this.password = password; + this.nickname = nickname; + this.profileImg = profileImg; this.role = MemberRole.MEMBER; } - public static Member of(JoinRequest joinRequest, String password){ + public static Member of(String email, String password, String nickname, String profileImg) { return Member.builder() - .email(joinRequest.getEmail()) - .nickname(joinRequest.getNickname()) + .email(email) .password(password) + .nickname(nickname) + .profileImg(profileImg) .build(); } + + // 이메일 인증 상태 변경 메서드 + public void emailVerifiedSuccess() { + this.emailAuth = true; + } + + public void changePassword(String password) { + this.password = password; + } } diff --git a/src/main/java/com/api/trip/domain/member/service/MemberService.java b/src/main/java/com/api/trip/domain/member/service/MemberService.java index fc8796a..63b4fbd 100644 --- a/src/main/java/com/api/trip/domain/member/service/MemberService.java +++ b/src/main/java/com/api/trip/domain/member/service/MemberService.java @@ -1,7 +1,13 @@ package com.api.trip.domain.member.service; -import com.api.trip.common.security.JwtToken; -import com.api.trip.common.security.JwtTokenProvider; +import com.api.trip.common.security.jwt.JwtToken; +import com.api.trip.common.security.jwt.JwtTokenProvider; +import com.api.trip.common.security.util.SecurityUtils; +import com.api.trip.domain.aws.util.MultipartFileUtils; +import com.api.trip.domain.aws.service.AmazonS3Service; +import com.api.trip.domain.email.model.EmailAuth; +import com.api.trip.domain.email.repository.EmailAuthRepository; +import com.api.trip.domain.member.controller.dto.DeleteRequest; import com.api.trip.domain.member.controller.dto.JoinRequest; import com.api.trip.domain.member.controller.dto.LoginRequest; import com.api.trip.domain.member.controller.dto.LoginResponse; @@ -11,10 +17,13 @@ import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.core.Authentication; +import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; +import java.io.IOException; import java.util.stream.Collectors; @Service @@ -23,12 +32,47 @@ public class MemberService { private final MemberRepository memberRepository; + private final EmailAuthRepository emailAuthRepository; private final PasswordEncoder passwordEncoder; + private final AmazonS3Service amazonS3Service; private final AuthenticationManagerBuilder authenticationManagerBuilder; private final JwtTokenProvider jwtTokenProvider; - public void join(JoinRequest joinRequest) { - Member member = Member.of(joinRequest, passwordEncoder.encode(joinRequest.getPassword())); + public void join(JoinRequest joinRequest, MultipartFile profileImg) throws IOException { + + // 중복된 회원이 있는지 검사 + memberRepository.findByEmail(joinRequest.getEmail()).ifPresent(it -> { + throw new RuntimeException("이미 존재하는 회원 입니다."); + }); + + // 이메일 인증이 완료 여부 검사 + EmailAuth emailAuth = emailAuthRepository.findByEmail(joinRequest.getEmail()) + .orElseThrow(() -> new RuntimeException("이메일 인증 토큰 정보가 없습니다!")); + + String profileImgUrl = ""; + if (profileImg == null || profileImg.isEmpty()) { + // 프로필 사진 데이터가 없으면 기본 이미지 세팅 + profileImgUrl = amazonS3Service.getDefaultProfileImg(); + } else { + // 파일 변조 여부 체크 + if (!MultipartFileUtils.isPermission(profileImg.getInputStream())) { + throw new RuntimeException("프로필 사진은 이미지 파일만 선택 가능합니다!"); + } + + // 요청 파일 이미지가 있는 경우 s3 업로드 + profileImgUrl = amazonS3Service.upload(profileImg); + } + + Member member = Member.of( + joinRequest.getEmail(), + passwordEncoder.encode(joinRequest.getPassword()), + joinRequest.getNickname(), + profileImgUrl + ); + + member.emailVerifiedSuccess(); + + // TODO: 회원 가입이 실패하는 경우 s3에 있는 파일 삭제 처리 고민 해보기 memberRepository.save(member); } @@ -43,4 +87,30 @@ public LoginResponse login(LoginRequest loginRequest) { JwtToken jwtToken = jwtTokenProvider.createJwtToken(loginRequest.getEmail(), authorities); return LoginResponse.of(jwtToken); } + + public void deleteMember(DeleteRequest deleteRequest) { + Member member = getAuthenticationMember(); + + if (!passwordEncoder.matches(deleteRequest.getPassword(), member.getPassword())) { + throw new RuntimeException("비밀번호가 일치하지 않습니다."); + } + + memberRepository.deleteById(member.getId()); + } + + // 회원의 비밀번호를 메일로 전송한 임시 비밀번호로 변경 + public void changePassword(String email, String password) { + Member member = memberRepository.findByEmail(email).orElseThrow(); + member.changePassword(passwordEncoder.encode(password)); + } + + @Transactional(readOnly = true) + public Member getMemberByEmail(String email) { + return memberRepository.findByEmail(email).orElseThrow(() -> new UsernameNotFoundException("가입된 회원이 아닙니다!")); + } + + @Transactional(readOnly = true) + public Member getAuthenticationMember() { + return memberRepository.findByEmail(SecurityUtils.getCurrentUsername()).orElseThrow(() -> new UsernameNotFoundException("가입된 회원이 아닙니다!")); + } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 5cff38d..366d765 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -7,7 +7,16 @@ spring: h2: console: enabled: true - + mail: + host: smtp.gmail.com + data: + web: + pageable: + one-indexed-parameters: true + servlet: + multipart: + max-file-size: 20MB + max-request-size: 50MB logging: level: diff --git a/src/test/java/com/api/trip/TripApplicationTests.java b/src/test/java/com/api/trip/TripApplicationTests.java index a0fcd5a..f4e3edb 100644 --- a/src/test/java/com/api/trip/TripApplicationTests.java +++ b/src/test/java/com/api/trip/TripApplicationTests.java @@ -1,7 +1,6 @@ package com.api.trip; import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; //@SpringBootTest class TripApplicationTests {