ありがとうございます。
ここから 第11段階:教材作成機能設計(Form・Validation・登録処理) に進みます。
今回は教師が以下を実行できる機能を設計します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 | 教師
教材作成
↓
教材タイプ選択
├ 動画
├ テキスト(Markdown)
├ クイズ
└ ファイル
↓
下書き保存
または
審査申請
|
第11段階の設計方針
教材作成では、以前設計した
| Content
|
└ ContentVersion
|
├ VideoContent
├ TextContent
├ QuizContent
└ FileContent
|
をそのまま利用します。
重要な設計ポイント:
-
Content:教材そのもの(ID固定)
-
ContentVersion:編集履歴
-
各タイプテーブル:教材固有情報
です。
配置:
| form
└ content
├ ContentCreateForm.java
├ VideoContentForm.java
├ TextContentForm.java
├ QuizContentForm.java
├ QuizChoiceForm.java
└ FileContentForm.java
|
ContentCreateForm.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32 | package com.example.lms.form.content;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@Data
public class ContentCreateForm {
@NotBlank(message="タイトルは必須です")
private String title;
@NotBlank(message="概要は必須です")
private String summary;
@NotNull(message="教材タイプを選択してください")
private String contentType;
@Valid
private VideoContentForm video;
@Valid
private TextContentForm text;
@Valid
private QuizContentForm quiz;
@Valid
private FileContentForm file;
}
|
対応:
-
YouTube
-
Vimeo
-
Cloudflare R2
VideoContentForm.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 | package com.example.lms.form.content;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
@Data
public class VideoContentForm {
@NotBlank(message="動画種別を入力してください")
private String videoType;
private String videoId;
private String videoUrl;
}
|
Markdown対応。
TextContentForm.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14 | package com.example.lms.form.content;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
@Data
public class TextContentForm {
@NotBlank(message="本文を入力してください")
private String body;
private String format="MARKDOWN";
}
|
複数正解対応です。
例:
にも対応します。
QuizContentForm.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31 | package com.example.lms.form.content;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
@Data
public class QuizContentForm {
@NotBlank(message="カテゴリを入力してください")
private String category;
@NotBlank(message="問題文を入力してください")
private String question;
private boolean multipleAnswer;
private Integer answerCount;
@Valid
private List<QuizChoiceForm> choices =
new ArrayList<>();
private String explanation;
}
|
1
2
3
4
5
6
7
8
9
10
11
12 | package com.example.lms.form.content;
import lombok.Data;
@Data
public class QuizChoiceForm {
private String text;
private boolean correct;
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13 | package com.example.lms.form.content;
import lombok.Data;
import org.springframework.web.multipart.MultipartFile;
@Data
public class FileContentForm {
private String description;
private MultipartFile file;
}
|
8. Controller設計
URL:
| GET
/teacher/content/create
POST
/teacher/content/create
|
ContentCreateController
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44 | @Controller
@RequestMapping("/teacher/content")
@RequiredArgsConstructor
public class ContentCreateController {
private final ContentCommandService service;
private final LoginUser loginUser;
@GetMapping("/create")
public String create(
Model model){
model.addAttribute(
"form",
new ContentCreateForm()
);
return "teacher/content/create";
}
@PostMapping("/create")
public String save(
@Valid
@ModelAttribute("form")
ContentCreateForm form,
BindingResult result){
if(result.hasErrors()){
return "teacher/content/create";
}
service.create(
form,
loginUser.get()
);
return "redirect:/teacher/content";
}
}
|
9. Service処理
ここが中心です。
ContentCommandService
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71 | @Transactional
public Long create(
ContentCreateForm form,
User teacher){
Content content =
new Content();
content.setAuthor(
teacher
);
content.setContentType(
ContentType.valueOf(
form.getContentType()
)
);
contentRepository.save(content);
ContentVersion version =
new ContentVersion();
version.setContent(
content
);
version.setVersionNo(
1
);
version.setTitle(
form.getTitle()
);
version.setSummary(
form.getSummary()
);
version.setStatus(
ContentStatus.DRAFT
);
contentVersionRepository.save(
version
);
saveDetail(
version,
form
);
content.setCurrentVersion(
version
);
return content.getId();
}
|
10. タイプ別登録
分岐
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39 | private void saveDetail(
ContentVersion version,
ContentCreateForm form){
switch(
ContentType.valueOf(
form.getContentType()
)
){
case VIDEO ->
saveVideo(
version,
form.getVideo()
);
case TEXT ->
saveText(
version,
form.getText()
);
case QUIZ ->
saveQuiz(
version,
form.getQuiz()
);
case FILE ->
saveFile(
version,
form.getFile()
);
}
}
|
11. Markdown登録
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24 | private void saveText(
ContentVersion version,
TextContentForm form){
TextContent text =
new TextContent();
text.setVersion(
version
);
text.setBody(
form.getBody()
);
text.setFormat(
form.getFormat()
);
textContentRepository.save(
text
);
}
|
12. 動画登録
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33 | private void saveVideo(
ContentVersion version,
VideoContentForm form){
VideoContent video =
new VideoContent();
video.setVersion(
version
);
video.setVideoType(
form.getVideoType()
);
video.setVideoId(
form.getVideoId()
);
video.setVideoUrl(
form.getVideoUrl()
);
videoRepository.save(
video
);
}
|
13. クイズ登録
複数正解対応。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74 | private void saveQuiz(
ContentVersion version,
QuizContentForm form){
QuizContent quiz =
new QuizContent();
quiz.setVersion(
version
);
quiz.setCategory(
form.getCategory()
);
quiz.setQuestion(
form.getQuestion()
);
quiz.setMultipleAnswer(
form.isMultipleAnswer()
);
quiz.setAnswerCount(
form.getAnswerCount()
);
quiz.setExplanation(
form.getExplanation()
);
for(
QuizChoiceForm choiceForm:
form.getChoices()
){
QuizChoice choice =
new QuizChoice();
choice.setQuiz(
quiz
);
choice.setChoiceText(
choiceForm.getText()
);
choice.setCorrect(
choiceForm.isCorrect()
);
quiz.getChoices()
.add(choice);
}
quizRepository.save(
quiz
);
}
|
14. 教材作成画面
画面:
| teacher/content/create.html
|
入力:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 | タイトル
概要
教材タイプ
[動画]
[テキスト]
[クイズ]
[ファイル]
保存
|
教材タイプ切替:
JavaScriptで表示制御します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 | <select id="type"
class="form-select">
<option value="VIDEO">
動画
</option>
<option value="TEXT">
テキスト
</option>
<option value="QUIZ">
クイズ
</option>
<option value="FILE">
ファイル
</option>
</select>
|
15. 保存後状態
登録直後:
| Content
ID=1
ContentVersion
version=1
status=DRAFT
|
教師操作:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 | 保存
↓
DRAFT
申請
↓
REVIEWING
管理者承認
↓
PUBLISHED
|
16. 現在の完成フロー
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27 | 教師ログイン
↓
教材作成
↓
ContentCreateForm
↓
ContentCommandService
↓
JPA Repository
↓
Content
ContentVersion
Video/Text/Quiz/File
↓
DRAFT
|
第11段階終了時点
実装できる機能:
✅ 教材登録
✅ 教材タイプ分類
✅ Markdown教材
✅ 動画教材
✅ Quiz(複数正解対応)
✅ ファイル教材
✅ Version管理
✅ 下書き保存
次の 第12段階 では、教材編集・申請・承認ワークフローを実装します。
内容:
-
教材編集
-
ContentVersion追加
-
差分管理
-
審査申請
-
管理者承認画面
-
公開処理
-
公開教材だけを受講者へ表示
-
監査ログ(誰がいつ変更したか)
まで進めます。