From 6b0c9a7c65393c18e241d2ba1e0e86533eb9c5cd Mon Sep 17 00:00:00 2001 From: erika0915 Date: Wed, 17 Dec 2025 18:19:02 +0900 Subject: [PATCH 1/5] =?UTF-8?q?[SCRUM-426]=20Chore:=20=EC=BD=94=EB=93=9C?= =?UTF-8?q?=EB=A0=88=EB=B9=97=20=EA=B4=80=EB=A0=A8=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .coderabbit.yaml | 14 -------------- 1 file changed, 14 deletions(-) delete mode 100644 .coderabbit.yaml diff --git a/.coderabbit.yaml b/.coderabbit.yaml deleted file mode 100644 index 2e8edd1..0000000 --- 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 From 225a965b056f77de9bd452aaa9572cd087ab9856 Mon Sep 17 00:00:00 2001 From: erika0915 Date: Thu, 18 Dec 2025 01:17:37 +0900 Subject: [PATCH 2/5] =?UTF-8?q?[SCRUM-426]=20Feat:=20csv=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EC=97=85=EB=A1=9C=EB=93=9C=20=EC=8B=9C=20=ED=95=B4?= =?UTF-8?q?=EB=8B=B9=20=EC=A7=80=EC=97=AD=20=EC=A0=84=EC=B2=B4=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20=ED=9B=84=20=EC=83=88=EB=A1=AD=EA=B2=8C=20=EC=97=85?= =?UTF-8?q?=EB=A1=9C=EB=93=9C=20=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/StoreUploadController.java | 4 +-- .../storeupload/StoreJdbcRepository.java | 2 +- .../storeupload/StoreJdbcRepositoryImpl.java | 20 ++++--------- .../be/store/service/StoreUploadService.java | 28 +++++-------------- 4 files changed, 14 insertions(+), 40 deletions(-) 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 bfe01c2..d9af696 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 df5ebf3..1fafbfb 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 @@ -7,5 +7,5 @@ public interface StoreJdbcRepository { void saveAllByJdbcTemplate(List stores); - List findExistingStoreKeys(List keys); + void deleteByRegion(String region); } 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 ef4dfce..b9dddbc 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 @@ -4,7 +4,6 @@ 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; @@ -52,20 +51,11 @@ public void saveAllByJdbcTemplate(List stores) { }); } - /// 이미 존재하는 가맹점 키를 조회 (name | address) @Override - public List findExistingStoreKeys(List keys) { - if (keys.isEmpty()) { - return List.of(); - } - - 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")); + @Transactional + public void deleteByRegion(String region) { + // 해당 지역의 모든 가맹점 삭제 + String sql = "DELETE FROM stores WHERE region = ?"; + jdbcTemplate.update(sql, region); } } 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 02bce35..c82ac79 100644 --- a/src/main/java/com/kkinikong/be/store/service/StoreUploadService.java +++ b/src/main/java/com/kkinikong/be/store/service/StoreUploadService.java @@ -7,7 +7,6 @@ import java.time.LocalDate; import java.util.ArrayList; import java.util.List; -import java.util.stream.Collectors; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -35,29 +34,16 @@ public StoreUploadResponse upload(MultipartFile file) { // CSV 파일을 파싱해서 Store 리스트로 변환 List stores = parseCsv(file); - // name|address 조합 key 생성 - List storeKeys = - stores.stream() - .map(store -> generateStoreKey(store.getName(), store.getAddress())) - .collect(Collectors.toList()); + // 파일의 첫 번째 데이터에서 지역 정보 추출 + String targetRegion = stores.get(0).getRegion(); - // 이미 존재하는 가맹점 key 조회 - List existingKeys = storeJdbcRepository.findExistingStoreKeys(storeKeys); + // db에서 해당 지역 데이터 전체 삭제 + storeJdbcRepository.deleteByRegion(targetRegion); - // 중복 제외하고 새로운 Store만 추출 - List newStores = - stores.stream() - .filter( - store -> - !existingKeys.contains(generateStoreKey(store.getName(), store.getAddress()))) - .collect(Collectors.toList()); + // 최신 csv 데이터 전체 삽입 + storeJdbcRepository.saveAllByJdbcTemplate(stores); - // 새로운 Store만 Batch Insert - if (!newStores.isEmpty()) { - storeJdbcRepository.saveAllByJdbcTemplate(newStores); - } - - return new StoreUploadResponse(stores.size(), newStores.size()); + return new StoreUploadResponse(stores.size(), stores.size()); } /// CSV 파일을 읽어서 Store 객체 리스트로 변환 From aa092832055eb7ae9a6a224aa92c2d75aca3c47b Mon Sep 17 00:00:00 2001 From: erika0915 Date: Thu, 18 Dec 2025 01:18:19 +0900 Subject: [PATCH 3/5] =?UTF-8?q?[SCRUM-426]=20Chore:=20=EB=B6=88=ED=95=84?= =?UTF-8?q?=EC=9A=94=ED=95=9C=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../be/store/service/StoreUploadService.java | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) 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 c82ac79..5278c95 100644 --- a/src/main/java/com/kkinikong/be/store/service/StoreUploadService.java +++ b/src/main/java/com/kkinikong/be/store/service/StoreUploadService.java @@ -46,7 +46,7 @@ public StoreUploadResponse upload(MultipartFile file) { return new StoreUploadResponse(stores.size(), stores.size()); } - /// CSV 파일을 읽어서 Store 객체 리스트로 변환 + // CSV 파일을 읽어서 Store 객체 리스트로 변환 private List parseCsv(MultipartFile file) { List stores = new ArrayList<>(); try (BufferedReader reader = @@ -67,7 +67,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()) @@ -80,7 +80,7 @@ private Store toStore(CSVRecord record) { .build(); } - /// 주소에서 "시 구" 부분만 추출 + // 주소에서 "시 구" 부분만 추출 private String extractRegion(String address) { String[] parts = address.split(" "); if (parts.length < 2) { @@ -88,9 +88,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(); - } } From b12e09e9ef44db4f0fa895b593f9573e7ff83905 Mon Sep 17 00:00:00 2001 From: erika0915 Date: Thu, 18 Dec 2025 21:14:57 +0900 Subject: [PATCH 4/5] =?UTF-8?q?[SCRUM-426]=20feat:=20=EC=83=88=EB=A1=9C?= =?UTF-8?q?=EC=9A=B4=20csv=20=ED=8C=8C=EC=9D=BC=EC=97=90=20=EC=9E=88?= =?UTF-8?q?=EB=8A=94=20=EA=B0=80=EB=A7=B9=EC=A0=90=EC=9D=B4=20=EC=9D=B4?= =?UTF-8?q?=EB=AF=B8=20=EC=A1=B4=EC=9E=AC=ED=95=98=EB=8A=94=20=EA=B2=BD?= =?UTF-8?q?=EC=9A=B0=20db=EC=97=90=20=EB=82=A8=EA=B3=A0,=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=EB=90=9C=20=ED=95=84=EB=93=9C=EB=A7=8C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../storeupload/StoreJdbcRepository.java | 4 +-- .../storeupload/StoreJdbcRepositoryImpl.java | 36 +++++++++++-------- .../be/store/service/StoreUploadService.java | 11 +++--- 3 files changed, 30 insertions(+), 21 deletions(-) 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 1fafbfb..4f617dd 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 @@ -5,7 +5,7 @@ import com.kkinikong.be.store.domain.Store; public interface StoreJdbcRepository { - void saveAllByJdbcTemplate(List stores); + void upsertStores(List stores); - void deleteByRegion(String region); + void deleteMissingStores(String region, List currentStoreNames); } 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 b9dddbc..a429b19 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,6 +1,5 @@ package com.kkinikong.be.store.repository.storeupload; -import java.time.LocalDate; import java.util.List; import org.springframework.jdbc.core.JdbcTemplate; @@ -20,15 +19,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, @@ -41,21 +45,23 @@ 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()); }); } @Override @Transactional - public void deleteByRegion(String region) { - // 해당 지역의 모든 가맹점 삭제 - String sql = "DELETE FROM stores WHERE region = ?"; - jdbcTemplate.update(sql, region); + public void deleteMissingStores(String region, List currentStoreNames) { + if (currentStoreNames.isEmpty()) return; + + // 해당 지역 데이터 중 이번 CSV 파일에 없는 가맹점만 삭제 + String sql = "DELETE FROM stores WHERE region = :region AND name NOT IN (:names)"; + + var params = + new org.springframework.jdbc.core.namedparam.MapSqlParameterSource() + .addValue("region", region) + .addValue("names", currentStoreNames); + + 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 5278c95..ce3f03b 100644 --- a/src/main/java/com/kkinikong/be/store/service/StoreUploadService.java +++ b/src/main/java/com/kkinikong/be/store/service/StoreUploadService.java @@ -37,11 +37,14 @@ public StoreUploadResponse upload(MultipartFile file) { // 파일의 첫 번째 데이터에서 지역 정보 추출 String targetRegion = stores.get(0).getRegion(); - // db에서 해당 지역 데이터 전체 삭제 - storeJdbcRepository.deleteByRegion(targetRegion); + // 기존 데이터는 유지하며 정보 갱신, 신규 데이터는 추가 + storeJdbcRepository.upsertStores(stores); - // 최신 csv 데이터 전체 삽입 - storeJdbcRepository.saveAllByJdbcTemplate(stores); + // 기존 파일에 존재하는 가맹점 이름 리스트 추출 + List currentStoreNames = stores.stream().map(Store::getName).toList(); + + // 새로운 파일에 없는 가맹점 삭제 + storeJdbcRepository.deleteMissingStores(targetRegion, currentStoreNames); return new StoreUploadResponse(stores.size(), stores.size()); } From d7d137c74e620c0b5e56fcc4ca3d34e791c094fb Mon Sep 17 00:00:00 2001 From: erika0915 Date: Fri, 19 Dec 2025 15:09:53 +0900 Subject: [PATCH 5/5] =?UTF-8?q?[SCRUM-426]=20Feat:=20=EC=8B=9C=EA=B0=84?= =?UTF-8?q?=EC=9D=84=20=ED=99=9C=EC=9A=A9=ED=95=9C=20=EC=97=85=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/storeupload/StoreJdbcRepository.java | 3 ++- .../storeupload/StoreJdbcRepositoryImpl.java | 10 ++++------ .../kkinikong/be/store/service/StoreUploadService.java | 8 ++++---- 3 files changed, 10 insertions(+), 11 deletions(-) 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 4f617dd..4052e79 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,5 +1,6 @@ package com.kkinikong.be.store.repository.storeupload; +import java.time.LocalDateTime; import java.util.List; import com.kkinikong.be.store.domain.Store; @@ -7,5 +8,5 @@ public interface StoreJdbcRepository { void upsertStores(List stores); - void deleteMissingStores(String region, List currentStoreNames); + 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 a429b19..b01e77f 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,5 +1,6 @@ package com.kkinikong.be.store.repository.storeupload; +import java.time.LocalDateTime; import java.util.List; import org.springframework.jdbc.core.JdbcTemplate; @@ -51,16 +52,13 @@ public void upsertStores(List stores) { @Override @Transactional - public void deleteMissingStores(String region, List currentStoreNames) { - if (currentStoreNames.isEmpty()) return; - - // 해당 지역 데이터 중 이번 CSV 파일에 없는 가맹점만 삭제 - String sql = "DELETE FROM stores WHERE region = :region AND name NOT IN (:names)"; + public void deleteMissingStores(String region, LocalDateTime startTime) { + String sql = "DELETE FROM stores WHERE region = :region AND modified_date < :startTime"; var params = new org.springframework.jdbc.core.namedparam.MapSqlParameterSource() .addValue("region", region) - .addValue("names", currentStoreNames); + .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 ce3f03b..ae2fae0 100644 --- a/src/main/java/com/kkinikong/be/store/service/StoreUploadService.java +++ b/src/main/java/com/kkinikong/be/store/service/StoreUploadService.java @@ -5,6 +5,7 @@ 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; @@ -31,6 +32,8 @@ public class StoreUploadService { @Transactional public StoreUploadResponse upload(MultipartFile file) { + LocalDateTime startTime = LocalDateTime.now().minusSeconds(5); + // CSV 파일을 파싱해서 Store 리스트로 변환 List stores = parseCsv(file); @@ -40,11 +43,8 @@ public StoreUploadResponse upload(MultipartFile file) { // 기존 데이터는 유지하며 정보 갱신, 신규 데이터는 추가 storeJdbcRepository.upsertStores(stores); - // 기존 파일에 존재하는 가맹점 이름 리스트 추출 - List currentStoreNames = stores.stream().map(Store::getName).toList(); - // 새로운 파일에 없는 가맹점 삭제 - storeJdbcRepository.deleteMissingStores(targetRegion, currentStoreNames); + storeJdbcRepository.deleteMissingStores(targetRegion, startTime); return new StoreUploadResponse(stores.size(), stores.size()); }