Skip to content

Commit 43ac5e2

Browse files
committed
♻️ refactor : 예외 코드 적용, NPE 방지, 조건문 오류 수정
1 parent a2048dd commit 43ac5e2

File tree

10 files changed

+102
-42
lines changed

10 files changed

+102
-42
lines changed

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

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@
1111
import io.f1.backend.domain.quiz.app.QuizService;
1212
import io.f1.backend.domain.quiz.entity.Quiz;
1313

14+
import io.f1.backend.global.exception.CustomException;
15+
import io.f1.backend.global.exception.errorcode.GameErrorCode;
16+
import io.f1.backend.global.exception.errorcode.RoomErrorCode;
1417
import lombok.RequiredArgsConstructor;
1518

1619
import org.springframework.context.ApplicationEventPublisher;
@@ -29,12 +32,12 @@ public class GameService {
2932
public GameStartData gameStart(Long roomId, Long quizId) {
3033

3134
Room room =
32-
roomRepository
33-
.findRoom(roomId)
34-
.orElseThrow(() -> new IllegalArgumentException("404 존재하지 않는 방입니다."));
35+
roomRepository
36+
.findRoom(roomId)
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: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
import io.f1.backend.domain.question.entity.TextQuestion;
1111
import io.f1.backend.domain.quiz.entity.Quiz;
1212

13+
import io.f1.backend.global.exception.CustomException;
14+
import io.f1.backend.global.exception.errorcode.QuestionErrorCode;
1315
import lombok.RequiredArgsConstructor;
1416

1517
import org.springframework.stereotype.Service;
@@ -44,7 +46,7 @@ public void updateQuestionContent(Long questionId, String content) {
4446
Question question =
4547
questionRepository
4648
.findById(questionId)
47-
.orElseThrow(() -> new NoSuchElementException("존재하지 않는 문제입니다."));
49+
.orElseThrow(() -> new CustomException(QuestionErrorCode.QUESTION_NOT_FOUND));
4850

4951
TextQuestion textQuestion = question.getTextQuestion();
5052
textQuestion.changeContent(content);
@@ -58,7 +60,7 @@ public void updateQuestionAnswer(Long questionId, String answer) {
5860
Question question =
5961
questionRepository
6062
.findById(questionId)
61-
.orElseThrow(() -> new NoSuchElementException("존재하지 않는 문제입니다."));
63+
.orElseThrow(() -> new CustomException(QuestionErrorCode.QUESTION_NOT_FOUND));
6264

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

7476
questionRepository.delete(question);
7577
}
7678

7779
private void validateAnswer(String answer) {
7880
if (answer.trim().length() < 5 || answer.trim().length() > 30) {
79-
throw new IllegalArgumentException("정답은 1자 이상 30자 이하로 입력해주세요.");
81+
throw new CustomException(QuestionErrorCode.INVALID_ANSWER_LENGTH);
8082
}
8183
}
8284

8385
private void validateContent(String content) {
8486
if (content.trim().length() < 5 || content.trim().length() > 30) {
85-
throw new IllegalArgumentException("문제는 5자 이상 30자 이하로 입력해주세요.");
87+
throw new CustomException(QuestionErrorCode.INVALID_CONTENT_LENGTH);
8688
}
8789
}
8890
}

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

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -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());

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
@@ -18,6 +18,9 @@
1818
import io.f1.backend.domain.user.dao.UserRepository;
1919
import io.f1.backend.domain.user.entity.User;
2020

21+
import io.f1.backend.global.exception.CustomException;
22+
import io.f1.backend.global.exception.errorcode.AuthErrorCode;
23+
import io.f1.backend.global.exception.errorcode.QuizErrorCode;
2124
import lombok.RequiredArgsConstructor;
2225
import lombok.extern.slf4j.Slf4j;
2326

@@ -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: 1 addition & 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(

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

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,21 +33,40 @@ public static Quiz quizCreateRequestToQuiz(
3333

3434
public static QuizCreateResponse quizToQuizCreateResponse(Quiz quiz) {
3535
// TODO : creatorId 넣어주는 부분에서 Getter를 안 쓰고, 현재 로그인한 유저의 id를 담는 식으로 바꿔도 될 듯
36+
getUserIdIfExists(quiz);
3637
return new QuizCreateResponse(
3738
quiz.getId(),
3839
quiz.getTitle(),
3940
quiz.getQuizType(),
4041
quiz.getDescription(),
4142
quiz.getThumbnailUrl(),
42-
quiz.getCreator().getId());
43+
getUserIdIfExists(quiz));
44+
}
45+
46+
private static Long getUserIdIfExists(Quiz quiz) {
47+
Long userId = null;
48+
if(quiz.getCreator()!=null){
49+
userId = quiz.getCreator().getId();
50+
}
51+
52+
return userId;
53+
}
54+
55+
private static String getUserNicknameIfExists(Quiz quiz) {
56+
String nickname = null;
57+
if(quiz.getCreator()!=null){
58+
nickname = quiz.getCreator().getNickname();
59+
}
60+
61+
return nickname;
4362
}
4463

4564
public static QuizListResponse quizToQuizListResponse(Quiz quiz) {
4665
return new QuizListResponse(
4766
quiz.getId(),
4867
quiz.getTitle(),
4968
quiz.getDescription(),
50-
quiz.getCreator().getNickname(),
69+
getUserNicknameIfExists(quiz),
5170
quiz.getQuestions().size(),
5271
quiz.getThumbnailUrl());
5372
}
@@ -79,7 +98,7 @@ public static QuizQuestionListResponse quizToQuizQuestionListResponse(Quiz quiz)
7998
return new QuizQuestionListResponse(
8099
quiz.getTitle(),
81100
quiz.getQuizType(),
82-
quiz.getCreator().getId(),
101+
getUserIdIfExists(quiz),
83102
quiz.getDescription(),
84103
quiz.getThumbnailUrl(),
85104
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+
import org.springframework.http.HttpStatus;
6+
7+
@Getter
8+
@RequiredArgsConstructor
9+
public enum GameErrorCode implements ErrorCode {
10+
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+
}

backend/src/main/java/io/f1/backend/global/exception/errorcode/QuestionErrorCode.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
@Getter
99
@RequiredArgsConstructor
1010
public enum QuestionErrorCode implements ErrorCode {
11+
INVALID_CONTENT_LENGTH("E400011", HttpStatus.BAD_REQUEST, "문제는 5자 이상 30자 이하로 입력해주세요."),
12+
INVALID_ANSWER_LENGTH("E400012", HttpStatus.BAD_REQUEST, "정답은 1자 이상 30자 이하로 입력해주세요."),
1113
QUESTION_NOT_FOUND("E404003", HttpStatus.NOT_FOUND, "존재하지 않는 문제입니다.");
1214

1315
private final String code;

backend/src/main/java/io/f1/backend/global/exception/errorcode/QuizErrorCode.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,14 @@
99
@RequiredArgsConstructor
1010
public enum QuizErrorCode implements ErrorCode {
1111
FILE_SIZE_TOO_LARGE("E400005", HttpStatus.BAD_REQUEST, "파일 크기가 너무 큽니다."),
12+
INVALID_TITLE_LENGTH("E400009", HttpStatus.BAD_REQUEST, "제목은 2자 이상 30자 이하로 입력해주세요."),
13+
INVALID_DESC_LENGTH("E400010", HttpStatus.BAD_REQUEST, "설명은 10자 이상 50자 이하로 입력해주세요."),
14+
UNSUPPORTED_IMAGE_FORMAT("E400013", HttpStatus.BAD_REQUEST, "지원하지 않는 이미지 형식입니다. (jpg, jpeg, png, webp 만 가능)"),
1215
UNSUPPORTED_MEDIA_TYPE("E415001", HttpStatus.UNSUPPORTED_MEDIA_TYPE, "지원하지 않는 파일 형식입니다."),
1316
INVALID_FILTER("E400007", HttpStatus.BAD_REQUEST, "title 또는 creator 중 하나만 입력 가능합니다."),
14-
QUIZ_NOT_FOUND("E404002", HttpStatus.NOT_FOUND, "존재하지 않는 퀴즈입니다.");
17+
QUIZ_NOT_FOUND("E404002", HttpStatus.NOT_FOUND, "존재하지 않는 퀴즈입니다."),
18+
THUMBNAIL_SAVE_FAILED("E500002", HttpStatus.INTERNAL_SERVER_ERROR, "썸네일 저장에 실패했습니다."),
19+
THUMBNAIL_DELETE_FAILED("E500003", HttpStatus.INTERNAL_SERVER_ERROR, "썸네일 삭제에 실패했습니다.");
1520

1621
private final String code;
1722

backend/src/main/java/io/f1/backend/global/exception/errorcode/RoomErrorCode.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
public enum RoomErrorCode implements ErrorCode {
1111
ROOM_USER_LIMIT_REACHED("E403002", HttpStatus.FORBIDDEN, "정원이 모두 찼습니다."),
1212
ROOM_GAME_IN_PROGRESS("E403003", HttpStatus.FORBIDDEN, "게임이 진행 중 입니다."),
13+
PLAYER_NOT_READY("E403004", HttpStatus.FORBIDDEN, "게임 시작을 위한 준비 상태가 아닙니다."),
1314
ROOM_NOT_FOUND("E404005", HttpStatus.NOT_FOUND, "존재하지 않는 방입니다."),
1415
WRONG_PASSWORD("E401006", HttpStatus.UNAUTHORIZED, "비밀번호가 일치하지않습니다.");
1516

0 commit comments

Comments
 (0)