Skip to content

Commit 5368fe0

Browse files
authored
Merge branch 'dev' into refactor/#47
2 parents 6a89558 + 4a5c6c2 commit 5368fe0

File tree

9 files changed

+332
-28
lines changed

9 files changed

+332
-28
lines changed

.gitignore

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

4242
### dev ###
4343
application-dev.yml
44+
45+
### google ###
46+
src/main/resources/credentials/dasomGoogle.json
47+
48+
### google ###
49+
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/repository/ApplicantRepository.java

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,12 @@
88
import org.springframework.data.jpa.repository.Query;
99

1010
import java.util.List;
11-
1211
import java.util.Optional;
1312

1413
public interface ApplicantRepository extends JpaRepository<Applicant, Long> {
1514

1615
@Query("SELECT a FROM Applicant a ORDER BY a.id DESC")
1716
Page<Applicant> findAllWithPageRequest(final Pageable pageable);
18-
1917
// 상태별 지원자 조회
2018
List<Applicant> findByStatus(ApplicantStatus status);
2119
List<Applicant> findByStatusIn(List<ApplicantStatus> statuses);

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

Lines changed: 25 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,10 @@ public class ApplicantServiceImpl implements ApplicantService {
3436

3537
private final ApplicantRepository applicantRepository;
3638
private final EmailService emailService;
39+
private final GoogleApiService googleApiService;
40+
41+
@Value("${google.spreadsheet.id}")
42+
private String spreadSheetId;
3743

3844
// 지원자 저장
3945
@Override
@@ -46,13 +52,15 @@ public void apply(final ApplicantCreateRequestDto request) {
4652
if (!request.getIsOverwriteConfirmed())
4753
throw new CustomException(ErrorCode.DUPLICATED_STUDENT_NO);
4854

49-
// 기존 지원자 정보 갱신 수행
50-
applicant.get().overwrite(request);
55+
Applicant existingApplicant = applicant.get();
56+
existingApplicant.overwrite(request);
57+
58+
googleApiService.updateSheet(List.of(existingApplicant));
5159
return;
5260
}
5361

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

5866
// 지원자 조회
@@ -77,8 +85,21 @@ public ApplicantDetailsResponseDto getApplicant(final Long id) {
7785
@Override
7886
public ApplicantDetailsResponseDto updateApplicantStatus(final Long id, final ApplicantStatusUpdateRequestDto request) {
7987
final Applicant applicant = findById(id);
88+
// 지원자 상태 변경
8089
applicant.updateStatus(request.getStatus());
8190

91+
// Google Sheets에서 학번(Student No)을 기준으로 사용자 존재 여부 확인
92+
int rowIndex = googleApiService.findRowIndexByStudentNo(spreadSheetId, "Sheet1", applicant.getStudentNo());
93+
if (rowIndex == -1) {
94+
// Google Sheets에 사용자 추가
95+
googleApiService.appendToSheet(List.of(applicant));
96+
log.info("지원자가 Google Sheets에 없어서 새로 추가되었습니다: {}", applicant.getStudentNo());
97+
} else {
98+
// Google Sheets에서 사용자 상태 업데이트
99+
googleApiService.updateSheet(List.of(applicant));
100+
log.info("Google Sheets에서 지원자 상태가 업데이트되었습니다: {}", applicant.getStudentNo());
101+
}
102+
82103
return applicant.toApplicantDetailsResponse();
83104
}
84105

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
@@ -25,7 +25,9 @@ public enum ErrorCode {
2525
INVALID_DATETIME_FORMAT(400, "C016", "날짜 형식이 올바르지 않습니다."),
2626
INVALID_TIME_FORMAT(400, "C017", "시간 형식이 올바르지 않습니다."),
2727
INVALID_INQUIRY_PERIOD(400, "C018", "조회 기간이 아닙니다."),
28-
FILE_ENCODE_FAIL(400, "C028", "파일 인코딩에 실패하였습니다."),
28+
SHEET_WRITE_FAIL(400, "C019", "시트에 데이터를 쓰는데 실패하였습니다."),
29+
SHEET_READ_FAIL(400, "C200", "시트에 데이터를 쓰는데 실패하였습니다."),
30+
FILE_ENCODE_FAIL(400, "C028", "파일 인코딩에 실패하였습니다.")
2931
;
3032

3133
private final int status;

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

Lines changed: 123 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,18 +60,121 @@ 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);
7070
}
7171
}
7272

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+
public 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)){ // 학번(Student No)이 3번째 열(A=0 기준)
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);
175+
}
176+
}
177+
178+
179+
73180
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package dmu.dasom.api.global.healthcheck;
2+
3+
import io.swagger.v3.oas.annotations.tags.Tag;
4+
import org.springframework.http.ResponseEntity;
5+
import org.springframework.web.bind.annotation.GetMapping;
6+
import org.springframework.web.bind.annotation.RestController;
7+
8+
@RestController
9+
@Tag(name = "health-check", description = "서버 상태 체크")
10+
public class HealthCheck {
11+
@GetMapping("/api/health-check")
12+
public ResponseEntity<Void> healthCheck() {
13+
return ResponseEntity.ok().build();
14+
}
15+
}

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}

0 commit comments

Comments
 (0)