Skip to content

Commit 1fc11d7

Browse files
authored
컨트롤러 레이어 리팩토링 (#55)
* refactor: Question관련 Service 빈을 동적으로 추출하는 QuestionServiceFactory 추가 - questionType, Field, Act를 기반으로 Question Service 관련 빈을 반환 - 이를 통해 중복 URL API 감소 - MultipleChoiceQuestionGradeService에 적용(questionType : multiple, questionField: license,major, act:grade) * refactor: QuestionServiceFactory가 Service 빈을 제네릭 타입으로 반환하도록 수정 - 메소드 재사용성 극대화 * refactor: Question Update 관련 API 통일 - 본문 업데이트, 개시 허용, 해설 업데이트 API 축소 * refactor: Question Image Update API 수정 - 통일 * test: QuestionCommonUpdateController Test 추가 - 기존 공통된 Question Update관련 로직 테스트를 구현 * test: 기존 Update 관련 테스트를 QuestionCommonUpdateController로 이동 * refactor: QuestionChoice를 엔티티로 분리하고 조인 전략 적용 * style: 코드 컨벤션 적용 * refactor: QuestionCommonChoiceUpdate 로직을 통일 * style: 패키지 구조 수정 - 도메인 단위 구별-> 레이어별 구별 에서 레이어별 구별 -> 도메인 단위 구별로 수정 * refactor: Major Question 업데이트 페이지 API를 하나로 축소 및 제거 * refactor: 문제 조회시 분류 작업에 대하여 제너릭 와일드카드를 적용 - 호출부에선 Question으로 호출하여 재사용 극대화 - QuestionClassifyCategoryService에서 인자로 와일드카드를 받고 Question 명시적 반환 * test: 전공 문제 생성과 관련된 Test 수정 - URL 수정 * refactor: QuestonViewController 유지보수성 향상 - HTML 태그는 클래스단위로 관리 - 재사용 가능한 함수 분리 * refactor: Question 업데이트 페이지 API를 QuestionUpdateViewController로 통일 * test: Test 수정 * style: 코드 컨벤션 적용
1 parent 9f1ca7b commit 1fc11d7

File tree

95 files changed

+1292
-1396
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

95 files changed

+1292
-1396
lines changed

application/src/main/java/com/comssa/api/question/common/config/S3Config.java application/src/main/java/com/comssa/api/config/S3Config.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package com.comssa.api.question.common.config;
1+
package com.comssa.api.config;
22

33
import com.amazonaws.auth.AWSCredentials;
44
import com.amazonaws.auth.AWSStaticCredentialsProvider;
+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package com.comssa.api.question.major.common.exception;
1+
package com.comssa.api.exception;
22

33
public class DuplicateQuestionException extends Exception {
44
public DuplicateQuestionException() {

application/src/main/java/com/comssa/api/question/common/controller/QuestionGradeController.java

-36
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package com.comssa.api.question.controller.rest.common;
2+
3+
import com.comssa.api.question.service.rest.common.implement.QuestionChoiceUpdateService;
4+
import com.comssa.persistence.question.common.dto.request.RequestChangeContentDto;
5+
import com.comssa.persistence.question.common.dto.response.ResponseQuestionChoiceDto;
6+
import io.swagger.annotations.ApiOperation;
7+
import lombok.RequiredArgsConstructor;
8+
import org.springframework.http.ResponseEntity;
9+
import org.springframework.web.bind.annotation.DeleteMapping;
10+
import org.springframework.web.bind.annotation.PatchMapping;
11+
import org.springframework.web.bind.annotation.PathVariable;
12+
import org.springframework.web.bind.annotation.RequestBody;
13+
import org.springframework.web.bind.annotation.RequestMapping;
14+
import org.springframework.web.bind.annotation.RestController;
15+
16+
@RestController
17+
@RequestMapping("/admin")
18+
@RequiredArgsConstructor
19+
public class QuestionChoiceUpdateController {
20+
private final QuestionChoiceUpdateService questionChoiceUpdateService;
21+
22+
@ApiOperation("단답형 선택지 업데이트 - 선택지 지문 업데이트")
23+
@PatchMapping(value = "/question/common/choice/{id}")
24+
public ResponseEntity<ResponseQuestionChoiceDto> changeChoiceContent(
25+
@PathVariable("id") Long choiceId,
26+
@RequestBody RequestChangeContentDto requestChangeContentDto) {
27+
28+
return ResponseEntity.ok(ResponseQuestionChoiceDto.of(
29+
questionChoiceUpdateService.changeContent(
30+
choiceId,
31+
requestChangeContentDto)));
32+
}
33+
34+
@ApiOperation("단답형 선택지 업데이트 - 정답 여부 변경")
35+
@PatchMapping(value = "/question/common/choice/{id}/toggle")
36+
public ResponseEntity<ResponseQuestionChoiceDto> changeChoiceContent(
37+
@PathVariable("id") Long licenseChoiceId) {
38+
return ResponseEntity.ok(ResponseQuestionChoiceDto.of(
39+
questionChoiceUpdateService
40+
.toggleAnswerStatus(
41+
licenseChoiceId)));
42+
}
43+
44+
@ApiOperation("단답형 선택지 업데이트 - 선택지 삭제")
45+
@DeleteMapping(value = "/question/common/choice/{id}")
46+
public ResponseEntity<Void> deleteChoiceContent(
47+
@PathVariable("id") Long licenseChoiceId) {
48+
questionChoiceUpdateService.deleteQuestionChoice(
49+
licenseChoiceId);
50+
return ResponseEntity.noContent().build();
51+
}
52+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package com.comssa.api.question.controller.rest.common;
2+
3+
import com.comssa.api.question.service.rest.common.QuestionChoiceGradeService;
4+
import io.swagger.annotations.Api;
5+
import io.swagger.annotations.ApiOperation;
6+
import lombok.RequiredArgsConstructor;
7+
import org.springframework.http.ResponseEntity;
8+
import org.springframework.web.bind.annotation.PatchMapping;
9+
import org.springframework.web.bind.annotation.PathVariable;
10+
import org.springframework.web.bind.annotation.RestController;
11+
12+
@Api(tags = {"문제 채점"})
13+
@RestController
14+
@RequiredArgsConstructor
15+
public class QuestionGradeController {
16+
private final QuestionServiceFactory questionServiceFactory;
17+
18+
@ApiOperation("객관식 문제 채점")
19+
@PatchMapping("/question/{questionField}/{questionType}/choice/{choiceId}/{questionAct}")
20+
public ResponseEntity<Boolean> isMajorChoiceAnswer(
21+
@PathVariable("questionField") String questionField,
22+
@PathVariable("questionType") String questionType,
23+
@PathVariable("questionAct") String questionAct,
24+
@PathVariable("choiceId") Long choiceId
25+
) {
26+
QuestionChoiceGradeService questionChoiceGradeService
27+
= questionServiceFactory.getQuestionService(
28+
questionField,
29+
questionType,
30+
questionAct,
31+
QuestionChoiceGradeService.class
32+
);
33+
return ResponseEntity.ok(questionChoiceGradeService.isChoiceAnswer(choiceId));
34+
}
35+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
//package com.comssa.api.question.controller.rest.common;
2+
//
3+
//import io.swagger.annotations.Api;
4+
//import lombok.RequiredArgsConstructor;
5+
//import org.springframework.stereotype.Controller;
6+
//import org.springframework.web.bind.annotation.RequestMapping;
7+
//
8+
//@Controller
9+
//@RequestMapping("/admin")
10+
//@Api(tags = {"문제 생성"})
11+
//@RequiredArgsConstructor
12+
//public class QuestionMakeController {
13+
// private final
14+
//}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package com.comssa.api.question.controller.rest.common;
2+
3+
4+
import org.springframework.context.ApplicationContext;
5+
import org.springframework.stereotype.Component;
6+
7+
import java.util.List;
8+
import java.util.stream.Collectors;
9+
10+
@Component
11+
public class QuestionServiceFactory {
12+
private final List<Object> questionService;
13+
14+
public QuestionServiceFactory(ApplicationContext applicationContext) {
15+
this.questionService =
16+
applicationContext.getBeansOfType(Object.class).values()
17+
.stream()
18+
.filter(
19+
bean -> {
20+
String name = bean.getClass().getSimpleName().toLowerCase();
21+
return name.contains("question") && name.contains("service");
22+
}
23+
)
24+
.collect(Collectors.toList());
25+
}
26+
27+
public <T> T getQuestionService(
28+
String questionField,
29+
String questionType,
30+
String questionAct,
31+
Class<T> classType
32+
) {
33+
return findServiceByClassName(questionField, questionType, questionAct, classType, false);
34+
}
35+
36+
public <T> T getAdminQuestionService(
37+
String questionField,
38+
String questionType,
39+
String questionAct,
40+
Class<T> classType
41+
) {
42+
return findServiceByClassName(questionField, questionType, questionAct, classType, true);
43+
}
44+
45+
private <T> T findServiceByClassName(
46+
String questionField,
47+
String questionType,
48+
String questionAct,
49+
Class<T> classType,
50+
boolean isAdmin
51+
) {
52+
return questionService.stream()
53+
.filter(service -> {
54+
String name = service.getClass().getSimpleName().toLowerCase();
55+
boolean matches = name.contains(questionField.toLowerCase())
56+
&& name.contains(questionType.toLowerCase())
57+
&& name.contains(questionAct.toLowerCase());
58+
if (isAdmin) {
59+
matches = matches && name.contains("admin");
60+
}
61+
return matches;
62+
})
63+
.map(classType::cast)
64+
/*
65+
이 부분으로 반환값을 Object대신 제네릭을 사용함에도 ClassCastException이 여전히 발생할 수 있지만,
66+
제네릭을 사용함으로 에러 처리를 호출부가 아니라 현 클래스에서 집중할 수 있음
67+
*/
68+
.findFirst()
69+
.orElseThrow(() -> new IllegalArgumentException(
70+
"No service found for questionField: " + questionField
71+
+ ", questionType: " + questionType
72+
+ ", questionAct: " + questionAct
73+
));
74+
}
75+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package com.comssa.api.question.controller.rest.common;
2+
3+
4+
import com.comssa.api.question.service.rest.common.implement.QuestionUpdateService;
5+
import com.comssa.persistence.question.common.dto.request.RequestChangeContentDto;
6+
import com.comssa.persistence.question.common.dto.response.ResponseMultipleChoiceQuestionDto;
7+
import io.swagger.annotations.Api;
8+
import io.swagger.annotations.ApiOperation;
9+
import lombok.RequiredArgsConstructor;
10+
import org.springframework.http.ResponseEntity;
11+
import org.springframework.web.bind.annotation.DeleteMapping;
12+
import org.springframework.web.bind.annotation.PatchMapping;
13+
import org.springframework.web.bind.annotation.PathVariable;
14+
import org.springframework.web.bind.annotation.RequestBody;
15+
import org.springframework.web.bind.annotation.RequestMapping;
16+
import org.springframework.web.bind.annotation.RequestPart;
17+
import org.springframework.web.bind.annotation.RestController;
18+
import org.springframework.web.multipart.MultipartFile;
19+
20+
@RestController
21+
@RequiredArgsConstructor
22+
@Api(tags = {"문제 수정"})
23+
@RequestMapping("/admin")
24+
public class QuestionUpdateController {
25+
private final QuestionUpdateService questionUpdateService;
26+
27+
@ApiOperation("문제 개시 허용")
28+
@PatchMapping(value = "/question/common/{id}/toggle-approve")
29+
public ResponseEntity<ResponseMultipleChoiceQuestionDto> toggleApproveNormalQuestion(
30+
@PathVariable("id") Long questionId
31+
) {
32+
questionUpdateService.toggleApprove(questionId);
33+
return ResponseEntity.ok().build();
34+
}
35+
36+
@ApiOperation("문제 본문 업데이트")
37+
@PatchMapping(value = "/question/common/{id}/content")
38+
public ResponseEntity<ResponseMultipleChoiceQuestionDto> changeQuestion(
39+
@PathVariable("id") Long questionId,
40+
@RequestBody RequestChangeContentDto requestChangeContentDto) {
41+
questionUpdateService.changeContent(questionId, requestChangeContentDto);
42+
return ResponseEntity.ok().build();
43+
}
44+
45+
@ApiOperation("해설 업데이트")
46+
@PatchMapping(value = "/question/common/{id}/description")
47+
public ResponseEntity<ResponseMultipleChoiceQuestionDto> changeDescription(
48+
@PathVariable("id") Long questionId,
49+
@RequestBody RequestChangeContentDto requestChangeContentDto
50+
) {
51+
questionUpdateService.changeDescription(questionId, requestChangeContentDto);
52+
return ResponseEntity.ok().build();
53+
}
54+
55+
@ApiOperation("이미지 업로드")
56+
@PatchMapping("/question/common/{id}/image")
57+
public ResponseEntity<String> updateLicenseQuestionWithImage(
58+
@PathVariable("id") Long licenseQuestionId,
59+
@RequestPart(value = "image") MultipartFile file) {
60+
return ResponseEntity.ok(questionUpdateService.updateImage(licenseQuestionId, file));
61+
}
62+
63+
@ApiOperation(" 문제 삭제")
64+
@DeleteMapping(value = "/question/common/{id}")
65+
public ResponseEntity<Void> changeDescription(
66+
@PathVariable("id") Long questionId) {
67+
questionUpdateService.deleteQuestion(questionId);
68+
return ResponseEntity.noContent().build();
69+
}
70+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package com.comssa.api.question.controller.rest.license;
2+
3+
import com.comssa.api.question.service.rest.license.implement.AdminLicenseQuestionMakeService;
4+
import com.comssa.persistence.question.common.dto.response.ResponseMultipleChoiceQuestionDto;
5+
import com.comssa.persistence.question.license.dto.request.RequestMakeLicenseMultipleChoiceQuestionDto;
6+
import io.swagger.annotations.Api;
7+
import io.swagger.annotations.ApiOperation;
8+
import lombok.RequiredArgsConstructor;
9+
import org.springframework.http.ResponseEntity;
10+
import org.springframework.web.bind.annotation.PostMapping;
11+
import org.springframework.web.bind.annotation.RequestBody;
12+
import org.springframework.web.bind.annotation.RequestMapping;
13+
import org.springframework.web.bind.annotation.RestController;
14+
15+
import java.util.List;
16+
17+
@RestController
18+
@RequestMapping("/admin/question/license")
19+
@Api(tags = {"자격증 문제 - ADMIN"})
20+
@RequiredArgsConstructor
21+
public class AdminLicenseQuestionController {
22+
private final AdminLicenseQuestionMakeService adminLicenseQuestionMakeService;
23+
24+
@ApiOperation("단답형 문제 세션으로 생성")
25+
@PostMapping
26+
public ResponseEntity<List<ResponseMultipleChoiceQuestionDto>> makeLicenseQuestionOfSession(
27+
@RequestBody RequestMakeLicenseMultipleChoiceQuestionDto requestMakeLicenseMultipleChoiceQuestionDto) {
28+
return ResponseEntity.ok(
29+
adminLicenseQuestionMakeService.makeLicenseNormalQuestion(requestMakeLicenseMultipleChoiceQuestionDto));
30+
}
31+
}

0 commit comments

Comments
 (0)