Skip to content
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
1 change: 1 addition & 0 deletions backend/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-aop'
implementation 'org.springframework.boot:spring-boot-starter-amqp'
implementation 'org.springframework.retry:spring-retry'
implementation 'com.google.cloud:spring-cloud-gcp-storage:5.8.0'
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotEmpty;
import java.util.List;
import lombok.AllArgsConstructor;
import moadong.club.payload.request.ClubApplicantDeleteRequest;
import moadong.club.payload.request.ClubApplicantEditRequest;
Expand All @@ -18,17 +17,11 @@
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

import java.util.List;


@RestController
@RequestMapping("/api/club")
Expand All @@ -43,7 +36,7 @@ public class ClubApplyAdminController {
@PreAuthorize("isAuthenticated()")
@SecurityRequirement(name = "BearerAuth")
public ResponseEntity<?> createClubApplicationForm(@CurrentUser CustomUserDetails user,
@RequestBody @Validated ClubApplicationFormCreateRequest request) {
@RequestBody @Validated ClubApplicationFormCreateRequest request) {
clubApplyAdminService.createClubApplicationForm(user, request);
return Response.ok("success create application");
}
Expand All @@ -65,7 +58,7 @@ public ResponseEntity<?> editClubApplicationForm(@PathVariable String applicatio
@SecurityRequirement(name = "BearerAuth")
public ResponseEntity<?> getClubApplications(@CurrentUser CustomUserDetails user,
@RequestParam(defaultValue = "agg") String mode) { //agg면 aggregation사용, server면, 서비스에서 그룹 및 정렬
if("server".equalsIgnoreCase(mode)) {
if ("server".equalsIgnoreCase(mode)) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

앞으론 if 뒤에 띄어쓰기 잘 하겠습니다,,

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

꿀팁 한개 드리면 option + command + l 누르면 intellij 자동으로 포맷팅해줍니다 ㅇㅁㅇ

return Response.ok(clubApplyAdminService.getGroupedClubApplicationForms(user));
}
return Response.ok(clubApplyAdminService.getClubApplicationForms(user));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package moadong.club.payload.dto;

public record ApplicantSummaryMessage(
String applicationFormId,
String applicantId
) {
}
Original file line number Diff line number Diff line change
@@ -1,30 +1,21 @@
package moadong.club.service;

import jakarta.transaction.Transactional;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import moadong.club.entity.ClubApplicant;
import moadong.club.entity.ClubApplicationForm;
import moadong.club.entity.ClubQuestionAnswer;
import moadong.club.entity.ClubApplicationFormQuestion;
import moadong.club.entity.ClubQuestionItem;
import moadong.club.entity.ClubQuestionOption;
import moadong.club.entity.*;
import moadong.club.enums.SemesterTerm;
import moadong.club.payload.dto.*;
import moadong.club.payload.request.ClubApplicantDeleteRequest;
import moadong.club.payload.request.ClubApplicantEditRequest;
import moadong.club.payload.request.ClubApplicationFormCreateRequest;
import moadong.club.payload.request.ClubApplicationFormEditRequest;
import moadong.club.payload.request.ClubApplicantEditRequest;
import moadong.club.payload.request.ClubApplicantDeleteRequest;
import moadong.club.payload.response.*;
import moadong.club.repository.*;
import moadong.club.payload.response.ClubApplicationFormsResponse;
import moadong.club.payload.response.ClubApplyInfoResponse;
import moadong.club.repository.ClubApplicantsRepository;
import moadong.club.repository.ClubApplicationFormsRepository;
import moadong.club.repository.ClubApplicationFormsRepositoryCustom;
import moadong.club.repository.ClubRepository;
import moadong.global.exception.ErrorCode;
import moadong.global.exception.RestApiException;
import moadong.global.util.AESCipher;
Expand All @@ -37,6 +28,14 @@
import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springframework.transaction.support.TransactionSynchronization;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;

@Service
@AllArgsConstructor
@Slf4j
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@
import moadong.club.payload.response.ClubApplicationFormResponse;
import moadong.club.repository.ClubApplicantsRepository;
import moadong.club.repository.ClubApplicationFormsRepository;
import moadong.club.summary.ApplicantIdMessagePublisher;
import moadong.global.exception.ErrorCode;
import moadong.global.exception.RestApiException;
import moadong.global.payload.Response;
import moadong.global.util.AESCipher;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.*;
import java.util.function.Function;
Expand All @@ -32,49 +34,35 @@ public class ClubApplyPublicService {
private final ClubApplicationFormsRepository clubApplicationFormsRepository;
private final ClubApplicantsRepository clubApplicantsRepository;
private final AESCipher cipher;

private final ApplicantIdMessagePublisher applicantIdMessagePublisher;

public ClubActiveFormsResponse getActiveApplicationForms(String clubId) {
List<ClubActiveFormSlim> forms = clubApplicationFormsRepository.findClubActiveFormsByClubId(clubId);

if (forms == null || forms.isEmpty())
throw new RestApiException(ErrorCode.ACTIVE_APPLICATION_NOT_FOUND);
if (forms == null || forms.isEmpty()) throw new RestApiException(ErrorCode.ACTIVE_APPLICATION_NOT_FOUND);

List<ClubActiveFormResult> results = new ArrayList<>();
for (ClubActiveFormSlim form : forms) {
ClubActiveFormResult result = ClubActiveFormResult.builder()
.id(form.getId())
.title(form.getTitle())
.build();
ClubActiveFormResult result = ClubActiveFormResult.builder().id(form.getId()).title(form.getTitle()).build();
results.add(result);
}

return ClubActiveFormsResponse.builder()
.forms(results)
.build();
return ClubActiveFormsResponse.builder().forms(results).build();

}

public ResponseEntity<?> getClubApplicationForm(String clubId, String applicationFormId) {
ClubApplicationForm clubApplicationForm = clubApplicationFormsRepository.findByClubIdAndId(clubId, applicationFormId)
.orElseThrow(() -> new RestApiException(ErrorCode.APPLICATION_NOT_FOUND));
ClubApplicationForm clubApplicationForm = clubApplicationFormsRepository.findByClubIdAndId(clubId, applicationFormId).orElseThrow(() -> new RestApiException(ErrorCode.APPLICATION_NOT_FOUND));


ClubApplicationFormResponse clubApplicationFormResponse = ClubApplicationFormResponse.builder()
.title(clubApplicationForm.getTitle())
.description(Optional.ofNullable(clubApplicationForm.getDescription()).orElse(""))
.questions(clubApplicationForm.getQuestions())
.semesterYear(clubApplicationForm.getSemesterYear())
.semesterTerm(clubApplicationForm.getSemesterTerm())
.status(clubApplicationForm.getStatus())
.build();
ClubApplicationFormResponse clubApplicationFormResponse = ClubApplicationFormResponse.builder().title(clubApplicationForm.getTitle()).description(Optional.ofNullable(clubApplicationForm.getDescription()).orElse("")).questions(clubApplicationForm.getQuestions()).semesterYear(clubApplicationForm.getSemesterYear()).semesterTerm(clubApplicationForm.getSemesterTerm()).status(clubApplicationForm.getStatus()).build();

return Response.ok(clubApplicationFormResponse);
}

@Transactional
public void applyToClub(String clubId, String applicationFormId, ClubApplyRequest request) {
ClubApplicationForm clubApplicationForm = clubApplicationFormsRepository.findByClubIdAndId(clubId, applicationFormId)
.orElseThrow(() -> new RestApiException(ErrorCode.APPLICATION_NOT_FOUND));
ClubApplicationForm clubApplicationForm = clubApplicationFormsRepository.findByClubIdAndId(clubId, applicationFormId).orElseThrow(() -> new RestApiException(ErrorCode.APPLICATION_NOT_FOUND));

validateAnswers(request.questions(), clubApplicationForm);

Expand All @@ -83,32 +71,25 @@ public void applyToClub(String clubId, String applicationFormId, ClubApplyReques
try {
for (ClubApplyRequest.Answer answer : request.questions()) {
String encryptedValue = cipher.encrypt(answer.value());
answers.add(ClubQuestionAnswer.builder()
.id(answer.id())
.value(encryptedValue)
.build());
answers.add(ClubQuestionAnswer.builder().id(answer.id()).value(encryptedValue).build());
}
} catch (Exception e) {
log.error("AES_CIPHER_ERROR", e);
throw new RestApiException(ErrorCode.AES_CIPHER_ERROR);
}

ClubApplicant application = ClubApplicant.builder()
.formId(applicationFormId)
.answers(answers)
.build();
ClubApplicant applicant = ClubApplicant.builder().formId(applicationFormId).answers(answers).build();

clubApplicantsRepository.save(applicant);

clubApplicantsRepository.save(application);
applicantIdMessagePublisher.addApplicantIdToQueue(applicationFormId, applicant.getId());
}

private void validateAnswers(List<ClubApplyRequest.Answer> answers, ClubApplicationForm clubApplicationForm) {
// 미리 질문과 응답 id 만들어두기
Map<Long, ClubApplicationFormQuestion> questionMap = clubApplicationForm.getQuestions().stream()
.collect(Collectors.toMap(ClubApplicationFormQuestion::getId, Function.identity()));
Map<Long, ClubApplicationFormQuestion> questionMap = clubApplicationForm.getQuestions().stream().collect(Collectors.toMap(ClubApplicationFormQuestion::getId, Function.identity()));

Set<Long> answerIds = answers.stream()
.map(ClubApplyRequest.Answer::id)
.collect(Collectors.toSet());
Set<Long> answerIds = answers.stream().map(ClubApplyRequest.Answer::id).collect(Collectors.toSet());

// 필수 질문이 누락되었는지 검증
for (ClubApplicationFormQuestion question : clubApplicationForm.getQuestions()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package moadong.club.summary;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import moadong.club.entity.ClubApplicant;
import moadong.club.entity.ClubApplicationForm;
import moadong.club.entity.ClubApplicationFormQuestion;
import moadong.club.entity.ClubQuestionAnswer;
import moadong.club.payload.dto.ApplicantSummaryMessage;
import moadong.club.repository.ClubApplicantsRepository;
import moadong.club.repository.ClubApplicationFormsRepository;
import moadong.gemma.dto.AIResponse;
import moadong.gemma.service.GemmaService;
import moadong.global.exception.ErrorCode;
import moadong.global.exception.RestApiException;
import moadong.global.util.AESCipher;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;

@Component
@Slf4j
@RequiredArgsConstructor
public class ApplicantIdMessageConsumer {

private final ClubApplicantsRepository clubApplicantsRepository;
private final ClubApplicationFormsRepository clubApplicationFormsRepository;
private final AESCipher cipher;
private final GemmaService gemmaService;
private final ApplicantIdMessagePublisher publisher;

@RabbitListener(queues = "${rabbitmq.summary.queue}", concurrency = "1")
public void receiveMessage(ApplicantSummaryMessage message) {
StringBuilder prompt = new StringBuilder("너는 전문 면접관이다. 다음은 동아리 application의 질문과 지원자의 답변이다. 질문은 무시하고, 지원자의 '답변'에서 핵심만 뽑아라. summarize max length 100 response format: '{response: summarize}'. application: ");
ClubApplicant clubApplicant = clubApplicantsRepository.findById(message.applicantId()).orElseThrow(() -> new RestApiException(ErrorCode.APPLICANT_NOT_FOUND));
ClubApplicationForm clubApplicationForm = clubApplicationFormsRepository.findById(message.applicationFormId()).orElseThrow(() -> new RestApiException(ErrorCode.APPLICATION_NOT_FOUND));
Map<Long, ClubApplicationFormQuestion> questionMap = clubApplicationForm.getQuestions().stream()
.collect(Collectors.toMap(ClubApplicationFormQuestion::getId, Function.identity()));

try {
for (ClubQuestionAnswer answer : clubApplicant.getAnswers()) {
String decryptedValue = cipher.decrypt(answer.getValue());
prompt.append(answer.getId()).append(". ")
.append(questionMap.get(answer.getId()).getTitle())
.append(": ")
.append(decryptedValue);
prompt.append(",");
}
} catch (Exception e) {
log.error("AES_CIPHER_ERROR", e);
throw new RestApiException(ErrorCode.AES_CIPHER_ERROR);
}

AIResponse summarizeContent = gemmaService.getSummarizeContent(prompt.toString());

clubApplicant.updateMemo(summarizeContent.response());

clubApplicantsRepository.save(clubApplicant);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package moadong.club.summary;

import lombok.RequiredArgsConstructor;
import moadong.club.payload.dto.ApplicantSummaryMessage;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class ApplicantIdMessagePublisher {

private final RabbitTemplate applicantIdTemplate;

public void addApplicantIdToQueue(String applicationFormId, String applicantId) {
ApplicantSummaryMessage message = new ApplicantSummaryMessage(applicationFormId, applicantId);

applicantIdTemplate.convertAndSend(message);
}
}
12 changes: 12 additions & 0 deletions backend/src/main/java/moadong/gemma/dto/AIRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package moadong.gemma.dto;

import com.fasterxml.jackson.annotation.JsonProperty;

public record AIRequest(
String model,
String prompt,
String format,
boolean stream,
@JsonProperty("keep_alive") int keepAlive
) {
}
6 changes: 6 additions & 0 deletions backend/src/main/java/moadong/gemma/dto/AIResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package moadong.gemma.dto;

import com.fasterxml.jackson.annotation.JsonProperty;

public record AIResponse(@JsonProperty("response") String response) {
}
40 changes: 40 additions & 0 deletions backend/src/main/java/moadong/gemma/service/GemmaService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package moadong.gemma.service;

import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import moadong.gemma.dto.AIRequest;
import moadong.gemma.dto.AIResponse;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

@Service
@RequiredArgsConstructor
@Slf4j
public class GemmaService {

private final RestTemplate restTemplate;
private final ObjectMapper objectMapper;

@Value("${gemma.server.host}")
private String gemmaServerHost;

@Value("${gemma.server.port}")
private String gemmaServerPort;

public AIResponse getSummarizeContent(String prompt) {
try {
String gemmaServerUrl = "http://" + gemmaServerHost + ":" + gemmaServerPort + "/api/generate";
AIRequest request = new AIRequest("gemma3:4b", prompt, "json", false, -1);
AIResponse response = restTemplate.postForObject(gemmaServerUrl, request, AIResponse.class);
if (response != null) {
return objectMapper.readValue(response.response(), AIResponse.class);
}
} catch (Exception e) {
log.error("Json Serialize Error: ", e);
return null;
}
return null;
}
}
Loading
Loading