Skip to content

Commit 7c29b07

Browse files
committed
feat: 구글 스프레스 시트 연동 테스트 케이스 작성 및 기능 추가
1 parent 69fec1f commit 7c29b07

File tree

7 files changed

+189
-26
lines changed

7 files changed

+189
-26
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,6 @@ out/
4141

4242
### dev ###
4343
application-dev.yml
44+
45+
### google ###
46+
src/main/resources/credentials/dasomGoogle.json

src/main/java/dmu/dasom/api/domain/applicant/entity/Applicant.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
1717

1818
import java.time.LocalDateTime;
19+
import java.util.List;
1920

2021
@AllArgsConstructor
2122
@Builder
@@ -82,6 +83,23 @@ public void updateStatus(final ApplicantStatus status) {
8283
this.status = status;
8384
}
8485

86+
public List<Object> toGoogleSheetRow(){
87+
return List.of(
88+
this.id,
89+
this.name,
90+
this.studentNo,
91+
this.contact,
92+
this.email,
93+
this.grade,
94+
this.reasonForApply,
95+
this.activityWish,
96+
this.isPrivacyPolicyAgreed,
97+
this.status.name(),
98+
this.createdAt.toString(),
99+
this.updatedAt.toString()
100+
);
101+
}
102+
85103
public ApplicantResponseDto toApplicantResponse() {
86104
return ApplicantResponseDto.builder()
87105
.id(this.id)

src/main/java/dmu/dasom/api/domain/applicant/service/ApplicantServiceImpl.java

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@
1111
import dmu.dasom.api.domain.common.exception.ErrorCode;
1212
import dmu.dasom.api.domain.email.enums.MailType;
1313
import dmu.dasom.api.domain.email.service.EmailService;
14+
import dmu.dasom.api.domain.google.service.GoogleApiService;
1415
import dmu.dasom.api.global.dto.PageResponse;
1516
import jakarta.mail.MessagingException;
1617
import lombok.RequiredArgsConstructor;
1718
import lombok.extern.slf4j.Slf4j;
19+
import org.springframework.beans.factory.annotation.Value;
1820
import org.springframework.data.domain.Page;
1921
import org.springframework.data.domain.PageRequest;
2022
import org.springframework.stereotype.Service;
@@ -34,6 +36,7 @@ public class ApplicantServiceImpl implements ApplicantService {
3436

3537
private final ApplicantRepository applicantRepository;
3638
private final EmailService emailService;
39+
private final GoogleApiService googleApiService;
3740

3841
// 지원자 저장
3942
@Override
@@ -46,13 +49,15 @@ public void apply(final ApplicantCreateRequestDto request) {
4649
if (!request.getIsOverwriteConfirmed())
4750
throw new CustomException(ErrorCode.DUPLICATED_STUDENT_NO);
4851

49-
// 기존 지원자 정보 갱신 수행
50-
applicant.get().overwrite(request);
52+
Applicant existingApplicant = applicant.get();
53+
existingApplicant.overwrite(request);
54+
55+
googleApiService.updateSheet(List.of(existingApplicant));
5156
return;
5257
}
5358

54-
// 새로운 지원자일 경우 저장
55-
applicantRepository.save(request.toEntity());
59+
Applicant savedApplicant = applicantRepository.save(request.toEntity());
60+
googleApiService.appendToSheet(List.of(savedApplicant));
5661
}
5762

5863
// 지원자 조회
@@ -77,7 +82,9 @@ public ApplicantDetailsResponseDto getApplicant(final Long id) {
7782
@Override
7883
public ApplicantDetailsResponseDto updateApplicantStatus(final Long id, final ApplicantStatusUpdateRequestDto request) {
7984
final Applicant applicant = findById(id);
85+
8086
applicant.updateStatus(request.getStatus());
87+
googleApiService.updateSheet(List.of(applicant));
8188

8289
return applicant.toApplicantDetailsResponse();
8390
}

src/main/java/dmu/dasom/api/domain/common/exception/ErrorCode.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@ public enum ErrorCode {
2424
MAIL_TYPE_NOT_VALID(400, "C015", "메일 타입이 올바르지 않습니다."),
2525
INVALID_DATETIME_FORMAT(400, "C016", "날짜 형식이 올바르지 않습니다."),
2626
INVALID_TIME_FORMAT(400, "C017", "시간 형식이 올바르지 않습니다."),
27-
INVALID_INQUIRY_PERIOD(400, "C018", "조회 기간이 아닙니다.")
27+
INVALID_INQUIRY_PERIOD(400, "C018", "조회 기간이 아닙니다."),
28+
SHEET_WRITE_FAIL(400, "C019", "시트에 데이터를 쓰는데 실패하였습니다."),
29+
SHEET_READ_FAIL(400, "C200", "시트에 데이터를 쓰는데 실패하였습니다."),
2830
;
2931

3032
private final int status;

src/main/java/dmu/dasom/api/domain/google/service/GoogleApiService.java

Lines changed: 121 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@
55
import com.google.api.client.json.jackson2.JacksonFactory;
66

77
import com.google.api.services.sheets.v4.Sheets;
8-
import com.google.api.services.sheets.v4.model.UpdateValuesResponse;
8+
import com.google.api.services.sheets.v4.model.BatchUpdateValuesRequest;
9+
import com.google.api.services.sheets.v4.model.BatchUpdateValuesResponse;
910
import com.google.api.services.sheets.v4.model.ValueRange;
10-
import com.google.auth.Credentials;
1111
import com.google.auth.http.HttpCredentialsAdapter;
1212
import com.google.auth.oauth2.GoogleCredentials;
13+
import dmu.dasom.api.domain.applicant.entity.Applicant;
1314
import dmu.dasom.api.domain.common.exception.CustomException;
1415
import dmu.dasom.api.domain.common.exception.ErrorCode;
1516
import lombok.RequiredArgsConstructor;
@@ -19,10 +20,9 @@
1920
import org.springframework.core.io.ClassPathResource;
2021
import org.springframework.stereotype.Service;
2122

22-
import java.io.ByteArrayInputStream;
2323
import java.io.IOException;
24-
import java.nio.charset.StandardCharsets;
2524
import java.security.GeneralSecurityException;
25+
import java.util.ArrayList;
2626
import java.util.Collections;
2727
import java.util.List;
2828

@@ -33,19 +33,23 @@ public class GoogleApiService {
3333
private static final Logger logger = LoggerFactory.getLogger(GoogleApiService.class);
3434
private static final String APPLICATION_NAME = "Recruit Form";
3535
private static final JsonFactory JSON_FACTORY = JacksonFactory.getDefaultInstance();
36-
@Value("${google.credentials.json}")
37-
private String credentialsJson;
36+
@Value("${google.credentials.file.path}")
37+
private String credentialsFilePath;
38+
@Value("${google.spreadsheet.id}")
39+
private String spreadSheetId;
3840
private Sheets sheetsService;
3941

4042
// Google Sheets API 서비스 객체를 생성하는 메소드
4143
private Sheets getSheetsService() throws IOException, GeneralSecurityException{
4244
if(sheetsService == null){
43-
ByteArrayInputStream credentialsStream = new ByteArrayInputStream(credentialsJson.getBytes(StandardCharsets.UTF_8));
45+
ClassPathResource resource = new ClassPathResource(credentialsFilePath);
4446
GoogleCredentials credentials = GoogleCredentials
45-
.fromStream(credentialsStream)
47+
.fromStream(resource.getInputStream())
4648
.createScoped(Collections.singletonList("https://www.googleapis.com/auth/spreadsheets"));
4749

48-
sheetsService = new Sheets.Builder(GoogleNetHttpTransport.newTrustedTransport(), JSON_FACTORY, new HttpCredentialsAdapter(credentials))
50+
sheetsService = new Sheets.Builder(GoogleNetHttpTransport.newTrustedTransport(),
51+
JSON_FACTORY,
52+
new HttpCredentialsAdapter(credentials))
4953
.setApplicationName(APPLICATION_NAME)
5054
.build();
5155
}
@@ -56,17 +60,118 @@ public void writeToSheet(String spreadsheetId, String range, List<List<Object>>
5660
try {
5761
Sheets service = getSheetsService();
5862
ValueRange body = new ValueRange().setValues(values);
59-
UpdateValuesResponse result = service.spreadsheets().values()
63+
service.spreadsheets().values()
6064
.update(spreadsheetId, range, body)
6165
.setValueInputOption("USER_ENTERED")
6266
.execute();
63-
logger.info("Updated rows: {}", result.getUpdatedRows());
64-
} catch (IOException e) {
65-
logger.error("Failed to write data to the spreadsheet", e);
67+
} catch (IOException | GeneralSecurityException e) {
68+
logger.error("구글 시트에 데이터를 쓰는 데 실패했습니다.", e);
6669
throw new CustomException(ErrorCode.WRITE_FAIL);
67-
} catch (GeneralSecurityException e) {
68-
logger.error("Failed to write data to the spreadsheet", e);
69-
throw new CustomException(ErrorCode.INTERNAL_SERVER_ERROR);
70+
}
71+
}
72+
73+
public void appendToSheet(List<Applicant> applicants) {
74+
processSheetsUpdate(applicants, true);
75+
}
76+
77+
public void updateSheet(List<Applicant> applicants) {
78+
processSheetsUpdate(applicants, false);
79+
}
80+
81+
private int findRowIndexByStudentNo(String spreadSheetId, String sheetName, String studentNo){
82+
try {
83+
List<List<Object>> rows = readSheet(spreadSheetId, sheetName + "!A:L"); // A열부터 L열까지 읽기
84+
85+
for (int i = 0; i < rows.size(); i++){
86+
List<Object> row = rows.get(i);
87+
if(!row.isEmpty() && row.get(2).equals(studentNo)){
88+
return i + 1;
89+
}
90+
}
91+
} catch (Exception e) {
92+
logger.error("구글시트에서 행 찾기 실패", e);
93+
}
94+
return -1;
95+
}
96+
97+
public List<List<Object>> readSheet(String spreadsheetId, String range) {
98+
try {
99+
Sheets service = getSheetsService();
100+
ValueRange response = service.spreadsheets().values()
101+
.get(spreadsheetId, range)
102+
.execute();
103+
104+
return response.getValues();
105+
} catch (IOException | GeneralSecurityException e) {
106+
logger.error("시트에서 데이터를 읽어오는데 실패했습니다.", e);
107+
throw new CustomException(ErrorCode.SHEET_READ_FAIL);
108+
}
109+
}
110+
111+
public int getLastRow(String spreadsheetId, String sheetName) {
112+
try {
113+
List<List<Object>> rows = readSheet(spreadsheetId, sheetName + "!A:L"); // A~L 열까지 읽기
114+
return rows == null ? 0 : rows.size(); // 데이터가 없으면 0 반환
115+
} catch (Exception e) {
116+
logger.error("Failed to retrieve last row from Google Sheet", e);
117+
throw new CustomException(ErrorCode.SHEET_READ_FAIL);
118+
}
119+
}
120+
121+
public void batchUpdateSheet(String spreadsheetId, List<ValueRange> valueRanges) {
122+
try {
123+
Sheets service = getSheetsService();
124+
125+
// BatchUpdate 요청 생성
126+
BatchUpdateValuesRequest batchUpdateRequest = new BatchUpdateValuesRequest()
127+
.setValueInputOption("USER_ENTERED") // 사용자 입력 형식으로 값 설정
128+
.setData(valueRanges); // 여러 ValueRange 추가
129+
130+
// BatchUpdate 실행
131+
BatchUpdateValuesResponse response = service.spreadsheets().values()
132+
.batchUpdate(spreadsheetId, batchUpdateRequest)
133+
.execute();
134+
135+
logger.info("Batch update completed. Total updated rows: {}", response.getTotalUpdatedRows());
136+
} catch (IOException | GeneralSecurityException e) {
137+
logger.error("Batch update failed", e);
138+
throw new CustomException(ErrorCode.SHEET_WRITE_FAIL);
139+
}
140+
}
141+
142+
143+
private ValueRange createValueRange(String range, List<List<Object>> values) {
144+
return new ValueRange()
145+
.setRange(range)
146+
.setMajorDimension("ROWS") // 행 단위로 데이터 설정
147+
.setValues(values);
148+
}
149+
150+
public void processSheetsUpdate(List<Applicant> applicants, boolean isAppend) {
151+
try {
152+
List<ValueRange> valueRanges = new ArrayList<>();
153+
int lastRow = isAppend ? getLastRow(spreadSheetId, "Sheet1") : -1;
154+
155+
for (Applicant applicant : applicants) {
156+
String range;
157+
if (isAppend) {
158+
range = "Sheet1!A" + (lastRow + 1);
159+
lastRow++;
160+
} else {
161+
int rowIndex = findRowIndexByStudentNo(spreadSheetId, "Sheet1", applicant.getStudentNo());
162+
if (rowIndex == -1) {
163+
logger.warn("구글시트에서 사용자를 찾을 수 없습니다. : {}", applicant.getStudentNo());
164+
continue;
165+
}
166+
range = "Sheet1!A" + rowIndex + ":L" + rowIndex;
167+
}
168+
valueRanges.add(createValueRange(range, List.of(applicant.toGoogleSheetRow())));
169+
}
170+
171+
batchUpdateSheet(spreadSheetId, valueRanges);
172+
} catch (Exception e) {
173+
logger.error("구글시트 업데이트에 실패했습니다.", e);
174+
throw new CustomException(ErrorCode.SHEET_WRITE_FAIL);
70175
}
71176
}
72177

src/main/resources/application-credentials.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ jwt:
3232
refresh-token-expiration: ${JWT_REFRESH_TOKEN_EXPIRATION}
3333
google:
3434
credentials:
35-
json: ${GOOGLE_CREDENTIALS_JSON}
35+
file:
36+
path: ${GOOGLE_CREDENTIALS_PATH}
3637
spreadsheet:
3738
id: ${GOOGLE_SPREADSHEET_ID}

src/test/java/dmu/dasom/api/domain/applicant/ApplicantServiceTest.java

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import dmu.dasom.api.domain.common.exception.ErrorCode;
1212
import dmu.dasom.api.domain.email.enums.MailType;
1313
import dmu.dasom.api.domain.email.service.EmailService;
14+
import dmu.dasom.api.domain.google.service.GoogleApiService;
1415
import dmu.dasom.api.global.dto.PageResponse;
1516
import jakarta.mail.MessagingException;
1617
import org.junit.jupiter.api.DisplayName;
@@ -23,6 +24,7 @@
2324
import org.springframework.data.domain.PageImpl;
2425
import org.springframework.data.domain.PageRequest;
2526

27+
import java.time.LocalDateTime;
2628
import java.util.Collections;
2729
import java.util.List;
2830
import java.util.Optional;
@@ -42,19 +44,43 @@ class ApplicantServiceTest {
4244
@InjectMocks
4345
private ApplicantServiceImpl applicantService;
4446

47+
@Mock
48+
private GoogleApiService googleApiService;
49+
4550
@Test
4651
@DisplayName("지원자 저장 - 성공")
4752
void apply_success() {
4853
// given
4954
ApplicantCreateRequestDto request = mock(ApplicantCreateRequestDto.class);
5055
when(request.getStudentNo()).thenReturn("20210000");
56+
57+
Applicant mockApplicant = Applicant.builder()
58+
.name("홍길동")
59+
.studentNo("20240001")
60+
.contact("010-1234-5678")
61+
62+
.grade(2)
63+
.reasonForApply("팀 활동 경험을 쌓고 싶습니다.")
64+
.activityWish("프로그래밍 스터디 참여")
65+
.isPrivacyPolicyAgreed(true)
66+
.status(ApplicantStatus.PENDING)
67+
.createdAt(LocalDateTime.now())
68+
.updatedAt(LocalDateTime.now())
69+
.build();
70+
71+
when(request.toEntity()).thenReturn(mockApplicant);
5172
when(applicantRepository.findByStudentNo("20210000")).thenReturn(Optional.empty());
73+
when(applicantRepository.save(any(Applicant.class))).thenReturn(mockApplicant);
74+
75+
// GoogleApiService의 appendToSheet() 동작을 가짜로 설정
76+
doNothing().when(googleApiService).appendToSheet(anyList());
5277

5378
// when
5479
applicantService.apply(request);
5580

5681
// then
57-
verify(applicantRepository).save(request.toEntity());
82+
verify(applicantRepository).save(mockApplicant);
83+
verify(googleApiService).appendToSheet(List.of(mockApplicant));
5884
}
5985

6086
@Test
@@ -82,16 +108,17 @@ void apply_overwrite() {
82108
// given
83109
ApplicantCreateRequestDto request = mock(ApplicantCreateRequestDto.class);
84110
when(request.getStudentNo()).thenReturn("20210000");
85-
Applicant applicant = mock(Applicant.class);
86-
when(applicantRepository.findByStudentNo("20210000")).thenReturn(Optional.of(applicant));
111+
Applicant existingApplicant = mock(Applicant.class); // 기존 Applicant 객체 모킹
112+
when(applicantRepository.findByStudentNo("20210000")).thenReturn(Optional.of(existingApplicant));
87113
when(request.getIsOverwriteConfirmed()).thenReturn(true);
88114

89115
// when
90116
applicantService.apply(request);
91117

92118
// then
93119
verify(applicantRepository).findByStudentNo("20210000");
94-
verify(applicant).overwrite(request);
120+
verify(existingApplicant).overwrite(request);
121+
verify(googleApiService).updateSheet(List.of(existingApplicant));
95122
}
96123

97124
@Test

0 commit comments

Comments
 (0)