Skip to content

Commit 727fa6f

Browse files
authored
Merge pull request #219 from prgrms-be-devcourse/217-feat-관리자-비밀편지-관리-페이지-프론트엔드-구현
[Fix] 관리자 페이지 버그 수정
2 parents 5bfd5ea + e974d8f commit 727fa6f

19 files changed

Lines changed: 597 additions & 278 deletions

File tree

.github/workflows/deploy-monitoring.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ jobs:
181181
if [[ -n "${RAW_GF_SERVER_ROOT_URL}" ]]; then
182182
NORMALIZED_GF_SERVER_ROOT_URL="${RAW_GF_SERVER_ROOT_URL%/}"
183183
else
184-
NORMALIZED_GF_SERVER_ROOT_URL="http://127.0.0.1:3000"
184+
NORMALIZED_GF_SERVER_ROOT_URL="https://monitor.maum-on.parksuyeon.site"
185185
fi
186186
case "${NORMALIZED_GF_SERVER_ROOT_URL}" in
187187
*/grafana)
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package com.back.global.initData;
2+
3+
import lombok.RequiredArgsConstructor;
4+
import org.springframework.boot.ApplicationArguments;
5+
import org.springframework.boot.ApplicationRunner;
6+
import org.springframework.context.annotation.Profile;
7+
import org.springframework.core.Ordered;
8+
import org.springframework.core.annotation.Order;
9+
import org.springframework.jdbc.core.JdbcTemplate;
10+
import org.springframework.stereotype.Component;
11+
12+
@Component
13+
@Profile("!test")
14+
@Order(Ordered.HIGHEST_PRECEDENCE + 10)
15+
@RequiredArgsConstructor
16+
public class LetterAdminActionLogTableSynchronizer implements ApplicationRunner {
17+
18+
private final JdbcTemplate jdbcTemplate;
19+
20+
@Override
21+
public void run(ApplicationArguments args) {
22+
if (!letterTableExists()) {
23+
return;
24+
}
25+
26+
jdbcTemplate.execute(
27+
"""
28+
CREATE TABLE IF NOT EXISTS letter_admin_action_logs
29+
(
30+
id BIGSERIAL PRIMARY KEY,
31+
create_date TIMESTAMP NULL,
32+
modify_date TIMESTAMP NULL,
33+
letter_id BIGINT NOT NULL,
34+
admin_member_id BIGINT NOT NULL,
35+
admin_nickname VARCHAR(60) NOT NULL,
36+
action_type VARCHAR(40) NOT NULL,
37+
memo TEXT,
38+
CONSTRAINT fk_letter_admin_action_logs_letter
39+
FOREIGN KEY (letter_id) REFERENCES letter (id)
40+
)
41+
""");
42+
43+
jdbcTemplate.execute(
44+
"""
45+
CREATE INDEX IF NOT EXISTS idx_letter_admin_action_logs_letter_id_create_date
46+
ON letter_admin_action_logs (letter_id, create_date DESC)
47+
""");
48+
}
49+
50+
private boolean letterTableExists() {
51+
Boolean exists =
52+
jdbcTemplate.queryForObject(
53+
"SELECT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'letter')",
54+
Boolean.class);
55+
56+
return Boolean.TRUE.equals(exists);
57+
}
58+
}

back/src/main/java/com/back/letter/application/service/LetterService.java

Lines changed: 46 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@
1818
import com.back.member.application.MemberService;
1919
import com.back.member.domain.Member;
2020
import com.back.member.domain.MemberRepository;
21+
import lombok.extern.slf4j.Slf4j;
2122
import lombok.RequiredArgsConstructor;
23+
import org.springframework.dao.DataAccessException;
2224
import org.springframework.cache.annotation.Cacheable;
2325
import org.springframework.context.ApplicationEventPublisher;
2426
import org.springframework.data.domain.*;
@@ -31,6 +33,7 @@
3133
import java.util.List;
3234

