Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
4 changes: 4 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ dependencies {
// implementation 'org.mongodb:mongodb-driver-sync'
// implementation 'org.mongodb:mongodb-driver-core'

// Caffeine
implementation 'org.springframework.boot:spring-boot-starter-cache'
implementation 'com.github.ben-manes.caffeine:caffeine'

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,25 @@
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.Arrays;
import java.util.List;

@Repository
public interface UserInterestRepository extends JpaRepository<UserInterest, Long> {
/**
* μ‚¬μš©μž ID에 ν•΄λ‹Ήν•˜λŠ” λͺ¨λ“  관심사 ν•­λͺ©μ„ μ‘°νšŒν•©λ‹ˆλ‹€.
*
* @param userId μ‚¬μš©μž ID
* @return μ‚¬μš©μžμ˜ 관심사 λͺ©λ‘
*/
List<UserInterest> findByUserId(Long userId);

/**
* μ‚¬μš©μž ID둜 관심사 쑴재 μ—¬λΆ€ 확인
*
* @param userId μ‚¬μš©μž ID
* @return 관심사 쑴재 μ—¬λΆ€
*/
boolean existsByUserId(Long userId);

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.hyetaekon.hyetaekon.common.exception.GlobalException;
import com.hyetaekon.hyetaekon.publicservice.entity.PublicService;
import com.hyetaekon.hyetaekon.publicservice.repository.PublicServiceRepository;
import com.hyetaekon.hyetaekon.publicservice.service.PublicServiceHandler;
import com.hyetaekon.hyetaekon.user.entity.User;
import com.hyetaekon.hyetaekon.user.repository.UserRepository;
import jakarta.transaction.Transactional;
Expand All @@ -20,6 +21,7 @@ public class BookmarkService {
private final BookmarkRepository bookmarkRepository;
private final UserRepository userRepository;
private final PublicServiceRepository publicServiceRepository;
private final PublicServiceHandler publicServiceHandler;

public void addBookmark(String serviceId, Long userId) {
User user = userRepository.findById(userId)
Expand All @@ -42,6 +44,9 @@ public void addBookmark(String serviceId, Long userId) {

// 뢁마크 수 증가
publicService.increaseBookmarkCount();

// 인기 μ„œλΉ„μŠ€ μΊμ‹œ λ¬΄νš¨ν™”
publicServiceHandler.refreshPopularServices();
}

@Transactional
Expand All @@ -54,5 +59,8 @@ public void removeBookmark(String serviceId, Long userId) {
// 뢁마크 수 κ°μ†Œ
PublicService publicService = bookmark.getPublicService();
publicService.decreaseBookmarkCount();

// 인기 μ„œλΉ„μŠ€ μΊμ‹œ λ¬΄νš¨ν™”
publicServiceHandler.refreshPopularServices();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.hyetaekon.hyetaekon.common.config;

import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.hyetaekon.hyetaekon.publicservice.entity.mongodb.CacheType;

import java.time.Duration;
import java.util.Arrays;

@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();

// 각 μΊμ‹œ νƒ€μž…μ— λŒ€ν•œ μ„€μ • 등둝
Arrays.stream(CacheType.values())
.forEach(cacheType -> {
cacheManager.registerCustomCache(cacheType.getCacheName(),
Caffeine.newBuilder()
.recordStats() // μΊμ‹œ 톡계 기둝
.expireAfterWrite(Duration.ofHours(cacheType.getExpiredAfterWrite())) // ν•­λͺ© 만료 μ‹œκ°„
.maximumSize(cacheType.getMaximumSize()) // μ΅œλŒ€ 크기
.build()
);
});

return cacheManager;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ public class SecurityPath {
"/api/services/popular",
"/api/services/category/*",
"/api/services/detail/*",
"/api/public-data/serviceList/test"
"/api/public-data/serviceList/test",
"/api/mongo/services/search",
"/api/mongo/services/search/autocomplete"
};

// hasRole("USER")
Expand All @@ -31,7 +33,7 @@ public class SecurityPath {

// hasRole("ADMIN")
public static final String[] ADMIN_ENDPOINTS = {
"/api/admin/users/**",
"/api/admin/**",
"/api/public-data/serviceDetailList",
"/api/public-data/supportConditionsList",
"/api/public-data/serviceList"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,19 @@

import com.hyetaekon.hyetaekon.common.publicdata.mongodb.document.PublicData;
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.data.mongodb.repository.Query;
import org.springframework.stereotype.Repository;

import java.util.Collection;
import java.util.List;
import java.util.Optional;

@Repository
public interface PublicDataMongoRepository extends MongoRepository<PublicData, String> {
Optional<PublicData> findByPublicServiceId(String publicServiceId);
List<PublicData> findAllByPublicServiceId(String publicServiceId);
List<PublicData> findAllByPublicServiceIdIn(Collection<String> publicServiceIds);

@Query(value = "{}", fields = "{ 'publicServiceId' : 1 }")
List<String> findAllPublicServiceIds();
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,20 @@
import com.hyetaekon.hyetaekon.common.publicdata.mongodb.document.PublicData;
import com.hyetaekon.hyetaekon.common.publicdata.mongodb.repository.PublicDataMongoRepository;
import com.hyetaekon.hyetaekon.publicservice.entity.PublicService;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Sort;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.aggregation.Aggregation;
import org.springframework.data.mongodb.core.aggregation.AggregationResults;
import org.springframework.data.mongodb.core.index.Index;
import org.springframework.data.mongodb.core.index.IndexInfo;
import org.bson.Document;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.Map;
Expand All @@ -29,17 +39,6 @@ public PublicData saveToMongo(PublicService publicService) {
return mongoRepository.save(document);
}

/**
* μ—¬λŸ¬ κ³΅κ³΅μ„œλΉ„μŠ€ μ—”ν‹°ν‹°λ₯Ό MongoDB에 μ €μž₯
*/
public List<PublicData> saveAllToMongo(List<PublicService> publicServices) {
List<PublicData> documents = publicServices.stream()
.map(this::convertToDocument)
.collect(Collectors.toList());

return mongoRepository.saveAll(documents);
}

/**
* κ³΅κ³΅μ„œλΉ„μŠ€ μ—”ν‹°ν‹°λ₯Ό MongoDB λ¬Έμ„œλ‘œ λ³€ν™˜
*/
Expand Down Expand Up @@ -109,29 +108,129 @@ public PublicData updateOrCreateDocument(PublicService publicService) {
/**
* μ—¬λŸ¬ μ„œλΉ„μŠ€ λ¬Έμ„œ μ—…λ°μ΄νŠΈ λ˜λŠ” 생성
*/
public List<PublicData> updateOrCreateBulkDocuments(List<PublicService> services) {
// κΈ°μ‘΄ ID λͺ©λ‘ κ°€μ Έμ˜€κΈ°
public void updateOrCreateBulkDocuments(List<PublicService> services) {
// λͺ¨λ“  service ID λͺ©λ‘
List<String> serviceIds = services.stream()
.map(PublicService::getId)
.collect(Collectors.toList());

// ID에 ν•΄λ‹Ήν•˜λŠ” λ¬Έμ„œ λ§΅ 생성
Map<String, PublicData> existingDocsMap = mongoRepository.findAllById(serviceIds).stream()
.collect(Collectors.toMap(PublicData::getPublicServiceId, doc -> doc, (a, b) -> a));
// publicServiceId둜 κΈ°μ‘΄ λ¬Έμ„œ 쑰회 (μ€‘μš”: findAllByPublicServiceIdλ₯Ό μ‚¬μš©)
Map<String, PublicData> existingDocsMap = mongoRepository.findAllByPublicServiceIdIn(serviceIds).stream()
.collect(Collectors.toMap(
PublicData::getPublicServiceId,
doc -> doc,
(a, b) -> a // 쀑볡 μ‹œ 첫 번째 λ¬Έμ„œ μœ μ§€
));

// 각 μ„œλΉ„μŠ€ 처리
// μ²˜λ¦¬ν•  λ¬Έμ„œ μ€€λΉ„
List<PublicData> docsToSave = services.stream()
.map(service -> {
PublicData doc = convertToDocument(service);
if (existingDocsMap.containsKey(service.getId())) {
// κΈ°μ‘΄ λ¬Έμ„œ ID μœ μ§€
// κΈ°μ‘΄ λ¬Έμ„œμ˜ ID μœ μ§€
doc.setId(existingDocsMap.get(service.getId()).getId());
}
return doc;
})
.collect(Collectors.toList());

// 일괄 μ €μž₯
return mongoRepository.saveAll(docsToSave);
// μ €μž₯
mongoRepository.saveAll(docsToSave);
}

// 첫 μ‹€ν–‰ μ‹œμ—λ§Œ 쀑볡 제거 및 인덱슀 생성
@PostConstruct
public void ensureIndexes() {
try {
// 1. κΈ°μ‘΄ 인덱슀 확인
boolean hasUniqueIndex = false;
for (IndexInfo indexInfo : mongoTemplate.indexOps("service_info").getIndexInfo()) {
if ("publicServiceId_1".equals(indexInfo.getName())) {
hasUniqueIndex = indexInfo.isUnique();
break;
}
}

// 2. μœ λ‹ˆν¬ μΈλ±μŠ€κ°€ μ—†λŠ” 경우만 처리
if (!hasUniqueIndex) {
// 2.1 일반 인덱슀 쑴재 μ—¬λΆ€ 확인
boolean hasNonUniqueIndex = false;
for (IndexInfo indexInfo : mongoTemplate.indexOps("service_info").getIndexInfo()) {
if ("publicServiceId_1".equals(indexInfo.getName()) && !indexInfo.isUnique()) {
hasNonUniqueIndex = true;
break;
}
}

// 2.2 일반 μΈλ±μŠ€κ°€ μžˆλ‹€λ©΄ μ‚­μ œ
if (hasNonUniqueIndex) {
mongoTemplate.indexOps("service_info").dropIndex("publicServiceId_1");
log.info("κΈ°μ‘΄ λΉ„μœ λ‹ˆν¬ 인덱슀 μ‚­μ œ: publicServiceId_1");
}

// 2.3 μ΅œμ ν™”λœ 쀑볡 제거 μ‹€ν–‰
deduplicateMongoDocuments();

// 2.4 μœ λ‹ˆν¬ 인덱슀 생성
mongoTemplate.indexOps("service_info").ensureIndex(
new Index().on("publicServiceId", Sort.Direction.ASC).unique()
);
log.info("MongoDB 인덱슀 μ„€μ • μ™„λ£Œ: publicServiceId (unique)");
} else {
log.info("MongoDB μœ λ‹ˆν¬ 인덱슀 이미 μ‘΄μž¬ν•¨: publicServiceId_1");
}
} catch (Exception e) {
log.error("MongoDB 인덱슀 μ„€μ • 쀑 였λ₯˜ λ°œμƒ: {}", e.getMessage());
// 인덱슀 생성 μ‹€νŒ¨ν•΄λ„ μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ€ μ‹œμž‘λ˜λ„λ‘ 함
}
}

@Transactional
public void deduplicateMongoDocuments() {
log.info("MongoDB λ¬Έμ„œ 쀑볡 제거 μ‹œμž‘ (μ΅œμ ν™” 버전)");

// λͺ¨λ“  publicServiceId와 ν•΄λ‹Ή λ¬Έμ„œ IDλ₯Ό κ·Έλ£Ήν™”ν•˜μ—¬ ν•œ λ²ˆμ— 쑰회
AggregationResults<Document> results = mongoTemplate.aggregate(
Aggregation.newAggregation(
Aggregation.group("publicServiceId")
.first("_id").as("firstId")
.push("_id").as("allIds")
.count().as("count")
),
"service_info",
Document.class
);

int totalProcessed = 0;
int totalRemoved = 0;

for (Document doc : results.getMappedResults()) {
int count = doc.getInteger("count");

// 쀑볡이 μžˆλŠ” κ²½μš°μ—λ§Œ 처리
if (count > 1) {
String publicServiceId = doc.getString("_id");
List<Object> allIds = (List<Object>) doc.get("allIds");
Object firstId = doc.get("firstId");

// 첫 번째 λ¬Έμ„œλ₯Ό μ œμ™Έν•œ λ‚˜λ¨Έμ§€ λ¬Έμ„œ μ‚­μ œ
for (int i = 0; i < allIds.size(); i++) {
Object currentId = allIds.get(i);
if (!currentId.equals(firstId)) {
mongoTemplate.remove(Query.query(Criteria.where("_id").is(currentId)), "service_info");
totalRemoved++;
}
}
}

totalProcessed++;
if (totalProcessed % 1000 == 0) {
log.info("쀑볡 제거 μ§„ν–‰ 쀑: {}/{} κ·Έλ£Ή 처리, {}개 제거됨",
totalProcessed, results.getMappedResults().size(), totalRemoved);
}
}

log.info("MongoDB λ¬Έμ„œ 쀑볡 제거 μ™„λ£Œ: 총 {}개 κ·Έλ£Ή 쀑 {}개 쀑볡 제거됨",
totalProcessed, totalRemoved);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -240,17 +240,15 @@ public List<PublicServiceDataDto.Data> upsertServiceData(List<PublicServiceDataD
// 배치 처리 μ΅œμ ν™”: 1000개 λ‹¨μœ„λ‘œ μ €μž₯
if (entitiesToSave.size() >= 1000) {
List<PublicService> savedEntities = publicServiceRepository.saveAll(entitiesToSave);

publicDataMongoService.saveAllToMongo(savedEntities);
publicDataMongoService.updateOrCreateBulkDocuments(savedEntities);
entitiesToSave.clear();
}
}

// λ‚˜λ¨Έμ§€ 데이터 μ €μž₯
if (!entitiesToSave.isEmpty()) {
List<PublicService> savedEntities = publicServiceRepository.saveAll(entitiesToSave);

publicDataMongoService.saveAllToMongo(savedEntities);
publicDataMongoService.updateOrCreateBulkDocuments(savedEntities);
}

log.info("κ³΅κ³΅μ„œλΉ„μŠ€ λͺ©λ‘ 데이터 {}건 μ €μž₯ μ™„λ£Œ", validatedData.size());
Expand Down Expand Up @@ -296,17 +294,15 @@ public List<PublicServiceDetailDataDto.Data> upsertServiceDetailData(List<Public
// 배치 처리 μ΅œμ ν™”: 1000개 λ‹¨μœ„λ‘œ μ €μž₯
if (entitiesToSave.size() >= 1000) {
List<PublicService> savedEntities = publicServiceRepository.saveAll(entitiesToSave);

publicDataMongoService.saveAllToMongo(savedEntities);
publicDataMongoService.updateOrCreateBulkDocuments(savedEntities);
entitiesToSave.clear();
}
}

// λ‚˜λ¨Έμ§€ 데이터 μ €μž₯
if (!entitiesToSave.isEmpty()) {
List<PublicService> savedEntities = publicServiceRepository.saveAll(entitiesToSave);

publicDataMongoService.saveAllToMongo(savedEntities);
publicDataMongoService.updateOrCreateBulkDocuments(savedEntities);
}

log.info("κ³΅κ³΅μ„œλΉ„μŠ€ 상세정보 데이터 {}건 μ €μž₯ μ™„λ£Œ", validatedData.size());
Expand Down Expand Up @@ -356,17 +352,15 @@ public List<PublicServiceConditionsDataDto.Data> upsertSupportConditionsData(Lis
// 배치 처리 μ΅œμ ν™”: 1000개 λ‹¨μœ„λ‘œ μ €μž₯
if (entitiesToSave.size() >= 1000) {
List<PublicService> savedEntities = publicServiceRepository.saveAll(entitiesToSave);

publicDataMongoService.saveAllToMongo(savedEntities);
publicDataMongoService.updateOrCreateBulkDocuments(savedEntities);
entitiesToSave.clear();
}
}

// λ‚˜λ¨Έμ§€ 데이터 μ €μž₯
if (!entitiesToSave.isEmpty()) {
List<PublicService> savedEntities = publicServiceRepository.saveAll(entitiesToSave);

publicDataMongoService.saveAllToMongo(savedEntities);
publicDataMongoService.updateOrCreateBulkDocuments(savedEntities);
}

log.info("κ³΅κ³΅μ„œλΉ„μŠ€ 지원쑰건 데이터 {}건 μ €μž₯ μ™„λ£Œ", validatedData.size());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public class MyPostListResponseDto {
private Long postId;
private String title;
private String content;
private Long nickName; // μž‘μ„±μž λ‹‰λ„€μž„
private String nickName; // μž‘μ„±μž λ‹‰λ„€μž„
private LocalDateTime createdAt;
private int recommendCnt;
private int commentCnt;
Expand Down
Loading