コンテンツにスキップ

ありがとうございます。

ここから 第21段階:教材ライブラリ・コンテンツ管理実装設計 に進みます。

今回は教材管理システムの中心となる部分です。

対象:

 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
教師

 ↓

教材作成

 ↓

教材ライブラリ(Content)

 ↓

Version管理

 ↓

管理者審査

 ↓

公開

 ↓

受講者利用

第21段階 全体構成

 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
                     Content

                        |

                ContentVersion

                        |

        +---------------+---------------+

        Video          Text          Quiz

                        |

                     File


                        |

                 LessonContent

                        |

                      Lesson

                        |

                      Course

1. Content設計

役割

教材そのものを管理します。

重要:

Contentは「教材の箱」です。

内容変更はVersionで管理します。


例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
教材ID:100

Java入門


Version1
  初版


Version2
  Spring Boot追記


Version3
  修正版

2. Content Entity

 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
@Entity
@Table(name="contents")
@Getter
@Setter
public class Content extends BaseEntity {


    @Id
    @GeneratedValue(
        strategy=GenerationType.IDENTITY
    )
    private Long id;


    @ManyToOne(fetch=FetchType.LAZY)
    @JoinColumn(
        name="author_id"
    )
    private User author;


    @ManyToOne(fetch=FetchType.LAZY)
    @JoinColumn(
        name="category_id"
    )
    private ContentCategory category;


    @Enumerated(EnumType.STRING)
    private ContentType contentType;


    @OneToOne(fetch=FetchType.LAZY)
    @JoinColumn(
        name="current_version_id"
    )
    private ContentVersion currentVersion;


}

3. ContentType

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public enum ContentType {


    VIDEO,

    TEXT,

    QUIZ,

    FILE

}

4. ContentVersion Entity

教材内容はここで管理します。

 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
@Entity
@Table(name="content_versions")
@Getter
@Setter
public class ContentVersion
extends BaseEntity {


    @Id
    @GeneratedValue(
        strategy=GenerationType.IDENTITY
    )
    private Long id;


    @ManyToOne(fetch=FetchType.LAZY)
    @JoinColumn(
        name="content_id"
    )
    private Content content;


    private Integer versionNo;


    private String title;


    @Column(
        columnDefinition="TEXT"
    )
    private String summary;


    @Enumerated(EnumType.STRING)
    private ContentStatus status;


    private LocalDateTime submittedAt;


    private LocalDateTime approvedAt;


    private LocalDateTime publishedAt;


}

5. ContentStatus

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public enum ContentStatus {


    DRAFT,

    REVIEWING,

    WAITING_PUBLISH,

    PUBLISHED,

    REJECTED,

    ARCHIVED

}

6. 動画教材設計

対応:

  • YouTube

  • Vimeo

  • Cloudflare R2

  • HLS配信


VideoContent

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
CREATE TABLE video_contents (

id BIGINT PRIMARY KEY,

video_type VARCHAR(30),

video_id VARCHAR(255),

video_url TEXT,

duration_seconds INT

);

7. VideoContent Entity

 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
@Entity
@Table(name="video_contents")
@Getter
@Setter
public class VideoContent {


    @Id
    private Long id;


    @OneToOne
    @MapsId
    private ContentVersion contentVersion;


    @Enumerated(EnumType.STRING)
    private VideoType videoType;


    private String videoId;


    private String videoUrl;


    private Integer durationSeconds;


}

8. VideoType

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public enum VideoType {


    YOUTUBE,

    VIMEO,

    CLOUDFLARE_R2

}

9. テキスト教材設計

Markdown対応します。


TextContent

1
2
3
4
5
6
7
8
9
CREATE TABLE text_contents (

id BIGINT PRIMARY KEY,

format VARCHAR(30),

body TEXT

);

10. TextContent Entity

 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
@Entity
@Table(name="text_contents")
@Getter
@Setter
public class TextContent {


    @Id
    private Long id;


    @OneToOne
    @MapsId
    private ContentVersion contentVersion;


    @Enumerated(EnumType.STRING)
    private TextFormat format;


    @Column(
        columnDefinition="TEXT"
    )
    private String body;


}

11. TextFormat

1
2
3
4
5
6
7
8
public enum TextFormat {


    PLAIN,

    MARKDOWN

}

12. Markdown変換Service

利用:

1
2
3
4
5
6
7
8
9
保存

Markdown

 ↓

表示時

HTML変換

 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
@Service
public class MarkdownService {


    private final Parser parser =
        Parser.builder()
        .build();


    private final HtmlRenderer renderer =
        HtmlRenderer.builder()
        .build();



    public String convert(
        String markdown){


        Node document =
            parser.parse(markdown);


        return renderer.render(
            document
        );

    }

}

13. クイズ教材設計

要件:

  • 4択1正解

  • 6択2正解

両方対応。


構造:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
Quiz

 |

Question

 |

Choice

 |

CorrectFlag

14. QuizContent