3335
@Service
36+
@Slf4j
3437
@RequiredArgsConstructor
3538
@Transactional
3639
public class LetterService implements SendLetterUseCase, InquiryLetterUseCase, AdminLetterUseCase {
@@ -178,18 +181,11 @@ public AdminLetterListRes getAdminLetters(String status, String query, int page,
178181
onlyUnassigned,
179182
pageable);
180183

181-
Page<AdminLetterListItem> mappedPage = letterPage.map(letter -> {
182-
String latestAction = letterAdminActionLogRepository
183-
.findByLetterIdOrderByCreateDateDesc(letter.getId())
184-
.stream()
185-
.findFirst()
186-
.map(log -> log.getActionType().name())
187-
.orElse(null);
188-
return AdminLetterListItem.from(
189-
letter,
190-
letterRedisRepository.isWriting(letter.getId()),
191-
latestAction);
192-
});
184+
Page<AdminLetterListItem> mappedPage = letterPage.map(letter ->
185+
AdminLetterListItem.from(
186+
letter,
187+
isWritingStatusAvailable(letter.getId()),
188+
getLatestAdminActionSafely(letter.getId())));
193189

194190
return AdminLetterListRes.from(mappedPage);
195191
}
@@ -200,15 +196,11 @@ public AdminLetterDetailRes getAdminLetter(long id) {
200196
Letter letter = letterPort.findByIdForAdmin(id)
201197
.orElseThrow(() -> new ServiceException("404-1", "존재하지 않는 편지입니다."));
202198

203-
List<AdminLetterActionLogItem> actionLogs = letterAdminActionLogRepository
204-
.findByLetterIdOrderByCreateDateDesc(letter.getId())
205-
.stream()
206-
.map(AdminLetterActionLogItem::from)
207-
.toList();
199+
List<AdminLetterActionLogItem> actionLogs = getAdminActionLogsSafely(letter.getId());
208200

209201
return AdminLetterDetailRes.from(
210202
letter,
211-
letterRedisRepository.isWriting(letter.getId()),
203+
isWritingStatusAvailable(letter.getId()),
212204
actionLogs);
213205
}
214206

@@ -325,6 +317,42 @@ private String resolveAdminNickname(long adminMemberId) {
325317
.orElse("관리자#" + adminMemberId);
326318
}
327319

