Skip to content

Commit 1bec0d7

Browse files
♻️ refactor : 커스텀 예외 적용, NPE 방지, 조건문 오류 수정 (#56)
* ♻️ refactor : 예외 코드 적용, NPE 방지, 조건문 오류 수정 * chore: Java 스타일 수정 * 🔧 chore : 설정파일에 최대 파일 크기 설정 * chore: Java 스타일 수정 * ✨ feat: 페이징 예외 적용 * chore: Java 스타일 수정 * 🔧 chore: 시큐리티 임시 허용 해제 * ♻️ refactor : 메서드 Quiz 내부로 리팩토링 * ♻️ refactor : PR 리뷰 반영 (탈퇴한 사용자 표시) * ♻️ refactor : 메서드 명 수정 --------- Co-authored-by: github-actions <>
1 parent 51c5ff3 commit 1bec0d7

File tree

11 files changed

+114
-44
lines changed

11 files changed

+114
-44
lines changed

backend/src/main/java/io/f1/backend/domain/game/app/GameService.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
import io.f1.backend.domain.game.store.RoomRepository;
1111
import io.f1.backend.domain.quiz.app.QuizService;
1212
import io.f1.backend.domain.quiz.entity.Quiz;
13+
import io.f1.backend.global.exception.CustomException;
14+
import io.f1.backend.global.exception.errorcode.GameErrorCode;
15+
import io.f1.backend.global.exception.errorcode.RoomErrorCode;
1316

1417
import lombok.RequiredArgsConstructor;
1518

@@ -31,10 +34,10 @@ public GameStartData gameStart(Long roomId, Long quizId) {
3134
Room room =
3235
roomRepository
3336
.findRoom(roomId)
34-
.orElseThrow(() -> new IllegalArgumentException("404 존재하지 않는 방입니다."));
37+
.orElseThrow(() -> new CustomException(RoomErrorCode.ROOM_NOT_FOUND));
3538

3639
if (!validateReadyStatus(room)) {
37-
throw new IllegalArgumentException("E403004 : 레디 상태가 아닙니다.");
40+
throw new CustomException(RoomErrorCode.PLAYER_NOT_READY);
3841
}
3942

4043
// 방의 gameSetting에 설정된 퀴즈랑 요청 퀴즈랑 같은지 체크 후 GameSetting에서 라운드 가져오기
@@ -58,7 +61,7 @@ private Integer checkGameSetting(Room room, Long quizId) {
5861
GameSetting gameSetting = room.getGameSetting();
5962

6063
if (!gameSetting.checkQuizId(quizId)) {
61-
throw new IllegalArgumentException("E409002 : 게임 설정이 다릅니다. (게임을 시작할 수 없습니다.)");
64+
throw new CustomException(GameErrorCode.GAME_SETTING_CONFLICT);
6265
}
6366

6467
return gameSetting.getRound();

backend/src/main/java/io/f1/backend/domain/question/app/QuestionService.java

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,14 @@
99
import io.f1.backend.domain.question.entity.Question;
1010
import io.f1.backend.domain.question.entity.TextQuestion;
1111
import io.f1.backend.domain.quiz.entity.Quiz;
12+
import io.f1.backend.global.exception.CustomException;
13+
import io.f1.backend.global.exception.errorcode.QuestionErrorCode;
1214

1315
import lombok.RequiredArgsConstructor;
1416

1517
import org.springframework.stereotype.Service;
1618
import org.springframework.transaction.annotation.Transactional;
1719

18-
import java.util.NoSuchElementException;
19-
2020
@Service
2121
@RequiredArgsConstructor
2222
public class QuestionService {
@@ -44,7 +44,8 @@ public void updateQuestionContent(Long questionId, String content) {
4444
Question question =
4545
questionRepository
4646
.findById(questionId)
47-
.orElseThrow(() -> new NoSuchElementException("존재하지 않는 문제입니다."));
47+
.orElseThrow(
48+
() -> new CustomException(QuestionErrorCode.QUESTION_NOT_FOUND));
4849

4950
TextQuestion textQuestion = question.getTextQuestion();
5051
textQuestion.changeContent(content);
@@ -58,7 +59,8 @@ public void updateQuestionAnswer(Long questionId, String answer) {
5859
Question question =
5960
questionRepository
6061
.findById(questionId)
61-
.orElseThrow(() -> new NoSuchElementException("존재하지 않는 문제입니다."));
62+
.orElseThrow(
63+
() -> new CustomException(QuestionErrorCode.QUESTION_NOT_FOUND));
6264

6365
question.changeAnswer(answer);
6466
}
@@ -69,20 +71,21 @@ public void deleteQuestion(Long questionId) {
6971
Question question =
7072
questionRepository
7173
.findById(questionId)
72-
.orElseThrow(() -> new NoSuchElementException("존재하지 않는 문제입니다."));
74+
.orElseThrow(
75+
() -> new CustomException(QuestionErrorCode.QUESTION_NOT_FOUND));
7376

7477
questionRepository.delete(question);
7578
}
7679

7780
private void validateAnswer(String answer) {
7881
if (answer.trim().length() < 5 || answer.trim().length() > 30) {
79-
throw new IllegalArgumentException("정답은 1자 이상 30자 이하로 입력해주세요.");
82+
throw new CustomException(QuestionErrorCode.INVALID_ANSWER_LENGTH);
8083
}
8184
}
8285

8386
private void validateContent(String content) {
8487
if (content.trim().length() < 5 || content.trim().length() > 30) {
85-
throw new IllegalArgumentException("문제는 5자 이상 30자 이하로 입력해주세요.");
88+
throw new CustomException(QuestionErrorCode.INVALID_CONTENT_LENGTH);
8689
}
8790
}
8891
}

backend/src/main/java/io/f1/backend/domain/quiz/api/QuizController.java

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
import io.f1.backend.domain.quiz.dto.QuizListPageResponse;
77
import io.f1.backend.domain.quiz.dto.QuizQuestionListResponse;
88
import io.f1.backend.domain.quiz.dto.QuizUpdateRequest;
9+
import io.f1.backend.global.exception.CustomException;
10+
import io.f1.backend.global.exception.errorcode.CommonErrorCode;
911

1012
import jakarta.validation.Valid;
1113

@@ -28,8 +30,6 @@
2830
import org.springframework.web.bind.annotation.RestController;
2931
import org.springframework.web.multipart.MultipartFile;
3032

31-
import java.io.IOException;
32-
3333
@RestController
3434
@RequestMapping("/quizzes")
3535
@RequiredArgsConstructor
@@ -40,8 +40,7 @@ public class QuizController {
4040
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
4141
public ResponseEntity<QuizCreateResponse> saveQuiz(
4242
@RequestPart(required = false) MultipartFile thumbnailFile,
43-
@Valid @RequestPart QuizCreateRequest request)
44-
throws IOException {
43+
@Valid @RequestPart QuizCreateRequest request) {
4544
QuizCreateResponse response = quizService.saveQuiz(thumbnailFile, request);
4645

4746
return ResponseEntity.status(HttpStatus.CREATED).body(response);
@@ -58,8 +57,7 @@ public ResponseEntity<Void> deleteQuiz(@PathVariable Long quizId) {
5857
public ResponseEntity<Void> updateQuiz(
5958
@PathVariable Long quizId,
6059
@RequestPart(required = false) MultipartFile thumbnailFile,
61-
@RequestPart QuizUpdateRequest request)
62-
throws IOException {
60+
@RequestPart QuizUpdateRequest request) {
6361

6462
if (request.title() != null) {
6563
quizService.updateQuizTitle(quizId, request.title());
@@ -83,6 +81,13 @@ public ResponseEntity<QuizListPageResponse> getQuizzes(
8381
@RequestParam(required = false) String title,
8482
@RequestParam(required = false) String creator) {
8583

84+
if (page <= 0) {
85+
throw new CustomException(CommonErrorCode.INVALID_PAGINATION);
86+
}
87+
if (size <= 0 || size > 100) {
88+
throw new CustomException(CommonErrorCode.INVALID_PAGINATION);
89+
}
90+
8691
Pageable pageable =
8792
PageRequest.of(page - 1, size, Sort.by(Sort.Direction.DESC, "createdAt"));
8893
QuizListPageResponse quizzes = quizService.getQuizzes(title, creator, pageable);

backend/src/main/java/io/f1/backend/domain/quiz/app/QuizService.java

Lines changed: 35 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717
import io.f1.backend.domain.quiz.entity.Quiz;
1818
import io.f1.backend.domain.user.dao.UserRepository;
1919
import io.f1.backend.domain.user.entity.User;
20+
import io.f1.backend.global.exception.CustomException;
21+
import io.f1.backend.global.exception.errorcode.AuthErrorCode;
22+
import io.f1.backend.global.exception.errorcode.QuizErrorCode;
2023

2124
import lombok.RequiredArgsConstructor;
2225
import lombok.extern.slf4j.Slf4j;
@@ -48,15 +51,15 @@ public class QuizService {
4851
private String defaultThumbnailPath;
4952

5053
private final String DEFAULT = "default";
54+
private static final long MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
5155

5256
// TODO : 시큐리티 구현 이후 삭제해도 되는 의존성 주입
5357
private final UserRepository userRepository;
5458
private final QuestionService questionService;
5559
private final QuizRepository quizRepository;
5660

5761
@Transactional
58-
public QuizCreateResponse saveQuiz(MultipartFile thumbnailFile, QuizCreateRequest request)
59-
throws IOException {
62+
public QuizCreateResponse saveQuiz(MultipartFile thumbnailFile, QuizCreateRequest request) {
6063
String thumbnailPath = defaultThumbnailPath;
6164

6265
if (thumbnailFile != null && !thumbnailFile.isEmpty()) {
@@ -81,29 +84,38 @@ public QuizCreateResponse saveQuiz(MultipartFile thumbnailFile, QuizCreateReques
8184
private void validateImageFile(MultipartFile thumbnailFile) {
8285

8386
if (!thumbnailFile.getContentType().startsWith("image")) {
84-
// TODO : 이후 커스텀 예외로 변경
85-
throw new IllegalArgumentException("이미지 파일을 업로드해주세요.");
87+
throw new CustomException(QuizErrorCode.UNSUPPORTED_MEDIA_TYPE);
8688
}
8789

8890
List<String> allowedExt = List.of("jpg", "jpeg", "png", "webp");
89-
if (!allowedExt.contains(getExtension(thumbnailFile.getOriginalFilename()))) {
90-
throw new IllegalArgumentException("지원하지 않는 확장자입니다.");
91+
String ext = getExtension(thumbnailFile.getOriginalFilename());
92+
if (!allowedExt.contains(ext)) {
93+
throw new CustomException(QuizErrorCode.UNSUPPORTED_IMAGE_FORMAT);
94+
}
95+
96+
if (thumbnailFile.getSize() > MAX_FILE_SIZE) {
97+
throw new CustomException(QuizErrorCode.FILE_SIZE_TOO_LARGE);
9198
}
9299
}
93100

94-
private String convertToThumbnailPath(MultipartFile thumbnailFile) throws IOException {
101+
private String convertToThumbnailPath(MultipartFile thumbnailFile) {
95102
String originalFilename = thumbnailFile.getOriginalFilename();
96103
String ext = getExtension(originalFilename);
97104
String savedFilename = UUID.randomUUID().toString() + "." + ext;
98105

99-
Path savePath = Paths.get(uploadPath, savedFilename).toAbsolutePath();
100-
thumbnailFile.transferTo(savePath.toFile());
106+
try {
107+
Path savePath = Paths.get(uploadPath, savedFilename).toAbsolutePath();
108+
thumbnailFile.transferTo(savePath.toFile());
109+
} catch (IOException e) {
110+
log.error("썸네일 업로드 중 IOException 발생", e);
111+
throw new CustomException(QuizErrorCode.THUMBNAIL_SAVE_FAILED);
112+
}
101113

102114
return "/images/thumbnail/" + savedFilename;
103115
}
104116

105117
private String getExtension(String filename) {
106-
return filename.substring(filename.lastIndexOf(".") + 1);
118+
return filename.substring(filename.lastIndexOf(".") + 1).toLowerCase();
107119
}
108120

109121
@Transactional
@@ -112,11 +124,11 @@ public void deleteQuiz(Long quizId) {
112124
Quiz quiz =
113125
quizRepository
114126
.findById(quizId)
115-
.orElseThrow(() -> new NoSuchElementException("존재하지 않는 퀴즈입니다."));
127+
.orElseThrow(() -> new CustomException(QuizErrorCode.QUIZ_NOT_FOUND));
116128

117129
// TODO : util 메서드에서 사용자 ID 꺼내쓰는 식으로 수정하기
118130
if (1L != quiz.getCreator().getId()) {
119-
throw new RuntimeException("권한이 없습니다.");
131+
throw new CustomException(AuthErrorCode.FORBIDDEN);
120132
}
121133

122134
deleteThumbnailFile(quiz.getThumbnailUrl());
@@ -128,7 +140,7 @@ public void updateQuizTitle(Long quizId, String title) {
128140
Quiz quiz =
129141
quizRepository
130142
.findById(quizId)
131-
.orElseThrow(() -> new NoSuchElementException("존재하지 않는 퀴즈입니다."));
143+
.orElseThrow(() -> new CustomException(QuizErrorCode.QUIZ_NOT_FOUND));
132144

133145
validateTitle(title);
134146
quiz.changeTitle(title);
@@ -140,19 +152,19 @@ public void updateQuizDesc(Long quizId, String description) {
140152
Quiz quiz =
141153
quizRepository
142154
.findById(quizId)
143-
.orElseThrow(() -> new NoSuchElementException("존재하지 않는 퀴즈입니다."));
155+
.orElseThrow(() -> new CustomException(QuizErrorCode.QUIZ_NOT_FOUND));
144156

145157
validateDesc(description);
146158
quiz.changeDescription(description);
147159
}
148160

149161
@Transactional
150-
public void updateThumbnail(Long quizId, MultipartFile thumbnailFile) throws IOException {
162+
public void updateThumbnail(Long quizId, MultipartFile thumbnailFile) {
151163

152164
Quiz quiz =
153165
quizRepository
154166
.findById(quizId)
155-
.orElseThrow(() -> new NoSuchElementException("존재하지 않는 퀴즈입니다."));
167+
.orElseThrow(() -> new CustomException(QuizErrorCode.QUIZ_NOT_FOUND));
156168

157169
validateImageFile(thumbnailFile);
158170
String newThumbnailPath = convertToThumbnailPath(thumbnailFile);
@@ -163,13 +175,13 @@ public void updateThumbnail(Long quizId, MultipartFile thumbnailFile) throws IOE
163175

164176
private void validateDesc(String desc) {
165177
if (desc.trim().length() < 10 || desc.trim().length() > 50) {
166-
throw new IllegalArgumentException("설명은 10자 이상 50자 이하로 입력해주세요.");
178+
throw new CustomException(QuizErrorCode.INVALID_DESC_LENGTH);
167179
}
168180
}
169181

170182
private void validateTitle(String title) {
171183
if (title.trim().length() < 2 || title.trim().length() > 30) {
172-
throw new IllegalArgumentException("제목은 2자 이상 30자 이하로 입력해주세요.");
184+
throw new CustomException(QuizErrorCode.INVALID_TITLE_LENGTH);
173185
}
174186
}
175187

@@ -192,7 +204,7 @@ private void deleteThumbnailFile(String oldFilename) {
192204
}
193205
} catch (IOException e) {
194206
log.error("기존 썸네일 삭제 중 오류 : {}", filePath);
195-
throw new RuntimeException(e);
207+
throw new CustomException(QuizErrorCode.THUMBNAIL_DELETE_FAILED);
196208
}
197209
}
198210

@@ -202,9 +214,9 @@ public QuizListPageResponse getQuizzes(String title, String creator, Pageable pa
202214
Page<Quiz> quizzes;
203215

204216
// 검색어가 있을 때
205-
if (StringUtils.isBlank(title)) {
217+
if (!StringUtils.isBlank(title)) {
206218
quizzes = quizRepository.findQuizzesByTitleContaining(title, pageable);
207-
} else if (StringUtils.isBlank(creator)) {
219+
} else if (!StringUtils.isBlank(creator)) {
208220
quizzes = quizRepository.findQuizzesByCreator_NicknameContaining(creator, pageable);
209221
} else { // 검색어가 없을 때 혹은 빈 문자열일 때
210222
quizzes = quizRepository.findAll(pageable);
@@ -220,7 +232,7 @@ public Quiz getQuizWithQuestionsById(Long quizId) {
220232
Quiz quiz =
221233
quizRepository
222234
.findQuizWithQuestionsById(quizId)
223-
.orElseThrow(() -> new RuntimeException("E404002: 존재하지 않는 퀴즈입니다."));
235+
.orElseThrow(() -> new CustomException(QuizErrorCode.QUIZ_NOT_FOUND));
224236
return quiz;
225237
}
226238

@@ -234,7 +246,7 @@ public QuizQuestionListResponse getQuizWithQuestions(Long quizId) {
234246
Quiz quiz =
235247
quizRepository
236248
.findById(quizId)
237-
.orElseThrow(() -> new NoSuchElementException("존재하지 않는 퀴즈입니다."));
249+
.orElseThrow(() -> new CustomException(QuizErrorCode.QUIZ_NOT_FOUND));
238250

239251
return quizToQuizQuestionListResponse(quiz);
240252
}

backend/src/main/java/io/f1/backend/domain/quiz/entity/Quiz.java

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ public class Quiz extends BaseEntity {
5050
private String thumbnailUrl;
5151

5252
@ManyToOne
53-
@JoinColumn(name = "creator_id")
53+
@JoinColumn(name = "creator_id", nullable = true)
5454
private User creator;
5555

5656
public Quiz(
@@ -81,4 +81,20 @@ public void changeDescription(String description) {
8181
public void changeThumbnailUrl(String thumbnailUrl) {
8282
this.thumbnailUrl = thumbnailUrl;
8383
}
84+
85+
public Long findCreatorId() {
86+
if (this.creator == null) {
87+
return null;
88+
}
89+
90+
return this.creator.getId();
91+
}
92+
93+
public String findCreatorNickname() {
94+
if (this.creator == null) {
95+
return "탈퇴한 사용자";
96+
}
97+
98+
return this.creator.getNickname();
99+
}
84100
}

backend/src/main/java/io/f1/backend/domain/quiz/mapper/QuizMapper.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,15 +39,15 @@ public static QuizCreateResponse quizToQuizCreateResponse(Quiz quiz) {
3939
quiz.getQuizType(),
4040
quiz.getDescription(),
4141
quiz.getThumbnailUrl(),
42-
quiz.getCreator().getId());
42+
quiz.findCreatorId());
4343
}
4444

4545
public static QuizListResponse quizToQuizListResponse(Quiz quiz) {
4646
return new QuizListResponse(
4747
quiz.getId(),
4848
quiz.getTitle(),
4949
quiz.getDescription(),
50-
quiz.getCreator().getNickname(),
50+
quiz.findCreatorNickname(),
5151
quiz.getQuestions().size(),
5252
quiz.getThumbnailUrl());
5353
}
@@ -79,7 +79,7 @@ public static QuizQuestionListResponse quizToQuizQuestionListResponse(Quiz quiz)
7979
return new QuizQuestionListResponse(
8080
quiz.getTitle(),
8181
quiz.getQuizType(),
82-
quiz.getCreator().getId(),
82+
quiz.findCreatorId(),
8383
quiz.getDescription(),
8484
quiz.getThumbnailUrl(),
8585
quiz.getQuestions().size(),
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package io.f1.backend.global.exception.errorcode;
2+
3+
import lombok.Getter;
4+
import lombok.RequiredArgsConstructor;
5+
6+
import org.springframework.http.HttpStatus;
7+
8+
@Getter
9+
@RequiredArgsConstructor
10+
public enum GameErrorCode implements ErrorCode {
11+
GAME_SETTING_CONFLICT("E409002", HttpStatus.CONFLICT, "게임 설정이 맞지 않습니다.");
12+
13+
private final String code;
14+
15+
private final HttpStatus httpStatus;
16+
17+
private final String message;
18+
}

0 commit comments

Comments
 (0)