ありがとうございます。
ここから 第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
| public enum VideoType {
YOUTUBE,
VIMEO,
CLOUDFLARE_R2
}
|
9. テキスト教材設計
Markdown対応します。
TextContent
| 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
| public enum TextFormat {
PLAIN,
MARKDOWN
}
|
12. Markdown変換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 | @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. クイズ教材設計
要件:
両方対応。
構造:
1
2
3
4
5
6
7
8
9
10
11
12
13 | Quiz
|
Question
|
Choice
|
CorrectFlag
|
14. QuizContent
| 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. ファイル教材設計
対応:
Attachment共通利用
以前設計した共通ファイル管理を利用します。
構造:
| 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
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. 管理者承認処理
状態:
| 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:
検索:
| キーワード
カテゴリ
種類
タグ
作成者
公開状態
|
27. 第21段階終了時点
実装可能:
✅ 教材基本管理
✅ Version管理
✅ 動画教材
✅ YouTube/Vimeo/R2対応
✅ Markdown教材
✅ クイズ教材
✅ 複数正解対応
✅ ファイル教材
✅ 共通添付管理
✅ 教材申請
✅ 管理者承認
✅ 教材検索基盤
次の 第22段階 では、教材をコースへ組み込む部分を実装します。
内容:
-
Course Entity
-
Section階層(無限階層)
-
Lesson設計
-
LessonContent紐付け
-
ドラッグ&ドロップ並び替え
-
コース作成画面
-
教材ライブラリから教材利用
-
教師用コース編集機能
へ進みます。