コンテンツにスキップ

ありがとうございます。

ここから 第11段階:教材作成機能設計(Form・Validation・登録処理) に進みます。

今回は教師が以下を実行できる機能を設計します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
教師

教材作成
 ↓
教材タイプ選択

 ├ 動画
 ├ テキスト(Markdown)
 ├ クイズ
 └ ファイル

 ↓

下書き保存

または

審査申請

第11段階の設計方針

教材作成では、以前設計した

1
2
3
4
5
6
7
8
Content
   |
   └ ContentVersion
          |
          ├ VideoContent
          ├ TextContent
          ├ QuizContent
          └ FileContent

をそのまま利用します。

重要な設計ポイント:

  • Content:教材そのもの(ID固定)

  • ContentVersion:編集履歴

  • 各タイプテーブル:教材固有情報

です。


1. Form構成

配置:

1
2
3
4
5
6
7
8
9
form

└ content
   ├ ContentCreateForm.java
   ├ VideoContentForm.java
   ├ TextContentForm.java
   ├ QuizContentForm.java
   ├ QuizChoiceForm.java
   └ FileContentForm.java

2. 共通教材登録Form

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;

}

3. 動画Form

対応:

  • 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;

}

4. テキストForm

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";

}

5. クイズForm

複数正解対応です。

例:

1
2
3
6択

正解2個

にも対応します。


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;

}

6. 選択肢Form


 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;

}

7. ファイルForm


 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:

1
2
3
4
5
6
7
8
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. 教材作成画面

画面:

1
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. 保存後状態

登録直後:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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段階 では、教材編集・申請・承認ワークフローを実装します。

内容:

  1. 教材編集

  2. ContentVersion追加

  3. 差分管理

  4. 審査申請

  5. 管理者承認画面

  6. 公開処理

  7. 公開教材だけを受講者へ表示

  8. 監査ログ(誰がいつ変更したか)

まで進めます。