1
2
3
4
5
6
7
8
9
CREATE TABLE quiz_contents (

id BIGINT PRIMARY KEY,

question TEXT,

answer_count INT

);

15. QuizChoice

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
CREATE TABLE quiz_choices (

id BIGINT AUTO_INCREMENT PRIMARY KEY,

quiz_id BIGINT,

choice_text VARCHAR(500),

correct BOOLEAN,

sort_order INT

);

16. Quiz Entity

 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
@Entity
@Table(name="quiz_contents")
@Getter
@Setter
public class QuizContent {


@Id
private Long id;


@OneToOne
@MapsId
private ContentVersion contentVersion;


@Column(
columnDefinition="TEXT"
)
private String question;


private Integer answerCount;



@OneToMany(
mappedBy="quiz"
,
cascade=CascadeType.ALL
)
private List<QuizChoice> choices;


}

17. QuizChoice Entity

 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
@Entity
@Getter
@Setter
public class QuizChoice {


@Id
@GeneratedValue
private Long id;


@ManyToOne
@JoinColumn(
name="quiz_id"
)
private QuizContent quiz;


private String choiceText;


private Boolean correct;


private Integer sortOrder;


}

18. ファイル教材設計

対応:

  • PDF

  • ZIP

  • TXT

  • Office


Attachment共通利用

以前設計した共通ファイル管理を利用します。


構造:

1
2
3
4
5
6
7
8
9
Attachment

 |

ContentAttachment

 |

ContentVersion

19. Attachment Entity

 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
@Entity
@Table(name="attachments")
@Getter
@Setter
public class Attachment {


@Id
@GeneratedValue
private Long id;


private String originalName;


private String storageName;


private String contentType;


private Long size;


private String storagePath;


}

20. ContentAttachment

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
@Entity
@Table(name="content_attachments")
@Getter
@Setter
public class ContentAttachment {


@Id
@GeneratedValue
private Long id;


@ManyToOne
private ContentVersion contentVersion;


@ManyToOne
private Attachment attachment;


}

21. Content登録処理

処理:

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

↓

Content作成

↓

Version作成

↓

詳細Content作成

↓

保存

Service:

 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
75
76
77
78
@Service
@RequiredArgsConstructor
public class ContentCreateService {


private final ContentRepository contentRepository;

private final ContentVersionRepository versionRepository;



@Transactional
public Long create(
        ContentCreateForm form,
        User author){


Content content =
new Content();


content.setAuthor(
author
);


content.setContentType(
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
);



versionRepository.save(
version
);



return content.getId();

}

}

22. 教材申請処理

状態変更:

1
2
3
4
5
DRAFT

↓

REVIEWING

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Transactional
public void submit(
Long contentId){


ContentVersion version =
repository
.findCurrentVersion(
contentId
);


version.setStatus(
ContentStatus.REVIEWING
);


version.setSubmittedAt(
LocalDateTime.now()
);


}

23. 管理者承認処理

状態:

1
2
3
4
5
6
7
8
9
REVIEWING

↓

WAITING_PUBLISH

↓

PUBLISHED

 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
@Transactional
public void approve(
Long versionId,
User admin){


ContentVersion version =
repository
.findById(versionId)
.get();


version.setStatus(
ContentStatus.PUBLISHED
);


version.setApprovedAt(
LocalDateTime.now()
);


version.setPublishedAt(
LocalDateTime.now()
);


}

24. ContentMapper

検索用。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
@Mapper
public interface ContentMapper {


List<ContentListDto> search(
ContentSearchCondition condition
);



ContentDetailDto findDetail(
Long id
);


}

25. ContentController

 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
@Controller
@RequestMapping(
"/teacher/content"
)
@RequiredArgsConstructor
public class ContentController {


private final ContentCreateService service;



@GetMapping("/create")
public String create(
Model model){


model.addAttribute(
"form",
new ContentCreateForm()
);


return
"teacher/content/create";

}



@PostMapping("/create")
public String save(
@Valid
ContentCreateForm form){


service.create(
form,
loginUser
);


return
"redirect:/teacher/content";

}


}

26. 教材ライブラリ画面

URL:

1
/teacher/library

検索:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
キーワード

カテゴリ

種類

タグ

作成者

公開状態

27. 第21段階終了時点

実装可能:

✅ 教材基本管理
✅ Version管理
✅ 動画教材
✅ YouTube/Vimeo/R2対応
✅ Markdown教材
✅ クイズ教材
✅ 複数正解対応
✅ ファイル教材
✅ 共通添付管理
✅ 教材申請
✅ 管理者承認
✅ 教材検索基盤


次の 第22段階 では、教材をコースへ組み込む部分を実装します。

内容:

  1. Course Entity

  2. Section階層(無限階層)

  3. Lesson設計

  4. LessonContent紐付け

  5. ドラッグ&ドロップ並び替え

  6. コース作成画面

  7. 教材ライブラリから教材利用

  8. 教師用コース編集機能

へ進みます。