diff --git a/.gitignore b/.gitignore index 364c76a8..fa951fe1 100644 --- a/.gitignore +++ b/.gitignore @@ -24,7 +24,8 @@ bin/ /pinpoint-agent-2.2.3-NCP/logs/ComSsa-local-Id/pinpoint.log /src/main/resources/templates/normal-question-update.html /src/main/resources/static/js/updateQuestion.js - +/uploadData.json +/uploadData.jsonl ### IntelliJ IDEA ### .idea *.iws diff --git a/src/main/java/com/server/computerscience/chatbot/config/ChatGptConfig.java b/src/main/java/com/server/computerscience/chatbot/config/ChatGptConfig.java deleted file mode 100644 index 0b38acfe..00000000 --- a/src/main/java/com/server/computerscience/chatbot/config/ChatGptConfig.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.server.computerscience.chatbot.config; - -import java.util.ArrayList; -import java.util.List; - -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.http.HttpHeaders; -import org.springframework.http.MediaType; -import org.springframework.http.client.ClientHttpRequestInterceptor; -import org.springframework.web.client.RestTemplate; - -@Configuration -public class ChatGptConfig { - - @Value("${openai.secret-key}") - private String secretKey; - - @Bean - public RestTemplate restTemplate() { - RestTemplate restTemplate = new RestTemplate(); - // Interceptor를 사용하여 기본 헤더 설정 - List interceptors = new ArrayList<>(restTemplate.getInterceptors()); - interceptors.add((request, body, execution) -> { - HttpHeaders headers = request.getHeaders(); - headers.setBearerAuth(secretKey); - headers.setContentType(MediaType.APPLICATION_JSON); - return execution.execute(request, body); - }); - restTemplate.setInterceptors(interceptors); - return restTemplate; - } -} diff --git a/src/main/java/com/server/computerscience/chatbot/config/RestTemplateService.java b/src/main/java/com/server/computerscience/chatbot/config/RestTemplateService.java new file mode 100644 index 00000000..2997303f --- /dev/null +++ b/src/main/java/com/server/computerscience/chatbot/config/RestTemplateService.java @@ -0,0 +1,33 @@ +package com.server.computerscience.chatbot.config; + +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class RestTemplateService { + private final RestTemplate restTemplate; + + // 공통화된 sendPostRequest 메서드 + public ResponseEntity sendPostRequest( + String url, + String bearerToken, + MediaType contentType, + Object body, + Class responseType + ) { + HttpHeaders headers = new HttpHeaders(); + headers.set("Authorization", "Bearer " + bearerToken); + headers.setContentType(contentType); + HttpEntity requestEntity = new HttpEntity<>(body, headers); + + return restTemplate.exchange(url, HttpMethod.POST, requestEntity, responseType); + } +} diff --git a/src/main/java/com/server/computerscience/chatbot/controller/ExternalQuestionController.java b/src/main/java/com/server/computerscience/chatbot/controller/ExternalQuestionController.java new file mode 100644 index 00000000..793efda1 --- /dev/null +++ b/src/main/java/com/server/computerscience/chatbot/controller/ExternalQuestionController.java @@ -0,0 +1,42 @@ +package com.server.computerscience.chatbot.controller; + +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; + +import com.server.computerscience.chatbot.dto.request.ChatGptBatchRequestDto; +import com.server.computerscience.chatbot.dto.response.ChatGptBatchResponseDto; +import com.server.computerscience.chatbot.dto.response.ChatGptFileUploadResponseDto; +import com.server.computerscience.chatbot.service.implement.ChatGptService; +import com.server.computerscience.question.common.dto.request.RequestQuestionCommandDto; +import com.server.computerscience.question.common.service.ExternalQuestionService; + +import io.swagger.annotations.Api; +import lombok.RequiredArgsConstructor; + +@Controller +@Api(tags = {"AI 문제 수정 - ADMIN"}) +@RequestMapping("/admin") +@RequiredArgsConstructor +public class ExternalQuestionController { + + private final ExternalQuestionService externalQuestionService; + + private final ChatGptService chatGptService; + + @PostMapping("/chat-gpt/file/question") + public ResponseEntity updateQuestionToChatGpt( + @RequestBody RequestQuestionCommandDto requestQuestionCommandDto + ) { + return ResponseEntity.ok(externalQuestionService.sendQuestionToExternal(requestQuestionCommandDto)); + } + + @PostMapping("/chat-gpt/batch") + public ResponseEntity createBatchToChatGpt( + @RequestBody ChatGptBatchRequestDto requestBatchDto + ) { + return ResponseEntity.ok(chatGptService.sendBatchMessage(requestBatchDto)); + } +} diff --git a/src/main/java/com/server/computerscience/chatbot/domain/QuestionToChatGptContentMapper.java b/src/main/java/com/server/computerscience/chatbot/domain/QuestionToChatGptContentMapper.java new file mode 100644 index 00000000..5c17a261 --- /dev/null +++ b/src/main/java/com/server/computerscience/chatbot/domain/QuestionToChatGptContentMapper.java @@ -0,0 +1,43 @@ +package com.server.computerscience.chatbot.domain; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.stereotype.Service; + +import com.server.computerscience.chatbot.dto.request.ChatContentDto; +import com.server.computerscience.question.common.domain.Question; +import com.server.computerscience.question.license.domain.LicenseMultipleChoiceQuestion; +import com.server.computerscience.question.major.common.domain.MajorMultipleChoiceQuestion; + +@Service +public class QuestionToChatGptContentMapper { + + public List getContentsFromQuestion(List questions) { + List chatContentDtos = new ArrayList<>(); + for (Question question : questions) { + ChatContentDto chatContentDto = createChatContentDtoForChatGpt(question); + if (chatContentDto != null) { + chatContentDtos.add(chatContentDto); + } + } + return chatContentDtos; + } + + private ChatContentDto createChatContentDtoForChatGpt(Question question) { + if (question instanceof LicenseMultipleChoiceQuestion) { + LicenseMultipleChoiceQuestion licenseQuestion = (LicenseMultipleChoiceQuestion)question; + return ChatContentDto.from( + ChatContentType.TEXT, + licenseQuestion.toString() + ); + } else if (question instanceof MajorMultipleChoiceQuestion) { + MajorMultipleChoiceQuestion majorQuestion = (MajorMultipleChoiceQuestion)question; + return ChatContentDto.from( + ChatContentType.TEXT, + majorQuestion.toString() + ); + } + return null; // 지원되지 않는 Question 타입인 경우 null 반환 + } +} diff --git a/src/main/java/com/server/computerscience/chatbot/dto/request/ChatGptBatchRequestDto.java b/src/main/java/com/server/computerscience/chatbot/dto/request/ChatGptBatchRequestDto.java new file mode 100644 index 00000000..988b1f0e --- /dev/null +++ b/src/main/java/com/server/computerscience/chatbot/dto/request/ChatGptBatchRequestDto.java @@ -0,0 +1,33 @@ +package com.server.computerscience.chatbot.dto.request; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@NoArgsConstructor +@AllArgsConstructor +@Builder +@Getter +@ToString +public class ChatGptBatchRequestDto { + @JsonProperty("input_file_id") + private String inputFileId; + + @JsonProperty("endpoint") + private String endpoint; + + @JsonProperty("completion_window") + private String completionWindow; + + public static ChatGptBatchRequestDto from(String inputFileId, String endpoint, String completionWindow) { + return ChatGptBatchRequestDto.builder() + .inputFileId(inputFileId) + .endpoint(endpoint) + .completionWindow(completionWindow) + .build(); + } +} diff --git a/src/main/java/com/server/computerscience/chatbot/dto/request/ChatGptRequestFileUploadDto.java b/src/main/java/com/server/computerscience/chatbot/dto/request/ChatGptRequestFileUploadDto.java new file mode 100644 index 00000000..a8118653 --- /dev/null +++ b/src/main/java/com/server/computerscience/chatbot/dto/request/ChatGptRequestFileUploadDto.java @@ -0,0 +1,35 @@ +package com.server.computerscience.chatbot.dto.request; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Getter +@AllArgsConstructor +@NoArgsConstructor +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +@Builder +@ToString +public class ChatGptRequestFileUploadDto { + private String customId; + private String method; + private String url; + private ChatGptRestRequestDto body; + + public static ChatGptRequestFileUploadDto from(ChatGptRestRequestDto body, + String customId, + String method, + String url) { + return ChatGptRequestFileUploadDto.builder() + .body(body) + .customId(customId) + .method(method) + .url(url) + .build(); + } +} diff --git a/src/main/java/com/server/computerscience/chatbot/dto/request/ChatMessageDto.java b/src/main/java/com/server/computerscience/chatbot/dto/request/ChatMessageDto.java index b5475c38..66519ca4 100644 --- a/src/main/java/com/server/computerscience/chatbot/dto/request/ChatMessageDto.java +++ b/src/main/java/com/server/computerscience/chatbot/dto/request/ChatMessageDto.java @@ -22,9 +22,17 @@ public class ChatMessageDto { private List content; public static ChatMessageDto from(String text, ChatRole role) { + return ChatMessageDto.builder() .role(role.getLower()) .content(Collections.singletonList(ChatContentDto.from(ChatContentType.TEXT, text))) .build(); } + + public static ChatMessageDto from(List chatContents, ChatRole role) { + return ChatMessageDto.builder() + .role(role.getLower()) + .content(chatContents) + .build(); + } } diff --git a/src/main/java/com/server/computerscience/chatbot/dto/response/ChatGptBatchResponseDto.java b/src/main/java/com/server/computerscience/chatbot/dto/response/ChatGptBatchResponseDto.java new file mode 100644 index 00000000..1f0a5b9e --- /dev/null +++ b/src/main/java/com/server/computerscience/chatbot/dto/response/ChatGptBatchResponseDto.java @@ -0,0 +1,40 @@ +package com.server.computerscience.chatbot.dto.response; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Getter +@NoArgsConstructor +@ToString +public class ChatGptBatchResponseDto { + private String id; + private String object; + private String endpoint; + private String errors; + private String inputFileId; + private String completionWindow; + private String status; + private String outputFileId; + private String errorFileId; + private Long createdAt; + private Long inProgressAt; + private Long expiresAt; + private Long finalizingAt; + private Long completedAt; + private Long failedAt; + private Long expiredAt; + private Long cancellingAt; + private Long cancelledAt; + private RequestCounts requestCounts; + private Object metadata; + + @Getter + @NoArgsConstructor + @ToString + public static class RequestCounts { + private int total; + private int completed; + private int failed; + } +} diff --git a/src/main/java/com/server/computerscience/chatbot/dto/response/ChatGptFileUploadResponseDto.java b/src/main/java/com/server/computerscience/chatbot/dto/response/ChatGptFileUploadResponseDto.java new file mode 100644 index 00000000..5aa5fecb --- /dev/null +++ b/src/main/java/com/server/computerscience/chatbot/dto/response/ChatGptFileUploadResponseDto.java @@ -0,0 +1,21 @@ +package com.server.computerscience.chatbot.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Getter +@AllArgsConstructor +@NoArgsConstructor +@ToString +public class ChatGptFileUploadResponseDto { + private String id; + private String object; + private int bytes; + private long createdAt; + private String filename; + private String purpose; + private String status; + private String statusDetails; +} diff --git a/src/main/java/com/server/computerscience/chatbot/dto/response/ChatGptResponseDto.java b/src/main/java/com/server/computerscience/chatbot/dto/response/ChatGptResponseDto.java index 1405e9ec..47042791 100644 --- a/src/main/java/com/server/computerscience/chatbot/dto/response/ChatGptResponseDto.java +++ b/src/main/java/com/server/computerscience/chatbot/dto/response/ChatGptResponseDto.java @@ -19,6 +19,10 @@ public class ChatGptResponseDto { private Usage usage; private List choices; + public String getFirstChoiceContent() { + return this.getChoices().get(0).getMessage().getContent(); + } + @NoArgsConstructor @AllArgsConstructor @Getter diff --git a/src/main/java/com/server/computerscience/chatbot/service/implement/ChatGptService.java b/src/main/java/com/server/computerscience/chatbot/service/implement/ChatGptService.java index 8a52bc9f..c819e25e 100644 --- a/src/main/java/com/server/computerscience/chatbot/service/implement/ChatGptService.java +++ b/src/main/java/com/server/computerscience/chatbot/service/implement/ChatGptService.java @@ -1,12 +1,24 @@ package com.server.computerscience.chatbot.service.implement; +import java.time.LocalDateTime; +import java.util.Collections; import java.util.List; +import java.util.Objects; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.io.ByteArrayResource; +import org.springframework.http.MediaType; import org.springframework.stereotype.Service; -import org.springframework.web.client.RestTemplate; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import com.server.computerscience.chatbot.config.RestTemplateService; +import com.server.computerscience.chatbot.dto.request.ChatGptBatchRequestDto; +import com.server.computerscience.chatbot.dto.request.ChatGptRequestFileUploadDto; import com.server.computerscience.chatbot.dto.request.ChatGptRestRequestDto; import com.server.computerscience.chatbot.dto.request.ChatMessageDto; +import com.server.computerscience.chatbot.dto.response.ChatGptBatchResponseDto; +import com.server.computerscience.chatbot.dto.response.ChatGptFileUploadResponseDto; import com.server.computerscience.chatbot.dto.response.ChatGptResponseDto; import lombok.RequiredArgsConstructor; @@ -15,16 +27,70 @@ @RequiredArgsConstructor public class ChatGptService { private final String model = "gpt-4o-mini"; - private final String payingApiUrl = "https://api.openai.com/v1/chat/completions"; - private final RestTemplate restTemplate; + private final String BASE_URL = "https://api.openai.com"; + private final String advancedChatApiUrl = "/v1/chat/completions"; + private final String fileUploadUrl = "/v1/files"; + private final String batchCreateUrl = "/v1/batches"; + private final RestTemplateService restTemplateService; + private final FileConvertService fileConvertService; + @Value("${openai.secret-key}") + private String secretKey; - public String chat(List chatMessages) { + private static MultiValueMap makeBodyForFileUpload(ByteArrayResource resource) { + // body 생성 + MultiValueMap body = new LinkedMultiValueMap<>(); + body.add("file", resource); + body.add("purpose", "batch"); + return body; + } + + public String sendChatMessage(List chatMessages) { ChatGptRestRequestDto chatGptRestRequestDto = ChatGptRestRequestDto.from(model, chatMessages); - ChatGptResponseDto chatGptResponseDto = restTemplate.postForObject( - payingApiUrl, - chatGptRestRequestDto, - ChatGptResponseDto.class); - return chatGptResponseDto.getChoices().get(0).getMessage().getContent(); + return Objects.requireNonNull(restTemplateService.sendPostRequest( + BASE_URL + advancedChatApiUrl, + secretKey, + MediaType.APPLICATION_JSON, + chatGptRestRequestDto, + ChatGptResponseDto.class + ).getBody()) + .getFirstChoiceContent(); + } + + public ChatGptFileUploadResponseDto sendFileUploadMessage(List chatMessages) { + List dataForFile = makeFileUploadDto(chatMessages); + // 메모리 내 ByteArrayResource로 변환 + ByteArrayResource resource = fileConvertService.dataToChatGptJson(dataForFile); + MultiValueMap body = makeBodyForFileUpload(resource); + + return restTemplateService.sendPostRequest( + BASE_URL + fileUploadUrl, + secretKey, + MediaType.MULTIPART_FORM_DATA, + body, + ChatGptFileUploadResponseDto.class + ).getBody(); + } + + private List makeFileUploadDto(List chatMessages) { + ChatGptRequestFileUploadDto chatGptRequestFileUploadDto = ChatGptRequestFileUploadDto.from( + ChatGptRestRequestDto.from(model, chatMessages), + String.valueOf(LocalDateTime.now()), + "POST", + advancedChatApiUrl + ); + + List dataForFile = Collections.singletonList(chatGptRequestFileUploadDto); + return dataForFile; + } + + public ChatGptBatchResponseDto sendBatchMessage(ChatGptBatchRequestDto chatGptBatchRequestDto) { + return restTemplateService.sendPostRequest( + BASE_URL + batchCreateUrl, + secretKey, + MediaType.APPLICATION_JSON, + chatGptBatchRequestDto, + ChatGptBatchResponseDto.class + ).getBody(); } } diff --git a/src/main/java/com/server/computerscience/chatbot/service/implement/ChatManageService.java b/src/main/java/com/server/computerscience/chatbot/service/implement/ChatManageService.java index fbe9e225..e70af1fc 100644 --- a/src/main/java/com/server/computerscience/chatbot/service/implement/ChatManageService.java +++ b/src/main/java/com/server/computerscience/chatbot/service/implement/ChatManageService.java @@ -1,12 +1,15 @@ package com.server.computerscience.chatbot.service.implement; +import java.util.Arrays; import java.util.List; import org.springframework.stereotype.Service; import com.server.computerscience.chatbot.domain.ChatRole; import com.server.computerscience.chatbot.dto.request.ChatBotRequestDto; +import com.server.computerscience.chatbot.dto.request.ChatContentDto; import com.server.computerscience.chatbot.dto.request.ChatMessageDto; +import com.server.computerscience.chatbot.dto.response.ChatGptFileUploadResponseDto; import lombok.RequiredArgsConstructor; @@ -19,7 +22,7 @@ public class ChatManageService { private final ChatCacheService chatCacheService; private final ChatGptService chatGptService; - public String respond(String userId, ChatBotRequestDto chatBotRequestDto) { + public String talkForChat(String userId, ChatBotRequestDto chatBotRequestDto) { if (chatCacheService.getUsedChance(userId) >= MAX_CHAT_CHANCE) { return NO_MORE_CHANCE; } @@ -31,8 +34,8 @@ public String respond(String userId, ChatBotRequestDto chatBotRequestDto) { /** * 챗봇에게 받은 답변 또한 이전 대화 기록에 넣는다. */ - String answer = chatGptService.chat(chatMessages); - afterRespond(userId, answer, chatMessages); + String answer = chatGptService.sendChatMessage(chatMessages); + afterRespond(userId, answer); return answer; } @@ -41,9 +44,15 @@ private List beforeRespond(String userId, ChatBotRequestDto chat return chatCacheService.saveChatMessage(userId, chatMessageFromUser, MAX_MESSAGES_SIZE); } - private void afterRespond(String userId, String answer, List chatMessages) { + private void afterRespond(String userId, String answer) { ChatMessageDto chatMessageFromAssistant = ChatMessageDto.from(answer, ChatRole.ASSISTANT); chatCacheService.saveChatMessage(userId, chatMessageFromAssistant, MAX_MESSAGES_SIZE); chatCacheService.increaseUsedChance(userId); } + + public ChatGptFileUploadResponseDto talkForBatch(List chatMessages, String command) { + ChatMessageDto commandMessage = ChatMessageDto.from(command, ChatRole.SYSTEM); + ChatMessageDto chatMessage = ChatMessageDto.from(chatMessages, ChatRole.USER); + return chatGptService.sendFileUploadMessage(Arrays.asList(commandMessage, chatMessage)); + } } diff --git a/src/main/java/com/server/computerscience/chatbot/service/implement/FileConvertService.java b/src/main/java/com/server/computerscience/chatbot/service/implement/FileConvertService.java new file mode 100644 index 00000000..5c314e9d --- /dev/null +++ b/src/main/java/com/server/computerscience/chatbot/service/implement/FileConvertService.java @@ -0,0 +1,42 @@ +package com.server.computerscience.chatbot.service.implement; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.util.List; + +import org.springframework.core.io.ByteArrayResource; +import org.springframework.stereotype.Service; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.server.computerscience.chatbot.dto.request.ChatGptRequestFileUploadDto; + +@Service +public class FileConvertService { + private final ObjectMapper objectMapper = new ObjectMapper(); + + public ByteArrayResource dataToChatGptJson(List dataForFile) { + try ( + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + OutputStreamWriter writer = new OutputStreamWriter(outputStream) + ) { + for (ChatGptRequestFileUploadDto dto : dataForFile) { + String jsonLine = objectMapper.writeValueAsString(dto); + writer.write(jsonLine); + writer.write("\n"); // 줄 바꿈 추가 + } + writer.flush(); // OutputStreamWriter 버퍼 비우기 + + return new ByteArrayResource(outputStream.toByteArray()) { + @Override + public String getFilename() { + return "uploadData.jsonl"; + } + }; + } catch (IOException e) { + e.printStackTrace(); + return null; + } + } + +} diff --git a/src/main/java/com/server/computerscience/chatbot/service/implement/LoginChatBotService.java b/src/main/java/com/server/computerscience/chatbot/service/implement/LoginChatBotService.java index 20a73a25..9e36e50a 100644 --- a/src/main/java/com/server/computerscience/chatbot/service/implement/LoginChatBotService.java +++ b/src/main/java/com/server/computerscience/chatbot/service/implement/LoginChatBotService.java @@ -23,6 +23,6 @@ public String talkToAssistant(ChatBotRequestDto chatBotRequestDto, OAuth2User us return NOT_LOGIN; } String userId = (String)user.getAttributes().get(userIdentifier); - return chatManageService.respond(userId, chatBotRequestDto); + return chatManageService.talkForChat(userId, chatBotRequestDto); } } diff --git a/src/main/java/com/server/computerscience/config/WebConfig.java b/src/main/java/com/server/computerscience/config/WebConfig.java index 77f1a918..221a3a62 100644 --- a/src/main/java/com/server/computerscience/config/WebConfig.java +++ b/src/main/java/com/server/computerscience/config/WebConfig.java @@ -1,6 +1,8 @@ package com.server.computerscience.config; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @@ -12,4 +14,9 @@ public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/robots.txt").addResourceLocations("classpath:/static/robots.txt"); registry.addResourceHandler("/sitemap.xml").addResourceLocations("classpath:/static/sitemap.xml"); } + + @Bean + public RestTemplate restTemplate() { + return new RestTemplate(); + } } diff --git a/src/main/java/com/server/computerscience/question/common/domain/QuestionChoice.java b/src/main/java/com/server/computerscience/question/common/domain/QuestionChoice.java index 25210911..dfa7019d 100644 --- a/src/main/java/com/server/computerscience/question/common/domain/QuestionChoice.java +++ b/src/main/java/com/server/computerscience/question/common/domain/QuestionChoice.java @@ -4,12 +4,14 @@ import lombok.Getter; import lombok.NoArgsConstructor; +import lombok.ToString; import lombok.experimental.SuperBuilder; @Getter @SuperBuilder @NoArgsConstructor @MappedSuperclass +@ToString(exclude = "question") public abstract class QuestionChoice { private String text; private int selectedCount; diff --git a/src/main/java/com/server/computerscience/question/common/dto/request/RequestQuestionCommandDto.java b/src/main/java/com/server/computerscience/question/common/dto/request/RequestQuestionCommandDto.java new file mode 100644 index 00000000..e25ac830 --- /dev/null +++ b/src/main/java/com/server/computerscience/question/common/dto/request/RequestQuestionCommandDto.java @@ -0,0 +1,20 @@ +package com.server.computerscience.question.common.dto.request; + +import java.util.List; + +import com.server.computerscience.question.common.domain.QuestionCategory; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class RequestQuestionCommandDto { + private String command; + private boolean multipleChoice; + private List questionCategories; +} diff --git a/src/main/java/com/server/computerscience/question/common/service/ExternalQuestionService.java b/src/main/java/com/server/computerscience/question/common/service/ExternalQuestionService.java new file mode 100644 index 00000000..0f3610fb --- /dev/null +++ b/src/main/java/com/server/computerscience/question/common/service/ExternalQuestionService.java @@ -0,0 +1,11 @@ +package com.server.computerscience.question.common.service; + +import org.springframework.stereotype.Service; + +import com.server.computerscience.chatbot.dto.response.ChatGptFileUploadResponseDto; +import com.server.computerscience.question.common.dto.request.RequestQuestionCommandDto; + +@Service +public interface ExternalQuestionService { + ChatGptFileUploadResponseDto sendQuestionToExternal(RequestQuestionCommandDto requestQuestionCommandDto); +} diff --git a/src/main/java/com/server/computerscience/question/common/service/QuestionSelectorService.java b/src/main/java/com/server/computerscience/question/common/service/QuestionSelectorService.java index b219ac93..bf2fb7b3 100644 --- a/src/main/java/com/server/computerscience/question/common/service/QuestionSelectorService.java +++ b/src/main/java/com/server/computerscience/question/common/service/QuestionSelectorService.java @@ -4,6 +4,8 @@ import org.springframework.stereotype.Service; +import com.server.computerscience.question.common.domain.Question; +import com.server.computerscience.question.common.domain.QuestionCategory; import com.server.computerscience.question.license.domain.LicenseCategory; import com.server.computerscience.question.license.domain.LicenseSession; @@ -16,4 +18,9 @@ public interface QuestionSelectorService { List getLicenseCategories(); List getLicenseSessions(LicenseCategory licenseCategory); + + /* + 카테고리에 맞는 모든 문제를 가져온다(혼합될 수 있음) + */ + List getAllQuestions(List questionCategories, boolean multipleChoice); } diff --git a/src/main/java/com/server/computerscience/question/common/service/implement/BasicQuestionSelectorService.java b/src/main/java/com/server/computerscience/question/common/service/implement/BasicQuestionSelectorService.java index ea99be27..ef007800 100644 --- a/src/main/java/com/server/computerscience/question/common/service/implement/BasicQuestionSelectorService.java +++ b/src/main/java/com/server/computerscience/question/common/service/implement/BasicQuestionSelectorService.java @@ -1,17 +1,21 @@ package com.server.computerscience.question.common.service.implement; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; import org.springframework.stereotype.Service; +import com.server.computerscience.question.common.domain.Question; import com.server.computerscience.question.common.domain.QuestionCategory; import com.server.computerscience.question.common.domain.QuestionLevel; import com.server.computerscience.question.common.service.QuestionSelectorService; import com.server.computerscience.question.license.domain.LicenseCategory; import com.server.computerscience.question.license.domain.LicenseSession; +import com.server.computerscience.question.license.repository.LicenseMultipleChoiceQuestionRepository; import com.server.computerscience.question.license.repository.LicenseSessionRepository; +import com.server.computerscience.question.major.common.repository.MajorMultipleChoiceQuestionRepository; import lombok.RequiredArgsConstructor; @@ -19,6 +23,8 @@ @RequiredArgsConstructor public class BasicQuestionSelectorService implements QuestionSelectorService { private final LicenseSessionRepository licenseSessionRepository; + private final MajorMultipleChoiceQuestionRepository majorMultipleChoiceQuestionRepository; + private final LicenseMultipleChoiceQuestionRepository licenseMultipleChoiceQuestionRepository; @Override public List getCategories() { @@ -45,4 +51,16 @@ public List getLicenseCategories() { public List getLicenseSessions(LicenseCategory licenseCategory) { return licenseSessionRepository.findAllByLicenseCategory(licenseCategory); } + + @Override + public List getAllQuestions(List questionCategories, boolean multipleChoice) { + List questions = new ArrayList<>(); + if (multipleChoice) { + questions.addAll(licenseMultipleChoiceQuestionRepository.findAllByQuestionCategoriesFetchChoices( + questionCategories)); + questions.addAll(majorMultipleChoiceQuestionRepository.findAllByQuestionCategoriesFetchChoices( + questionCategories)); + } + return questions; + } } diff --git a/src/main/java/com/server/computerscience/question/common/service/implement/ExternalSenderQuestion.java b/src/main/java/com/server/computerscience/question/common/service/implement/ExternalSenderQuestion.java new file mode 100644 index 00000000..b773ac08 --- /dev/null +++ b/src/main/java/com/server/computerscience/question/common/service/implement/ExternalSenderQuestion.java @@ -0,0 +1,36 @@ +package com.server.computerscience.question.common.service.implement; + +import java.util.List; + +import org.springframework.stereotype.Service; + +import com.server.computerscience.chatbot.domain.QuestionToChatGptContentMapper; +import com.server.computerscience.chatbot.dto.response.ChatGptFileUploadResponseDto; +import com.server.computerscience.chatbot.service.implement.ChatManageService; +import com.server.computerscience.question.common.domain.Question; +import com.server.computerscience.question.common.dto.request.RequestQuestionCommandDto; +import com.server.computerscience.question.common.service.ExternalQuestionService; +import com.server.computerscience.question.common.service.QuestionSelectorService; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class ExternalSenderQuestion implements ExternalQuestionService { + private final QuestionSelectorService questionSelectorService; + private final QuestionToChatGptContentMapper questionToChatGptContentMapper; + private final ChatManageService chatManageService; + + @Override + public ChatGptFileUploadResponseDto sendQuestionToExternal(RequestQuestionCommandDto requestQuestionCommandDto) { + /** + * 카테고리에 해당된 모든 문제를 가져온다. + */ + List question = questionSelectorService.getAllQuestions(requestQuestionCommandDto + .getQuestionCategories(), + requestQuestionCommandDto.isMultipleChoice()); + + return chatManageService.talkForBatch(questionToChatGptContentMapper.getContentsFromQuestion(question), + requestQuestionCommandDto.getCommand()); + } +} diff --git a/src/main/java/com/server/computerscience/question/license/domain/LicenseMultipleChoiceQuestion.java b/src/main/java/com/server/computerscience/question/license/domain/LicenseMultipleChoiceQuestion.java index d154d53d..1a94744e 100644 --- a/src/main/java/com/server/computerscience/question/license/domain/LicenseMultipleChoiceQuestion.java +++ b/src/main/java/com/server/computerscience/question/license/domain/LicenseMultipleChoiceQuestion.java @@ -22,12 +22,14 @@ import lombok.Getter; import lombok.NoArgsConstructor; +import lombok.ToString; import lombok.experimental.SuperBuilder; @Entity @Getter @SuperBuilder @NoArgsConstructor +@ToString public class LicenseMultipleChoiceQuestion extends Question implements ChoiceProvider { @Enumerated(value = EnumType.STRING) protected LicenseCategory licenseCategory; @@ -37,7 +39,7 @@ public class LicenseMultipleChoiceQuestion extends Question implements ChoicePro @ManyToOne @JoinColumn(name = "license_session_id") private LicenseSession licenseSession; - @OneToMany(mappedBy = "licenseMultipleChoiceQuestion", cascade = CascadeType.ALL, orphanRemoval = true) + @OneToMany(mappedBy = "question", cascade = CascadeType.ALL, orphanRemoval = true) private List questionChoices; public static LicenseMultipleChoiceQuestion makeWithDto(RequestMakeMultipleChoiceQuestionDto dto, diff --git a/src/main/java/com/server/computerscience/question/license/domain/LicenseQuestionChoice.java b/src/main/java/com/server/computerscience/question/license/domain/LicenseQuestionChoice.java index 577f84c8..43de2fa1 100644 --- a/src/main/java/com/server/computerscience/question/license/domain/LicenseQuestionChoice.java +++ b/src/main/java/com/server/computerscience/question/license/domain/LicenseQuestionChoice.java @@ -24,12 +24,12 @@ public class LicenseQuestionChoice extends QuestionChoice { private Long id; @ManyToOne @JoinColumn(name = "license_multiple_choice_question_id") - private LicenseMultipleChoiceQuestion licenseMultipleChoiceQuestion; + private LicenseMultipleChoiceQuestion question; public LicenseQuestionChoice(String text, int selectedCount, boolean answerStatus, - LicenseMultipleChoiceQuestion licenseMultipleChoiceQuestion) { + LicenseMultipleChoiceQuestion question) { super(text, selectedCount, answerStatus); - this.licenseMultipleChoiceQuestion = licenseMultipleChoiceQuestion; + this.question = question; } public static LicenseQuestionChoice from( @@ -39,7 +39,7 @@ public static LicenseQuestionChoice from( .text(dto.getText()) .selectedCount(0) .answerStatus(dto.isAnswerStatus()) - .licenseMultipleChoiceQuestion(licenseMultipleChoiceQuestion) + .question(licenseMultipleChoiceQuestion) .build(); licenseMultipleChoiceQuestion.getQuestionChoices().add(questionChoice); return questionChoice; diff --git a/src/main/java/com/server/computerscience/question/license/repository/LicenseMultipleChoiceQuestionRepository.java b/src/main/java/com/server/computerscience/question/license/repository/LicenseMultipleChoiceQuestionRepository.java index d88bf47f..b42b734b 100644 --- a/src/main/java/com/server/computerscience/question/license/repository/LicenseMultipleChoiceQuestionRepository.java +++ b/src/main/java/com/server/computerscience/question/license/repository/LicenseMultipleChoiceQuestionRepository.java @@ -8,6 +8,7 @@ import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; +import com.server.computerscience.question.common.domain.QuestionCategory; import com.server.computerscience.question.license.domain.LicenseMultipleChoiceQuestion; @Repository @@ -23,4 +24,11 @@ List findAllByLicenseSessionIdFetchChoices( + "LEFT JOIN FETCH lnq.questionChoices " + "WHERE lnq.id = :questionId ") Optional findByIdFetchChoices(@Param("questionId") Long questionId); + + @Query("SELECT DISTINCT lnq FROM LicenseMultipleChoiceQuestion lnq " + + "LEFT JOIN FETCH lnq.questionChoices " + + "WHERE lnq.questionCategory IN :questionCategories ") + List findAllByQuestionCategoriesFetchChoices( + @Param("questionCategories") List questionCategories); + } diff --git a/src/main/java/com/server/computerscience/question/major/admin/controller/AdminMajorQuestionViewController.java b/src/main/java/com/server/computerscience/question/major/admin/controller/AdminMajorQuestionViewController.java index 0dc7f854..49bf7a15 100644 --- a/src/main/java/com/server/computerscience/question/major/admin/controller/AdminMajorQuestionViewController.java +++ b/src/main/java/com/server/computerscience/question/major/admin/controller/AdminMajorQuestionViewController.java @@ -1,11 +1,14 @@ package com.server.computerscience.question.major.admin.controller; +import java.util.stream.Collectors; + import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; +import com.server.computerscience.question.common.dto.response.ResponseClassifiedMultipleQuestionDto; import com.server.computerscience.question.major.admin.service.AdminMajorQuestionClassifiedGetService; import lombok.RequiredArgsConstructor; @@ -23,7 +26,10 @@ public class AdminMajorQuestionViewController { @GetMapping("/question/update") public String updateQuestionPage(Model model) { model.addAttribute("classifiedQuestions", - adminMajorQuestionClassifiedGetService.getClassifiedAllMajorQuestions()); + adminMajorQuestionClassifiedGetService.getClassifiedAllMajorQuestions() + .entrySet().stream() + .map(entry -> ResponseClassifiedMultipleQuestionDto.forAdmin(entry.getKey(), entry.getValue())) + .collect(Collectors.toList())); model.addAttribute(baseUrl, resourceBaseUrl); return "major-question-update"; } diff --git a/src/main/java/com/server/computerscience/question/major/admin/service/implement/BasicAdminMajorQuestionMakeService.java b/src/main/java/com/server/computerscience/question/major/admin/service/implement/BasicAdminMajorQuestionMakeService.java index fb1b908e..6990273b 100644 --- a/src/main/java/com/server/computerscience/question/major/admin/service/implement/BasicAdminMajorQuestionMakeService.java +++ b/src/main/java/com/server/computerscience/question/major/admin/service/implement/BasicAdminMajorQuestionMakeService.java @@ -12,7 +12,7 @@ import com.server.computerscience.question.major.admin.service.DuplicateQuestionDetector; import com.server.computerscience.question.major.common.domain.MajorMultipleChoiceQuestion; import com.server.computerscience.question.major.common.exception.DuplicateQuestionException; -import com.server.computerscience.question.major.common.repository.MajorQuestionRepository; +import com.server.computerscience.question.major.common.repository.MajorMultipleChoiceQuestionRepository; import lombok.RequiredArgsConstructor; @@ -21,7 +21,7 @@ @Transactional public class BasicAdminMajorQuestionMakeService implements AdminMajorQuestionMakeService { - private final MajorQuestionRepository majorQuestionRepository; + private final MajorMultipleChoiceQuestionRepository majorMultipleChoiceQuestionRepository; private final QuestionChoiceService questionChoiceService; private final DuplicateQuestionDetector duplicateQuestionDetector; @@ -55,7 +55,7 @@ public MajorMultipleChoiceQuestion makeMultipleChoiceQuestion( * 매번 DB에서 새롭게 조회 후 검증한다.(DTO 자체의 중복된 데이터) */ private boolean isNotDuplicateQuestion(RequestMakeMultipleChoiceQuestionDto requestDto) { - return majorQuestionRepository.findAll().stream() + return majorMultipleChoiceQuestionRepository.findAll().stream() .noneMatch(existingQuestion -> duplicateQuestionDetector.isQuestionDuplicate( existingQuestion.getContent(), requestDto.getContent())); } @@ -66,7 +66,7 @@ private boolean isNotDuplicateQuestion(RequestMakeMultipleChoiceQuestionDto requ private MajorMultipleChoiceQuestion saveMajorMultipleChoiceQuestion( RequestMakeMultipleChoiceQuestionDto requestDto) { MajorMultipleChoiceQuestion question = MajorMultipleChoiceQuestion.makeWithDto(requestDto); - majorQuestionRepository.save(question); + majorMultipleChoiceQuestionRepository.save(question); questionChoiceService.saveWith(requestDto, question); return question; } diff --git a/src/main/java/com/server/computerscience/question/major/common/domain/MajorMultipleChoiceQuestion.java b/src/main/java/com/server/computerscience/question/major/common/domain/MajorMultipleChoiceQuestion.java index bebde0d9..32e11d31 100644 --- a/src/main/java/com/server/computerscience/question/major/common/domain/MajorMultipleChoiceQuestion.java +++ b/src/main/java/com/server/computerscience/question/major/common/domain/MajorMultipleChoiceQuestion.java @@ -32,7 +32,7 @@ public class MajorMultipleChoiceQuestion extends Question implements ChoiceProvi @Id @GeneratedValue(strategy = GenerationType.SEQUENCE) private Long id; - @OneToMany(mappedBy = "majorMultipleChoiceQuestion", cascade = CascadeType.ALL, orphanRemoval = true) + @OneToMany(mappedBy = "question", cascade = CascadeType.ALL, orphanRemoval = true) private List questionChoices; public static MajorMultipleChoiceQuestion makeWithDto(RequestMakeMultipleChoiceQuestionDto dto) { diff --git a/src/main/java/com/server/computerscience/question/major/common/domain/MajorQuestionChoice.java b/src/main/java/com/server/computerscience/question/major/common/domain/MajorQuestionChoice.java index ae9bc1dd..20d34563 100644 --- a/src/main/java/com/server/computerscience/question/major/common/domain/MajorQuestionChoice.java +++ b/src/main/java/com/server/computerscience/question/major/common/domain/MajorQuestionChoice.java @@ -19,19 +19,18 @@ @NoArgsConstructor @SuperBuilder public class MajorQuestionChoice extends QuestionChoice { - @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @ManyToOne @JoinColumn(name = "major_multiple_choice_question_id") - private MajorMultipleChoiceQuestion majorMultipleChoiceQuestion; + private MajorMultipleChoiceQuestion question; public MajorQuestionChoice(String text, int selectedCount, boolean answerStatus, - MajorMultipleChoiceQuestion majorMultipleChoiceQuestion) { + MajorMultipleChoiceQuestion question) { super(text, selectedCount, answerStatus); - this.majorMultipleChoiceQuestion = majorMultipleChoiceQuestion; + this.question = question; } public static MajorQuestionChoice fromMajorQuestion(RequestMakeQuestionChoiceDto dto, @@ -40,10 +39,9 @@ public static MajorQuestionChoice fromMajorQuestion(RequestMakeQuestionChoiceDto .text(dto.getText()) .selectedCount(0) .answerStatus(dto.isAnswerStatus()) - .majorMultipleChoiceQuestion(majorMultipleChoiceQuestion) + .question(majorMultipleChoiceQuestion) .build(); majorMultipleChoiceQuestion.getQuestionChoices().add(majorQuestionChoice); return majorQuestionChoice; } - } diff --git a/src/main/java/com/server/computerscience/question/major/common/repository/MajorQuestionRepository.java b/src/main/java/com/server/computerscience/question/major/common/repository/MajorMultipleChoiceQuestionRepository.java similarity index 87% rename from src/main/java/com/server/computerscience/question/major/common/repository/MajorQuestionRepository.java rename to src/main/java/com/server/computerscience/question/major/common/repository/MajorMultipleChoiceQuestionRepository.java index 7dd40d9a..169e438a 100644 --- a/src/main/java/com/server/computerscience/question/major/common/repository/MajorQuestionRepository.java +++ b/src/main/java/com/server/computerscience/question/major/common/repository/MajorMultipleChoiceQuestionRepository.java @@ -13,7 +13,7 @@ import com.server.computerscience.question.major.common.domain.MajorMultipleChoiceQuestion; @Repository -public interface MajorQuestionRepository extends JpaRepository { +public interface MajorMultipleChoiceQuestionRepository extends JpaRepository { /** * 허용되지 않은 문제까지 조회 */ @@ -44,6 +44,13 @@ List findFetchChoicesWithCategoriesAndLevels( + "WHERE nq.canBeShortAnswered = true") List findFetchChoicesShortAnswered(); + @Query("SELECT DISTINCT nq FROM MajorMultipleChoiceQuestion nq " + + "LEFT JOIN FETCH nq.questionChoices " + + "WHERE nq.questionCategory IN :questionCategories ") + List findAllByQuestionCategoriesFetchChoices( + @Param("questionCategories") List questionCategories + ); + /** * 허용된 문제들만 조회 (ifApproved가 true인 경우) */ diff --git a/src/main/java/com/server/computerscience/question/major/common/service/implement/MajorMultipleChoiceQuestionDbService.java b/src/main/java/com/server/computerscience/question/major/common/service/implement/MajorMultipleChoiceQuestionDbService.java index bfd9f56a..93a44668 100644 --- a/src/main/java/com/server/computerscience/question/major/common/service/implement/MajorMultipleChoiceQuestionDbService.java +++ b/src/main/java/com/server/computerscience/question/major/common/service/implement/MajorMultipleChoiceQuestionDbService.java @@ -9,7 +9,7 @@ import com.server.computerscience.question.common.domain.QuestionCategory; import com.server.computerscience.question.common.domain.QuestionLevel; import com.server.computerscience.question.major.common.domain.MajorMultipleChoiceQuestion; -import com.server.computerscience.question.major.common.repository.MajorQuestionRepository; +import com.server.computerscience.question.major.common.repository.MajorMultipleChoiceQuestionRepository; import lombok.RequiredArgsConstructor; @@ -17,50 +17,51 @@ @RequiredArgsConstructor @Transactional public class MajorMultipleChoiceQuestionDbService { - private final MajorQuestionRepository majorQuestionRepository; + private final MajorMultipleChoiceQuestionRepository majorMultipleChoiceQuestionRepository; /** * 허용되지 않은 모든 문제까지 조회 (주로 관리자) */ // 기본 개별 조회 public MajorMultipleChoiceQuestion findById(Long id) { - return majorQuestionRepository.findById(id).orElseThrow(NoSuchElementException::new); + return majorMultipleChoiceQuestionRepository.findById(id).orElseThrow(NoSuchElementException::new); } // 카테고리, 레벨로 조회 - 선택지까지 public List getFetchChoicesByCategoriesAndLevels(List categories, List questionLevels) { - return majorQuestionRepository.findFetchChoicesWithCategoriesAndLevels(categories, questionLevels); + return majorMultipleChoiceQuestionRepository.findFetchChoicesWithCategoriesAndLevels(categories, + questionLevels); } // 전체 조회 - 선택지까지, 주관식 가능한 것들만 public List findAllFetchChoicesShortAnswered() { - return majorQuestionRepository.findFetchChoicesShortAnswered(); + return majorMultipleChoiceQuestionRepository.findFetchChoicesShortAnswered(); } // 전체 조회 -선택지까지 public List findAllFetchChoices() { - return majorQuestionRepository.findFetchChoices(); + return majorMultipleChoiceQuestionRepository.findFetchChoices(); } //전체 조회 - 선택지까지, 정렬 public List findAllFetchChoicesSortedByApproveAndShortAnswered() { - return majorQuestionRepository.findFetchChoicesSortedByIfApprovedAndCanBeShortAnswered(); + return majorMultipleChoiceQuestionRepository.findFetchChoicesSortedByIfApprovedAndCanBeShortAnswered(); } // 개별 조회 - 선택지까지 public MajorMultipleChoiceQuestion findByIdFetchChoices(Long id) { - return majorQuestionRepository.findByIdFetchChoices(id).orElseThrow(NoSuchElementException::new); + return majorMultipleChoiceQuestionRepository.findByIdFetchChoices(id).orElseThrow(NoSuchElementException::new); } // id로 삭제 public void deleteById(Long id) { - majorQuestionRepository.deleteById(id); + majorMultipleChoiceQuestionRepository.deleteById(id); } // 개별 삭제 public void deleteNormalQuestion(MajorMultipleChoiceQuestion majorMultipleChoiceQuestion) { - majorQuestionRepository.delete(majorMultipleChoiceQuestion); + majorMultipleChoiceQuestionRepository.delete(majorMultipleChoiceQuestion); } /** @@ -69,14 +70,16 @@ public void deleteNormalQuestion(MajorMultipleChoiceQuestion majorMultipleChoice // 카테고리, 레벨로 조회 - 선택지까지 public List findAllFetchChoicesByCategoriesAndLevelsApproved( List categories, List questionLevels) { - return majorQuestionRepository.findFetchChoicesWithCategoriesAndLevelsAndIfApproved(categories, questionLevels); + return majorMultipleChoiceQuestionRepository.findFetchChoicesWithCategoriesAndLevelsAndIfApproved(categories, + questionLevels); } // 카테고리, 레벨로 조회 - 선택지까지, 주관식만 public List findAllFetchChoicesByCategoriesAndLevelsApprovedAndShortAnswered( List categories, List questionLevels) { - return majorQuestionRepository.findFetchChoicesWithCategoriesAndLevelsAndIfApprovedAndCanBeShortAnswered( - categories, questionLevels); + return majorMultipleChoiceQuestionRepository + .findFetchChoicesWithCategoriesAndLevelsAndIfApprovedAndCanBeShortAnswered( + categories, questionLevels); } } diff --git a/src/main/resources/static/css/global-index/style.css b/src/main/resources/static/css/global-index/style.css index 77a53a3f..c3ae9112 100644 --- a/src/main/resources/static/css/global-index/style.css +++ b/src/main/resources/static/css/global-index/style.css @@ -23,7 +23,6 @@ margin: 0; padding: 0; position: relative; - font-family: "NanumSquare Neo OTF-Bd", Helvetica; /* 좀 더 굵은 폰트 스타일 */ color: #333333; /* 글씨를 약간 더 부드러운 검정으로 */ font-size: 2vw; line-height: 4vh; @@ -78,7 +77,6 @@ transform: translateX(-50%); /* 수평 가운데 정렬 */ display: flex; align-items: center; - /*font-family: "Markazi Text-Bold", Helvetica;*/ font-weight: 700; color: #000000; text-align: center; @@ -89,6 +87,7 @@ .screen .mainTitle { font-size: 3vh; font-family: 'Cambay', sans-serif; + } .screen .main-logo-box .logo-image { diff --git a/src/main/resources/static/css/global.css b/src/main/resources/static/css/global.css index f41f9471..88c2074a 100644 --- a/src/main/resources/static/css/global.css +++ b/src/main/resources/static/css/global.css @@ -1,4 +1,5 @@ @import url("https://cdnjs.cloudflare.com/ajax/libs/meyer-reset/2.0/reset.min.css"); +@import url("//cdn.jsdelivr.net/npm/font-kopub@1.0"); * { -webkit-font-smoothing: antialiased; @@ -9,7 +10,7 @@ html, body { margin: 0px; height: 100%; - + font-family: 'KoPub Dotum', 'KoPub Batang', sans-serif; } /* a blue color as a generic focus style */ diff --git a/src/main/resources/static/css/index/style.css b/src/main/resources/static/css/index/style.css index 88c232ce..2f560762 100644 --- a/src/main/resources/static/css/index/style.css +++ b/src/main/resources/static/css/index/style.css @@ -53,7 +53,6 @@ position: relative; align-self: stretch; margin-top: -1px; - font-family: "NanumSquare Neo OTF-cBd", Helvetica; color: #404040; font-size: 25px; line-height: 37.5px; @@ -116,7 +115,8 @@ .screen .check-box-text { position: relative; width: fit-content; - font-family: "NanumSquare Neo OTF-cBd", Helvetica; + + color: #404040; font-size: 20px; line-height: 30px; @@ -174,16 +174,6 @@ /* width: 100px;*/ /* height: 130px;*/ /*}*/ -.screen .questionTitle { - font-family: "NanumSquare Neo OTF-Regular", Helvetica; - font-weight: 400; - color: #000000; - font-size: 24px; - letter-spacing: 0; - line-height: 36px; - width: 100%; - align-content: center; -} /** @@ -239,7 +229,6 @@ /* 텍스트 스타일 */ .start-button-text { - font-family: "NanumSquare Neo OTF-dEb", Helvetica; color: white; font-size: 25px; line-height: 37.5px; diff --git a/src/main/resources/static/css/license-index/style.css b/src/main/resources/static/css/license-index/style.css index 29f556bd..f2cc6897 100644 --- a/src/main/resources/static/css/license-index/style.css +++ b/src/main/resources/static/css/license-index/style.css @@ -53,7 +53,7 @@ position: relative; align-self: stretch; margin-top: -1px; - font-family: "NanumSquare Neo OTF-cBd", Helvetica; + color: #404040; font-size: 25px; line-height: 37.5px; @@ -162,16 +162,6 @@ /* width: 100px;*/ /* height: 130px;*/ /*}*/ -.screen .questionTitle { - font-family: "NanumSquare Neo OTF-Regular", Helvetica; - font-weight: 400; - color: #000000; - font-size: 24px; - letter-spacing: 0; - line-height: 36px; - width: 100%; - align-content: center; -} /** 메인페이지 - 시작버튼 @@ -225,7 +215,7 @@ /* 텍스트 스타일 */ .start-button-text { - font-family: "NanumSquare Neo OTF-dEb", Helvetica; + color: white; font-size: 25px; line-height: 37.5px; diff --git a/src/main/resources/static/css/normal-question-update/style.css b/src/main/resources/static/css/normal-question-update/style.css index aba7db6f..261f4c50 100644 --- a/src/main/resources/static/css/normal-question-update/style.css +++ b/src/main/resources/static/css/normal-question-update/style.css @@ -32,14 +32,14 @@ h2 { } /* 질문 텍스트 스타일 */ -.questionTitle { +.questionItem { font-size: 1.2em; font-weight: bold; color: #34495e; } /* 해설 텍스트 스타일 (파란색) */ -.description { +.descriptionText { font-size: 0.9em; margin-bottom: 15px; color: #2980b9; /* 파란색 */ diff --git a/src/main/resources/static/css/question/chatbot.css b/src/main/resources/static/css/question/chatbot.css index 5996d2b4..5e0aee9b 100644 --- a/src/main/resources/static/css/question/chatbot.css +++ b/src/main/resources/static/css/question/chatbot.css @@ -48,6 +48,11 @@ overflow-y: scroll; } +.descriptionText { + line-height: 1.4; /* 줄 간격 설정 */ + margin-bottom: 2%; /* 메시지 간의 간격 */ +} + .description-content { padding: 1%; background-color: #ffffff; diff --git a/src/main/resources/static/css/question/style.css b/src/main/resources/static/css/question/style.css index fc9f84c0..252b3b14 100644 --- a/src/main/resources/static/css/question/style.css +++ b/src/main/resources/static/css/question/style.css @@ -29,7 +29,6 @@ position: absolute; top: 5%; left: 10%; - font-family: "Markazi Text-Bold", Helvetica; font-weight: 700; color: #000000; font-size: 5vw; @@ -112,7 +111,7 @@ flex: 1; align-self: stretch; margin-top: -1px; - font-family: "NanumSquare Neo OTF-dEb", Helvetica; + font-weight: 400; color: #000000; font-size: 20px; @@ -178,7 +177,7 @@ margin-top: -5px; margin-bottom: -7px; - font-family: "NanumSquare Neo OTF-dEb", Helvetica; + font-weight: 400; color: #000000; font-size: 17px; @@ -202,7 +201,6 @@ height: 24px; top: -1px; left: 0; - font-family: "NanumSquare Neo OTF-dEb", Helvetica; font-size: 12px; text-align: center; letter-spacing: 0.72px; @@ -225,7 +223,7 @@ .quiz .questionItem { margin-top: -1px; - font-family: "NanumSquare Neo OTF-dEb", Helvetica; + color: #000000; font-size: 20px; line-height: 30px; @@ -254,7 +252,7 @@ } .quiz .div-4 { - font-family: "NanumSquare Neo OTF-Regular", Helvetica; + color: transparent; font-size: 17px; line-height: 32.6px; @@ -322,7 +320,6 @@ position: relative; border-radius: 8px; border: 13px solid; /* 테두리 설정 */ - font-family: "NanumSquare Neo OTF-dEb", Helvetica; font-size: 22px; letter-spacing: 0; line-height: 33px; @@ -334,7 +331,6 @@ overflow: hidden; /* 넘치는 텍스트는 잘리게 */ text-overflow: ellipsis; /* 잘린 텍스트에 줄임표 추가 */ max-width: 100%; /* 텍스트가 부모의 너비를 넘지 않도록 설정 */ - font-family: "NanumSquare Neo OTF-dEb", Helvetica; font-size: 16px; color: #333; } @@ -355,7 +351,6 @@ position: relative; width: fit-content; margin-top: -13px; - font-family: "NanumSquare Neo OTF-dEb", Helvetica; font-weight: 400; color: #63a572; font-size: 22px; @@ -369,7 +364,6 @@ height: 24px; top: -1px; left: 6px; - font-family: "NanumSquare Neo OTF-dEb", Helvetica; font-size: 12px; text-align: center; letter-spacing: 0.72px; @@ -380,7 +374,7 @@ } .quiz .choice-list { - font-family: "NanumSquare Neo OTF-bRg", Helvetica; + /**/ color: #000000; font-size: 17px; line-height: 32.6px; @@ -436,7 +430,6 @@ position: absolute; top: -1px; left: 0; - font-family: "NanumSquare Neo OTF-bRg", Helvetica; font-size: 20px; text-align: center; letter-spacing: 0; @@ -464,7 +457,6 @@ .quiz .text-wrapper-8 { position: relative; width: fit-content; - font-family: "NanumSquare Neo OTF-cBd", Helvetica; font-size: 20px; text-align: center; letter-spacing: 0; @@ -524,7 +516,6 @@ 채점 버튼 */ .quiz .check-button { - font-family: "NanumSquare Neo OTF-dEb", Helvetica; font-size: 100%; letter-spacing: 0; line-height: 40px; @@ -604,7 +595,7 @@ Side bar } .left-sidebar p { - font-family: "NanumSquare Neo OTF", Helvetica; + font-size: 0.8vw; /* 폰트 크기를 뷰포트 너비에 맞게 설정 */ font-weight: 500; /* 폰트를 중간 두께로 설정 */ color: #404040; /* 텍스트 색상을 조정 */ @@ -612,7 +603,7 @@ Side bar } .left-sidebar span { - font-family: "NanumSquare Neo OTF", Helvetica; + font-size: 1.2vw; /* 숫자를 조금 더 강조 */ font-weight: 700; } diff --git a/src/main/resources/static/js/chatbot.js b/src/main/resources/static/js/chatbot.js index 82862a83..502c4c6f 100644 --- a/src/main/resources/static/js/chatbot.js +++ b/src/main/resources/static/js/chatbot.js @@ -1,5 +1,3 @@ -// import {formatText} from './translateTextForHtml.js' - document.addEventListener("DOMContentLoaded", function () { const input = document.getElementById("chatbotInput"); @@ -10,8 +8,6 @@ document.addEventListener("DOMContentLoaded", function () { sendMessage(); } }); - - }); function sendMessage() { @@ -48,7 +44,7 @@ function addMessageToChat(role, text) { message.classList.add("message", role); // formatText 함수를 사용하여 HTML 포맷팅을 한 후 innerHTML에 설정 - message.innerHTML = formatText(text); // 여기에서 innerHTML 사용 + message.innerHTML = text; messageContainer.appendChild(message); @@ -56,13 +52,4 @@ function addMessageToChat(role, text) { message.scrollIntoView({behavior: "smooth", block: "end"}); } -// 텍스트 포맷팅 함수 -function formatText(text) { - // 줄 바꿈을
로 변환 - let formattedText = text.replace(/\n/g, '
'); - // **로 둘러싸인 단어를 굵게 표시 - formattedText = formattedText.replace(/\*\*(.*?)\*\*/g, '$1'); - - return formattedText; -} diff --git a/src/main/resources/static/js/solveQuestion.js b/src/main/resources/static/js/solveQuestion.js index d94522db..46eee189 100644 --- a/src/main/resources/static/js/solveQuestion.js +++ b/src/main/resources/static/js/solveQuestion.js @@ -348,6 +348,11 @@ document.addEventListener('DOMContentLoaded', function () { // 줄 바꿈을
로 변환 let formattedText = text.replace(/\n/g, '
'); + // #으로 시작하는 텍스트를 대제목과 소제목으로 변환 + formattedText = formattedText.replace(/^### (.*?)(|$)/gm, '$1'); + formattedText = formattedText.replace(/^## (.*?)(|$)/gm, '$1'); + formattedText = formattedText.replace(/^# (.*?)(|$)/gm, '$1'); + // **로 둘러싸인 단어를 굵게 표시 formattedText = formattedText.replace(/\*\*(.*?)\*\*/g, '$1'); diff --git a/src/main/resources/static/js/translateTextForHtml.js b/src/main/resources/static/js/translateTextForHtml.js index 79f4e087..1b9fb8d0 100644 --- a/src/main/resources/static/js/translateTextForHtml.js +++ b/src/main/resources/static/js/translateTextForHtml.js @@ -1,18 +1,59 @@ document.addEventListener("DOMContentLoaded", function () { - const descriptions = document.querySelectorAll('.questionItem'); - descriptions.forEach(function (element) { - // 줄 바꿈을
로 변환 - let formattedText = element.textContent.replace(/\n/g, '
'); + // 포맷팅이 필요한 클래스 목록을 배열로 관리 + const classesToFormat = ['questionItem', 'descriptionText', 'message user', 'message bot']; - // **로 둘러싸인 단어를 굵게 표시 - formattedText = formattedText.replace(/\*\*(.*?)\*\*/g, '$1'); + // 초기 로딩된 요소에 대해 포맷팅 적용 + classesToFormat.forEach(classNames => { + document.querySelectorAll(`.${classNames.replace(' ', '.')}`).forEach(element => { + if (classNames === 'message user') { + formatTextWithLineBreakOnly(element); + } else { + formatElementText(element); + } + }); + }); - // 최종적으로 HTML에 적용 - element.innerHTML = formattedText; + // MutationObserver 설정(동적으로 추가되는 요소에 대해 포맷팅 적용) + const observer = new MutationObserver(function (mutationsList) { + mutationsList.forEach(function (mutation) { + mutation.addedNodes.forEach(function (node) { + if (node.nodeType === Node.ELEMENT_NODE) { + classesToFormat.some(classNames => { + if (node.classList.contains(...classNames.split(' '))) { + if (classNames === 'message user') { + formatTextWithLineBreakOnly(node); + } else { + formatElementText(node); + } + return true; + } + return false; + }); + } + }); + }); }); + + // body 요소에 대해 DOM 변경사항 관찰 시작 + observer.observe(document.body, {childList: true, subtree: true}); }); +// 포맷팅 함수 +function formatElementText(element) { + let formattedText = element.textContent.replace(/\n/g, '
'); + + // #으로 시작하는 텍스트를 대제목과 소제목으로 변환 + formattedText = formattedText.replace(/^### (.*?)(|$)/gm, '$1'); + formattedText = formattedText.replace(/^## (.*?)(|$)/gm, '$1'); + formattedText = formattedText.replace(/^# (.*?)(|$)/gm, '$1'); + + // **로 둘러싸인 단어를 굵게 표시 + formattedText = formattedText.replace(/\*\*(.*?)\*\*/g, '$1'); + + element.innerHTML = formattedText; +} -/* -동적으로 추가되는 것에는 JS로 직접 해야함.처음 초기화된 것에만 eventListener가 적용됨 - */ +// message.user와 같이 줄바꿈만 적용 +function formatTextWithLineBreakOnly(element) { + element.innerHTML = element.textContent.replace(/\n/g, '
'); +} diff --git a/src/main/resources/templates/license-index.html b/src/main/resources/templates/license-index.html index 467c62d7..7e0039c2 100644 --- a/src/main/resources/templates/license-index.html +++ b/src/main/resources/templates/license-index.html @@ -18,7 +18,7 @@ CS 전공 및 자격증 기출 문제 모음 | 컴싸 - + diff --git a/src/main/resources/templates/license-question-update.html b/src/main/resources/templates/license-question-update.html index 907795df..76999d90 100644 --- a/src/main/resources/templates/license-question-update.html +++ b/src/main/resources/templates/license-question-update.html @@ -6,6 +6,7 @@ + @@ -28,7 +29,7 @@

자격증 질문 업데이트

-

질문

@@ -62,7 +63,7 @@

자격증 질문 업데이트

-

설명

diff --git a/src/main/resources/templates/major-question-update.html b/src/main/resources/templates/major-question-update.html index 9cd71f0b..e4b0026c 100644 --- a/src/main/resources/templates/major-question-update.html +++ b/src/main/resources/templates/major-question-update.html @@ -3,9 +3,11 @@ 일반 질문 업데이트 + + @@ -23,7 +25,7 @@

일반 질문 업데이트

th:id="'question-'+${question.id}">
-

질문

@@ -45,7 +47,7 @@

일반 질문 업데이트

-

설명

diff --git a/src/test/java/com/server/computerscience/question/major/admin/service/implement/AdminMajorQuestionClassifiedGetServiceTest.java b/src/test/java/com/server/computerscience/question/major/admin/service/implement/AdminMajorQuestionClassifiedGetServiceTest.java index 484646f6..4cfec83a 100644 --- a/src/test/java/com/server/computerscience/question/major/admin/service/implement/AdminMajorQuestionClassifiedGetServiceTest.java +++ b/src/test/java/com/server/computerscience/question/major/admin/service/implement/AdminMajorQuestionClassifiedGetServiceTest.java @@ -13,14 +13,14 @@ import com.server.computerscience.ServiceIntegrationTest; import com.server.computerscience.question.common.domain.QuestionCategory; import com.server.computerscience.question.major.common.domain.MajorMultipleChoiceQuestion; -import com.server.computerscience.question.major.common.repository.MajorQuestionRepository; +import com.server.computerscience.question.major.common.repository.MajorMultipleChoiceQuestionRepository; @DisplayName("전공 문제 - Admin Get Service 계층 이하 통합 테스트") class AdminMajorQuestionClassifiedServiceTest extends ServiceIntegrationTest { @Autowired private BasicAdminMajorQuestionClassifiedGetService basicAdminMajorQuestionClassifiedGetService; @Autowired - private MajorQuestionRepository majorQuestionRepository; + private MajorMultipleChoiceQuestionRepository majorMultipleChoiceQuestionRepository; private MajorMultipleChoiceQuestion majorMultipleChoiceQuestion; @@ -33,7 +33,7 @@ void setUp() { @DisplayName("관리자 조회시 비허용 문제 존재 여부 조회") void checkMajorQuestionIsApproved() { //given - majorQuestionRepository.save(majorMultipleChoiceQuestion); + majorMultipleChoiceQuestionRepository.save(majorMultipleChoiceQuestion); //when Map> questions = basicAdminMajorQuestionClassifiedGetService @@ -53,9 +53,9 @@ void checkMajorQuestionIsSorted() { MajorMultipleChoiceQuestion approvedMajorQuestion = MajorMultipleChoiceQuestion.makeForTest(); approvedMajorQuestion.toggleApproved(); - majorQuestionRepository.save(approvedMajorQuestion); + majorMultipleChoiceQuestionRepository.save(approvedMajorQuestion); - majorQuestionRepository.save(majorMultipleChoiceQuestion); + majorMultipleChoiceQuestionRepository.save(majorMultipleChoiceQuestion); //when Map> questions = basicAdminMajorQuestionClassifiedGetService diff --git a/src/test/java/com/server/computerscience/question/major/admin/service/implement/BasicAdminMajorQuestionMakeServiceTest.java b/src/test/java/com/server/computerscience/question/major/admin/service/implement/BasicAdminMajorQuestionMakeServiceTest.java index f1f015aa..716d8ff9 100644 --- a/src/test/java/com/server/computerscience/question/major/admin/service/implement/BasicAdminMajorQuestionMakeServiceTest.java +++ b/src/test/java/com/server/computerscience/question/major/admin/service/implement/BasicAdminMajorQuestionMakeServiceTest.java @@ -11,7 +11,7 @@ import com.server.computerscience.ServiceIntegrationTest; import com.server.computerscience.question.major.admin.dto.RequestMakeMultipleChoiceQuestionDto; import com.server.computerscience.question.major.common.domain.MajorMultipleChoiceQuestion; -import com.server.computerscience.question.major.common.repository.MajorQuestionRepository; +import com.server.computerscience.question.major.common.repository.MajorMultipleChoiceQuestionRepository; @DisplayName("전공 문제 - Admin Get Service 계층 이하 통합 테스트") class BasicAdminMajorQuestionMakeServiceTest extends ServiceIntegrationTest { @@ -19,7 +19,7 @@ class BasicAdminMajorQuestionMakeServiceTest extends ServiceIntegrationTest { @Autowired private BasicAdminMajorQuestionMakeService basicAdminMajorQuestionMakeService; @Autowired - private MajorQuestionRepository majorQuestionRepository; + private MajorMultipleChoiceQuestionRepository majorMultipleChoiceQuestionRepository; private MajorMultipleChoiceQuestion majorMultipleChoiceQuestion; @BeforeEach @@ -44,7 +44,8 @@ void makeMajorMultipleChoiceQuestions() { /* DB 저장 확인 */ - Assertions.assertThat(majorQuestionRepository.findById(newMultipleChoiceQuestion.getId())).isNotNull(); + Assertions.assertThat(majorMultipleChoiceQuestionRepository.findById(newMultipleChoiceQuestion.getId())) + .isNotNull(); } @Test @@ -64,6 +65,6 @@ void makeMajorMultipleChoiceQuestionsDuplicateContent() { then DB에 저장되어야하는 엔티티는 하나 */ - Assertions.assertThat(majorQuestionRepository.findAll().size()).isEqualTo(1); + Assertions.assertThat(majorMultipleChoiceQuestionRepository.findAll().size()).isEqualTo(1); } } diff --git a/src/test/java/com/server/computerscience/question/major/user/service/implement/MajorQuestionClassifiedGetServiceTest.java b/src/test/java/com/server/computerscience/question/major/user/service/implement/MajorQuestionClassifiedGetServiceTest.java index 26a5a988..81a5ebf5 100644 --- a/src/test/java/com/server/computerscience/question/major/user/service/implement/MajorQuestionClassifiedGetServiceTest.java +++ b/src/test/java/com/server/computerscience/question/major/user/service/implement/MajorQuestionClassifiedGetServiceTest.java @@ -15,7 +15,7 @@ import com.server.computerscience.question.common.domain.QuestionCategory; import com.server.computerscience.question.common.domain.QuestionLevel; import com.server.computerscience.question.major.common.domain.MajorMultipleChoiceQuestion; -import com.server.computerscience.question.major.common.repository.MajorQuestionRepository; +import com.server.computerscience.question.major.common.repository.MajorMultipleChoiceQuestionRepository; import com.server.computerscience.question.major.user.dto.request.RequestGetQuestionByCategoryAndLevelDto; @DisplayName("전공 문제 - Service 계층 이하 통합 테스트") @@ -31,7 +31,7 @@ class MajorQuestionClassifiedGetServiceTest extends ServiceIntegrationTest { @Autowired private BasicMajorQuestionClassifiedGetService basicMajorQuestionClassifiedGetService; @Autowired - private MajorQuestionRepository majorQuestionRepository; + private MajorMultipleChoiceQuestionRepository majorMultipleChoiceQuestionRepository; private MajorMultipleChoiceQuestion majorMultipleChoiceQuestion; @BeforeEach @@ -45,7 +45,7 @@ void setUp() { void getApprovedClassifiedMajorMultipleChoiceQuestions() { //given majorMultipleChoiceQuestion.toggleApproved(); - majorQuestionRepository.save(majorMultipleChoiceQuestion); + majorMultipleChoiceQuestionRepository.save(majorMultipleChoiceQuestion); RequestGetQuestionByCategoryAndLevelDto allQuestionRequestDto = RequestGetQuestionByCategoryAndLevelDto.fromKorean(majorCategories, levels); @@ -66,7 +66,7 @@ void getApprovedClassifiedShortAnsweredMajorQuestions() { majorMultipleChoiceQuestion.toggleApproved(); //문제 주관식 가능 여부 허용 majorMultipleChoiceQuestion.toggleCanBeShortAnswered(); - majorQuestionRepository.save(majorMultipleChoiceQuestion); + majorMultipleChoiceQuestionRepository.save(majorMultipleChoiceQuestion); RequestGetQuestionByCategoryAndLevelDto allQuestionRequestDto = RequestGetQuestionByCategoryAndLevelDto.fromKorean(majorCategories, levels);