Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ dependencies {
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"
testImplementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'

// awaitility
testImplementation 'org.awaitility:awaitility:4.3.0'
}

// ------------------
Expand Down
1 change: 1 addition & 0 deletions src/main/java/com/pitchain/bmscrap/domain/BmScrap.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Table(uniqueConstraints = @UniqueConstraint(columnNames = {"member_id", "bm_id"}))
public class BmScrap {

@Id
Expand Down
16 changes: 16 additions & 0 deletions src/main/java/com/pitchain/common/config/RedisConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,21 @@
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.data.redis.listener.ChannelTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.listener.adapter.MessageListenerAdapter;
import org.springframework.data.redis.repository.configuration.EnableRedisRepositories;
import org.springframework.scripting.ScriptSource;
import org.springframework.scripting.support.ResourceScriptSource;

import java.io.IOException;
import java.util.List;

@Configuration
@EnableRedisRepositories
Expand Down Expand Up @@ -45,4 +53,12 @@ public MessageListenerAdapter messageListenerAdapter(RedisSubscriber subscriber)
return new MessageListenerAdapter(subscriber, "onMessage");
}

@Bean
public RedisScript<List> script() throws IOException {
ScriptSource scriptSource = new ResourceScriptSource(new ClassPathResource("META-INF/scripts/getanddeleteall.lua"));
String script = scriptSource.getScriptAsString();

return new DefaultRedisScript<>(script, List.class);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,17 @@
import com.pitchain.common.apiPayload.ErrorStatus;
import jakarta.validation.ConstraintViolationException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.MessageSource;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@Slf4j
@RestControllerAdvice
@RequiredArgsConstructor
public class GlobalExceptionHandler {
Expand Down Expand Up @@ -72,4 +75,17 @@ public ResponseEntity<CustomResponse> handleJWTVerificationException(JWTVerifica
.status(HttpStatus.UNAUTHORIZED)
.body(customResponse);
}

@ExceptionHandler(DataIntegrityViolationException.class)
public ResponseEntity<CustomResponse> handleDataIntegrityViolationException(DataIntegrityViolationException e) {
log.error("DataIntegrityViolationException = {}", e.getMessage());
String errorMessage = "DataIntegrityViolationException(제약 조건 위반 오류)가 발생했습니다.";

HttpStatus httpStatus = HttpStatus.BAD_REQUEST;
CustomResponse customResponse = CustomResponse.onFailure(httpStatus.name(), errorMessage);

return ResponseEntity
.status(httpStatus)
.body(customResponse);
}
}
33 changes: 33 additions & 0 deletions src/main/java/com/pitchain/common/redis/RedisHashRepository.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.pitchain.common.redis;

import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Component;

import java.util.Collections;
import java.util.List;
import java.util.Map;

@Component
@RequiredArgsConstructor
public class RedisHashRepository {

private final RedisTemplate<String, String> redisTemplate;
private final RedisScript<List> script;

public void increment(String key, String hashKey, Long value) {
redisTemplate.opsForHash().increment(key, hashKey, value);
}

public Map<String, String> findAll(String key) {
HashOperations<String, String, String> ops = redisTemplate.opsForHash();
return ops.entries(key);
}

public List<String> getAndDeleteAll(String key) {
return redisTemplate.execute(script, Collections.singletonList(key));
}

}
3 changes: 2 additions & 1 deletion src/main/java/com/pitchain/sp/application/SpService.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
public class SpService {
private final SpCommandService spCommandService;
private final SpQueryService spQueryService;
private final SpViewsService spViewsService;

@Transactional
public void createSp(MemberDetails memberDetails, Long bmId, SpCreateReq spCreateReq, MultipartFile thumbnailImg) {
Expand All @@ -34,8 +35,8 @@ public InfinityScrollRes<SpDetailRes> getSpDetailsFilteredCategory(MemberDetails
return spQueryService.getSpDetailsFilteredCategory(memberDetails, mainCategoryInKorean, lastSpId, size);
}

@Transactional(readOnly = true)
public SpDetailRes getSpDetail(MemberDetails memberDetails, Long bmId, Long spId) {
spViewsService.updateSpView(spId);
return spQueryService.getSpDetail(memberDetails, bmId, spId);
}

Expand Down
80 changes: 80 additions & 0 deletions src/main/java/com/pitchain/sp/application/SpViewsService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package com.pitchain.sp.application;

import com.pitchain.common.redis.RedisHashRepository;
import com.pitchain.sp.infrastucture.SpRepositoryCustom;
import com.pitchain.sp.infrastucture.dto.SpViewsDto;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.ArrayList;
import java.util.List;

@Slf4j
@Service
@RequiredArgsConstructor
public class SpViewsService {

private final SpRepositoryCustom spRepositoryCustom;
private final RedisHashRepository redisHashRepository;

private static final String SP_VIEW_REDIS_KEY = "spView";

/**
* Redis에 Sp 조회수 증가
* @param spId
*/
public void updateSpView(Long spId) {
redisHashRepository.increment(SP_VIEW_REDIS_KEY, String.valueOf(spId), 1L);
}

/**
* Redis에서 DB로 Sp 조회수 업데이트
*/
@Transactional
public void updateSpViews() {
List<String> spViewsResult = redisHashRepository.getAndDeleteAll(SP_VIEW_REDIS_KEY);
List<SpViewsDto> spViewsDtoList = parseResult(spViewsResult);

for (SpViewsDto spViewsDto : spViewsDtoList) {
spRepositoryCustom.updateSpView(spViewsDto.spId(), spViewsDto.views());
}
}

/**
* 1분마다 Redis에서 DB로 Sp 조회수 업데이트하는 작업 수행
*/
@Scheduled(cron = "0 */1 * * * *")
public void runUpdateSpViews() {
try {
updateSpViews();
} catch (Exception e) {
log.error("Scheduling task [runUpdateSpViews] failed", e);
}
}

/**
* Redis에서 가져온 Sp 조회수 String 리스트를 Dto 리스트로 파싱
* @param List<String>
* @return List<SpViewsDto>
*/
private List<SpViewsDto> parseResult(List<String> spViewsStringList) {
if (spViewsStringList.size() % 2 != 0){
log.error("spViewsStringList 개수가 올바르지 않습니다.");
throw new IllegalArgumentException("spViewsStringList 개수가 올바르지 않습니다.");
}

List<SpViewsDto> spViewsDtoList = new ArrayList<>();
for (int i = 0; i < spViewsStringList.size(); i += 2) {
Long spId = Long.parseLong(spViewsStringList.get(i));
Long views = Long.parseLong(spViewsStringList.get(i + 1));

SpViewsDto spViewsDto = new SpViewsDto(spId, views);
spViewsDtoList.add(spViewsDto);
}

return spViewsDtoList;
}
Comment on lines +65 to +81
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

파싱 로직에 예외 처리 개선 필요

기본적인 검증은 있지만 Long.parseLong() 실행 시 발생할 수 있는 NumberFormatException에 대한 처리가 필요합니다.

 private List<SpViewsDto> parseResult(List<String> spViewsStringList) {
     if (spViewsStringList.size() % 2 != 0){
         log.error("spViewsStringList 개수가 올바르지 않습니다.");
         throw new IllegalArgumentException("spViewsStringList 개수가 올바르지 않습니다.");
     }

     List<SpViewsDto> spViewsDtoList = new ArrayList<>();
     for (int i = 0; i < spViewsStringList.size(); i += 2) {
-        Long spId = Long.parseLong(spViewsStringList.get(i));
-        Long views = Long.parseLong(spViewsStringList.get(i + 1));
+        try {
+            Long spId = Long.parseLong(spViewsStringList.get(i));
+            Long views = Long.parseLong(spViewsStringList.get(i + 1));
+            
+            SpViewsDto spViewsDto = new SpViewsDto(spId, views);
+            spViewsDtoList.add(spViewsDto);
+        } catch (NumberFormatException e) {
+            log.error("Invalid number format in Redis data: spId={}, views={}", 
+                     spViewsStringList.get(i), spViewsStringList.get(i + 1), e);
+            // 잘못된 데이터는 건너뛰고 계속 처리
+        }

-        SpViewsDto spViewsDto = new SpViewsDto(spId, views);
-        spViewsDtoList.add(spViewsDto);
     }

     return spViewsDtoList;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private List<SpViewsDto> parseResult(List<String> spViewsStringList) {
if (spViewsStringList.size() % 2 != 0){
log.error("spViewsStringList 개수가 올바르지 않습니다.");
throw new IllegalArgumentException("spViewsStringList 개수가 올바르지 않습니다.");
}
List<SpViewsDto> spViewsDtoList = new ArrayList<>();
for (int i = 0; i < spViewsStringList.size(); i += 2) {
Long spId = Long.parseLong(spViewsStringList.get(i));
Long views = Long.parseLong(spViewsStringList.get(i + 1));
SpViewsDto spViewsDto = new SpViewsDto(spId, views);
spViewsDtoList.add(spViewsDto);
}
return spViewsDtoList;
}
private List<SpViewsDto> parseResult(List<String> spViewsStringList) {
if (spViewsStringList.size() % 2 != 0){
log.error("spViewsStringList 개수가 올바르지 않습니다.");
throw new IllegalArgumentException("spViewsStringList 개수가 올바르지 않습니다.");
}
List<SpViewsDto> spViewsDtoList = new ArrayList<>();
for (int i = 0; i < spViewsStringList.size(); i += 2) {
try {
Long spId = Long.parseLong(spViewsStringList.get(i));
Long views = Long.parseLong(spViewsStringList.get(i + 1));
SpViewsDto spViewsDto = new SpViewsDto(spId, views);
spViewsDtoList.add(spViewsDto);
} catch (NumberFormatException e) {
log.error(
"Invalid number format in Redis data: spId={}, views={}",
spViewsStringList.get(i),
spViewsStringList.get(i + 1),
e
);
// 잘못된 데이터는 건너뛰고 계속 처리
}
}
return spViewsDtoList;
}
🤖 Prompt for AI Agents
In src/main/java/com/pitchain/sp/application/SpViewsService.java around lines 63
to 79, the parseResult method lacks handling for NumberFormatException that can
be thrown by Long.parseLong. Add try-catch blocks around the parsing statements
to catch NumberFormatException, log an appropriate error message, and throw a
meaningful exception to handle invalid number formats gracefully.

}
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public record SpDetailRes(
@S3Url
String thumbnailImgURL,
@NotNull
Integer views,
Long views,
@NotBlank
String name,
@NotBlank
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/com/pitchain/sp/domain/Sp.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public class Sp extends BaseEntity {
@Column(nullable = false)
private String thumbnailImgKey;

private int views = 0;
private Long views;

@Column(nullable = false)
private String name;
Expand All @@ -42,7 +42,7 @@ public static Sp of(Bm bm, String thumbnailImgKey, String name) {
sp.thumbnailImgKey = thumbnailImgKey;
sp.name = name;
sp.spStatus = SpStatus.TRANSCODING;
sp.views = 0;
sp.views = 0L;
return sp;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import com.querydsl.jpa.impl.JPAQueryFactory;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

Expand Down Expand Up @@ -46,4 +47,13 @@ private static BooleanExpression eqMainCategory(MainCategory category) {
private BooleanExpression ltSpId(Long lastSpId) {
return lastSpId == null ? null : sp.id.lt(lastSpId);
}

@Transactional
public long updateSpView(Long spId, Long views) {
return queryFactory
.update(sp)
.set(sp.views, sp.views.add(views))
.where(sp.id.eq(spId))
.execute();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com.pitchain.sp.infrastucture.dto;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

패키지명 오타 확인 요청

패키지명에 "infrastucture"가 "infrastructure"의 오타로 보입니다. 프로젝트 전체에서 일관된 명명 규칙을 사용하고 있는지 확인해 주세요.


🏁 Script executed:

#!/bin/bash
# 프로젝트 내 infrastructure/infrastucture 패키지명 사용 현황 확인
fd -t d "infrastucture"
fd -t d "infrastructure"

Length of output: 771


패키지명 철자 통일 필요

프로젝트에 com.pitchain.* 아래에 "infrastucture"(오타)와 "infrastructure"(올바름)가 혼용되어 있어, 모든 패키지를 "infrastructure"로 일관되게 리팩토링해야 합니다. IDE 리팩터링 기능 또는 스크립트를 활용해 디렉토리명과 package 선언, import 구문을 일괄 수정해 주세요.

영향 받는 오타 패키지 경로:

  • src/main/java/com/pitchain/bm/infrastucture/
  • src/main/java/com/pitchain/bmscrap/infrastucture/
  • src/main/java/com/pitchain/comment/infrastucture/
  • src/main/java/com/pitchain/investment/infrastucture/
  • src/main/java/com/pitchain/member/infrastucture/
  • src/main/java/com/pitchain/mysphistory/infrastucture/
  • src/main/java/com/pitchain/notificationhistory/infrastucture/
  • src/main/java/com/pitchain/sp/infrastucture/
  • src/main/java/com/pitchain/splike/infrastucture/
🤖 Prompt for AI Agents
In src/main/java/com/pitchain/sp/infrastucture/dto/SpViewsDto.java at line 1,
the package name "infrastucture" is misspelled and should be corrected to
"infrastructure". Refactor all occurrences of the misspelled package name in the
project, including directory names, package declarations, and import statements,
to use the correct spelling "infrastructure". Use your IDE's refactoring tools
or scripts to perform this change consistently across all affected packages
listed.


public record SpViewsDto(Long spId, Long views) {
}
1 change: 1 addition & 0 deletions src/main/java/com/pitchain/splike/domain/SpLike.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Table(uniqueConstraints = @UniqueConstraint(columnNames = {"member_id", "sp_id"}))
public class SpLike {

@Id
Expand Down
3 changes: 3 additions & 0 deletions src/main/resources/META-INF/scripts/getanddeleteall.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
local data = redis.call('HGETALL', KEYS[1])
redis.call('DEL', KEYS[1])
return data
Loading