Event 엔티티에 참여자 및 ROI 필드 추가 및 Frontend-Backend 통합

🔧 Backend 변경사항:
- Event 엔티티에 participants, targetParticipants, roi 필드 추가
- EventDetailResponse DTO 및 EventService 매퍼 업데이트
- ROI 자동 계산 비즈니스 로직 구현
- SecurityConfig CORS 설정 추가 (localhost:3000 허용)

🎨 Frontend 변경사항:
- TypeScript EventDetail 타입 정의 업데이트
- Events 페이지 실제 API 데이터 연동 (Mock 데이터 제거)
- 참여자 수 및 ROI 기반 통계 계산 로직 개선

📝 문서:
- Event 필드 추가 및 API 통합 테스트 결과서 작성

 테스트 완료:
- Backend API 응답 검증
- CORS 설정 검증
- Frontend-Backend 통합 테스트 성공

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
merrycoral
2025-10-29 13:23:09 +09:00
parent 284278180c
commit 95a419f104
15 changed files with 1239 additions and 2 deletions
@@ -36,6 +36,9 @@ public class EventDetailResponse {
private EventStatus status;
private UUID selectedImageId;
private String selectedImageUrl;
private Integer participants;
private Integer targetParticipants;
private Double roi;
@Builder.Default
private List<GeneratedImageDto> generatedImages = new ArrayList<>();
@@ -518,6 +518,9 @@ public class EventService {
.status(event.getStatus())
.selectedImageId(event.getSelectedImageId())
.selectedImageUrl(event.getSelectedImageUrl())
.participants(event.getParticipants())
.targetParticipants(event.getTargetParticipants())
.roi(event.getRoi())
.generatedImages(
event.getGeneratedImages().stream()
.map(img -> EventDetailResponse.GeneratedImageDto.builder()
@@ -8,6 +8,12 @@ import org.springframework.security.config.annotation.web.configurers.AbstractHt
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.Arrays;
import java.util.List;
/**
* Spring Security 설정 클래스
@@ -34,8 +40,8 @@ public class SecurityConfig {
// CSRF 보호 비활성화 (개발 환경)
.csrf(AbstractHttpConfigurer::disable)
// CORS 설정
.cors(AbstractHttpConfigurer::disable)
// CORS 설정 활성화
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
// 폼 로그인 비활성화
.formLogin(AbstractHttpConfigurer::disable)
@@ -62,4 +68,54 @@ public class SecurityConfig {
return http.build();
}
/**
* CORS 설정
* 개발 환경에서 프론트엔드(localhost:3000)의 요청을 허용합니다.
*
* @return CorsConfigurationSource CORS 설정 소스
*/
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
// 허용할 Origin (개발 환경)
configuration.setAllowedOrigins(Arrays.asList(
"http://localhost:3000",
"http://127.0.0.1:3000"
));
// 허용할 HTTP 메서드
configuration.setAllowedMethods(Arrays.asList(
"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"
));
// 허용할 헤더
configuration.setAllowedHeaders(Arrays.asList(
"Authorization",
"Content-Type",
"X-Requested-With",
"Accept",
"Origin",
"Access-Control-Request-Method",
"Access-Control-Request-Headers"
));
// 인증 정보 포함 허용
configuration.setAllowCredentials(true);
// Preflight 요청 캐시 시간 (초)
configuration.setMaxAge(3600L);
// 노출할 응답 헤더
configuration.setExposedHeaders(Arrays.asList(
"Authorization",
"Content-Type"
));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
@@ -69,6 +69,17 @@ public class Event extends BaseTimeEntity {
@Column(name = "selected_image_url", length = 500)
private String selectedImageUrl;
@Column(name = "participants")
@Builder.Default
private Integer participants = 0;
@Column(name = "target_participants")
private Integer targetParticipants;
@Column(name = "roi")
@Builder.Default
private Double roi = 0.0;
@ElementCollection(fetch = FetchType.LAZY)
@CollectionTable(
name = "event_channels",
@@ -139,6 +150,57 @@ public class Event extends BaseTimeEntity {
this.channels.addAll(channels);
}
/**
* 목표 참여자 수 설정
*/
public void updateTargetParticipants(Integer targetParticipants) {
if (targetParticipants != null && targetParticipants < 0) {
throw new IllegalArgumentException("목표 참여자 수는 0 이상이어야 합니다.");
}
this.targetParticipants = targetParticipants;
}
/**
* 참여자 수 증가
*/
public void incrementParticipants() {
this.participants = (this.participants == null ? 0 : this.participants) + 1;
updateRoi();
}
/**
* 참여자 수 직접 설정
*/
public void updateParticipants(Integer participants) {
if (participants != null && participants < 0) {
throw new IllegalArgumentException("참여자 수는 0 이상이어야 합니다.");
}
this.participants = participants;
updateRoi();
}
/**
* ROI 계산 및 업데이트
* ROI = (참여자 수 / 목표 참여자 수) * 100
*/
private void updateRoi() {
if (this.targetParticipants != null && this.targetParticipants > 0) {
this.roi = ((double) (this.participants == null ? 0 : this.participants) / this.targetParticipants) * 100.0;
} else {
this.roi = 0.0;
}
}
/**
* ROI 직접 설정 (외부 계산값 사용)
*/
public void updateRoi(Double roi) {
if (roi != null && roi < 0) {
throw new IllegalArgumentException("ROI는 0 이상이어야 합니다.");
}
this.roi = roi;
}
/**
* 이벤트 배포 (상태 변경: DRAFT → PUBLISHED)
*/