Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
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", "해당 슬롯이 비활성화 되었습니다."),
;

private final int status;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package dmu.dasom.api.domain.interview.dto;

import lombok.*;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class InterviewReservationRequestDto {

private Long slotId; // 예약할 슬롯 ID

private Long applicantId; // 지원자 ID

private String reservationCode; // 학번 뒤 4자리 + 전화번호 뒤 4자리 조합 코드
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package dmu.dasom.api.domain.interview.dto;

import lombok.*;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class InterviewReservationResponseDto {

private Long reservationId; // 예약 ID

private Long slotId; // 슬롯 ID

private Long applicantId; // 지원자 ID

private String reservationCode; // 예약 코드 (학번+전화번호 조합)

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

import lombok.*;

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

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class InterviewSlotRequestDto {

private LocalDate interviewDate; // 면접 날짜

private LocalTime startTime; // 시작 시간

private LocalTime endTime; // 종료 시간

private Integer maxCandidates; // 최대 지원자 수
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package dmu.dasom.api.domain.interview.dto;

import dmu.dasom.api.domain.interview.entity.InterviewSlot;
import lombok.*;

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

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class InterviewSlotResponseDto {

private Long id; // 슬롯 ID
private LocalDate interviewDate; // 면접 날짜
private LocalTime startTime; // 시작 시간
private LocalTime endTime; // 종료 시간
private Integer maxCandidates; // 최대 지원자 수
private Integer currentCandidates; // 현재 예약된 지원자 수

public InterviewSlotResponseDto(InterviewSlot slot){
this.id = slot.getId();
this.interviewDate = slot.getInterviewDate();
this.startTime = slot.getStartTime();
this.endTime = slot.getEndTime();
this.maxCandidates = slot.getMaxCandidates();
this.currentCandidates = slot.getCurrentCandidates();
}

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

import dmu.dasom.api.domain.applicant.entity.Applicant;
import jakarta.persistence.*;
import lombok.*;

@Entity
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class InterviewReservation {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "slot_id", nullable = false)
private InterviewSlot slot; // 연관된 면접 슬롯

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "applicant_id", nullable = false)
private Applicant applicant; // 지원자

@Column(nullable = false, unique = true, length = 8)
private String reservationCode; // 학번 뒤 4자리 + 전화번호 뒤 4자리 조합 코드
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package dmu.dasom.api.domain.interview.entity;

import dmu.dasom.api.domain.interview.enums.Status;
import jakarta.persistence.*;
import lombok.*;

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

@Entity
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class InterviewSlot {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(nullable = false)
private LocalDate interviewDate;

@Column(nullable = false)
private LocalTime startTime;

@Column(nullable = false)
private LocalTime endTime; // 종료 시간

@Column(nullable = false)
private Integer maxCandidates; // 최대 지원자 수

@Column(nullable = false)
private Integer currentCandidates; // 현재 예약된 지원자 수

@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 16)
private Status status; // 면접 슬롯 상태 (ACTIVE, INACTIVE, CLOSED)

public void incrementCurrentCandidates() {
this.currentCandidates++;
if (this.currentCandidates >= this.maxCandidates) {
this.status = Status.CLOSED; // 최대 지원자 수에 도달하면 상태 변경
}
}

public void decrementCurrentCandidates() {
this.currentCandidates--;
if (status == Status.CLOSED && this.currentCandidates < this.maxCandidates) {
this.status = Status.ACTIVE; // 지원자 수가 줄어들면 다시 활성화
}
}

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

public enum Status {
ACTIVE, // 활성화된 슬롯
INACTIVE, // 비활성화된 슬롯
CLOSED // 예약 마감된 슬롯
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package dmu.dasom.api.domain.interview.repositoty;

import dmu.dasom.api.domain.interview.entity.InterviewReservation;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface InterviewReservationRepository extends JpaRepository<InterviewReservation, Long> {
boolean existsByReservationCode(String reservationCode);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package dmu.dasom.api.domain.interview.repositoty;

import dmu.dasom.api.domain.interview.entity.InterviewSlot;
import dmu.dasom.api.domain.interview.enums.Status;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.Collection;
import java.util.List;

@Repository
public interface InterviewSlotRepository extends JpaRepository<InterviewSlot, Long> {
Collection<InterviewSlot> findAllByCurrentCandidatesLessThanMaxCandidates();
List<InterviewSlot> findAllByStatusAndCurrentCandidatesLessThanMaxCandidates(
Status status);

boolean exists();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package dmu.dasom.api.domain.interview.service;

import dmu.dasom.api.domain.interview.dto.InterviewReservationRequestDto;
import dmu.dasom.api.domain.interview.dto.InterviewSlotResponseDto;

import java.time.LocalDate;
import java.time.LocalTime;
import java.util.List;


public interface InterviewService {

// 면접 슬롯 생성
List<InterviewSlotResponseDto> createInterviewSlots(LocalDate newStartDate, LocalDate newEndDate, LocalTime newStartTime, LocalTime newEndTime);

// 예약 가능한 면접 슬롯 조회
List<InterviewSlotResponseDto> getAvailableSlots();

// 면접 예약
void reserveInterviewSlot(InterviewReservationRequestDto request);

// 면접 예약 취소
void cancelReservation(Long reservationId, Long applicantId);


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

import dmu.dasom.api.domain.applicant.entity.Applicant;
import dmu.dasom.api.domain.applicant.repository.ApplicantRepository;
import dmu.dasom.api.domain.common.exception.CustomException;
import dmu.dasom.api.domain.common.exception.ErrorCode;
import dmu.dasom.api.domain.interview.dto.InterviewReservationRequestDto;
import dmu.dasom.api.domain.interview.dto.InterviewSlotResponseDto;
import dmu.dasom.api.domain.interview.entity.InterviewReservation;
import dmu.dasom.api.domain.interview.entity.InterviewSlot;
import dmu.dasom.api.domain.interview.enums.Status;
import dmu.dasom.api.domain.interview.repositoty.InterviewReservationRepository;
import dmu.dasom.api.domain.interview.repositoty.InterviewSlotRepository;
import dmu.dasom.api.domain.recruit.service.RecruitServiceImpl;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDate;
import java.time.LocalTime;
import java.util.ArrayList;
import java.util.List;

@RequiredArgsConstructor
@Service
@Transactional(readOnly = true)
public class InterviewServiceImpl implements InterviewService{

private final InterviewSlotRepository interviewSlotRepository;
private final InterviewReservationRepository interviewReservationRepository;
private final ApplicantRepository applicantRepository;
private final RecruitServiceImpl recruitService;

// 면접 슬롯 생성
@Override
@Transactional
public List<InterviewSlotResponseDto> createInterviewSlots(LocalDate newStartDate, LocalDate newEndDate, LocalTime newStartTime, LocalTime newEndTime) {
boolean slotsExist = interviewSlotRepository.exists();

if(slotsExist){
interviewSlotRepository.deleteAll();
}

List<InterviewSlotResponseDto> newSlots = new ArrayList<>();
for(LocalDate date = newStartDate; !date.isAfter(newEndDate); date = date.plusDays(1)){
LocalTime currentTime = newStartTime;
while (currentTime.isBefore(newEndTime)){
LocalTime slotEndTime = currentTime.plusMinutes(20);

InterviewSlot slot = InterviewSlot.builder()
.interviewDate(date)
.startTime(currentTime)
.endTime(slotEndTime)
.maxCandidates(2)
.currentCandidates(0)
.status(Status.ACTIVE)
.build();

interviewSlotRepository.save(slot);
newSlots.add(new InterviewSlotResponseDto(slot));
currentTime = slotEndTime;
}
}

return newSlots;
}

// 예약 가능한 면접 슬롯 조회
@Override
public List<InterviewSlotResponseDto> getAvailableSlots() {
return interviewSlotRepository.findAllByStatusAndCurrentCandidatesLessThanMaxCandidates(Status.ACTIVE)
.stream()
.map(InterviewSlotResponseDto::new)
.toList();
}

@Override
@Transactional
public void reserveInterviewSlot(InterviewReservationRequestDto request) {
InterviewSlot slot = interviewSlotRepository.findById(request.getSlotId())
.orElseThrow(() -> new CustomException(ErrorCode.SLOT_NOT_FOUND));

if(!slot.getStatus().equals(Status.ACTIVE)){
throw new CustomException(ErrorCode.SLOT_NOT_ACTIVE);
}

if (slot.getCurrentCandidates() >= slot.getMaxCandidates()) {
throw new CustomException(ErrorCode.SLOT_FULL);
}

Applicant applicant = applicantRepository.findById(request.getApplicantId())
.orElseThrow(() -> new CustomException(ErrorCode.APPLICANT_NOT_FOUND));

boolean alreadyReserved = interviewReservationRepository.existsByReservationCode(request.getReservationCode());
if (alreadyReserved) {
throw new CustomException(ErrorCode.ALREADY_RESERVED);
}

// 예약 정보 저장
InterviewReservation reservation = InterviewReservation.builder()
.slot(slot)
.applicant(applicant)
.reservationCode(request.getReservationCode())
.build();

interviewReservationRepository.save(reservation);

// 현재 예약 인원 증가
slot.incrementCurrentCandidates();
}

// 면접 예약 취소
@Override
@Transactional
public void cancelReservation(Long reservationId, Long applicantId) {
InterviewReservation reservation = interviewReservationRepository.findById(reservationId)
.orElseThrow(() -> new CustomException(ErrorCode.RESERVATION_NOT_FOUND));

if (!reservation.getApplicant().getId().equals(applicantId)) {
throw new CustomException(ErrorCode.UNAUTHORIZED);
}

// 현재 예약 인원 감소
InterviewSlot slot = reservation.getSlot();
slot.decrementCurrentCandidates();

// 예약 삭제
interviewReservationRepository.delete(reservation);
}

}
Loading