Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
aa2484b
zoom header debugging
dongwooooooo Dec 1, 2025
8dd61d1
zoom testConnection 체
dongwooooooo Dec 1, 2025
e79f956
zoom event notification endpoint url validate api
dongwooooooo Dec 6, 2025
ebffde4
zoom event notification endpoint url validate api
dongwooooooo Dec 6, 2025
11ac775
zoom event notification endpoint url validate api
dongwooooooo Dec 6, 2025
7c4744c
zoom event notification endpoint url validate api
dongwooooooo Dec 6, 2025
55aaf53
zoom 안 쓰는 코드 정리
dongwooooooo Dec 6, 2025
800d96a
zoom 안 쓰는 코드 정리
dongwooooooo Dec 6, 2025
042258d
zoom api 토큰 설정
dongwooooooo Dec 6, 2025
0523e24
zoom api 토큰 설정
dongwooooooo Dec 6, 2025
8a398df
zoom 웹훅 api 수정 및 정리
dongwooooooo Dec 6, 2025
fc22dee
zoom webhook exception
dongwooooooo Dec 6, 2025
59ec0d9
spotlessApply
dongwooooooo Dec 6, 2025
276a716
zoomWebhook 인증 리팩터링
dongwooooooo Dec 6, 2025
9694580
spotlessApply
dongwooooooo Dec 6, 2025
a080e48
래빗 리뷰
dongwooooooo Dec 6, 2025
86c87ef
zoom 인증 경로 설정
dongwooooooo Dec 7, 2025
f953881
zoom 웹훅 이벤트 body 실종 수정
dongwooooooo Dec 7, 2025
5591e9a
spotlessApply
dongwooooooo Dec 7, 2025
be72a68
zoom 인증 경로 설정
dongwooooooo Dec 7, 2025
5213a47
zoomWebhook filter에서 aop로 인증 변경
dongwooooooo Dec 7, 2025
dc53254
spotlessApply
dongwooooooo Dec 7, 2025
0b812e2
zoom 12시 이후로 나가면 조회 못하는 에러 해결
dongwooooooo Dec 10, 2025
02288ea
spotlessApply
dongwooooooo Dec 10, 2025
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 @@ -34,8 +34,7 @@ public void todayCsPublishScheduler() {
String.format(
"[ERROR] CS 문제 발행 중 오류 발생!\n> 원인: %s\n> 시간: %s",
e.getMessage(), java.time.LocalDateTime.now());
eventPublisher.publishEvent(
new SlackEvent.Channel(errorMessage, SlackChannelType.EA));
eventPublisher.publishEvent(new SlackEvent.Channel(errorMessage, SlackChannelType.EA));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@

import backend.techeerzip.domain.zoom.entity.ZoomAttendance;
import backend.techeerzip.domain.zoom.repository.ZoomAttendanceRepository;
import backend.techeerzip.infra.zoom.client.ZoomApiClient;
import backend.techeerzip.infra.zoom.dto.ZoomWebhookEvent;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
Expand All @@ -21,14 +20,8 @@
@Transactional
public class ZoomAttendanceService {

private final ZoomApiClient zoomApiClient;
private final ZoomAttendanceRepository zoomAttendanceRepository;

/** API 연결 테스트 */
public boolean testConnection() {
return zoomApiClient.testConnection();
}

/** 웹훅 이벤트 처리 */
public void handleWebhookEvent(ZoomWebhookEvent event) {
try {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package backend.techeerzip.infra.zoom.dto;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;

import lombok.Getter;
import lombok.NoArgsConstructor;
Expand All @@ -20,6 +21,20 @@ public class ZoomWebhookEvent {
@JsonProperty("payload")
private WebhookPayload payload;

/**
* 디버깅용: raw JSON 문자열을 ZoomWebhookEvent 로 파싱하는 헬퍼. WebhookHandler 에서 헤더/바디를 먼저 로깅한 뒤, 기존 도메인 로직과
* 최대한 동일하게 DTO 를 사용하기 위해 추가.
*/
public static ZoomWebhookEvent fromJson(String json) {
try {
ObjectMapper mapper = new ObjectMapper();
return mapper.readValue(json, ZoomWebhookEvent.class);
} catch (Exception e) {
log.error("ZoomWebhookEvent JSON 파싱 실패", e);
throw new IllegalArgumentException("Invalid Zoom webhook payload", e);
}
}

@Getter
@NoArgsConstructor
public static class WebhookPayload {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
package backend.techeerzip.infra.zoom.webhook;

import java.util.Map;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RestController;

import backend.techeerzip.domain.zoom.service.ZoomAttendanceService;
import backend.techeerzip.infra.zoom.client.ZoomApiClient;
import backend.techeerzip.infra.zoom.config.ZoomApiConfig;
import backend.techeerzip.infra.zoom.dto.ZoomWebhookEvent;
import lombok.RequiredArgsConstructor;
Expand All @@ -18,13 +22,44 @@
public class ZoomWebhookHandler {

private final ZoomApiConfig zoomApiConfig;
private final ZoomApiClient zoomApiClient;
private final ZoomAttendanceService zoomAttendanceService;

@GetMapping("/api/v3/zoom")
public ResponseEntity<Void> handleWebhookEvent() {
boolean isConnected = zoomApiClient.testConnection();
log.debug("Zoom testConnection: {}", isConnected);
return ResponseEntity.ok().build();
}

/** Zoom Webhook 이벤트 수신 */
@PostMapping("/api/v3/zoom/webhook/events")
public ResponseEntity<String> handleWebhookEvent(
@RequestHeader(value = "authorization", required = false) String authHeader,
@RequestBody ZoomWebhookEvent event) {
@RequestHeader Map<String, String> headers, @RequestBody String rawBody) {

// 디버깅용 전체 헤더 & 바디 로깅
try {
log.info("Zoom Webhook headers: {}", headers);
log.info("Zoom Webhook raw body: {}", rawBody);
} catch (Exception e) {
log.warn("Zoom Webhook logging failed: {}", e.getMessage());
}

// 기존 로직과의 호환을 위해 authorization 헤더와 DTO를 다시 구성
String authHeader =
headers.getOrDefault("authorization", headers.getOrDefault("Authorization", null));

ZoomWebhookEvent event;
try {
// 기존 도메인 로직을 최대한 건드리지 않기 위해,
// 일단 기존 DTO 매핑이 동작하던 방식 그대로 유지하려면
// @RequestBody ZoomWebhookEvent 를 쓰는 원래 메서드를 래핑하는 편이 가장 좋지만,
// 지금은 빠른 헤더 확인을 위해 최소한의 파싱만 시도하지 않고, 토큰 검증/이벤트 타입까지만 본다.
event = ZoomWebhookEvent.fromJson(rawBody);
} catch (Exception e) {
log.error("Zoom Webhook body 파싱 실패: {}", e.getMessage(), e);
return ResponseEntity.badRequest().body("Invalid payload");
}

try {
// Webhook 검증
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -305,17 +305,18 @@ void acceptApplicantThrowsExceptionWhenNotFound() {
@DisplayName("실패: 이미 APPROVED 상태인 경우 PENDING으로 조회되지 않아 예외가 발생한다")
void acceptApplicantThrowsExceptionWhenAlreadyApproved() {
// Given
given(projectMemberRepository.findByProjectTeamIdAndUserIdAndStatus(
teamId, userId, StatusCategory.PENDING))
given(
projectMemberRepository.findByProjectTeamIdAndUserIdAndStatus(
teamId, userId, StatusCategory.PENDING))
.willReturn(Optional.empty());

// When & Then
assertThatThrownBy(() -> projectMemberService.acceptApplicant(teamId, userId))
.isInstanceOf(ProjectMemberNotFoundException.class);

// Verify: 서비스가 실제로 'PENDING' 상태를 조건으로 조회를 시도했는지 검증 (이 부분이 추가되어 중복 해결)
verify(projectMemberRepository).findByProjectTeamIdAndUserIdAndStatus(
teamId, userId, StatusCategory.PENDING);
verify(projectMemberRepository)
.findByProjectTeamIdAndUserIdAndStatus(teamId, userId, StatusCategory.PENDING);
}
}

Expand Down Expand Up @@ -364,17 +365,18 @@ void rejectApplicantThrowsExceptionWhenNotFound() {
@DisplayName("실패: 이미 REJECT 상태인 경우 PENDING으로 조회되지 않아 예외가 발생한다")
void rejectApplicantThrowsExceptionWhenAlreadyRejected() {
// Given
given(projectMemberRepository.findByProjectTeamIdAndUserIdAndStatus(
teamId, userId, StatusCategory.PENDING))
given(
projectMemberRepository.findByProjectTeamIdAndUserIdAndStatus(
teamId, userId, StatusCategory.PENDING))
.willReturn(Optional.empty());

// When & Then
assertThatThrownBy(() -> projectMemberService.rejectApplicant(teamId, userId))
.isInstanceOf(ProjectMemberNotFoundException.class);

// Verify: PENDING 상태로 조회를 시도했는지 호출 여부를 검증하여 코드 중복 해결
verify(projectMemberRepository).findByProjectTeamIdAndUserIdAndStatus(
teamId, userId, StatusCategory.PENDING);
verify(projectMemberRepository)
.findByProjectTeamIdAndUserIdAndStatus(teamId, userId, StatusCategory.PENDING);
}
}

Expand Down
Loading