Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ public ResponseEntity<Void> updatePhotoUrl(@PathVariable Long memberId,
@Override
@PostMapping("/{memberId}/photo/slack-sync")
public ResponseEntity<PhotoUrlResponse> syncPhotoFromSlack(@PathVariable Long memberId) {
return ResponseEntity.ok(new PhotoUrlResponse(memberDirectoryService.syncProfileImageFromSlack(memberId)));
return ResponseEntity.ok(new PhotoUrlResponse(slackProfileSyncService.syncOne(memberId)));
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import com.bcsdlab.bcsdinternalapiv2.member.controller.dto.response.AdminMemberDirectoryResponse;
import com.bcsdlab.bcsdinternalapiv2.member.controller.dto.response.MemberDirectoryResponse;
import com.bcsdlab.bcsdinternalapiv2.member.controller.dto.response.PhotoPresignedUrlResponse;
import com.bcsdlab.bcsdinternalapiv2.member.client.SlackClient;
import com.bcsdlab.bcsdinternalapiv2.member.exception.MemberException;
import com.bcsdlab.bcsdinternalapiv2.member.exception.MemberExceptionType;
import com.bcsdlab.bcsdinternalapiv2.member.model.AcademicStatus;
Expand Down Expand Up @@ -43,7 +42,6 @@ public class MemberDirectoryService {
private final TrackMasterRepository trackMasterRepository;
private final PositionRepository positionRepository;
private final PhotoStorageService photoStorageService;
private final SlackClient slackClient;

@Transactional(readOnly = true)
public MemberDirectoryResponse getDirectory(MemberDirectoryQuery query, Pageable pageable) {
Expand Down Expand Up @@ -140,16 +138,6 @@ public void updatePhotoUrl(Long memberId, String photoUrl) {
member.updateProfileImageUrl(photoUrl);
}

@Transactional
public String syncProfileImageFromSlack(Long memberId) {
Member member = memberRepository.findByIdForUpdate(memberId)
.orElseThrow(() -> new MemberException(MemberExceptionType.MEMBER_NOT_FOUND));
String imageUrl = slackClient.findProfileImageUrlByEmail(member.getEmail())
.orElseThrow(() -> new MemberException(MemberExceptionType.SLACK_PROFILE_NOT_FOUND));
member.updateProfileImageUrl(imageUrl);
return imageUrl;
}

private boolean isBlank(String value) {
return value == null || value.isBlank();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,19 @@ public class SlackProfileSyncService {
private final SlackClient slackClient;
private final MemberDirectoryService memberDirectoryService;

/**
* 한 회원의 프로필 사진을 Slack(이메일 조회)에서 가져와 저장한다. Slack 호출은 트랜잭션과 행 잠금 밖에서 하고,
* 저장만 짧은 트랜잭션으로 한다 — 잠금을 쥔 채 외부 호출을 기다리면 같은 회원의 로그인·토큰 갱신이 막힌다.
*/
public String syncOne(Long memberId) {
Member member = memberRepository.findById(memberId)
.orElseThrow(() -> new MemberException(MemberExceptionType.MEMBER_NOT_FOUND));
String imageUrl = slackClient.findProfileImageUrlByEmail(member.getEmail())
.orElseThrow(() -> new MemberException(MemberExceptionType.SLACK_PROFILE_NOT_FOUND));
memberDirectoryService.updatePhotoUrl(memberId, imageUrl);
return imageUrl;
}

public SlackProfileSyncResponse syncAll() {
// 가입 전·잠김 회원도 홈페이지에 노출될 수 있으므로 탈퇴한 회원만 제외한다.
List<Member> members = memberRepository.findAllByStatusNot(MemberStatus.WITHDRAWN);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,16 @@
import com.bcsdlab.bcsdinternalapiv2.track.repository.TrackMasterRepository;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.transaction.support.TransactionSynchronizationManager;

@TestPropertySource(properties = "app.mail.transport=log")
class MemberSlackIdIntegrationTest extends IntegrationTestSupport {
Expand Down Expand Up @@ -343,4 +346,24 @@ private String login(String studentNumber) throws Exception {
.getContentAsString();
return objectMapper.readTree(responseBody).get("accessToken").asText();
}

@Test
@DisplayName("Slack 사진 동기화는 트랜잭션(회원 행 잠금) 밖에서 Slack을 부르고 사진을 저장한다")
void Slack_사진_동기화는_트랜잭션_밖에서_Slack을_부른다() throws Exception {
Member member = saveMember("20240021", "윤사진", MemberRole.MEMBER);
AtomicBoolean calledInTransaction = new AtomicBoolean(true);
when(slackClient.findProfileImageUrlByEmail(member.getEmail())).thenAnswer(invocation -> {
calledInTransaction.set(TransactionSynchronizationManager.isActualTransactionActive());
return Optional.of("https://avatars.slack-edge.com/photo.png");
});

mockMvc.perform(post("/v1/admin/members/" + member.getId() + "/photo/slack-sync")
.header("Authorization", "Bearer " + adminToken))
.andExpect(status().isOk())
.andExpect(jsonPath("$.photoUrl").value("https://avatars.slack-edge.com/photo.png"));

assertThat(calledInTransaction).isFalse();
assertThat(memberRepository.findById(member.getId()).orElseThrow().getProfileImageUrl())
.isEqualTo("https://avatars.slack-edge.com/photo.png");
}
}
Loading