Skip to content

Commit d47fd91

Browse files
authored
Merge pull request #40 from AI-Tutor-2024/security
[ADD] 문제 저장 및 수정 코드 제작 및 수정
2 parents 659d859 + fbd67fb commit d47fd91

File tree

9 files changed

+232
-35
lines changed

9 files changed

+232
-35
lines changed

src/main/java/com/example/ai_tutor/domain/note/presentation/NoteController.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ public ResponseEntity<?> deleteNote(
106106

107107

108108

109-
@Operation(summary = "노트 STT 변환 API", security = { @SecurityRequirement(name = "BearerAuth") }, description = "노트의 강의 영상을 CLOVA API를 활용하여 STT 변환하는 API입니다. 처음 영상을 올리는 것이라면 필수적으로 이 API를 요청하여 영상을 TEXT로 변환하여야 합니다.")
109+
@Operation(summary = "1. 노트 STT 변환 API", security = { @SecurityRequirement(name = "BearerAuth") }, description = "노트의 강의 영상을 CLOVA API를 활용하여 STT 변환하는 API입니다. 처음 영상을 올리는 것이라면 필수적으로 이 API를 요청하여 영상을 TEXT로 변환하여야 합니다.")
110110
@ApiResponses(value = {
111111
@ApiResponse(responseCode = "200", description = "STT 변환 성공", content = { @Content(mediaType = "application/json", schema = @Schema(implementation = Message.class)) }),
112112
@ApiResponse(responseCode = "400", description = "STT 변환 실패", content = { @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class)) }),
@@ -136,7 +136,7 @@ public ResponseEntity<?> convertSpeechToText(
136136
// ===============================
137137
// 📑 노트 요약 생성 & 조회
138138
// ===============================
139-
@Operation(summary = "노트 요약 생성", security = { @SecurityRequirement(name = "BearerAuth") }, description = "저장된 STT 데이터를 기반으로 노트 요약을 생성합니다.")
139+
@Operation(summary = "2. 노트 요약 생성", security = { @SecurityRequirement(name = "BearerAuth") }, description = "저장된 STT 데이터를 기반으로 노트 요약을 생성합니다.")
140140
@ApiResponses({
141141
@ApiResponse(responseCode = "200", description = "노트 요약 생성 성공",
142142
content = @Content(mediaType = "application/json", schema = @Schema(implementation = SummaryRes.class))),

src/main/java/com/example/ai_tutor/domain/practice/application/ProfessorPracticeService.java

Lines changed: 60 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import com.example.ai_tutor.domain.practice.domain.repository.PracticeRepository;
99
import com.example.ai_tutor.domain.practice.dto.request.CreatePracticeReq;
1010
import com.example.ai_tutor.domain.practice.dto.request.SavePracticeReq;
11+
import com.example.ai_tutor.domain.practice.dto.request.UpdatePracticeReq;
1112
import com.example.ai_tutor.domain.practice.dto.response.CreatePracticeListRes;
1213
import com.example.ai_tutor.domain.practice.dto.response.CreatePracticeRes;
1314
import com.example.ai_tutor.domain.practice.dto.response.ProfessorPracticeRes;
@@ -19,6 +20,7 @@
1920
import com.example.ai_tutor.global.DefaultAssert;
2021
import com.example.ai_tutor.global.config.security.token.UserPrincipal;
2122
import com.example.ai_tutor.global.payload.ApiResponse;
23+
import com.example.ai_tutor.global.payload.ErrorCode;
2224
import lombok.RequiredArgsConstructor;
2325
import lombok.extern.slf4j.Slf4j;
2426
import org.springframework.http.ResponseEntity;
@@ -112,32 +114,59 @@ private ApiResponse<CreatePracticeListRes> buildApiResponse(List<CreatePracticeR
112114
.build();
113115
}
114116

117+
// 문제 저장 메서드
115118
@Transactional
116119
public ResponseEntity<?> savePractice(UserPrincipal userPrincipal, Long noteId, List<SavePracticeReq> savePracticeReqs) {
117-
User user = validateUser(userPrincipal);
118-
Professor professor = validateProfessor(userPrincipal);
120+
validateUser(userPrincipal);
121+
validateProfessor(userPrincipal);
119122
Note note = validateNote(noteId);
120123

121-
for (SavePracticeReq req : savePracticeReqs) {
122-
List<String> additionalRes = Objects.equals(req.getPracticeType(), "OX") ? null : req.getAdditionalResults();
123-
Practice practice = Practice.builder()
124+
125+
List<Practice> practices = savePracticeReqs.stream().map(request -> {
126+
PracticeType type = PracticeType.valueOf(request.getPracticeType());
127+
List<String> add = type == PracticeType.OX ? null : request.getAdditionalResults();
128+
return Practice.builder()
124129
.note(note)
125-
.sequence(req.getPracticeNumber())
126-
.content(req.getContent())
127-
.additionalResults(additionalRes)
128-
.result(req.getResult())
129-
.solution(req.getSolution())
130-
.practiceType(PracticeType.valueOf(req.getPracticeType()))
130+
.sequence(request.getPracticeNumber())
131+
.content(request.getContent())
132+
.additionalResults(add)
133+
.result(request.getResult())
134+
.solution(request.getSolution())
135+
.practiceType(type)
131136
.build();
132-
practiceRepository.save(practice);
133-
}
137+
}).toList();
134138

135-
ApiResponse<String> apiResponse = ApiResponse.<String>builder()
139+
practiceRepository.saveAll(practices); // 배치 저장
140+
141+
ApiResponse<List<ProfessorPracticeRes>> api = ApiResponse.<List<ProfessorPracticeRes>>builder()
136142
.check(true)
137-
.information("문제가 저장되었습니다.")
143+
.information(practices.stream().map(this::toResponse).toList())
138144
.build();
145+
return ResponseEntity.ok(api);
146+
}
139147

140-
return ResponseEntity.ok(apiResponse);
148+
@Transactional
149+
public ResponseEntity<?> updatePractice(UserPrincipal userPrincipal, Long noteId,
150+
Long practiceId, UpdatePracticeReq req) {
151+
validateUser(userPrincipal);
152+
validateProfessor(userPrincipal);
153+
Note note = validateNote(noteId); // 노트 존재 확인
154+
155+
Practice practice = practiceRepository.findById(practiceId)
156+
.orElseThrow(() -> new IllegalArgumentException("문제가 존재하지 않습니다."));
157+
158+
// 노트–문제 매핑 검증
159+
if (!practice.getNote().getNoteId().equals(note.getNoteId())) {
160+
throw new IllegalArgumentException("noteId와 practiceId가 일치하지 않습니다.");
161+
}
162+
163+
practice.update(req);
164+
practiceRepository.save(practice);
165+
ApiResponse<ProfessorPracticeRes> api = ApiResponse.<ProfessorPracticeRes>builder()
166+
.check(true)
167+
.information(toResponse(practice))
168+
.build();
169+
return ResponseEntity.ok(api);
141170
}
142171

143172
public ResponseEntity<?> getPractices(UserPrincipal userPrincipal, Long noteId) {
@@ -147,7 +176,7 @@ public ResponseEntity<?> getPractices(UserPrincipal userPrincipal, Long noteId)
147176

148177
List<ProfessorPracticeRes> practiceResList = practices.stream()
149178
.map(practice -> ProfessorPracticeRes.builder()
150-
.praticeId(practice.getPracticeId())
179+
.practiceId(practice.getPracticeId())
151180
.practiceNumber(practice.getSequence())
152181
.content(practice.getContent())
153182
.additionalResults(practice.getPracticeType() == PracticeType.OX ? null : practice.getAdditionalResults())
@@ -198,4 +227,18 @@ private User getUser(UserPrincipal userPrincipal) {
198227
return userRepository.findByEmail(email)
199228
.orElseThrow(() -> new IllegalArgumentException("사용자를 찾을 수 없습니다."));
200229
}
230+
231+
232+
/** Practice → ProfessorPracticeRes 매핑용 */
233+
private ProfessorPracticeRes toResponse(Practice p) {
234+
return ProfessorPracticeRes.builder()
235+
.practiceId(p.getPracticeId())
236+
.practiceNumber(p.getSequence())
237+
.content(p.getContent())
238+
.additionalResults(p.getPracticeType() == PracticeType.OX ? null : p.getAdditionalResults())
239+
.result(p.getResult())
240+
.solution(p.getSolution())
241+
.practiceType(p.getPracticeType().toString())
242+
.build();
243+
}
201244
}

src/main/java/com/example/ai_tutor/domain/practice/domain/Practice.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
import com.example.ai_tutor.domain.common.BaseEntity;
44
import com.example.ai_tutor.domain.note.domain.Note;
5+
import com.example.ai_tutor.domain.practice.dto.request.UpdatePracticeReq;
6+
import com.example.ai_tutor.domain.practice.dto.response.ProfessorPracticeRes;
57
import jakarta.persistence.*;
68
import lombok.Builder;
79
import lombok.Getter;
@@ -60,4 +62,16 @@ public Practice(Note note, String content, String solution, Integer sequence, S
6062
}
6163

6264
// public void updateUserAnswer(String userAnswer) { this.userAnswer = userAnswer; }
65+
public void update(UpdatePracticeReq dto) {
66+
if (dto.getPracticeNumber() != null) this.sequence = dto.getPracticeNumber();
67+
if (dto.getContent() != null) this.content = dto.getContent();
68+
if (dto.getSolution() != null) this.solution = dto.getSolution();
69+
if (dto.getResult() != null) this.result = dto.getResult();
70+
if (dto.getPracticeType() != null)
71+
this.practiceType = PracticeType.valueOf(dto.getPracticeType());
72+
if (dto.getAdditionalResults() != null
73+
&& !PracticeType.OX.name().equalsIgnoreCase(dto.getPracticeType())) {
74+
this.additionalResults = dto.getAdditionalResults();
75+
}
76+
}
6377
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package com.example.ai_tutor.domain.practice.dto.request;
2+
3+
import com.fasterxml.jackson.annotation.JsonInclude;
4+
import io.swagger.v3.oas.annotations.media.ArraySchema;
5+
import io.swagger.v3.oas.annotations.media.Schema;
6+
import lombok.AllArgsConstructor;
7+
import lombok.Getter;
8+
import lombok.NoArgsConstructor;
9+
10+
import java.util.List;
11+
12+
@Getter
13+
@NoArgsConstructor
14+
@AllArgsConstructor
15+
@JsonInclude(JsonInclude.Include.NON_NULL) // null 필드는 직렬화하지 않음
16+
public class UpdatePracticeReq {
17+
18+
@Schema(type="int", example="2", description="(선택) 문제 번호")
19+
private Integer practiceNumber;
20+
21+
@Schema(type="String", example="수정된 문제 내용", description="(선택) 문제 내용")
22+
private String content;
23+
24+
@ArraySchema(schema=@Schema(type="String", example="사용 안하고 있어요. 추가 인정 답안이라고 합니다.", description="(선택) 추가 인정 답안"))
25+
private List<String> additionalResults;
26+
27+
@Schema(type="String", example="O", description="(선택) 정답")
28+
private String result;
29+
30+
@Schema(type="String", example="수정된 해설", description="(선택) 해설")
31+
private String solution;
32+
33+
@Schema(type="String", example="SHORT", description="(선택) 문제 타입(OX 또는 SHORT)")
34+
private String practiceType;
35+
}
36+
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package com.example.ai_tutor.domain.practice.dto.response;
2+
3+
import io.swagger.v3.oas.annotations.media.ArraySchema;
4+
import io.swagger.v3.oas.annotations.media.Schema;
5+
import lombok.AllArgsConstructor;
6+
import lombok.Getter;
7+
import lombok.NoArgsConstructor;
8+
9+
import java.util.List;
10+
11+
@Getter
12+
@NoArgsConstructor
13+
@AllArgsConstructor
14+
@Schema(name = "PracticeSaveApiResponse", description = "문제 저장 응답")
15+
public class PracticeSaveApiResponse {
16+
17+
@Schema(description = "성공 여부", example = "true")
18+
private boolean check;
19+
20+
@ArraySchema(schema = @Schema(implementation = ProfessorPracticeRes.class))
21+
private List<ProfessorPracticeRes> information;
22+
}

src/main/java/com/example/ai_tutor/domain/practice/dto/response/ProfessorPracticeRes.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
public class ProfessorPracticeRes {
1717

1818
@Schema(type = "Long", example ="1", description="문제의 id입니다.")
19-
private Long praticeId;
19+
private Long practiceId;
2020

2121
@Schema(type = "int", example ="1", description="문제의 번호입니다.")
2222
private int practiceNumber;

src/main/java/com/example/ai_tutor/domain/practice/presentation/ProfessorPracticeController.java

Lines changed: 56 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,16 @@
44
import com.example.ai_tutor.domain.practice.dto.request.CreatePracticeReq;
55
import com.example.ai_tutor.domain.practice.dto.request.SavePracticeListReq;
66
import com.example.ai_tutor.domain.practice.dto.request.SavePracticeReq;
7+
import com.example.ai_tutor.domain.practice.dto.request.UpdatePracticeReq;
78
import com.example.ai_tutor.domain.practice.dto.response.CreatePracticeListRes;
9+
import com.example.ai_tutor.domain.practice.dto.response.PracticeSaveApiResponse;
810
import com.example.ai_tutor.domain.practice.dto.response.ProfessorPracticeListRes;
11+
import com.example.ai_tutor.domain.practice.dto.response.ProfessorPracticeRes;
912
import com.example.ai_tutor.global.config.security.token.UserPrincipal;
1013
import com.example.ai_tutor.global.payload.ErrorResponse;
1114
import io.swagger.v3.oas.annotations.Operation;
1215
import io.swagger.v3.oas.annotations.Parameter;
16+
import io.swagger.v3.oas.annotations.media.ArraySchema;
1317
import io.swagger.v3.oas.annotations.media.Content;
1418
import io.swagger.v3.oas.annotations.media.Schema;
1519
import io.swagger.v3.oas.annotations.responses.ApiResponse;
@@ -68,29 +72,74 @@ public Mono<ResponseEntity<com.example.ai_tutor.global.payload.ApiResponse<Creat
6872
security = { @SecurityRequirement(name = "BearerAuth") },
6973
description = "문제 생성 API를 통해 요청했던 문제들 중 교수님이 원하는 문제들만 선택하여 저장하는 API 입니다.",
7074
responses = {
71-
@ApiResponse(responseCode = "200", description = "Practice 문제 저장 성공",
72-
content = @Content(schema = @Schema(implementation = com.example.ai_tutor.global.payload.ApiResponse.class))),
75+
@ApiResponse(
76+
responseCode = "200",
77+
description = "Practice 문제 저장 성공",
78+
content = @Content(mediaType = "application/json",
79+
schema = @Schema(implementation = PracticeSaveApiResponse.class)
80+
)),
7381
@ApiResponse(responseCode = "400", description = "잘못된 요청",
7482
content = @Content(schema = @Schema(implementation = com.example.ai_tutor.global.payload.ApiResponse.class))),
7583
@ApiResponse(responseCode = "500", description = "서버 오류",
7684
content = @Content(schema = @Schema(implementation = com.example.ai_tutor.global.payload.ApiResponse.class)))
7785
})
86+
7887
@PostMapping("/{noteId}")
7988
@PreAuthorize("isAuthenticated()")
8089
public ResponseEntity<?> savePractice(
90+
@Parameter(description = "Access Token을 입력해주세요.", required = true)
91+
@AuthenticationPrincipal UserPrincipal userPrincipal,
8192

82-
@Parameter(description = "Access Token을 입력해주세요.", required = true) @AuthenticationPrincipal UserPrincipal userPrincipal,
8393
@io.swagger.v3.oas.annotations.parameters.RequestBody(
84-
description = "Schemas의 SavePracticeListReq를 참고해주세요",
94+
description = "SavePracticeReq 배열을 전달해주세요",
8595
required = true,
86-
content = @Content(schema = @Schema(implementation = SavePracticeListReq.class))
87-
)@RequestBody List<SavePracticeReq> savePracticeReqs,
88-
@Parameter(description = "note의 id를 입력해주세요", required = true) @PathVariable Long noteId
96+
content = @Content(array = @ArraySchema(schema = @Schema(implementation = SavePracticeReq.class)))
97+
)
98+
@RequestBody List<SavePracticeReq> savePracticeReqs,
8999

100+
@Parameter(description = "note의 id를 입력해주세요", required = true)
101+
@PathVariable Long noteId
90102
) {
91103
return professorPracticeService.savePractice(userPrincipal, noteId, savePracticeReqs);
92104
}
93105

106+
107+
// 저장된 문제 수정 메서드
108+
@PatchMapping("/{noteId}/{practiceId}")
109+
@PreAuthorize("isAuthenticated()")
110+
@Operation(summary = "문제 수정",
111+
security = { @SecurityRequirement(name = "BearerAuth") },
112+
description = "저장된 문제의 내용‧답안‧해설 등을 부분 수정합니다. additionalResults 필드는 신경쓰지마시고 필드에서 제외하고 요청해주셔도 됩니다.",
113+
responses = {
114+
@ApiResponse(
115+
responseCode = "200",
116+
description = "Practice 문제 저장 성공",
117+
content = @Content(mediaType = "application/json",
118+
schema = @Schema(implementation = PracticeSaveApiResponse.class)
119+
)),
120+
@ApiResponse(responseCode = "400", description = "잘못된 요청",
121+
content = @Content(schema = @Schema(implementation = com.example.ai_tutor.global.payload.ApiResponse.class))),
122+
@ApiResponse(responseCode = "500", description = "서버 오류",
123+
content = @Content(schema = @Schema(implementation = com.example.ai_tutor.global.payload.ApiResponse.class)))
124+
})
125+
public ResponseEntity<?> updatePractice(
126+
@Parameter(description = "Access Token을 입력해주세요.", required = true)
127+
@AuthenticationPrincipal UserPrincipal userPrincipal,
128+
@Parameter(description = "note의 id를 입력해주세요", required = true)
129+
@PathVariable Long noteId,
130+
@Parameter(description = "practice의 id를 입력해주세요", required = true)
131+
@PathVariable Long practiceId,
132+
@io.swagger.v3.oas.annotations.parameters.RequestBody(
133+
description = "수정할 필드만 포함한 UpdatePracticeReq",
134+
required = true,
135+
content = @Content(schema = @Schema(implementation = UpdatePracticeReq.class))
136+
)
137+
@RequestBody UpdatePracticeReq updateReq
138+
) {
139+
return professorPracticeService.updatePractice(
140+
userPrincipal, noteId, practiceId, updateReq);
141+
}
142+
94143
// 문제 조회
95144
@Operation(summary = "문제 조회", description = "생성된 문제, 답안, 해설을 조회합니다.")
96145
@ApiResponses(value = {
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package com.example.ai_tutor.global.config;
2+
3+
import io.swagger.v3.oas.models.OpenAPI;
4+
import io.swagger.v3.oas.models.Components;
5+
import io.swagger.v3.oas.models.info.Info;
6+
import io.swagger.v3.oas.models.security.SecurityRequirement;
7+
import io.swagger.v3.oas.models.security.SecurityScheme;
8+
import org.springframework.context.annotation.Bean;
9+
import org.springframework.context.annotation.Configuration;
10+
11+
@Configuration
12+
public class SwaggerConfig {
13+
14+
@Bean
15+
public OpenAPI openAPI() {
16+
return new OpenAPI()
17+
.components(new Components()
18+
.addSecuritySchemes("BearerAuth",
19+
new SecurityScheme()
20+
.type(SecurityScheme.Type.HTTP)
21+
.scheme("bearer")
22+
.bearerFormat("JWT")
23+
))
24+
.addSecurityItem(new SecurityRequirement().addList("BearerAuth"))
25+
.info(new Info()
26+
.title("AI Tutor API")
27+
.version("v1.0.0")
28+
.description("AI Tutor 백엔드 API 문서입니다.")
29+
);
30+
}
31+
}

0 commit comments

Comments
 (0)