Skip to content

Commit 7ff80b3

Browse files
committed
[feat]문제 이미지 업로드 기능 추가
1 parent 7f70a01 commit 7ff80b3

32 files changed

+812
-48
lines changed

build.gradle

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ dependencies {
4141
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0'
4242
implementation 'io.swagger.core.v3:swagger-core-jakarta:2.2.15'
4343

44+
//s3
45+
implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'
46+
4447
}
4548

4649
tasks.named('test') {

docker-compose-dev.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,13 @@ services:
1313
- SPRING_DATASOURCE_USERNAME=${MYSQL_USERNAME}
1414
- SPRING_DATASOURCE_PASSWORD=${MYSQL_PASSWORD}
1515
- SENTRY_DSN=${SENTRY_DSN}
16+
- CLOUD_AWS_S3_BUCKET=${S3_BUCKET}
17+
- CLOUD_AWS_S3_SIGNATURE_VERSION=AWS4-HMAC-SHA256
18+
- CLOUD_AWS_CREDENTIALS_ACCESS_KEY=${AWS_ACCESS_KEY}
19+
- CLOUD_AWS_CREDENTIALS_SECRET_KEY=${AWS_SECRET_KEY}
20+
- CLOUD_AWS_REGION_STATIC=${AWS_REGION}
21+
- CLOUD_AWS_REGION_AUTO=false
22+
- CLOUD_AWS_STACK_AUTO=false
1623
depends_on:
1724
- mysql
1825
networks:

src/main/java/com/moplus/moplus_server/domain/practiceTest/api/admin/PracticeTestAdminController.java

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -31,23 +31,6 @@ public String listPracticeTests(Model model) {
3131
return "practiceTestList";
3232
}
3333

34-
@PostMapping("/submitAnswers")
35-
@Operation(summary = "모의고사 문제 답안 생성 요청")
36-
public String submitAnswers(@RequestParam("practiceTestId") Long practiceTestId, HttpServletRequest request) {
37-
PracticeTest practiceTest = practiceTestService.getPracticeTestById(practiceTestId);
3834

39-
problemService.saveProblems(practiceTest, request);
40-
41-
return "redirect:/practiceTests";
42-
}
43-
44-
@PostMapping("/submitAnswers/{id}")
45-
@Operation(summary = "모의고사 문제 답안 수정 요청")
46-
public String updateAnswers(@PathVariable("id") Long id, HttpServletRequest request) {
47-
PracticeTest practiceTest = practiceTestService.getPracticeTestById(id);
48-
problemService.updateProblems(practiceTest, request);
49-
50-
return "redirect:/practiceTests";
51-
}
5235

5336
}

src/main/java/com/moplus/moplus_server/domain/practiceTest/api/admin/PracticeTestCreateController.java

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
11
package com.moplus.moplus_server.domain.practiceTest.api.admin;
22

