Skip to content

Commit d000297

Browse files
authored
feat: admin 대시보드 구현 완료
# 변경점 👍 admin 대시보드를 구현 완료했습니다. 1. 킬링파트 검수 기능 2. 유저 관리 기능 3. 알람 발송 기능
1 parent 9709525 commit d000297

42 files changed

Lines changed: 3455 additions & 4 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

build.gradle

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,9 @@ dependencies {
7979
// cache
8080
implementation 'org.springframework.boot:spring-boot-starter-cache'
8181
implementation 'com.github.ben-manes.caffeine:caffeine'
82+
83+
//thymeleaf
84+
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
8285
}
8386

8487

src/main/java/apptive/team5/admin/controller/AdminController.java

Lines changed: 407 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package apptive.team5.admin.dto;
2+
3+
import jakarta.validation.constraints.NotBlank;
4+
import lombok.Getter;
5+
import lombok.NoArgsConstructor;
6+
import lombok.Setter;
7+
8+
@Getter
9+
@Setter
10+
@NoArgsConstructor
11+
public class AdminLoginRequest {
12+
@NotBlank(message = "관리자 ID를 입력해주세요.")
13+
private String adminId;
14+
15+
@NotBlank(message = "비밀번호를 입력해주세요.")
16+
private String password;
17+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package apptive.team5.admin.dto;
2+
3+
import apptive.team5.diary.domain.DiaryMemoEntity;
4+
5+
public record AdminMemoItem(
6+
Long memoId,
7+
Long diaryId,
8+
String displayId,
9+
String musicTitle,
10+
String artist,
11+
String username,
12+
Long userId,
13+
String albumImageUrl,
14+
String memoContent,
15+
String diaryContent
16+
) {
17+
18+
public static AdminMemoItem from(DiaryMemoEntity memo) {
19+
var diary = memo.getDiary();
20+
21+
return new AdminMemoItem(
22+
memo.getId(),
23+
diary.getId(),
24+
"KP-" + diary.getId(),
25+
diary.getMusicTitle(),
26+
diary.getArtist(),
27+
diary.getUser().getUsername(),
28+
diary.getUser().getId(),
29+
diary.getAlbumImageUrl(),
30+
memo.getContent(),
31+
diary.getContent()
32+
);
33+
}
34+
}
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
package apptive.team5.admin.dto;
2+
3+
import apptive.team5.diary.domain.DiaryEntity;
4+
import apptive.team5.diary.domain.DiaryScope;
5+
6+
import java.time.Duration;
7+
import java.time.LocalDateTime;
8+
9+
public record AdminUgcItem(
10+
Long id,
11+
String displayId,
12+
String musicTitle,
13+
String artist,
14+
String albumImageUrl,
15+
String videoUrl,
16+
String username,
17+
Long userId,
18+
String segment,
19+
String registeredLabel,
20+
String content,
21+
String scopeLabel,
22+
long reportCount,
23+
String statusLabel,
24+
String statusClass,
25+
String filterState
26+
) {
27+
28+
public static AdminUgcItem from(DiaryEntity diary, long reportCount, LocalDateTime now) {
29+
String displayId = "KP-" + diary.getId();
30+
String username = diary.getUser().getUsername();
31+
String segment = diary.getStart() + "-" + diary.getEnd();
32+
String statusLabel = reportCount > 0 ? "신고 " + reportCount : "공개";
33+
String statusClass = reportCount > 0 ? "reported" : "";
34+
35+
return new AdminUgcItem(
36+
diary.getId(),
37+
displayId,
38+
diary.getMusicTitle(),
39+
diary.getArtist(),
40+
diary.getAlbumImageUrl(),
41+
toYoutubeEmbedUrl(diary.getVideoUrl()),
42+
username,
43+
diary.getUser().getId(),
44+
segment,
45+
formatRegisteredLabel(diary.getCreateDateTime(), now),
46+
diary.getContent(),
47+
scopeLabel(diary.getScope()),
48+
reportCount,
49+
statusLabel,
50+
statusClass,
51+
filterState(reportCount)
52+
);
53+
}
54+
55+
private static String filterState(long reportCount) {
56+
StringBuilder builder = new StringBuilder("all");
57+
if (reportCount > 0) {
58+
builder.append(" reported");
59+
}
60+
return builder.toString();
61+
}
62+
63+
private static String toYoutubeEmbedUrl(String videoUrl) {
64+
if (videoUrl == null || videoUrl.isBlank()) {
65+
return "about:blank";
66+
}
67+
68+
String trimmedUrl = videoUrl.trim();
69+
String videoId = extractYoutubeVideoId(trimmedUrl);
70+
if (videoId == null || videoId.isBlank()) {
71+
return "about:blank";
72+
}
73+
74+
return "https://www.youtube-nocookie.com/embed/" + videoId;
75+
}
76+
77+
private static String extractYoutubeVideoId(String videoUrl) {
78+
String marker;
79+
if (videoUrl.contains("/embed/")) {
80+
marker = "/embed/";
81+
return readVideoId(videoUrl.substring(videoUrl.indexOf(marker) + marker.length()));
82+
}
83+
if (videoUrl.contains("watch?v=")) {
84+
marker = "watch?v=";
85+
return readVideoId(videoUrl.substring(videoUrl.indexOf(marker) + marker.length()));
86+
}
87+
if (videoUrl.contains("youtu.be/")) {
88+
marker = "youtu.be/";
89+
return readVideoId(videoUrl.substring(videoUrl.indexOf(marker) + marker.length()));
90+
}
91+
if (videoUrl.contains("/shorts/")) {
92+
marker = "/shorts/";
93+
return readVideoId(videoUrl.substring(videoUrl.indexOf(marker) + marker.length()));
94+
}
95+
if (!videoUrl.contains("/") && videoUrl.length() == 11) {
96+
return videoUrl;
97+
}
98+
99+
return null;
100+
}
101+
102+
private static String readVideoId(String value) {
103+
int end = value.length();
104+
for (String delimiter : new String[]{"?", "&", "/"}) {
105+
int index = value.indexOf(delimiter);
106+
if (index >= 0) {
107+
end = Math.min(end, index);
108+
}
109+
}
110+
return value.substring(0, end);
111+
}
112+
113+
private static String scopeLabel(DiaryScope scope) {
114+
return switch (scope) {
115+
case PUBLIC -> "전체공개";
116+
case KILLING_PART -> "킬링파트만 공개";
117+
case PRIVATE -> "비공개";
118+
};
119+
}
120+
121+
private static String formatRegisteredLabel(LocalDateTime createdAt, LocalDateTime now) {
122+
long minutes = Math.max(0, Duration.between(createdAt, now).toMinutes());
123+
if (minutes < 1) {
124+
return "방금 전";
125+
}
126+
if (minutes < 60) {
127+
return minutes + "분 전";
128+
}
129+
130+
long hours = minutes / 60;
131+
if (hours < 24) {
132+
return hours + "시간 전";
133+
}
134+
135+
return (hours / 24) + "일 전";
136+
}
137+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package apptive.team5.admin.dto;
2+
3+
public enum AdminUgcSearchType {
4+
DIARY_ID("Diary ID"),
5+
MUSIC_TITLE("곡 제목"),
6+
ARTIST("아티스트"),
7+
USERNAME("작성자");
8+
9+
private final String label;
10+
11+
AdminUgcSearchType(String label) {
12+
this.label = label;
13+
}
14+
15+
public String getLabel() {
16+
return label;
17+
}
18+
19+
public static AdminUgcSearchType from(String value) {
20+
for (AdminUgcSearchType type : values()) {
21+
if (type.name().equalsIgnoreCase(value)) {
22+
return type;
23+
}
24+
}
25+
return MUSIC_TITLE;
26+
}
27+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package apptive.team5.admin.dto;
2+
3+
import apptive.team5.diary.domain.DiaryEntity;
4+
import apptive.team5.diary.domain.DiaryScope;
5+
6+
public record UserDiaryItem(
7+
Long diaryId,
8+
String displayId,
9+
String musicTitle,
10+
String artist,
11+
String scopeLabel,
12+
long reportCount,
13+
String reportLabel,
14+
String reportStatusClass,
15+
String content
16+
) {
17+
18+
public static UserDiaryItem from(DiaryEntity diary, long reportCount) {
19+
return new UserDiaryItem(
20+
diary.getId(),
21+
"KP-" + diary.getId(),
22+
diary.getMusicTitle(),
23+
diary.getArtist(),
24+
scopeLabel(diary.getScope()),
25+
reportCount,
26+
"신고-" + reportCount,
27+
reportCount > 0 ? "reported" : "",
28+
diary.getContent()
29+
);
30+
}
31+
32+
private static String scopeLabel(DiaryScope scope) {
33+
return switch (scope) {
34+
case PUBLIC -> "전체공개";
35+
case KILLING_PART -> "킬링파트만 공개";
36+
case PRIVATE -> "비공개";
37+
};
38+
}
39+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package apptive.team5.admin.dto;
2+
3+
import apptive.team5.user.domain.UserEntity;
4+
5+
public record UserListItem(
6+
Long userId,
7+
String email,
8+
String username,
9+
String tag,
10+
String socialType,
11+
String roleType,
12+
boolean locked,
13+
String lockedLabel,
14+
String lockedStatusClass
15+
) {
16+
17+
public static UserListItem from(UserEntity user) {
18+
return new UserListItem(
19+
user.getId(),
20+
user.getEmail(),
21+
user.getUsername(),
22+
user.getTag(),
23+
user.getSocialType().name(),
24+
user.getRoleType().name(),
25+
user.isLocked(),
26+
user.isLocked() ? "정지" : "정상",
27+
user.isLocked() ? "reported" : ""
28+
);
29+
}
30+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package apptive.team5.admin.dto;
2+
3+
public enum UserSearchType {
4+
USER_ID("User ID"),
5+
EMAIL("Email"),
6+
TAG("Tag"),
7+
USERNAME("Username");
8+
9+
private final String label;
10+
11+
UserSearchType(String label) {
12+
this.label = label;
13+
}
14+
15+
public String getLabel() {
16+
return label;
17+
}
18+
19+
public static UserSearchType from(String value) {
20+
for (UserSearchType type : values()) {
21+
if (type.name().equalsIgnoreCase(value)) {
22+
return type;
23+
}
24+
}
25+
return USER_ID;
26+
}
27+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package apptive.team5.admin.entity;
2+
3+
import apptive.team5.global.entity.BaseTimeEntity;
4+
import jakarta.persistence.*;
5+
import lombok.Getter;
6+
import lombok.NoArgsConstructor;
7+
8+
@Entity
9+
@Table(name = "admin_user")
10+
@Getter
11+
@NoArgsConstructor
12+
public class Admin extends BaseTimeEntity {
13+
@Id
14+
@GeneratedValue(strategy = GenerationType.IDENTITY)
15+
private Long id;
16+
17+
@Column(unique = true, nullable = false)
18+
private String adminId;
19+
20+
@Column(nullable = false)
21+
private String password;
22+
23+
public Admin(String adminId, String password) {
24+
this.adminId = adminId;
25+
this.password = password;
26+
}
27+
}

0 commit comments

Comments
 (0)