goldenChat base source add

This commit is contained in:
aidev
2026-05-23 15:11:48 +09:00
commit a4ea7762b5
2081 changed files with 1155760 additions and 0 deletions
@@ -0,0 +1,22 @@
package com.goldenchart.config;
import com.goldenchart.service.AuthService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
@Slf4j
public class AdminUserInitializer implements ApplicationRunner {
private final AuthService authService;
@Override
public void run(ApplicationArguments args) {
authService.ensureDefaultAdmin();
log.info("[Auth] default admin user ensured (username=admin)");
}
}
@@ -0,0 +1,44 @@
package com.goldenchart.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.List;
@Configuration
@ConfigurationProperties(prefix = "goldenchart.cors")
public class CorsConfig {
private List<String> allowedOrigins = List.of("http://localhost:5173", "http://localhost:3000");
public List<String> getAllowedOrigins() {
return allowedOrigins;
}
public void setAllowedOrigins(List<String> allowedOrigins) {
this.allowedOrigins = allowedOrigins;
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
// setAllowedOriginPatterns 사용: wildcard(*) + allowCredentials 동시 지원
if (allowedOrigins.contains("*")) {
config.setAllowedOriginPatterns(List.of("*"));
} else {
config.setAllowedOriginPatterns(allowedOrigins);
}
config.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"));
config.setAllowedHeaders(List.of("*"));
config.setAllowCredentials(true);
config.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}
}
@@ -0,0 +1,58 @@
package com.goldenchart.config;
import com.google.auth.oauth2.GoogleCredentials;
import com.google.firebase.FirebaseApp;
import com.google.firebase.FirebaseOptions;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import java.io.FileInputStream;
import java.io.InputStream;
@Configuration
@Slf4j
public class FirebaseConfig {
@Value("${firebase.service-account-file:firebase-service-account.json}")
private String serviceAccountFile;
@Value("${firebase.enabled:false}")
private boolean firebaseEnabled;
@PostConstruct
public void initialize() {
if (!firebaseEnabled) {
log.info("[Firebase] disabled (firebase.enabled=false)");
return;
}
if (!FirebaseApp.getApps().isEmpty()) return;
try (InputStream in = openServiceAccount()) {
if (in == null) {
log.warn("[Firebase] service account not found — FCM disabled");
return;
}
FirebaseOptions options = FirebaseOptions.builder()
.setCredentials(GoogleCredentials.fromStream(in))
.build();
FirebaseApp.initializeApp(options);
log.info("[Firebase] Admin SDK initialized — FCM enabled");
} catch (Exception e) {
log.warn("[Firebase] init failed: {}", e.getMessage());
}
}
private InputStream openServiceAccount() {
try {
var res = new ClassPathResource(serviceAccountFile);
if (res.exists()) return res.getInputStream();
} catch (Exception ignored) { }
try {
return new FileInputStream(serviceAccountFile);
} catch (Exception ignored) { }
return null;
}
}
@@ -0,0 +1,52 @@
package com.goldenchart.config;
import com.goldenchart.entity.GcAppSettings;
import com.goldenchart.entity.GcWatchlist;
import com.goldenchart.repository.GcWatchlistRepository;
import com.goldenchart.service.AppSettingsService;
import com.goldenchart.service.LiveStrategyEvaluator;
import com.goldenchart.websocket.DynamicSubscriptionManager;
import com.goldenchart.websocket.UpbitWebSocketClient;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;
/**
* 서버 기동 시 isLiveCheck=true 인 모든 종목을 업비트 WS·Ta4jStorage 에 고정 구독한다.
* (재시작 후에도 실시간 전략 평가가 끊기지 않도록)
*/
@Component
@RequiredArgsConstructor
@Slf4j
public class LiveStrategyStartupRunner implements ApplicationRunner {
private final AppSettingsService appSettingsService;
private final GcWatchlistRepository watchlistRepo;
private final DynamicSubscriptionManager subscriptionManager;
private final LiveStrategyEvaluator liveStrategyEvaluator;
private final UpbitWebSocketClient upbitWebSocketClient;
@Override
public void run(ApplicationArguments args) {
// 디바이스별 전역 설정이 켜져 있고 전략이 지정된 경우, DB 관심종목 전체 고정 구독
int pinned = 0;
for (GcWatchlist w : watchlistRepo.findAll()) {
String deviceId = w.getDeviceId();
if (deviceId == null) continue;
GcAppSettings app = appSettingsService.getEntity(null, deviceId);
if (!Boolean.TRUE.equals(app.getLiveStrategyCheck()) || app.getLiveStrategyId() == null) {
continue;
}
subscriptionManager.ensureMarketPinned(w.getSymbol(), "1m");
liveStrategyEvaluator.invalidateCache(w.getSymbol());
pinned++;
}
if (pinned > 0) {
log.info("[LiveStrategyStartup] 관심종목 기준 마켓 {}개 고정 구독 완료", pinned);
}
// WS 연결 직후 addMarket 이 구독 패킷을 놓치는 경우 대비
upbitWebSocketClient.resubscribeAll();
}
}
@@ -0,0 +1,34 @@
package com.goldenchart.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
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;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http, CorsConfig corsConfig) throws Exception {
http
.cors(cors -> cors.configurationSource(corsConfig.corsConfigurationSource()))
.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
// 개발 단계: 전체 허용 (운영 시 JWT 필터 추가)
.requestMatchers("/**").permitAll()
);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
@@ -0,0 +1,49 @@
package com.goldenchart.config;
import com.goldenchart.entity.GcAppSettings;
import com.goldenchart.repository.GcAppSettingsRepository;
import com.goldenchart.security.SecretCryptoService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
/**
* 기존 평문 업비트 API 키를 AES 암호문으로 일괄 변환 (1회성·멱등).
*/
@Component
@RequiredArgsConstructor
@Slf4j
public class UpbitKeyEncryptionMigrator implements ApplicationRunner {
private final GcAppSettingsRepository repo;
private final SecretCryptoService secretCrypto;
@Override
@Transactional
public void run(ApplicationArguments args) {
int migrated = 0;
for (GcAppSettings s : repo.findAll()) {
boolean changed = false;
String access = s.getUpbitAccessKey();
if (access != null && !access.isBlank() && !secretCrypto.isEncrypted(access)) {
s.setUpbitAccessKey(secretCrypto.encrypt(access));
changed = true;
}
String secret = s.getUpbitSecretKey();
if (secret != null && !secret.isBlank() && !secretCrypto.isEncrypted(secret)) {
s.setUpbitSecretKey(secretCrypto.encrypt(secret));
changed = true;
}
if (changed) {
repo.save(s);
migrated++;
}
}
if (migrated > 0) {
log.info("[UpbitKeyEncryptionMigrator] {} settings row(s) API keys encrypted", migrated);
}
}
}
@@ -0,0 +1,39 @@
package com.goldenchart.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
/**
* STOMP WebSocket 브로커 설정.
*
* 클라이언트 연결 엔드포인트:
* ws(s)://host/ws/trading (SockJS fallback 지원)
*
* 구독 채널 (명세서 5.2):
* /sub/charts/{market}/{type} — 실시간 캔들 배포
*
* 발행 채널:
* /pub/... — 클라이언트 → 서버 (향후 확장용)
*/
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
// 인메모리 심플 브로커: /sub 접두사 구독 채널에 메시지 라우팅
registry.enableSimpleBroker("/sub");
// 클라이언트 발행 경로 접두사 (서버측 @MessageMapping 핸들러로 라우팅)
registry.setApplicationDestinationPrefixes("/pub");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws/trading")
.setAllowedOriginPatterns("*")
.withSockJS(); // SockJS fallback (브라우저 환경 대비)
}
}