diff --git a/.coderabbit.yaml b/.coderabbit.yaml deleted file mode 100644 index 2e8edd18..00000000 --- a/.coderabbit.yaml +++ /dev/null @@ -1,14 +0,0 @@ -language: "ko-KR" # 한국말로 설정 -early_access: false -reviews: - profile: "chill" # 리뷰 너무 빡세게는 안한다는 뜻 - request_changes_workflow: true # 코드래빗이 리뷰 끝나면 알아서 PR 승인 - high_level_summary: true - poem: true - review_status: true - collapse_walkthrough: false - auto_review: - enabled: true - drafts: false -chat: - auto_reply: true \ No newline at end of file diff --git a/src/main/java/com/kkinikong/be/store/controller/StoreUploadController.java b/src/main/java/com/kkinikong/be/store/controller/StoreUploadController.java index bfe01c20..d9af696d 100644 --- a/src/main/java/com/kkinikong/be/store/controller/StoreUploadController.java +++ b/src/main/java/com/kkinikong/be/store/controller/StoreUploadController.java @@ -31,8 +31,6 @@ public ResponseEntity> 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()))); } } diff --git a/src/main/java/com/kkinikong/be/store/repository/storeupload/StoreJdbcRepository.java b/src/main/java/com/kkinikong/be/store/repository/storeupload/StoreJdbcRepository.java index df5ebf30..4052e798 100644 --- a/src/main/java/com/kkinikong/be/store/repository/storeupload/StoreJdbcRepository.java +++ b/src/main/java/com/kkinikong/be/store/repository/storeupload/StoreJdbcRepository.java @@ -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 stores); + void upsertStores(List stores); - List findExistingStoreKeys(List keys); + void deleteMissingStores(String region, LocalDateTime startTime); } diff --git a/src/main/java/com/kkinikong/be/store/repository/storeupload/StoreJdbcRepositoryImpl.java b/src/main/java/com/kkinikong/be/store/repository/storeupload/StoreJdbcRepositoryImpl.java index ef4dfcec..b01e77f2 100644 --- a/src/main/java/com/kkinikong/be/store/repository/storeupload/StoreJdbcRepositoryImpl.java +++ b/src/main/java/com/kkinikong/be/store/repository/storeupload/StoreJdbcRepositoryImpl.java @@ -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; @@ -21,15 +20,20 @@ public class StoreJdbcRepositoryImpl implements StoreJdbcRepository { private static final int BATCH_SIZE = 1000; - /// 새로운 가맹점 리스트를 Batch Insert @Override @Transactional - public void saveAllByJdbcTemplate(List stores) { + public void upsertStores(List 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, @@ -42,30 +46,20 @@ public void saveAllByJdbcTemplate(List 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 findExistingStoreKeys(List 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); } } diff --git a/src/main/java/com/kkinikong/be/store/service/StoreUploadService.java b/src/main/java/com/kkinikong/be/store/service/StoreUploadService.java index 02bce353..ae2fae01 100644 --- a/src/main/java/com/kkinikong/be/store/service/StoreUploadService.java +++ b/src/main/java/com/kkinikong/be/store/service/StoreUploadService.java @@ -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; @@ -32,35 +32,24 @@ public class StoreUploadService { @Transactional public StoreUploadResponse upload(MultipartFile file) { + LocalDateTime startTime = LocalDateTime.now().minusSeconds(5); + // CSV 파일을 파싱해서 Store 리스트로 변환 List stores = parseCsv(file); - // name|address 조합 key 생성 - List storeKeys = - stores.stream() - .map(store -> generateStoreKey(store.getName(), store.getAddress())) - .collect(Collectors.toList()); - - // 이미 존재하는 가맹점 key 조회 - List existingKeys = storeJdbcRepository.findExistingStoreKeys(storeKeys); + // 파일의 첫 번째 데이터에서 지역 정보 추출 + String targetRegion = stores.get(0).getRegion(); - // 중복 제외하고 새로운 Store만 추출 - List 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 parseCsv(MultipartFile file) { List stores = new ArrayList<>(); try (BufferedReader reader = @@ -81,7 +70,7 @@ private List parseCsv(MultipartFile file) { return stores; } - /// CSV 한 줄을 Store 객체로 변환 + // CSV 한 줄을 Store 객체로 변환 private Store toStore(CSVRecord record) { return Store.builder() .name(record.get(0).trim()) @@ -94,7 +83,7 @@ private Store toStore(CSVRecord record) { .build(); } - /// 주소에서 "시 구" 부분만 추출 + // 주소에서 "시 구" 부분만 추출 private String extractRegion(String address) { String[] parts = address.split(" "); if (parts.length < 2) { @@ -102,9 +91,4 @@ private String extractRegion(String address) { } return parts[0] + " " + parts[1]; } - - /// name + address 조합으로 store 고유 key 생성 - private String generateStoreKey(String name, String address) { - return name.trim() + "|" + address.trim(); - } }