Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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 @@ -37,6 +37,12 @@ Optional<ZoomAttendance> findActiveSessionByParticipantUuidAndMeetingDate(
@Param("participantUuid") String participantUuid,
@Param("meetingDate") LocalDate meetingDate);

/** participantUuid로 활성 세션 조회 (날짜 경계 문제 해결을 위해 날짜 조건 제외) */
@Query(
"SELECT z FROM ZoomAttendance z WHERE z.participantUuid = :participantUuid AND z.leaveTime IS NULL")
Optional<ZoomAttendance> findActiveSessionByParticipantUuid(
@Param("participantUuid") String participantUuid);

/** 특정 사용자의 월별 통계 조회 */
@Query(
"SELECT new backend.techeerzip.domain.zoom.dto.response.ZoomMonthlyStatsDto("
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@

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 backend.techeerzip.infra.zoom.dto.ZoomWebhookEvent.WebhookParticipant;
import backend.techeerzip.infra.zoom.type.ZoomLeaveReason;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

Expand All @@ -21,69 +21,42 @@
@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 {
String eventType = event.getEvent();

if ("meeting.participant_joined".equals(eventType)) {
handleParticipantJoined(event);
} else if ("meeting.participant_left".equals(eventType)) {
handleParticipantLeft(event);
}

log.debug("ZoomService 처리 완료: {}", eventType);

} catch (Exception e) {
log.error("ZoomService 처리 오류: {}", e.getMessage(), e);
}
}

