Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,4 @@ out/
application-dev.yml

### google ###
src/main/resources/credentials/dasomGoogle.json

### google ###
src/main/resources/credentials/dasomGoogle.json
src/main/resources/credentials/dasomGoogleSheet.json
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ public List<Object> toGoogleSheetRow(){
this.email,
this.grade,
this.reasonForApply,
this.activityWish,
this.activityWish != null ? this.activityWish : "없음", // null일 경우 "없음" 반환
this.isPrivacyPolicyAgreed,
this.status.name(),
this.createdAt.toString(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import dmu.dasom.api.domain.applicant.entity.Applicant;
import dmu.dasom.api.domain.applicant.enums.ApplicantStatus;
import io.lettuce.core.dynamic.annotation.Param;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
Expand All @@ -20,4 +21,8 @@ public interface ApplicantRepository extends JpaRepository<Applicant, Long> {

Optional<Applicant> findByStudentNo(final String studentNo);

@Query("SELECT a FROM Applicant a WHERE a.studentNo = :studentNo AND a.contact LIKE %:contactLastDigits")
Optional<Applicant> findByStudentNoAndContactEndsWith(@Param("studentNo") String studentNo,
@Param("contactLastDigits") String contactLastDigits);

}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,14 @@ public enum ErrorCode {
INVALID_TIME_FORMAT(400, "C017", "시간 형식이 올바르지 않습니다."),
INVALID_INQUIRY_PERIOD(400, "C018", "조회 기간이 아닙니다."),
SHEET_WRITE_FAIL(400, "C019", "시트에 데이터를 쓰는데 실패하였습니다."),
SHEET_READ_FAIL(400, "C200", "시트에 데이터를 쓰는데 실패하였습니다."),
SHEET_READ_FAIL(400, "C200", "시트에 데이터를 읽는데 실패하였습니다."),
SLOT_NOT_FOUND(400, "C021", "슬롯을 찾을 수 없습니다."),
APPLICANT_NOT_FOUND(400, "C022", "지원자를 찾을 수 없습니다."),
ALREADY_RESERVED(400, "C023", "이미 예약된 지원자입니다."),
RESERVED_SLOT_CANNOT_BE_DELETED(400, "C024", "예약된 슬롯은 삭제할 수 없습니다."),
SLOT_FULL(400, "C025", "해당 슬롯이 가득 찼습니다."),
RESERVATION_NOT_FOUND(400, "C026", "예약을 찾을 수 없습니다."),
SLOT_NOT_ACTIVE(400, "C027", "해당 슬롯이 비활성화 되었습니다."),
FILE_ENCODE_FAIL(400, "C028", "파일 인코딩에 실패하였습니다.")
;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,44 +14,52 @@
import dmu.dasom.api.domain.common.exception.CustomException;
import dmu.dasom.api.domain.common.exception.ErrorCode;
import lombok.RequiredArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Service;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collections;
import java.util.List;

@RequiredArgsConstructor
@Service
@Slf4j
public class GoogleApiService {

private static final Logger logger = LoggerFactory.getLogger(GoogleApiService.class);
private static final String APPLICATION_NAME = "Recruit Form";
private static final JsonFactory JSON_FACTORY = JacksonFactory.getDefaultInstance();
@Value("${google.credentials.file.path}")
private String credentialsFilePath;
@Value("${google.credentials.json}")
private String credentialsJson;

@Value("${google.spreadsheet.id}")
private String spreadSheetId;

private static final String APPLICATION_NAME = "Recruit Form";
private static final JsonFactory JSON_FACTORY = JacksonFactory.getDefaultInstance();

private Sheets sheetsService;

// Google Sheets API 서비스 객체를 생성하는 메소드
private Sheets getSheetsService() throws IOException, GeneralSecurityException{
if(sheetsService == null){
ClassPathResource resource = new ClassPathResource(credentialsFilePath);
private Sheets getSheetsService() throws IOException, GeneralSecurityException {
if (sheetsService == null) {
ByteArrayInputStream decodedStream = new ByteArrayInputStream(Base64.getDecoder()
.decode(credentialsJson));

GoogleCredentials credentials = GoogleCredentials
.fromStream(resource.getInputStream())
.createScoped(Collections.singletonList("https://www.googleapis.com/auth/spreadsheets"));

sheetsService = new Sheets.Builder(GoogleNetHttpTransport.newTrustedTransport(),
JSON_FACTORY,
new HttpCredentialsAdapter(credentials))
.setApplicationName(APPLICATION_NAME)
.build();
.fromStream(decodedStream)
.createScoped(Collections.singletonList("https://www.googleapis.com/auth/spreadsheets"));

sheetsService = new Sheets.Builder(
GoogleNetHttpTransport.newTrustedTransport(),
JSON_FACTORY,
new HttpCredentialsAdapter(credentials)
)
.setApplicationName(APPLICATION_NAME)
.build();
}
return sheetsService;
}
Expand All @@ -60,12 +68,13 @@ public void writeToSheet(String spreadsheetId, String range, List<List<Object>>
try {
Sheets service = getSheetsService();
ValueRange body = new ValueRange().setValues(values);
service.spreadsheets().values()
.update(spreadsheetId, range, body)
.setValueInputOption("USER_ENTERED")
.execute();
} catch (IOException | GeneralSecurityException e) {
logger.error("구글 시트에 데이터를 쓰는 데 실패했습니다.", e);
service.spreadsheets()
.values()
.update(spreadsheetId, range, body)
.setValueInputOption("USER_ENTERED")
.execute();
} catch (IOException | GeneralSecurityException e) {
log.error("시트에 데이터를 쓰는 데 실패했습니다.");
throw new CustomException(ErrorCode.WRITE_FAIL);
}
}
Expand All @@ -78,32 +87,34 @@ public void updateSheet(List<Applicant> applicants) {
processSheetsUpdate(applicants, false);
}

public int findRowIndexByStudentNo(String spreadSheetId, String sheetName, String studentNo){
public int findRowIndexByStudentNo(String spreadSheetId, String sheetName, String studentNo) {
try {
List<List<Object>> rows = readSheet(spreadSheetId, sheetName + "!A:L"); // A열부터 L열까지 읽기

for (int i = 0; i < rows.size(); i++){
for (int i = 0; i < rows.size(); i++) {
List<Object> row = rows.get(i);
if(!row.isEmpty() && row.get(2).equals(studentNo)){ // 학번(Student No)이 3번째 열(A=0 기준)
if (!row.isEmpty() && row.get(2)
.equals(studentNo)) { // 학번(Student No)이 3번째 열(A=0 기준)
return i + 1;
}
}
} catch (Exception e) {
logger.error("구글시트에서 행 찾기 실패", e);
log.error("시트에서 행 찾기 실패");
}
return -1;
}

public List<List<Object>> readSheet(String spreadsheetId, String range) {
try {
Sheets service = getSheetsService();
ValueRange response = service.spreadsheets().values()
.get(spreadsheetId, range)
.execute();
ValueRange response = service.spreadsheets()
.values()
.get(spreadsheetId, range)
.execute();

return response.getValues();
} catch (IOException | GeneralSecurityException e) {
logger.error("시트에서 데이터를 읽어오는데 실패했습니다.", e);
log.error("시트에서 데이터를 읽어오는 데 실패했습니다.");
throw new CustomException(ErrorCode.SHEET_READ_FAIL);
}
}
Expand All @@ -113,7 +124,7 @@ public int getLastRow(String spreadsheetId, String sheetName) {
List<List<Object>> rows = readSheet(spreadsheetId, sheetName + "!A:L"); // A~L 열까지 읽기
return rows == null ? 0 : rows.size(); // 데이터가 없으면 0 반환
} catch (Exception e) {
logger.error("Failed to retrieve last row from Google Sheet", e);
log.error("시트에서 마지막 행 찾기 실패");
throw new CustomException(ErrorCode.SHEET_READ_FAIL);
}
}
Expand All @@ -124,27 +135,28 @@ public void batchUpdateSheet(String spreadsheetId, List<ValueRange> valueRanges)

// BatchUpdate 요청 생성
BatchUpdateValuesRequest batchUpdateRequest = new BatchUpdateValuesRequest()
.setValueInputOption("USER_ENTERED") // 사용자 입력 형식으로 값 설정
.setData(valueRanges); // 여러 ValueRange 추가
.setValueInputOption("USER_ENTERED") // 사용자 입력 형식으로 값 설정
.setData(valueRanges); // 여러 ValueRange 추가

// BatchUpdate 실행
BatchUpdateValuesResponse response = service.spreadsheets().values()
.batchUpdate(spreadsheetId, batchUpdateRequest)
.execute();
BatchUpdateValuesResponse response = service.spreadsheets()
.values()
.batchUpdate(spreadsheetId, batchUpdateRequest)
.execute();

logger.info("Batch update completed. Total updated rows: {}", response.getTotalUpdatedRows());
log.info("시트 업데이트 성공. {}", response.getTotalUpdatedRows());
} catch (IOException | GeneralSecurityException e) {
logger.error("Batch update failed", e);
log.error("시트 업데이트 실패.");
throw new CustomException(ErrorCode.SHEET_WRITE_FAIL);
}
}


private ValueRange createValueRange(String range, List<List<Object>> values) {
return new ValueRange()
.setRange(range)
.setMajorDimension("ROWS") // 행 단위로 데이터 설정
.setValues(values);
.setRange(range)
.setMajorDimension("ROWS") // 행 단위로 데이터 설정
.setValues(values);
}

public void processSheetsUpdate(List<Applicant> applicants, boolean isAppend) {
Expand All @@ -160,7 +172,7 @@ public void processSheetsUpdate(List<Applicant> applicants, boolean isAppend) {
} else {
int rowIndex = findRowIndexByStudentNo(spreadSheetId, "Sheet1", applicant.getStudentNo());
if (rowIndex == -1) {
logger.warn("구글시트에서 사용자를 찾을 수 없습니다. : {}", applicant.getStudentNo());
log.warn("시트에서 지원자를 찾을 수 없습니다. : {}", applicant.getStudentNo());
continue;
}
range = "Sheet1!A" + rowIndex + ":L" + rowIndex;
Expand All @@ -170,11 +182,10 @@ public void processSheetsUpdate(List<Applicant> applicants, boolean isAppend) {

batchUpdateSheet(spreadSheetId, valueRanges);
} catch (Exception e) {
logger.error("구글시트 업데이트에 실패했습니다.", e);
log.error("시트 업데이트에 실패했습니다.", e);
throw new CustomException(ErrorCode.SHEET_WRITE_FAIL);
}
}



}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package dmu.dasom.api.domain.interview.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import lombok.*;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(name = "InterviewReservationResponseDto", description = "면접 예약 응답 DTO")
public class InterviewReservationRequestDto {

@NotNull(message = "슬롯 ID는 필수 값입니다.")
@Schema(description = "예약할 면접 슬롯의 ID", example = "1")
private Long slotId; // 예약할 슬롯 ID

@NotNull(message = "예약 코드는 필수 값입니다.")
@Pattern(regexp = "^[0-9]{8}[0-9]{4}$", message = "예약 코드는 학번 전체와 전화번호 뒤 4자리로 구성되어야 합니다.")
@Schema(description = "학번 전체와 전화번호 뒤 4자리로 구성된 예약 코드", example = "202500010542")
private String reservationCode; // 학번 전체 + 전화번호 뒤 4자리 조합 코드
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package dmu.dasom.api.domain.interview.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.*;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(name = "InterviewSlotCreateRequestDto", description = "면접 슬롯 생성 요청 DTO")
public class InterviewReservationResponseDto {

@NotNull(message = "예약 ID는 필수 값입니다.")
@Schema(description = "예약의 고유 ID", example = "1")
private Long reservationId; // 예약 ID

@NotNull(message = "슬롯 ID는 필수 값입니다.")
@Schema(description = "예약된 면접 슬롯의 ID", example = "10")
private Long slotId; // 슬롯 ID

@NotNull(message = "지원자 ID는 필수 값입니다.")
@Schema(description = "예약한 지원자의 ID", example = "1001")
private Long applicantId; // 지원자 ID

@NotNull(message = "예약 코드는 필수 값입니다.")
@Schema(description = "학번 전체와 전화번호 뒤 4자리로 구성된 예약 코드", example = "202500010542")
private String reservationCode; // 예약 코드 (학번+전화번호 조합)

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package dmu.dasom.api.domain.interview.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.*;

import java.time.LocalDate;
import java.time.LocalTime;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(name = "InterviewSlotCreateRequestDto", description = "면접 슬롯 생성 요청 DTO")
public class InterviewSlotCreateRequestDto {

@NotNull(message = "시작 날짜는 필수 값입니다.")
@Schema(description = "면접 시작 날짜", example = "2025-03-12")
private LocalDate startDate; // 면접 시작 날짜

@NotNull(message = "종료 날짜는 필수 값입니다.")
@Schema(description = "면접 종료 날짜", example = "2025-03-14")
private LocalDate endDate; // 면접 종료 날짜

@NotNull(message = "시작 시간은 필수 값입니다.")
@Schema(description = "하루의 시작 시간", example = "10:00")
private LocalTime startTime; // 하루의 시작 시간

@NotNull(message = "종료 시간은 필수 값입니다.")
@Schema(description = "하루의 종료 시간", example = "17:00")
private LocalTime endTime; // 하루의 종료 시간
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package dmu.dasom.api.domain.interview.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import lombok.*;

import java.time.LocalDate;
import java.time.LocalTime;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(name = "InterviewSlotRequestDto", description = "면접 슬롯 요청 DTO")
public class InterviewSlotRequestDto {

@NotNull(message = "면접 날짜는 필수 입력 값입니다.")
@Schema(description = "면접이 진행되는 날짜", example = "2025-03-12")
private LocalDate interviewDate; // 면접 날짜

@NotNull(message = "시작 시간은 필수 입력 값입니다.")
@Schema(description = "면접 시작 시간", example = "10:00")
private LocalTime startTime; // 시작 시간

@NotNull(message = "종료 시간은 필수 입력 값입니다.")
@Schema(description = "면접 종료 시간", example = "10:20")
private LocalTime endTime; // 종료 시간

@NotNull(message = "최대 지원자 수는 필수 입력 값입니다.")
@Min(value = 1, message = "최대 지원자 수는 최소 1명 이상이어야 합니다.")
@Max(value = 100, message = "최대 지원자 수는 최대 100명까지 가능합니다.")
@Schema(description = "해당 슬롯에서 허용되는 최대 지원자 수", example = "2")
private Integer maxCandidates; // 최대 지원자 수
}
Loading