Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Interview 관련 api #168

Merged
merged 2 commits into from
Nov 2, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
15 changes: 15 additions & 0 deletions ddl.sql
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ create schema post;
create schema tag;
create schema score;
create schema employer;
create schema interview;
create sequence post.memo_series_order_seq start 1;

CREATE TABLE tag.memo_to_tag (
Expand Down Expand Up @@ -283,3 +284,17 @@ create table score.info (
created_date timestamp default current_timestamp,
primary key (user_id,score_type,post_id)
);

create table interview.info(
id bigserial primary key,
employer_id int8,
employee_id int8,
status varchar(8) default 'READY', ---'DONE', 'ING_Q1','ING_Q2','ING_Q3', 'READY'
question_1 varchar,
answer_1 varchar,
question_2 varchar,
answer_2 varchar,
question_3 varchar,
answer_3 varchar,
unique (employer_id, employee_id)
);
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ public class ForbiddenException extends RuntimeException{

public ForbiddenException(String message) {
this.message = message;
this.errorResult = new ErrorResult(HttpStatus.NOT_FOUND, message);
this.errorResult = new ErrorResult(HttpStatus.FORBIDDEN, message);
}

public ForbiddenException(String message, Throwable cause) {
super(cause);
this.message = message;
this.errorResult = new ErrorResult(HttpStatus.NOT_FOUND, message);
this.errorResult = new ErrorResult(HttpStatus.FORBIDDEN, message);
}
}
3 changes: 3 additions & 0 deletions src/main/java/Funssion/Inforum/config/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Excepti
"/users/check-duplication").permitAll()
.requestMatchers(HttpMethod.GET, "/users/profile/**").permitAll()
.requestMatchers("/employer/**").hasRole("EMPLOYER")
.requestMatchers(HttpMethod.POST,"/interview/questions/**").hasRole("EMPLOYER")
.requestMatchers(HttpMethod.GET,"/interview/answers/**").hasAnyRole("EMPLOYER","USER")
.requestMatchers(HttpMethod.POST,"/interview/answers/**").hasRole("USER")
.requestMatchers(HttpMethod.GET, "/score/**").permitAll()
.requestMatchers(HttpMethod.GET,"/score/rank/**").authenticated()
.requestMatchers(HttpMethod.POST, "/users/login").authenticated() //spring security filter에서 redirect
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package Funssion.Inforum.domain.interview.constant;

