Skip to content

Commit

Permalink
Merge pull request #97 from reading-log/develop
Browse files Browse the repository at this point in the history
04.08 MAIN MERGE
  • Loading branch information
don9m1n authored Apr 8, 2024
2 parents 868891a + 1a28ed4 commit 3df2415
Show file tree
Hide file tree
Showing 31 changed files with 602 additions and 75 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839

application-*.yml
src/main/generated

.idea
# User-specific stuff
Expand Down
54 changes: 29 additions & 25 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -22,50 +22,50 @@ repositories {
}

dependencies {
/* Spring Boot Starter */
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-web'

/* Lombok */
// jwt
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
implementation 'io.jsonwebtoken:jjwt-impl:0.11.5'
implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5'

// 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"

// lombok
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'

/* Web Client */
// webflux
implementation 'org.springframework.boot:spring-boot-starter-webflux'

/* Database */
//runtimeOnly 'com.h2database:h2'
runtimeOnly 'com.mysql:mysql-connector-j'

/* Testing */
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'

/* AWS S3 */
// aws s3
implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'

/* JWT */
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
implementation 'io.jsonwebtoken:jjwt-impl:0.11.5'
implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5'

// swagger ui
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0'

/* Email */
implementation group: 'io.swagger.core.v3', name: 'swagger-core-jakarta', version: '2.2.7'
implementation 'org.springframework.boot:spring-boot-starter-mail'

/* Thymeleaf */
// thymeleaf
implementation'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect'

/* Redis */
// redis
implementation 'org.springframework.boot:spring-boot-starter-data-redis'

/* Swagger */
implementation group: 'io.swagger.core.v3', name: 'swagger-core-jakarta', version: '2.2.7'
runtimeOnly 'com.h2database:h2'
runtimeOnly 'com.mysql:mysql-connector-j'

testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
}