/** 참가자 입장 이벤트 처리 */
private void handleParticipantJoined(ZoomWebhookEvent event) {
public void handleParticipantJoined(WebhookParticipant participant) {
try {
ZoomWebhookEvent.WebhookParticipant participant =
event.getPayload().getObject().getParticipant();

if (participant == null) {
log.warn("참가자 정보가 없습니다.");
return;
}

String participantUuid = participant.getParticipantUuid();
String userName = participant.getUserName();
String joinTimeStr = participant.getJoinTime();

if (participantUuid == null || joinTimeStr == null) {
log.warn("필수 정보가 누락되었습니다. uuid: {}, joinTime: {}", participantUuid, joinTimeStr);
if (joinTimeStr == null) {
log.warn("joinTimeStr 정보가 누락되었습니다. uuid: {}", participantUuid);
return;
}

// 시간 파싱 (Zoom은 UTC ISO 8601 형식)
LocalDateTime joinTime = parseZoomDateTime(joinTimeStr);
LocalDate meetingDate = joinTime.toLocalDate();

// 기존 출석 기록 조회
Optional<ZoomAttendance> existingRecord =
zoomAttendanceRepository.findByParticipantUuidAndMeetingDate(
participantUuid, meetingDate);
// 활성 세션 조회 (leaveTime이 null인 기록)
// participant_uuid는 링크 접속 시 생성되고, 완전히 나갔다 다시 들어오면 새로 할당됨
// 따라서 같은 participant_uuid로 활성 세션이 있으면 = 소회의실에서 메인으로 복귀한 경우
Optional<ZoomAttendance> activeSession =
zoomAttendanceRepository.findActiveSessionByParticipantUuid(participantUuid);

if (existingRecord.isPresent()) {
log.info(
"기존 출석 기록이 존재합니다. participantUuid: {}, date: {}",
if (activeSession.isPresent()) {
log.debug(
"활성 세션이 존재합니다. participantUuid: {}, date: {} (소회의실에서 메인으로 복귀 무시)",
participantUuid,
meetingDate);
return; // 이미 있는 경우 무시 (소회의실에서 복귀한 경우)
return; // 활성 세션이 있으면 무시 (소회의실에서 메인으로 복귀한 경우)
}

// 완전히 나갔다 다시 들어온 경우 (새로운 participant_uuid) 또는 첫 입장
// 기존에 완료된 세션이 있는지 확인 (같은 날짜에 다른 participant_uuid로 기록이 있을 수 있음)
// 하지만 participant_uuid가 다르면 새로운 세션이므로 별도 엔티티로 저장

// 새로운 출석 기록 생성 (입장 시에는 leaveTime과 durationMinutes는 null)
ZoomAttendance attendance =
ZoomAttendance.builder()
Expand All @@ -109,53 +82,43 @@ private void handleParticipantJoined(ZoomWebhookEvent event) {
}

/** 참가자 퇴장 이벤트 처리 */
private void handleParticipantLeft(ZoomWebhookEvent event) {
public void handleParticipantLeft(WebhookParticipant participant) {
try {
ZoomWebhookEvent.WebhookParticipant participant =
event.getPayload().getObject().getParticipant();

if (participant == null) {
log.warn("참가자 정보가 없습니다.");
return;
}

String participantUuid = participant.getParticipantUuid();
String leaveReason = participant.getLeaveReason();
String leaveTimeStr = participant.getLeaveTime();

if (participantUuid == null || leaveTimeStr == null) {
log.warn("필수 정보가 누락되었습니다. uuid: {}, leaveTime: {}", participantUuid, leaveTimeStr);
if (leaveTimeStr == null) {
log.warn("leaveTimeStr 정보가 누락되었습니다. uuid: {}", participantUuid);
return;
}

// 소회의실 관련 이벤트 무시
if (leaveReason != null) {
if (leaveReason.contains("join breakout room")
|| leaveReason.contains("leave breakout room to join main meeting")) {
log.debug("소회의실 관련 이벤트 무시: {} - {}", participantUuid, leaveReason);
return;
}
// 완전히 나간 케이스만 처리 (소회의실 이동, 대기실 관련 등은 제외)
if (!ZoomLeaveReason.isCompleteExit(leaveReason)) {
log.debug(
"완전히 나간 케이스가 아닙니다. participantUuid: {}, leaveReason: {} (무시)",
participantUuid,
leaveReason);
return;
}

// 시간 파싱
LocalDateTime leaveTime = parseZoomDateTime(leaveTimeStr);
LocalDate meetingDate = leaveTime.toLocalDate();

// 기존 출석 기록 조회
Optional<ZoomAttendance> existingRecord =
zoomAttendanceRepository.findByParticipantUuidAndMeetingDate(
participantUuid, meetingDate);
// 활성 세션 조회 (leaveTime이 null인 기록만)
// participantUuid만으로 조회하여 날짜 경계 문제 해결 (밤 11시 입장 → 다음 날 새벽 퇴장 케이스)
Optional<ZoomAttendance> activeSession =
zoomAttendanceRepository.findActiveSessionByParticipantUuid(participantUuid);

if (existingRecord.isEmpty()) {
log.warn(
"출석 기록을 찾을 수 없습니다. participantUuid: {}, date: {}",
participantUuid,
meetingDate);
return;
if (activeSession.isEmpty()) {
log.debug(
"활성 세션을 찾을 수 없습니다. participantUuid: {} (이미 완료된 세션이거나 소회의실 퇴장)",
participantUuid);
return; // 활성 세션이 없으면 무시 (이미 완료된 세션이거나 소회의실 퇴장)
}

// 퇴장 시간 업데이트
ZoomAttendance attendance = existingRecord.get();
// 활성 세션의 퇴장 시간 업데이트 (완전히 나간 경우)
ZoomAttendance attendance = activeSession.get();
attendance.updateLeaveTime(leaveTime);

zoomAttendanceRepository.save(attendance);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
Expand Down Expand Up @@ -36,10 +37,9 @@ public SecurityFilterChain swaggerSecurityFilterChain(HttpSecurity http) throws
http.securityMatcher("/api/v3/docs/**", "/api/v3/swagger-ui/**", "/api/v3/api-docs/**")
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
.httpBasic(httpBasic -> httpBasic.realmName("Swagger API Documentation"))
.csrf(csrf -> csrf.disable())
.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(
session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));

return http.build();
}

Expand Down Expand Up @@ -111,7 +111,7 @@ public SecurityFilterChain apiSecurityFilterChain(HttpSecurity http) throws Exce
.permitAll()
.anyRequest()
.authenticated())
.csrf(csrf -> csrf.disable())
.csrf(AbstractHttpConfigurer::disable)
.exceptionHandling(
ex ->
ex.authenticationEntryPoint(customAuthenticationEntryPoint)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
@Configuration
public class WebConfig implements WebMvcConfigurer {

private OctetStreamReadMsgConverter octetStreamReadMsgConverter;
private final OctetStreamReadMsgConverter octetStreamReadMsgConverter;

@Override
public void addCorsMappings(CorsRegistry registry) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,21 @@ public enum ErrorCode {
GITHUB_INVALID_URL(HttpStatus.BAD_REQUEST, "GH001", "유효하지 않은 GitHub URL입니다."),
GITHUB_USER_NOT_FOUND(HttpStatus.NOT_FOUND, "GH002", "GitHub 사용자를 찾을 수 없습니다. 사용자명을 확인해주세요."),
GITHUB_API_ERROR(HttpStatus.BAD_GATEWAY, "GH003", "GitHub API 호출 중 오류가 발생했습니다."),
GITHUB_SERVER_ERROR(HttpStatus.SERVICE_UNAVAILABLE, "GH004", "GitHub 서버와 통신할 수 없습니다.");
GITHUB_SERVER_ERROR(HttpStatus.SERVICE_UNAVAILABLE, "GH004", "GitHub 서버와 통신할 수 없습니다."),

// Zoom
ZOOM_WEBHOOK_SECRET_TOKEN_NOT_CONFIGURED(
HttpStatus.INTERNAL_SERVER_ERROR, "ZOOM001", "웹훅 secret token이 설정되지 않았습니다."),
ZOOM_TOKEN_ENCRYPTION_FAILED(
HttpStatus.INTERNAL_SERVER_ERROR, "ZOOM002", "토큰 암호화 중 오류가 발생했습니다."),
ZOOM_OAUTH_TOKEN_GENERATION_FAILED(
HttpStatus.INTERNAL_SERVER_ERROR, "ZOOM003", "Zoom OAuth 토큰 생성에 실패했습니다."),
ZOOM_WEBHOOK_AUTHENTICATION_FAILED(
HttpStatus.UNAUTHORIZED, "ZOOM004", "Zoom Webhook 인증에 실패했습니다."),
ZOOM_WEBHOOK_PROCESSING_FAILED(
HttpStatus.INTERNAL_SERVER_ERROR, "ZOOM005", "Zoom Webhook 처리 중 오류가 발생했습니다."),
ZOOM_WEBHOOK_PLAIN_TOKEN_MISSING(
HttpStatus.BAD_REQUEST, "ZOOM006", "URL 검증 요청에 plainToken이 없습니다.");

private final HttpStatus status;
private final String code;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public ResponseEntity<ErrorResponse> handleValidationExceptions(
e.getBindingResult()
.getAllErrors()
.forEach(
(error) -> {
error -> {
if (error instanceof FieldError fieldError) {
String fieldName = fieldError.getField();
String errorMessage = error.getDefaultMessage();
Expand Down

This file was deleted.

Loading
Loading