Skip to content
Closed
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
14 changes: 0 additions & 14 deletions .coderabbit.yaml

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,6 @@ public ResponseEntity<ApiResponse<Object>> uploadStoreCsv(
@RequestPart("file") MultipartFile file) {
StoreUploadResponse response = storeUploadService.upload(file);
return ResponseEntity.ok(
ApiResponse.from(
String.format(
"CSV 파일 업로드 완료: 총 %d건 중 %d건 저장됨", response.totalCount(), response.saveCount())));
ApiResponse.from(String.format("CSV 파일 업로드 완료: 총 %d건 저장됨", response.saveCount())));
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
package com.kkinikong.be.store.repository.storeupload;

import java.time.LocalDateTime;
import java.util.List;

import com.kkinikong.be.store.domain.Store;

public interface StoreJdbcRepository {
void saveAllByJdbcTemplate(List<Store> stores);
void upsertStores(List<Store> stores);

List<String> findExistingStoreKeys(List<String> keys);
void deleteMissingStores(String region, LocalDateTime startTime);
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
package com.kkinikong.be.store.repository.storeupload;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;

import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
Expand All @@ -21,15 +20,20 @@ public class StoreJdbcRepositoryImpl implements StoreJdbcRepository {

private static final int BATCH_SIZE = 1000;

/// 새로운 가맹점 리스트를 Batch Insert
@Override
@Transactional
public void saveAllByJdbcTemplate(List<Store> stores) {
public void upsertStores(List<Store> stores) {
String sql =
"INSERT INTO stores "
+ "(name, region, category, address, latitude, longitude, "
+ "rating_avg, scrap_count, review_count, view_count, updated_date, created_date, modified_date) "
+ "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
+ "VALUES (?, ?, ?, ?, ?, ?, 0.0, 0, 0, 0, ?, NOW(), NOW()) "
+ "ON DUPLICATE KEY UPDATE "
+ "category = VALUES(category), "
+ "latitude = VALUES(latitude), "
+ "longitude = VALUES(longitude), "
+ "updated_date = VALUES(updated_date), "
+ "modified_date = NOW()";

jdbcTemplate.batchUpdate(
sql,
Expand All @@ -42,30 +46,20 @@ public void saveAllByJdbcTemplate(List<Store> stores) {
ps.setString(4, store.getAddress());
ps.setDouble(5, store.getLatitude());
ps.setDouble(6, store.getLongitude());
ps.setDouble(7, 0.0);
ps.setLong(8, 0L);
ps.setLong(9, 0L);
ps.setLong(10, 0L);
ps.setObject(11, store.getUpdatedDate());
ps.setObject(12, LocalDate.now());
ps.setObject(13, LocalDate.now());
ps.setObject(7, store.getUpdatedDate());
});
}

/// 이미 존재하는 가맹점 키를 조회 (name | address)
@Override
public List<String> findExistingStoreKeys(List<String> keys) {
if (keys.isEmpty()) {
return List.of();
}
@Transactional
public void deleteMissingStores(String region, LocalDateTime startTime) {
String sql = "DELETE FROM stores WHERE region = :region AND modified_date < :startTime";

String sql =
"""
SELECT CONCAT(name, '|', address) AS store_key
FROM stores
WHERE CONCAT(name, '|', address) IN (:keys)
""";
MapSqlParameterSource params = new MapSqlParameterSource("keys", keys);
return namedParameterJdbcTemplate.query(sql, params, (rs, rowNum) -> rs.getString("store_key"));
var params =
new org.springframework.jdbc.core.namedparam.MapSqlParameterSource()
.addValue("region", region)
.addValue("startTime", startTime);

namedParameterJdbcTemplate.update(sql, params);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
Expand All @@ -32,35 +32,24 @@ public class StoreUploadService {

@Transactional
public StoreUploadResponse upload(MultipartFile file) {
LocalDateTime startTime = LocalDateTime.now().minusSeconds(5);

// CSV 파일을 파싱해서 Store 리스트로 변환
List<Store> stores = parseCsv(file);

// name|address 조합 key 생성
List<String> storeKeys =
stores.stream()
.map(store -> generateStoreKey(store.getName(), store.getAddress()))
.collect(Collectors.toList());

// 이미 존재하는 가맹점 key 조회
List<String> existingKeys = storeJdbcRepository.findExistingStoreKeys(storeKeys);
// 파일의 첫 번째 데이터에서 지역 정보 추출
String targetRegion = stores.get(0).getRegion();

// 중복 제외하고 새로운 Store만 추출
List<Store> newStores =
stores.stream()
.filter(
store ->
!existingKeys.contains(generateStoreKey(store.getName(), store.getAddress())))
.collect(Collectors.toList());
// 기존 데이터는 유지하며 정보 갱신, 신규 데이터는 추가
storeJdbcRepository.upsertStores(stores);

// 새로운 Store만 Batch Insert
if (!newStores.isEmpty()) {
storeJdbcRepository.saveAllByJdbcTemplate(newStores);
}
// 새로운 파일에 없는 가맹점 삭제
storeJdbcRepository.deleteMissingStores(targetRegion, startTime);

return new StoreUploadResponse(stores.size(), newStores.size());
return new StoreUploadResponse(stores.size(), stores.size());
}

/// CSV 파일을 읽어서 Store 객체 리스트로 변환
// CSV 파일을 읽어서 Store 객체 리스트로 변환
private List<Store> parseCsv(MultipartFile file) {
List<Store> stores = new ArrayList<>();
try (BufferedReader reader =
Expand All @@ -81,7 +70,7 @@ private List<Store> parseCsv(MultipartFile file) {
return stores;
}

/// CSV 한 줄을 Store 객체로 변환
// CSV 한 줄을 Store 객체로 변환
private Store toStore(CSVRecord record) {
return Store.builder()
.name(record.get(0).trim())
Expand All @@ -94,17 +83,12 @@ private Store toStore(CSVRecord record) {
.build();
}

/// 주소에서 "시 구" 부분만 추출
// 주소에서 "시 구" 부분만 추출
private String extractRegion(String address) {
String[] parts = address.split(" ");
if (parts.length < 2) {
throw new StoreException(StoreErrorCode.INVALID_ADDRESS_FORMAT);
}
return parts[0] + " " + parts[1];
}

/// name + address 조합으로 store 고유 key 생성
private String generateStoreKey(String name, String address) {
return name.trim() + "|" + address.trim();
}
}