From 736ccf1a8630dfba00be6f2e05297f6baaba1153 Mon Sep 17 00:00:00 2001 From: JanooGwan Date: Mon, 23 Mar 2026 21:28:07 +0900 Subject: [PATCH 1/4] =?UTF-8?q?fix:=20sheet=20migrate=20=ED=8F=B4=EB=8D=94?= =?UTF-8?q?=20=EC=A7=80=EC=A0=95=20=EB=B2=84=EA=B7=B8=20=EC=88=98=EC=A0=95?= =?UTF-8?q?=20=EB=B0=8F=20import=20=EC=A4=91=EB=B3=B5=20=EB=B0=A9=EC=96=B4?= =?UTF-8?q?=20=EA=B0=95=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../club/repository/ClubMemberRepository.java | 12 +++++++++ .../club/service/SheetImportService.java | 9 +++++++ .../club/service/SheetMigrationService.java | 26 ++++++++++++++----- 3 files changed, 40 insertions(+), 7 deletions(-) diff --git a/src/main/java/gg/agit/konect/domain/club/repository/ClubMemberRepository.java b/src/main/java/gg/agit/konect/domain/club/repository/ClubMemberRepository.java index 6a08d2a91..4ee028226 100644 --- a/src/main/java/gg/agit/konect/domain/club/repository/ClubMemberRepository.java +++ b/src/main/java/gg/agit/konect/domain/club/repository/ClubMemberRepository.java @@ -193,4 +193,16 @@ long countByClubIdAndPosition( @Query("SELECT COUNT(cm) FROM ClubMember cm") long countAll(); + + @Query(""" + SELECT COUNT(cm) > 0 + FROM ClubMember cm + WHERE cm.club.id = :clubId + AND cm.user.studentNumber = :studentNumber + AND cm.user.deletedAt IS NULL + """) + boolean existsByClubIdAndUserStudentNumber( + @Param("clubId") Integer clubId, + @Param("studentNumber") String studentNumber + ); } diff --git a/src/main/java/gg/agit/konect/domain/club/service/SheetImportService.java b/src/main/java/gg/agit/konect/domain/club/service/SheetImportService.java index 108f0a31c..19a836d14 100644 --- a/src/main/java/gg/agit/konect/domain/club/service/SheetImportService.java +++ b/src/main/java/gg/agit/konect/domain/club/service/SheetImportService.java @@ -13,6 +13,7 @@ import gg.agit.konect.domain.club.model.Club; import gg.agit.konect.domain.club.model.ClubPreMember; import gg.agit.konect.domain.club.model.SheetColumnMapping; +import gg.agit.konect.domain.club.repository.ClubMemberRepository; import gg.agit.konect.domain.club.repository.ClubPreMemberRepository; import gg.agit.konect.domain.club.repository.ClubRepository; import lombok.RequiredArgsConstructor; @@ -27,6 +28,7 @@ public class SheetImportService { private final SheetHeaderMapper sheetHeaderMapper; private final ClubRepository clubRepository; private final ClubPreMemberRepository clubPreMemberRepository; + private final ClubMemberRepository clubMemberRepository; private final ClubPermissionValidator clubPermissionValidator; @Transactional @@ -61,6 +63,13 @@ public int importPreMembersFromSheet( continue; } + // 이미 실제 동아리 부원인 경우 skip + if (clubMemberRepository.existsByClubIdAndUserStudentNumber( + clubId, studentNumber + )) { + continue; + } + String positionStr = getCell(row, mapping, SheetColumnMapping.POSITION); ClubPosition position = resolvePosition(positionStr); diff --git a/src/main/java/gg/agit/konect/domain/club/service/SheetMigrationService.java b/src/main/java/gg/agit/konect/domain/club/service/SheetMigrationService.java index 2006b683c..bd6fbcc9c 100644 --- a/src/main/java/gg/agit/konect/domain/club/service/SheetMigrationService.java +++ b/src/main/java/gg/agit/konect/domain/club/service/SheetMigrationService.java @@ -198,16 +198,28 @@ private String copyTemplate(Drive driveService, String templateId, String clubNa String title = NEW_SHEET_TITLE_PREFIX + clubName; File copyMetadata = new File().setName(title); - if (targetFolderId != null) { - copyMetadata.setParents(Collections.singletonList(targetFolderId)); - } - + // Drive API v3에서 files().copy() 바디의 parents 필드는 무시됨. + // 복사 후 files().update()로 addParents/removeParents를 명시적으로 호출해야 폴더 이동이 적용됨. File copied = driveService.files().copy(templateId, copyMetadata) - .setFields("id") + .setFields("id, parents") .execute(); - log.info("Template copied by user. newId={}, folderId={}", copied.getId(), targetFolderId); - return copied.getId(); + String newFileId = copied.getId(); + + if (targetFolderId != null) { + List currentParents = copied.getParents(); + String removeParents = (currentParents != null && !currentParents.isEmpty()) + ? String.join(",", currentParents) + : ""; + driveService.files().update(newFileId, new File()) + .setAddParents(targetFolderId) + .setRemoveParents(removeParents) + .setFields("id, parents") + .execute(); + } + + log.info("Template copied by user. newId={}, folderId={}", newFileId, targetFolderId); + return newFileId; } catch (IOException e) { log.error("Failed to copy template. cause={}", e.getMessage(), e); From 90c258b864b7a5ea1d589f00561edf65b57ab00d Mon Sep 17 00:00:00 2001 From: JanooGwan Date: Mon, 23 Mar 2026 21:51:58 +0900 Subject: [PATCH 2/4] =?UTF-8?q?refactor:=20SheetImportService=20N+1=20?= =?UTF-8?q?=EC=BF=BC=EB=A6=AC=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../club/repository/ClubMemberRepository.java | 8 ++------ .../repository/ClubPreMemberRepository.java | 8 ++++++++ .../domain/club/service/SheetImportService.java | 17 ++++++++++------- 3 files changed, 20 insertions(+), 13 deletions(-) diff --git a/src/main/java/gg/agit/konect/domain/club/repository/ClubMemberRepository.java b/src/main/java/gg/agit/konect/domain/club/repository/ClubMemberRepository.java index 4ee028226..44981af49 100644 --- a/src/main/java/gg/agit/konect/domain/club/repository/ClubMemberRepository.java +++ b/src/main/java/gg/agit/konect/domain/club/repository/ClubMemberRepository.java @@ -195,14 +195,10 @@ long countByClubIdAndPosition( long countAll(); @Query(""" - SELECT COUNT(cm) > 0 + SELECT cm.user.studentNumber FROM ClubMember cm WHERE cm.club.id = :clubId - AND cm.user.studentNumber = :studentNumber AND cm.user.deletedAt IS NULL """) - boolean existsByClubIdAndUserStudentNumber( - @Param("clubId") Integer clubId, - @Param("studentNumber") String studentNumber - ); + Set findStudentNumbersByClubId(@Param("clubId") Integer clubId); } diff --git a/src/main/java/gg/agit/konect/domain/club/repository/ClubPreMemberRepository.java b/src/main/java/gg/agit/konect/domain/club/repository/ClubPreMemberRepository.java index b8ace676c..ee909d764 100644 --- a/src/main/java/gg/agit/konect/domain/club/repository/ClubPreMemberRepository.java +++ b/src/main/java/gg/agit/konect/domain/club/repository/ClubPreMemberRepository.java @@ -2,6 +2,7 @@ import java.util.List; import java.util.Optional; +import java.util.Set; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.Repository; @@ -63,6 +64,13 @@ List findAllByUniversityIdAndStudentNumberAndName( boolean existsByClubIdAndStudentNumberAndName(Integer clubId, String studentNumber, String name); + @Query(""" + SELECT CONCAT(cpm.studentNumber, '|', cpm.name) + FROM ClubPreMember cpm + WHERE cpm.club.id = :clubId + """) + Set findStudentNumberNameKeysByClubId(@Param("clubId") Integer clubId); + void deleteByClubIdAndStudentNumber(Integer clubId, String studentNumber); void delete(ClubPreMember preMember); diff --git a/src/main/java/gg/agit/konect/domain/club/service/SheetImportService.java b/src/main/java/gg/agit/konect/domain/club/service/SheetImportService.java index 19a836d14..4971f7ebb 100644 --- a/src/main/java/gg/agit/konect/domain/club/service/SheetImportService.java +++ b/src/main/java/gg/agit/konect/domain/club/service/SheetImportService.java @@ -2,6 +2,7 @@ import java.io.IOException; import java.util.List; +import java.util.Set; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -47,6 +48,13 @@ public int importPreMembersFromSheet( SheetColumnMapping mapping = analysis.memberListMapping(); List> rows = readDataRows(spreadsheetId, mapping); + + // N+1 방지: 루프 전에 기존 부원/사전 회원 학번 Set을 한 번만 조회 + Set existingMemberStudentNumbers = + clubMemberRepository.findStudentNumbersByClubId(clubId); + Set existingPreMemberKeys = + clubPreMemberRepository.findStudentNumberNameKeysByClubId(clubId); + int imported = 0; for (List row : rows) { @@ -57,16 +65,11 @@ public int importPreMembersFromSheet( continue; } - if (clubPreMemberRepository.existsByClubIdAndStudentNumberAndName( - clubId, studentNumber, name - )) { + if (existingPreMemberKeys.contains(studentNumber + "|" + name)) { continue; } - // 이미 실제 동아리 부원인 경우 skip - if (clubMemberRepository.existsByClubIdAndUserStudentNumber( - clubId, studentNumber - )) { + if (existingMemberStudentNumbers.contains(studentNumber)) { continue; } From 68207141bd89231f5c95892735119c7dfc1c0906 Mon Sep 17 00:00:00 2001 From: JanooGwan Date: Mon, 23 Mar 2026 22:04:31 +0900 Subject: [PATCH 3/4] =?UTF-8?q?refactor:=20PreMemberKey=20projection=20?= =?UTF-8?q?=EB=8F=84=EC=9E=85=20=EB=B0=8F=20=EB=A3=A8=ED=94=84=20=EB=82=B4?= =?UTF-8?q?=20=EC=A4=91=EB=B3=B5=20=EB=B0=A9=EC=96=B4=20=EA=B0=95=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/ClubPreMemberRepository.java | 10 +++++++--- .../club/service/SheetImportService.java | 18 +++++++++++++++--- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/src/main/java/gg/agit/konect/domain/club/repository/ClubPreMemberRepository.java b/src/main/java/gg/agit/konect/domain/club/repository/ClubPreMemberRepository.java index ee909d764..5b92f5d5f 100644 --- a/src/main/java/gg/agit/konect/domain/club/repository/ClubPreMemberRepository.java +++ b/src/main/java/gg/agit/konect/domain/club/repository/ClubPreMemberRepository.java @@ -2,7 +2,6 @@ import java.util.List; import java.util.Optional; -import java.util.Set; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.Repository; @@ -65,11 +64,16 @@ List findAllByUniversityIdAndStudentNumberAndName( boolean existsByClubIdAndStudentNumberAndName(Integer clubId, String studentNumber, String name); @Query(""" - SELECT CONCAT(cpm.studentNumber, '|', cpm.name) + SELECT cpm.studentNumber as studentNumber, cpm.name as name FROM ClubPreMember cpm WHERE cpm.club.id = :clubId """) - Set findStudentNumberNameKeysByClubId(@Param("clubId") Integer clubId); + List findStudentNumberAndNameByClubId(@Param("clubId") Integer clubId); + + interface PreMemberKey { + String getStudentNumber(); + String getName(); + } void deleteByClubIdAndStudentNumber(Integer clubId, String studentNumber); diff --git a/src/main/java/gg/agit/konect/domain/club/service/SheetImportService.java b/src/main/java/gg/agit/konect/domain/club/service/SheetImportService.java index 4971f7ebb..9c57bf149 100644 --- a/src/main/java/gg/agit/konect/domain/club/service/SheetImportService.java +++ b/src/main/java/gg/agit/konect/domain/club/service/SheetImportService.java @@ -1,6 +1,7 @@ package gg.agit.konect.domain.club.service; import java.io.IOException; +import java.util.HashSet; import java.util.List; import java.util.Set; @@ -52,8 +53,7 @@ public int importPreMembersFromSheet( // N+1 방지: 루프 전에 기존 부원/사전 회원 학번 Set을 한 번만 조회 Set existingMemberStudentNumbers = clubMemberRepository.findStudentNumbersByClubId(clubId); - Set existingPreMemberKeys = - clubPreMemberRepository.findStudentNumberNameKeysByClubId(clubId); + Set existingPreMemberKeys = buildPreMemberKeySet(clubId); int imported = 0; @@ -65,7 +65,7 @@ public int importPreMembersFromSheet( continue; } - if (existingPreMemberKeys.contains(studentNumber + "|" + name)) { + if (existingPreMemberKeys.contains(preMemberKey(studentNumber, name))) { continue; } @@ -84,6 +84,7 @@ public int importPreMembersFromSheet( .build(); clubPreMemberRepository.save(preMember); + existingPreMemberKeys.add(preMemberKey(studentNumber, name)); imported++; } @@ -132,4 +133,15 @@ private ClubPosition resolvePosition(String positionStr) { } return ClubPosition.MEMBER; } + + private Set buildPreMemberKeySet(Integer clubId) { + Set keys = new HashSet<>(); + clubPreMemberRepository.findStudentNumberAndNameByClubId(clubId) + .forEach(k -> keys.add(preMemberKey(k.getStudentNumber(), k.getName()))); + return keys; + } + + private String preMemberKey(String studentNumber, String name) { + return studentNumber + "\u0000" + name; + } } From 532918b685a7eb4d4fd666ca84224992d9495842 Mon Sep 17 00:00:00 2001 From: JanooGwan Date: Mon, 23 Mar 2026 22:05:56 +0900 Subject: [PATCH 4/4] =?UTF-8?q?fix:=20checkstyle=20EmptyLineSeparator=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../konect/domain/club/repository/ClubPreMemberRepository.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/gg/agit/konect/domain/club/repository/ClubPreMemberRepository.java b/src/main/java/gg/agit/konect/domain/club/repository/ClubPreMemberRepository.java index 5b92f5d5f..7e9dbc719 100644 --- a/src/main/java/gg/agit/konect/domain/club/repository/ClubPreMemberRepository.java +++ b/src/main/java/gg/agit/konect/domain/club/repository/ClubPreMemberRepository.java @@ -72,6 +72,7 @@ List findAllByUniversityIdAndStudentNumberAndName( interface PreMemberKey { String getStudentNumber(); + String getName(); }