import lombok.Getter;
import lombok.RequiredArgsConstructor;
@Getter
@RequiredArgsConstructor
public enum InterviewStatus {
READY("면접 시작 전입니다."),
ING_Q1("1번 문제를 푸는 중입니다."),
ING_Q2("2번 문제를 푸는 중입니다."),
ING_Q3("3번 문제를 푸는 중입니다."),
DONE("면접이 완료 되었습니다.");
private final String status;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package Funssion.Inforum.domain.interview.controller;

import Funssion.Inforum.common.dto.IsSuccessResponseDto;
import Funssion.Inforum.common.utils.SecurityContextUtils;
import Funssion.Inforum.domain.interview.constant.InterviewStatus;
import Funssion.Inforum.domain.interview.dto.InterviewAnswerDto;
import Funssion.Inforum.domain.interview.dto.InterviewQuestionDto;
import Funssion.Inforum.domain.interview.dto.QuestionsDto;
import Funssion.Inforum.domain.interview.exception.InterviewForbiddenException;
import Funssion.Inforum.domain.interview.service.InterviewService;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/interview")
@RequiredArgsConstructor
public class InterviewController {
private final InterviewService interviewService;
@PostMapping("/questions/{employeeId}")
public IsSuccessResponseDto saveInterviewQuestions(@PathVariable Long employeeId, @RequestBody @Valid QuestionsDto questionsDto){
return interviewService.saveQuestionsAndNotifyInterview(employeeId,questionsDto);
}
@GetMapping("/questions/{employerId}/{employeeId}")
public InterviewQuestionDto getInterviewQuestion(@PathVariable Long employerId, @PathVariable Long employeeId){
return interviewService.getInterviewInfoTo(employeeId, employerId);
}
@ApiResponse(description="1번 문제를 봤을 때 호출됩니다. 이에 따라 status가 1번문제 보는중으로 바뀜")
@PutMapping("/start/{employerId}")
public InterviewStatus startInterviewByEmployee(@PathVariable Long employerId){
Long userId = SecurityContextUtils.getAuthorizedUserId();

return interviewService.startInterview(employerId,userId);
}
@ApiResponse(description="답변을 제출할때마다 호출됩니다. 이에따라 다음문제 보는중으로 바뀜. ex) 1번 답변 제출시 2번문제 보는중으로 바뀜")
@PostMapping("/answers")
public IsSuccessResponseDto saveAnswerOfQuestion(@RequestBody InterviewAnswerDto interviewAnswerDto){
Long userId = SecurityContextUtils.getAuthorizedUserId();
if(!interviewService.authorizeInterview(interviewAnswerDto.getEmployerId(),userId)) throw new InterviewForbiddenException();

interviewService.saveAnswerOfQuestion(interviewAnswerDto,userId);
return new IsSuccessResponseDto(true, interviewAnswerDto.getQuestionNumber() + "번 질문의 답변이 성공적으로 등록되었습니다.");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package Funssion.Inforum.domain.interview.domain;

import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
@EqualsAndHashCode
@Builder
public class Interview {
private final Long employerId;
private final String status;
private final String question1;
private final String question2;
private final String question3;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package Funssion.Inforum.domain.interview.dto;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
@AllArgsConstructor
public class InterviewAnswerDto {
private Long employerId;
private Integer questionNumber;
private String answer;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package Funssion.Inforum.domain.interview.dto;

import Funssion.Inforum.domain.interview.domain.Interview;
import lombok.Getter;

@Getter
public class InterviewQuestionDto {
private final String question1;
private final String question2;
private final String question3;
private final String status;
private final Long employerId;
private String companyName;
public InterviewQuestionDto(Interview interviewQuestion, String companyName){
this.question1 = interviewQuestion.getQuestion1();
this.question2 = interviewQuestion.getQuestion2();
this.question3 = interviewQuestion.getQuestion3();
this.status = interviewQuestion.getStatus();
this.employerId = interviewQuestion.getEmployerId();
this.companyName = companyName;

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package Funssion.Inforum.domain.interview.dto;

import jakarta.validation.constraints.NotEmpty;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
@AllArgsConstructor
public class QuestionsDto {
@NotEmpty
private String question1;
@NotEmpty
private String question2;
@NotEmpty
private String question3;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package Funssion.Inforum.domain.interview.exception;

import Funssion.Inforum.common.exception.forbidden.ForbiddenException;

public class InterviewForbiddenException extends ForbiddenException {
public InterviewForbiddenException() {
super("해당 유저에게 할당된 인터뷰가 아닙니다.");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package Funssion.Inforum.domain.interview.repository;

import Funssion.Inforum.common.exception.badrequest.BadRequestException;
import Funssion.Inforum.common.utils.SecurityContextUtils;
import Funssion.Inforum.domain.interview.constant.InterviewStatus;
import Funssion.Inforum.domain.interview.domain.Interview;
import Funssion.Inforum.domain.interview.dto.InterviewAnswerDto;
import Funssion.Inforum.domain.interview.dto.QuestionsDto;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Repository;

import javax.sql.DataSource;

@Repository
public class InterviewRepository {
private final JdbcTemplate template;
public InterviewRepository(DataSource dataSource){
this.template = new JdbcTemplate(dataSource);
}

public void saveQuestions(Long employeeId, QuestionsDto questionsDto){
Long employerId = SecurityContextUtils.getAuthorizedUserId();

String sql = "INSERT INTO interview.info (employer_id, employee_id, question_1, question_2, question_3) values(?,?,?,?,?)";
template.update(sql,employerId,employeeId, questionsDto.getQuestion1(),questionsDto.getQuestion2(),questionsDto.getQuestion3());
}

public boolean findIfAlreadyInterviewing(Long employeeId){
Long employerId = SecurityContextUtils.getAuthorizedUserId();
String sql =
"SELECT EXISTS " +
"(SELECT 1 " +
"FROM interview.info " +
"WHERE employer_id = ? AND employee_id = ? )";
return template.queryForObject(sql,Boolean.class,employerId,employeeId);
}
public Interview getInterviewQuestionOf(Long employeeId, Long employerId){
String sql =
"SELECT employer_id, status, question_1, question_2, question_3 " +
"FROM interview.info " +
"WHERE employee_id = ? and employer_id = ?";
return template.queryForObject(sql,interviewRowMapper(),employeeId, employerId);
}

public void saveAnswerOfQuestion(InterviewAnswerDto interviewAnswerDto,Long userId) {
String columnNameOfAnswer = switch (interviewAnswerDto.getQuestionNumber()) {
case 1 -> "answer_1";
case 2 -> "answer_2";
case 3 -> "answer_3";
default -> throw new BadRequestException("인터뷰 답변객체의 번호가 '1','2','3' 이 아닙니다.");
};

String sql =
"UPDATE interview.info " +
"SET " + columnNameOfAnswer + " = ? " +
"WHERE employer_id = ? and employee_id = ?";

template.update(sql,interviewAnswerDto.getAnswer(), interviewAnswerDto.getEmployerId(),userId);
}

public InterviewStatus updateStatus(Long employerId, Long employeeId, InterviewStatus interviewStatus){
String sql =
"UPDATE interview.info " +
"SET status = ? " +
"WHERE employer_id = ? and employee_id = ?";
template.update(sql,interviewStatus.toString(), employerId, employeeId);
return getInterviewStatusOfUser(employerId, employeeId);
}
public InterviewStatus startInterview(Long employerId, Long employeeId) {
String sql =
"UPDATE interview.info " +
"SET status = ? " +
"WHERE employer_id = ? and employee_id = ?";
template.update(sql, InterviewStatus.ING_Q1.toString(),employerId,employeeId);

return getInterviewStatusOfUser(employerId,employeeId);
}

public InterviewStatus getInterviewStatusOfUser(Long employerId, Long userId){
String sql =
"SELECT status " +
"FROM interview.info " +
"WHERE employer_id = ? and employee_id = ?";
return InterviewStatus.valueOf(template.queryForObject(sql,String.class, employerId, userId));
}
public Boolean isAuthorizedInterview(Long employerId,Long employeeId) {
String sql =
"SELECT EXISTS(" +
"SELECT employee_id " +
"FROM interview.info " +
"WHERE employer_id = ? AND employee_id = ?)";
return template.queryForObject(sql, Boolean.class, employerId,employeeId);
}
private RowMapper<Interview> interviewRowMapper(){
return (rs,rowNum)->
Interview.builder()
.employerId(rs.getLong("employer_id"))
.status(rs.getString("status"))
.question1(rs.getString("question_1"))
.question2(rs.getString("question_2"))
.question3(rs.getString("question_3"))
.build();
}

public boolean isMismatchWithStatus(InterviewAnswerDto interviewAnswerDto, Long userId) {
InterviewStatus interviewStatusOfUser = getInterviewStatusOfUser(interviewAnswerDto.getEmployerId(), userId);
if (isStatusMismatchWithQuestionNumber(interviewAnswerDto, interviewStatusOfUser)) return true;
return false;
}

private static boolean isStatusMismatchWithQuestionNumber(InterviewAnswerDto interviewAnswerDto, InterviewStatus interviewStatusOfUser) {
return !interviewStatusOfUser.getStatus().startsWith(String.valueOf(interviewAnswerDto.getQuestionNumber()));
}


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package Funssion.Inforum.domain.interview.service;

import Funssion.Inforum.common.dto.IsSuccessResponseDto;
import Funssion.Inforum.common.exception.badrequest.BadRequestException;
import Funssion.Inforum.common.exception.etc.DuplicateException;
import Funssion.Inforum.domain.interview.constant.InterviewStatus;
import Funssion.Inforum.domain.interview.domain.Interview;
import Funssion.Inforum.domain.interview.dto.InterviewAnswerDto;
import Funssion.Inforum.domain.interview.dto.InterviewQuestionDto;
import Funssion.Inforum.domain.interview.dto.QuestionsDto;
import Funssion.Inforum.domain.interview.exception.InterviewForbiddenException;
import Funssion.Inforum.domain.interview.repository.InterviewRepository;
import Funssion.Inforum.domain.member.repository.MemberRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class InterviewService {
private final InterviewRepository interviewRepository;
private final MemberRepository memberRepository;
public IsSuccessResponseDto saveQuestionsAndNotifyInterview(Long employeeId, QuestionsDto questionsDto){
if(interviewRepository.findIfAlreadyInterviewing(employeeId)) throw new DuplicateException("이미 면접요청을 보낸 지원자입니다.");

interviewRepository.saveQuestions(employeeId,questionsDto);
// ToDo : notification

return new IsSuccessResponseDto(true, "성공적으로 면접알림을 전송하였습니다.");
}


public InterviewQuestionDto getInterviewInfoTo(Long employeeId, Long employerId){
Interview interviewInfo = interviewRepository.getInterviewQuestionOf(employeeId, employerId);
return new InterviewQuestionDto(interviewInfo,memberRepository.getCompanyName(interviewInfo.getEmployerId()));
}

public InterviewStatus startInterview(Long employerId, Long employeeId){
if (!authorizeInterview(employerId,employeeId)) throw new InterviewForbiddenException();

return interviewRepository.startInterview(employerId, employeeId);
}
public InterviewStatus saveAnswerOfQuestion(InterviewAnswerDto interviewAnswerDto, Long userId) {
// 풀고있는 상태와 다른 답변 문제가 제출될 경우의 에러처리
handlingOfMismatchWithStatus(interviewAnswerDto,userId);

interviewRepository.saveAnswerOfQuestion(interviewAnswerDto,userId);

InterviewStatus interviewStatusAfterAnswer = getInterviewStatus(interviewAnswerDto);
return interviewRepository.updateStatus(interviewAnswerDto.getEmployerId(),userId,interviewStatusAfterAnswer);

}

private static InterviewStatus getInterviewStatus(InterviewAnswerDto interviewAnswerDto) {
InterviewStatus interviewStatusAfterAnswer = switch (interviewAnswerDto.getQuestionNumber()) {
case 1 -> InterviewStatus.ING_Q2;
case 2 -> InterviewStatus.ING_Q3;
case 3 -> InterviewStatus.DONE;
default -> throw new BadRequestException("인터뷰 답변객체의 번호가 '1','2','3' 이 아닙니다.");
};
return interviewStatusAfterAnswer;
}

public Boolean authorizeInterview(Long employerId,Long employeeId){
return interviewRepository.isAuthorizedInterview(employerId,employeeId);
}
private void handlingOfMismatchWithStatus(InterviewAnswerDto interviewAnswerDto,Long userId){
if (interviewRepository.isMismatchWithStatus(interviewAnswerDto,userId)) throw new BadRequestException("잘못된 문제로 요청을 보내고 있습니다. 현 문제번호와 답변을 확인해주세요.");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,5 @@ public class SaveMemberResponseDto {
private final LocalDateTime createdDate;
private final String email;
private final String role;
private String companyName;
}
Loading
Loading