33
import com.moplus.moplus_server.domain.practiceTest.dto.admin.request.PracticeTestRequest;
4-
import com.moplus.moplus_server.domain.practiceTest.dto.client.response.ProblemGetResponse;
5-
import com.moplus.moplus_server.domain.practiceTest.domain.PracticeTest;
64
import com.moplus.moplus_server.domain.practiceTest.service.admin.PracticeTestAdminService;
75
import com.moplus.moplus_server.domain.practiceTest.service.client.PracticeTestService;
86
import com.moplus.moplus_server.domain.practiceTest.service.client.ProblemService;
97
import io.swagger.v3.oas.annotations.Operation;
10-
import java.util.List;
118
import lombok.RequiredArgsConstructor;
129
import org.springframework.stereotype.Controller;
1310
import org.springframework.ui.Model;
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package com.moplus.moplus_server.domain.practiceTest.api.admin;
2+
3+
import com.moplus.moplus_server.domain.practiceTest.domain.PracticeTest;
4+
import com.moplus.moplus_server.domain.practiceTest.service.client.PracticeTestService;
5+
import com.moplus.moplus_server.domain.practiceTest.service.client.ProblemService;
6+
import io.swagger.v3.oas.annotations.Operation;
7+
import jakarta.servlet.http.HttpServletRequest;
8+
import lombok.RequiredArgsConstructor;
9+
import org.springframework.stereotype.Controller;
10+
import org.springframework.web.bind.annotation.PathVariable;
11+
import org.springframework.web.bind.annotation.PostMapping;
12+
import org.springframework.web.bind.annotation.RequestMapping;
13+
import org.springframework.web.bind.annotation.RequestParam;
14+
15+
@Controller
16+
@RequestMapping("/admin/practiceTests")
17+
@RequiredArgsConstructor
18+
public class ProblemAdminController {
19+
20+
private final ProblemService problemService;
21+
private final PracticeTestService practiceTestService;
22+
23+
@PostMapping("/submitAnswers")
24+
@Operation(summary = "모의고사 문제 답안 생성 요청")
25+
public String submitAnswers(@RequestParam("practiceTestId") Long practiceTestId, HttpServletRequest request) {
26+
PracticeTest practiceTest = practiceTestService.getPracticeTestById(practiceTestId);
27+
28+
problemService.saveProblems(practiceTest, request);
29+
30+
return String.format("redirect:/admin/practiceTests/imageUploadPage/%d", practiceTestId);
31+
}
32+
33+
@PostMapping("/submitAnswers/{id}")
34+
@Operation(summary = "모의고사 문제 답안 수정 요청")
35+
public String updateAnswers(@PathVariable("id") Long practiceTestId, HttpServletRequest request) {
36+
PracticeTest practiceTest = practiceTestService.getPracticeTestById(practiceTestId);
37+
problemService.updateProblems(practiceTest, request);
38+
39+
return String.format("redirect:/admin/practiceTests/imageUploadPage/%d", practiceTestId);
40+
}
41+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package com.moplus.moplus_server.domain.practiceTest.api.admin;
2+
3+
import com.moplus.moplus_server.domain.practiceTest.service.admin.ProblemImageUploadService;
4+
import io.swagger.v3.oas.annotations.Operation;
5+
import lombok.RequiredArgsConstructor;
6+
import org.springframework.stereotype.Controller;
7+
import org.springframework.ui.Model;
8+
import org.springframework.web.bind.annotation.GetMapping;
9+
import org.springframework.web.bind.annotation.PathVariable;
10+
import org.springframework.web.bind.annotation.PostMapping;
11+
import org.springframework.web.bind.annotation.RequestMapping;
12+
import org.springframework.web.bind.annotation.RequestParam;
13+
import org.springframework.web.bind.annotation.RestController;
14+
import org.springframework.web.multipart.MultipartFile;
15+
16+
@Controller
17+
@RequestMapping("/admin/practiceTests")
18+
@RequiredArgsConstructor
19+
public class ProblemImageUploadController {
20+
21+
private final ProblemImageUploadService problemImageUploadService;
22+
23+
@GetMapping("/imageUploadPage/{practiceTestId}")
24+
@Operation(summary = "문제 이미지 업로드 페이지 HTML 요청")
25+
public String showImageUploadPage(@PathVariable("practiceTestId") Long practiceTestId, Model model) {
26+
problemImageUploadService.setProblemImagesByPracticeTestId(practiceTestId, model);
27+
model.addAttribute("practiceTestId", practiceTestId);
28+
29+
return "imageUploadPage";
30+
}
31+
32+
@PostMapping("/uploadImage/{problemId}")
33+
@Operation(summary = "문제 이미지 업로드 요청")
34+
public String uploadImage(@RequestParam("practiceTestId") Long practiceTestId, @PathVariable("problemId") Long problemId, @RequestParam("image") MultipartFile image) {
35+
// 이미지 업로드 처리
36+
problemImageUploadService.uploadImage(practiceTestId ,problemId, image);
37+
38+
return "redirect:/admin/practiceTests/imageUploadPage/" + practiceTestId;
39+
}
40+
}

src/main/java/com/moplus/moplus_server/domain/practiceTest/api/client/PracticeTestController.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
public class PracticeTestController {
2020

2121
private final PracticeTestService practiceTestService;
22-
private final OptimisticLockPracticeTestFacade optimisticLockPracticeTestFacade;
2322

2423
@GetMapping("/all")
2524
@Operation(summary = "모든 모의고사 목록 조회 (검색용)")
@@ -30,7 +29,7 @@ public ResponseEntity<List<PracticeTestGetResponse>> getAllPracticeTests() {
3029
@PutMapping("/{practiceTestid}/viewCount")
3130
@Operation(summary = "조회수 업데이트하기")
3231
public ResponseEntity<Void> updateViewCount(@PathVariable("practiceTestid") Long id) throws InterruptedException {
33-
optimisticLockPracticeTestFacade.updateViewCount(id);
32+
practiceTestService.updateViewCount(id);
3433
return ResponseEntity.ok().body(null);
3534
}
3635

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package com.moplus.moplus_server.domain.practiceTest.domain;
2+
3+
import com.moplus.moplus_server.global.error.exception.ErrorCode;
4+
import com.moplus.moplus_server.global.error.exception.NotFoundException;
5+
import java.util.Arrays;
6+
import lombok.AllArgsConstructor;
7+
import lombok.Getter;
8+
9+
@Getter
10+
@AllArgsConstructor
11+
public enum FileExtension {
12+
JPEG("jpeg"),
13+
JPG("jpg"),
14+
PNG("png"),
15+
PDF("pdf"),
16+
;
17+
18+
private final String uploadExtension;
19+
20+
public static FileExtension from(String uploadExtension) {
21+
return Arrays.stream(values())
22+
.filter(
23+
imageFileExtension ->
24+
imageFileExtension.uploadExtension.equals(uploadExtension))
25+
.findFirst()
26+
.orElseThrow(() -> new NotFoundException(ErrorCode.IMAGE_FILE_EXTENSION_NOT_FOUND));
27+
}
28+
}

src/main/java/com/moplus/moplus_server/domain/practiceTest/domain/Problem.java

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.moplus.moplus_server.domain.practiceTest.domain;
22

33
import com.moplus.moplus_server.global.common.BaseEntity;
4+
import jakarta.persistence.CascadeType;
45
import jakarta.persistence.Column;
56
import jakarta.persistence.Entity;
67
import jakarta.persistence.EnumType;
@@ -11,6 +12,7 @@
1112
import jakarta.persistence.Id;
1213
import jakarta.persistence.JoinColumn;
1314
import jakarta.persistence.ManyToOne;
15+
import jakarta.persistence.OneToOne;
1416
import lombok.Builder;
1517
import lombok.Getter;
1618
import lombok.NoArgsConstructor;
@@ -35,13 +37,21 @@ public class Problem extends BaseEntity {
3537
private String subunit;
3638
double correctRate;
3739

38-
@ManyToOne(fetch = FetchType.LAZY)
40+
@Enumerated(EnumType.STRING)
41+
ProblemRating problemRating;
42+
43+
@ManyToOne()
3944
@JoinColumn(name = "practice_test_id")
4045
private PracticeTest practiceTest;
4146

47+
@OneToOne(cascade = CascadeType.REMOVE, fetch = FetchType.LAZY, orphanRemoval = true)
48+
@JoinColumn(name = "problem_image_id")
49+
private ProblemImage image;
50+
4251
@Builder
4352
public Problem(String problemNumber, AnswerFormat answerFormat, String answer, int point, Long incorrectNum,
44-
String conceptType, String unit, String subunit, PracticeTest practiceTest, double correctRate) {
53+
String conceptType, String unit, String subunit, double correctRate, ProblemRating problemRating,
54+
PracticeTest practiceTest) {
4555
this.problemNumber = problemNumber;
4656
this.answerFormat = answerFormat;
4757
this.answer = answer;
@@ -50,8 +60,18 @@ public Problem(String problemNumber, AnswerFormat answerFormat, String answer, i
5060
this.conceptType = conceptType;
5161
this.unit = unit;
5262
this.subunit = subunit;
53-
this.practiceTest = practiceTest;
5463
this.correctRate = correctRate;
64+
this.problemRating = problemRating;
65+
this.practiceTest = practiceTest;
66+
}
67+
68+
69+
public void addImage(ProblemImage image) {
70+
this.image = image;
71+
}
72+
73+
public void calculateProblemRating(){
74+
this.problemRating = ProblemRating.findProblemRating(this);
5575
}
5676

5777
public void updateAnswer(String answer) {
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package com.moplus.moplus_server.domain.practiceTest.domain;
2+
3+
import jakarta.persistence.Column;
4+
import jakarta.persistence.Entity;
5+
import jakarta.persistence.EnumType;
6+
import jakarta.persistence.Enumerated;
7+
import jakarta.persistence.GeneratedValue;
8+
import jakarta.persistence.GenerationType;
9+
import jakarta.persistence.Id;
10+
import lombok.Builder;
11+
import lombok.Getter;
12+
import lombok.NoArgsConstructor;
13+
14+
@Entity
15+
@Getter
16+
@NoArgsConstructor
17+
public class ProblemImage {
18+
19+
@Id
20+
@GeneratedValue(strategy = GenerationType.IDENTITY)
21+
@Column(name = "problem_image_id")
22+
private Long id;
23+
24+
private String fileName;
25+
26+
private String imageUrl;
27+
28+
private Long problemId;
29+
30+
@Builder
31+
public ProblemImage(String fileName, String imageUrl, Long problemId) {
32+
this.fileName = fileName;
33+
this.imageUrl = imageUrl;
34+
this.problemId = problemId;
35+
}
36+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package com.moplus.moplus_server.domain.practiceTest.domain;
2+
3+
import java.util.Arrays;
4+
import lombok.AllArgsConstructor;
5+
import lombok.Builder;
6+
import lombok.Getter;
7+
8+
@Getter
9+
@AllArgsConstructor
10+
public enum ProblemRating {
11+
12+
EXTREME("극상위권", 0, 30, "최상"),
13+
TIER_1("1등급", 30, 50, "상"),
14+
TIER_2("2등급", 50, 60, "중상"),
15+
TIER_3("3등급", 60, 80, "중"),
16+
TIER_4("4등급", 80, 90, "중하"),
17+
OTHER("5등급 이하", 90, 100, "하"),
18+
;
19+
20+
private String rating;
21+
private double startCorrectRateRange;
22+
private double endCorrectRateRange;
23+
private String difficultyLevel;
24+
25+
public static ProblemRating findProblemRating(Problem problem) {
26+
return Arrays.stream(values())
27+
.filter(problemRating -> problemRating.startCorrectRateRange <= problem.getCorrectRate()
28+
&& problemRating.endCorrectRateRange > problem.getCorrectRate())
29+
.findFirst()
30+
.orElse(OTHER);
31+
}
32+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package com.moplus.moplus_server.domain.practiceTest.dto.admin.request;
2+
3+
import com.moplus.moplus_server.domain.practiceTest.domain.Problem;
4+
import lombok.Builder;
5+
6+
@Builder
7+
public record ProblemImageRequest(
8+
Long problemId,
9+
String problemNumber,
10+
String imageUrl
11+
) {
12+
13+
public static ProblemImageRequest of(Problem problem) {
14+
return ProblemImageRequest.builder()
15+
.problemId(problem.getId())
16+
.problemNumber(problem.getProblemNumber())
17+
.imageUrl(problem.getImage() != null ? problem.getImage().getImageUrl() : null)
18+
.build();
19+
}
20+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.moplus.moplus_server.domain.practiceTest.repository;
2+
3+
import com.moplus.moplus_server.domain.practiceTest.domain.ProblemImage;
4+
import java.util.Optional;
5+
import javax.swing.JPanel;
6+
import org.springframework.data.jpa.repository.JpaRepository;
7+
8+
public interface ProblemImageRepository extends JpaRepository<ProblemImage, Long> {
9+
10+
Optional<ProblemImage> findByProblemId(Long problemId);
11+
}

src/main/java/com/moplus/moplus_server/domain/practiceTest/repository/ProblemRepository.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
package com.moplus.moplus_server.domain.practiceTest.repository;
22

3+
import com.moplus.moplus_server.domain.practiceTest.domain.PracticeTest;
34
import com.moplus.moplus_server.domain.practiceTest.domain.Problem;
5+
import io.swagger.v3.oas.annotations.media.Schema;
6+
import jakarta.persistence.LockModeType;
47
import java.util.List;
58
import java.util.Optional;
69
import org.springframework.data.jpa.repository.JpaRepository;
10+
import org.springframework.data.jpa.repository.Lock;
11+
import org.springframework.data.jpa.repository.Query;
12+
import org.springframework.data.repository.query.Param;
713

814
public interface ProblemRepository extends JpaRepository<Problem, Long> {
915

@@ -12,4 +18,8 @@ public interface ProblemRepository extends JpaRepository<Problem, Long> {
1218
void deleteAllByPracticeTestId(Long id);
1319

1420
Optional<Problem> findByProblemNumberAndPracticeTestId(String problemNumber, Long practiceTest_id);
21+
22+
@Lock(LockModeType.PESSIMISTIC_WRITE)
23+
@Query("SELECT p FROM Problem p WHERE p.problemNumber = :problem_number AND p.practiceTest.id = :practice_test_id")
24+
Optional<Problem> findByProblemNumberAndPracticeTestIdWithPessimisticLock(@Param("problem_number") String problemNumber,@Param("practice_test_id") Long practiceTest_id);
1525
}

0 commit comments

Comments
 (0)