320+
private String getLatestAdminActionSafely(long letterId) {
321+
try {
322+
return letterAdminActionLogRepository
323+
.findByLetterIdOrderByCreateDateDesc(letterId)
324+
.stream()
325+
.findFirst()
326+
.map(log -> log.getActionType().name())
327+
.orElse(null);
328+
} catch (DataAccessException exception) {
329+
log.warn("관리자 편지 최신 조치를 불러오지 못해 기본값으로 대체합니다. letterId={}", letterId, exception);
330+
return null;
331+
}
332+
}
333+
334+
private List<AdminLetterActionLogItem> getAdminActionLogsSafely(long letterId) {
335+
try {
336+
return letterAdminActionLogRepository
337+
.findByLetterIdOrderByCreateDateDesc(letterId)
338+
.stream()
339+
.map(AdminLetterActionLogItem::from)
340+
.toList();
341+
} catch (DataAccessException exception) {
342+
log.warn("관리자 편지 조치 이력을 불러오지 못해 빈 목록으로 대체합니다. letterId={}", letterId, exception);
343+
return List.of();
344+
}
345+
}
346+
347+
private boolean isWritingStatusAvailable(long letterId) {
348+
try {
349+
return letterRedisRepository.isWriting(letterId);
350+
} catch (DataAccessException exception) {
351+
log.warn("관리자 편지 작성 중 상태를 불러오지 못해 false로 대체합니다. letterId={}", letterId, exception);
352+
return false;
353+
}
354+
}
355+
328356
private String normalizeQuery(String query) {
329357
if (query == null || query.isBlank()) {
330358
return null;

back/src/main/java/com/back/member/adapter/in/web/dto/AdminMemberDetailResponse.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
import com.back.auth.domain.OAuthAccount;
44
import com.back.member.domain.Member;
5+
import com.back.member.domain.MemberRole;
6+
import com.back.member.domain.MemberStatus;
57
import java.time.LocalDateTime;
68
import java.util.List;
79

@@ -31,6 +33,8 @@ public static AdminMemberDetailResponse from(
3133
List<AdminMemberReportHistoryItem> receivedReports,
3234
List<AdminMemberPostHistoryItem> recentPosts,
3335
List<AdminMemberLetterHistoryItem> recentLetters) {
36+
MemberRole role = member.getRole() == null ? MemberRole.USER : member.getRole();
37+
MemberStatus status = member.getStatus() == null ? MemberStatus.ACTIVE : member.getStatus();
3438
List<String> providers =
3539
oauthAccounts.stream().map(OAuthAccount::getProvider).distinct().toList();
3640
LocalDateTime lastLoginAt =
@@ -44,8 +48,8 @@ public static AdminMemberDetailResponse from(
4448
member.getId(),
4549
member.getEmail(),
4650
member.getNickname(),
47-
member.getRole().name(),
48-
member.getStatus().name(),
51+
role.name(),
52+
status.name(),
4953
member.isRandomReceiveAllowed(),
5054
!providers.isEmpty(),
5155
member.getCreateDate(),

back/src/main/java/com/back/member/adapter/in/web/dto/AdminMemberListItem.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package com.back.member.adapter.in.web.dto;
22

33
import com.back.member.domain.Member;
4+
import com.back.member.domain.MemberRole;
5+
import com.back.member.domain.MemberStatus;
46
import java.time.LocalDateTime;
57

68
public record AdminMemberListItem(
@@ -16,12 +18,15 @@ public record AdminMemberListItem(
1618

1719
public static AdminMemberListItem from(
1820
Member member, boolean socialAccount, LocalDateTime lastLoginAt) {
21+
MemberRole role = member.getRole() == null ? MemberRole.USER : member.getRole();
22+
MemberStatus status = member.getStatus() == null ? MemberStatus.ACTIVE : member.getStatus();
23+
1924
return new AdminMemberListItem(
2025
member.getId(),
2126
member.getEmail(),
2227
member.getNickname(),
23-
member.getRole().name(),
24-
member.getStatus().name(),
28+
role.name(),
29+
status.name(),
2530
member.isRandomReceiveAllowed(),
2631
socialAccount,
2732
member.getCreateDate(),

back/src/main/java/com/back/member/application/MemberService.java

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -493,14 +493,14 @@ private void blockMember(
493493
if (adminMemberId != null && adminMemberId.equals(member.getId())) {
494494
throw new ServiceException(ERROR_CODE_CONFLICT, ERROR_MSG_CANNOT_BLOCK_SELF);
495495
}
496-
if (member.getStatus() == MemberStatus.WITHDRAWN) {
496+
if (safeStatus(member) == MemberStatus.WITHDRAWN) {
497497
throw new ServiceException(
498498
ERROR_CODE_CONFLICT, ERROR_MSG_WITHDRAWN_MEMBER_MODERATION_UNSUPPORTED);
499499
}
500500

501501
validateAdminCapabilityReduction(member, adminMemberId, MemberStatus.BLOCKED, null);
502502

503-
String beforeValue = "status:" + member.getStatus().name();
503+
String beforeValue = "status:" + safeStatus(member).name();
504504
member.updateStatus(MemberStatus.BLOCKED);
505505
member.updateRandomReceiveAllowed(false);
506506
if (revokeSessions) {
@@ -518,12 +518,12 @@ private void blockMember(
518518

519519
private void unblockMember(
520520
Member member, Long adminMemberId, String reason, boolean revokeSessions) {
521-
if (member.getStatus() == MemberStatus.WITHDRAWN) {
521+
if (safeStatus(member) == MemberStatus.WITHDRAWN) {
522522
throw new ServiceException(
523523
ERROR_CODE_CONFLICT, ERROR_MSG_WITHDRAWN_MEMBER_MODERATION_UNSUPPORTED);
524524
}
525525

526-
String beforeValue = "status:" + member.getStatus().name();
526+
String beforeValue = "status:" + safeStatus(member).name();
527527
member.updateStatus(MemberStatus.ACTIVE);
528528
if (revokeSessions) {
529529
refreshTokenDomainService.revokeAllByMemberId(member.getId(), LocalDateTime.now(clock));
@@ -540,17 +540,19 @@ private void unblockMember(
540540

541541
private void validateAdminCapabilityReduction(
542542
Member member, Long adminMemberId, MemberStatus nextStatus, MemberRole nextRole) {
543+
MemberRole currentRole = safeRole(member);
544+
MemberStatus currentStatus = safeStatus(member);
543545
if (adminMemberId != null
544546
&& adminMemberId.equals(member.getId())
545547
&& nextRole != null
546-
&& member.getRole() == MemberRole.ADMIN
548+
&& currentRole == MemberRole.ADMIN
547549
&& nextRole != MemberRole.ADMIN) {
548550
throw new ServiceException(ERROR_CODE_CONFLICT, ERROR_MSG_CANNOT_CHANGE_OWN_ROLE);
549551
}
550552

551553
boolean removesActiveAdminCapability =
552-
member.getRole() == MemberRole.ADMIN
553-
&& member.getStatus() == MemberStatus.ACTIVE
554+
currentRole == MemberRole.ADMIN
555+
&& currentStatus == MemberStatus.ACTIVE
554556
&& ((nextStatus != null && nextStatus != MemberStatus.ACTIVE)
555557
|| (nextRole != null && nextRole != MemberRole.ADMIN));
556558

@@ -589,6 +591,14 @@ private String resolveAdminNickname(Long adminMemberId) {
589591
.orElse("admin#" + adminMemberId);
590592
}
591593

594+
private MemberRole safeRole(Member member) {
595+
return member.getRole() == null ? MemberRole.USER : member.getRole();
596+
}
597+
598+
private MemberStatus safeStatus(Member member) {
599+
return member.getStatus() == null ? MemberStatus.ACTIVE : member.getStatus();
600+
}
601+
592602
private record OAuthAccountSnapshot(boolean socialAccount, LocalDateTime lastLoginAt) {
593603
private static OAuthAccountSnapshot empty() {
594604
return new OAuthAccountSnapshot(false, null);
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package com.back.global.initData;
2+
3+
import static org.mockito.Mockito.inOrder;
4+
import static org.mockito.Mockito.never;
5+
import static org.mockito.Mockito.verify;
6+
import static org.mockito.Mockito.when;
7+
8+
import org.junit.jupiter.api.DisplayName;
9+
import org.junit.jupiter.api.Test;
10+
import org.junit.jupiter.api.extension.ExtendWith;
11+
import org.mockito.InOrder;
12+
import org.mockito.Mock;
13+
import org.mockito.junit.jupiter.MockitoExtension;
14+
import org.springframework.boot.DefaultApplicationArguments;
15+
import org.springframework.jdbc.core.JdbcTemplate;
16+
17+
@ExtendWith(MockitoExtension.class)
18+
class LetterAdminActionLogTableSynchronizerTest {
19+
20+
@Mock private JdbcTemplate jdbcTemplate;
21+
22+
@Test
23+
@DisplayName("letter 테이블이 있으면 관리자 조치 이력 테이블과 인덱스를 보정한다")
24+
void runWhenLetterTableExists() {
25+
LetterAdminActionLogTableSynchronizer synchronizer =
26+
new LetterAdminActionLogTableSynchronizer(jdbcTemplate);
27+
28+
when(
29+
jdbcTemplate.queryForObject(
30+
"SELECT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'letter')",
31+
Boolean.class))
32+
.thenReturn(true);
33+
34+
synchronizer.run(new DefaultApplicationArguments(new String[0]));
35+
36+
InOrder inOrder = inOrder(jdbcTemplate);
37+
inOrder.verify(jdbcTemplate)
38+
.queryForObject(
39+
"SELECT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'letter')",
40+
Boolean.class);
41+
inOrder.verify(jdbcTemplate)
42+
.execute(
43+
"""
44+
CREATE TABLE IF NOT EXISTS letter_admin_action_logs
45+
(
46+
id BIGSERIAL PRIMARY KEY,
47+
create_date TIMESTAMP NULL,
48+
modify_date TIMESTAMP NULL,
49+
letter_id BIGINT NOT NULL,
50+
admin_member_id BIGINT NOT NULL,
51+
admin_nickname VARCHAR(60) NOT NULL,
52+
action_type VARCHAR(40) NOT NULL,
53+
memo TEXT,
54+
CONSTRAINT fk_letter_admin_action_logs_letter
55+
FOREIGN KEY (letter_id) REFERENCES letter (id)
56+
)
57+
""");
58+
inOrder.verify(jdbcTemplate)
59+
.execute(
60+
"""
61+
CREATE INDEX IF NOT EXISTS idx_letter_admin_action_logs_letter_id_create_date
62+
ON letter_admin_action_logs (letter_id, create_date DESC)
63+
""");
64+
}
65+
66+
@Test
67+
@DisplayName("letter 테이블이 없으면 관리자 조치 이력 테이블 보정을 건너뛴다")
68+
void runWhenLetterTableDoesNotExist() {
69+
LetterAdminActionLogTableSynchronizer synchronizer =
70+
new LetterAdminActionLogTableSynchronizer(jdbcTemplate);
71+
72+
when(
73+
jdbcTemplate.queryForObject(
74+
"SELECT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'letter')",
75+
Boolean.class))
76+
.thenReturn(false);
77+
78+
synchronizer.run(new DefaultApplicationArguments(new String[0]));
79+
80+
verify(jdbcTemplate, never()).execute(org.mockito.ArgumentMatchers.anyString());
81+
}
82+
}

0 commit comments

Comments
 (0)