tasks.named('bootBuildImage') {
Expand All @@ -75,3 +75,7 @@ tasks.named('bootBuildImage') {
tasks.named('test') {
useJUnitPlatform()
}

clean {
delete file('src/main/generated')
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.api.readinglog.common.cache;

import com.api.readinglog.domain.likesummary.service.LikeSummaryService;
import com.api.readinglog.domain.summary.entity.Summary;
import com.api.readinglog.domain.summary.repository.SummaryRepository;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class CacheInitializer {

private final LikeSummaryService likeSummaryService;
private final SummaryRepository summaryRepository;

// 서버 재시작 시 캐시의 좋아요 개수를 DB에 초기화
@EventListener(ApplicationReadyEvent.class)
public void initializeCacheWithSummaries(ApplicationReadyEvent event) {
List<Summary> summaries = summaryRepository.findAll();

summaries.forEach(summary -> {
Long summaryId = summary.getId();
int likeCount = likeSummaryService.getSummaryLikeCount(summaryId);

summary.setLikeCount(likeCount);
summaryRepository.save(summary);
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.api.readinglog.common.querydsl;

import com.querydsl.jpa.impl.JPAQueryFactory;
import jakarta.persistence.EntityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class QuerydslConfig {

@Bean
public JPAQueryFactory jpaQueryFactory(EntityManager em) {
return new JPAQueryFactory(em);
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
package com.api.readinglog.common.redis.service;

import java.util.Collections;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor

public class RedisService {

private final RedisTemplate<String, Object> redisTemplate;
Expand All @@ -17,18 +19,55 @@ public void setData(String key, Object value, Long time, TimeUnit timeUnit) {
redisTemplate.opsForValue().set(key, value.toString(), time, timeUnit);
}

public void setLikeData(String userKey, Long summaryId) {
redisTemplate.opsForSet().add(userKey, summaryId.toString());
}

public Object getData(String key) {
return redisTemplate.opsForValue().get(key);
}

public Set<Long> getLikeData(String key) {
Set<Object> rawData = redisTemplate.opsForSet().members(key);

if (rawData == null) {
return Collections.emptySet();
}

return rawData.stream()
.map(Object::toString)
.map(Long::parseLong)
.collect(Collectors.toSet());
}

public void deleteData(String key) {
redisTemplate.delete(key);
}

public void deleteLikeData(String userKey, Long summaryId) {
redisTemplate.opsForSet().remove(userKey, summaryId.toString());
}

public void increaseLikeCount(String key) {
redisTemplate.opsForValue().increment(key, 1);
}

public void decreaseLikeCount(String key) {
Integer currentLikeCount = getLikeCount(key);

if (currentLikeCount == null || currentLikeCount <= 0) {
return;
}
redisTemplate.opsForValue().increment(key, -1);
}

public boolean isPresent(String userKey, Long summaryId) {
return Boolean.TRUE.equals(redisTemplate.opsForSet().isMember(userKey, summaryId.toString()));
}

public void increaseData(String key) {
redisTemplate.opsForValue().increment(key);
public Integer getLikeCount(String key) {
Object result = redisTemplate.opsForValue().get(key);
return result != null ? (Integer) result : 0;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@Tag(name = "Book", description = "Book API")
@Tag(name = "Book", description = " API 목록입니다.")
@Slf4j
@RestController
@RequestMapping("/api/books")
Expand All @@ -36,22 +36,22 @@ public class BookController {

private final BookService bookService;

@Operation(summary = "Find book by ID", description = "사용자가 등록한 책 정보 조회")
@Operation(summary = "등록한 책 조회", description = "사용자가 등록한 책 정보를 조회합니다.")
@GetMapping("/{bookId}")
public Response<BookDetailResponse> getBookInfo(@AuthenticationPrincipal CustomUserDetail user, @PathVariable Long bookId) {

return Response.success(HttpStatus.OK, String.format("%d번 책 정보 응답 성공", bookId), bookService.getBookInfo(user.getId(), bookId));
}

@Operation(summary = "Search book", description = "책 검색")
@Operation(summary = "책 검색", description = "검색어를 통해 책을 검색합니다.")
@GetMapping("/search")
public Response<BookSearchApiResponse> searchBooks(@RequestParam(required = false) String q,
@RequestParam(defaultValue = "1") int start) {

return Response.success(HttpStatus.OK, "책 검색 성공", bookService.searchBooks(q, start));
return Response.success(HttpStatus.OK, "책 검색 결과 조회", bookService.searchBooks(q, start));
}

@Operation(summary = "Add a new book after search", description = "책 검색 후 등록")
@Operation(summary = "책 검색 후 등록", description = "검색한 책 정보를 통해 책을 등록합니다.")
@PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE)
public Response<Void> registerBookAfterSearch(@AuthenticationPrincipal CustomUserDetail user,
@RequestBody @Valid BookRegisterRequest request) {
Expand All @@ -60,7 +60,7 @@ public Response<Void> registerBookAfterSearch(@AuthenticationPrincipal CustomUse
return Response.success(HttpStatus.CREATED, "책 등록 성공");
}

@Operation(summary = "Add a new book direct registration", description = "책 직접 등록")
@Operation(summary = "책 직접 등록", description = "책 정보를 직접 입력해서 책을 등록합니다.")
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public Response<Void> registerBookDirect(@AuthenticationPrincipal CustomUserDetail user,
@ModelAttribute @Valid BookDirectRequest request) {
Expand All @@ -69,7 +69,7 @@ public Response<Void> registerBookDirect(@AuthenticationPrincipal CustomUserDeta
return Response.success(HttpStatus.CREATED, "책 등록 성공");
}

@Operation(summary = "Modify book info", description = "책 정보 수정")
@Operation(summary = "책 정보 수정", description = "등록한 책 정보를 수정합니다.")
@PatchMapping("/{bookId}")
public Response<Void> modifyBook(@AuthenticationPrincipal CustomUserDetail user,
@ModelAttribute BookModifyRequest bookModifyRequest,
Expand All @@ -79,10 +79,9 @@ public Response<Void> modifyBook(@AuthenticationPrincipal CustomUserDetail user,
return Response.success(HttpStatus.OK, "책 수정 성공");
}

@Operation(summary = "Delete book", description = "책 삭제")
@Operation(summary = "책 삭제", description = "등록한 책을 삭제합니다.")
@DeleteMapping("/{bookId}")
public Response<Void> deleteBook(@AuthenticationPrincipal CustomUserDetail user, @PathVariable Long bookId) {
// TODO: 책이 삭제될 때 해당 책에 관한 기록, 포스트 함께 삭제?
bookService.deleteBook(user.getId(), bookId);
return Response.success(HttpStatus.OK, "%d번 책 삭제 성공".formatted(bookId));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.api.readinglog.domain.book.dto;

import java.time.LocalDateTime;
import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public class BookCalendarResponse {

private Long bookId;
private String title;
private LocalDateTime createdAt;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.api.readinglog.domain.book.dto;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public class BookCategoryResponse {

private String category;
private long count;
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public class Book extends BaseTimeEntity {
@JoinColumn(name = "member_id")
private Member member;

@Column(name = "book_item_id", unique = true)
@Column(name = "book_item_id")
private Integer itemId; // 책 고유 번호

@Column(name = "book_title", nullable = false)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
import com.api.readinglog.domain.book.entity.Book;
import org.springframework.data.jpa.repository.JpaRepository;

public interface BookRepository extends JpaRepository<Book, Long> {
public interface BookRepository extends JpaRepository<Book, Long>, CustomBookRepository {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.api.readinglog.domain.book.repository;

import com.api.readinglog.domain.book.dto.BookCalendarResponse;
import com.api.readinglog.domain.book.dto.BookCategoryResponse;
import java.util.List;

public interface CustomBookRepository {

Long getBookTotalCountInMonth(long memberId, int month);

List<BookCategoryResponse> getBookCountGroupByCategoryInMonth(Long memberId, int month);

List<BookCalendarResponse> getBookCalendarInMonth(Long memberId, int month);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.api.readinglog.domain.book.repository;

import static com.api.readinglog.domain.book.entity.QBook.book;

import com.api.readinglog.domain.book.dto.BookCalendarResponse;
import com.api.readinglog.domain.book.dto.BookCategoryResponse;
import com.querydsl.core.types.Projections;
import com.querydsl.jpa.impl.JPAQueryFactory;
import java.util.List;
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
public class CustomBookRepositoryImpl implements CustomBookRepository{

private final JPAQueryFactory queryFactory;

@Override
public Long getBookTotalCountInMonth(long memberId, int month) {
return queryFactory
.select(book.count())
.from(book)
.where(book.member.id.eq(memberId).and(book.createdAt.month().eq(month)))
.fetchFirst();
}

@Override
public List<BookCategoryResponse> getBookCountGroupByCategoryInMonth(Long memberId, int month) {
return queryFactory
.select(Projections.constructor(BookCategoryResponse.class, book.category, book.count()))
.from(book)
.where(book.member.id.eq(memberId).and(book.createdAt.month().eq(month)))
.groupBy(book.category)
.orderBy(book.count().desc())
.fetch();
}

@Override
public List<BookCalendarResponse> getBookCalendarInMonth(Long memberId, int month) {
return queryFactory
.select(Projections.constructor(BookCalendarResponse.class, book.id, book.title, book.createdAt))
.from(book)
.where(book.member.id.eq(memberId).and(book.createdAt.month().eq(month)))
.orderBy(book.createdAt.asc())
.fetch();
}
}
Loading

0 comments on commit 3df2415

Please sign in to comment.