feat: 백엔드 Spring Boot + DB 스키마 + Python 분석서버 스켈레톤

- backend/: Spring Boot 3.2 + Java 17, Google OAuth + JWT 인증
  - AuthController/Service/Filter: id_token 검증 + gcsc.co.kr 도메인 제한
  - JPA Entity: users, login_history
  - 수집기 placeholder: GDELT, Google News, CENTCOM, Aircraft
  - ArticleClassifier: 프론트엔드 분류 정규식 이식
  - 프로파일: local / prod (PostgreSQL 211.208.115.83:5432/kcgdb)
- database/: 초기 스키마 (events, news, osint, users, login_history)
- prediction/: FastAPI placeholder (향후 해양 분석)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
htlee 2026-03-17 13:54:58 +09:00
부모 2534faa488
커밋 512020d6bb
32개의 변경된 파일908개의 추가작업 그리고 0개의 파일을 삭제

1
backend/.sdkmanrc Normal file
파일 보기

@ -0,0 +1 @@
java=17.0.18-amzn

106
backend/pom.xml Normal file
파일 보기

@ -0,0 +1,106 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.5</version>
<relativePath/>
</parent>
<groupId>gc.mda</groupId>
<artifactId>kcg</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>kcg</name>
<description>KCG Monitoring Dashboard Backend</description>
<properties>
<java.version>17</java.version>
<jjwt.version>0.12.6</jjwt.version>
</properties>
<dependencies>
<!-- Spring Boot Starters -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- Database -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>${jjwt.version}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>${jjwt.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>${jjwt.version}</version>
<scope>runtime</scope>
</dependency>
<!-- Google API Client -->
<dependency>
<groupId>com.google.api-client</groupId>
<artifactId>google-api-client</artifactId>
<version>2.7.0</version>
</dependency>
<!-- Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<finalName>kcg</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>

파일 보기

@ -0,0 +1,14 @@
package gc.mda.kcg;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
@EnableScheduling
public class KcgApplication {
public static void main(String[] args) {
SpringApplication.run(KcgApplication.class, args);
}
}

파일 보기

@ -0,0 +1,77 @@
package gc.mda.kcg.auth;
import gc.mda.kcg.auth.dto.AuthResponse;
import gc.mda.kcg.auth.dto.GoogleAuthRequest;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.CookieValue;
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.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthController {
private static final String JWT_COOKIE_NAME = "kcg_token";
private final AuthService authService;
private final JwtProvider jwtProvider;
/**
* Google OAuth2 id_token 검증 JWT 쿠키 발급
*/
@PostMapping("/google")
public ResponseEntity<AuthResponse> googleLogin(
@Valid @RequestBody GoogleAuthRequest request,
HttpServletResponse response) {
AuthResponse authResponse = authService.authenticateWithGoogle(request.getCredential());
String token = jwtProvider.generateToken(authResponse.getEmail(), authResponse.getName());
Cookie cookie = new Cookie(JWT_COOKIE_NAME, token);
cookie.setHttpOnly(true);
cookie.setSecure(false);
cookie.setPath("/");
cookie.setMaxAge((int) (jwtProvider.getExpirationMs() / 1000));
response.addCookie(cookie);
return ResponseEntity.ok(authResponse);
}
/**
* JWT 쿠키에서 현재 사용자 정보 반환
*/
@GetMapping("/me")
public ResponseEntity<AuthResponse> me(
@CookieValue(name = JWT_COOKIE_NAME, required = false) String token) {
if (token == null || token.isBlank()) {
return ResponseEntity.status(401).build();
}
AuthResponse authResponse = authService.getUserFromToken(token);
return ResponseEntity.ok(authResponse);
}
/**
* JWT 쿠키 삭제 (로그아웃)
*/
@PostMapping("/logout")
public ResponseEntity<Void> logout(HttpServletResponse response) {
Cookie cookie = new Cookie(JWT_COOKIE_NAME, "");
cookie.setHttpOnly(true);
cookie.setSecure(false);
cookie.setPath("/");
cookie.setMaxAge(0);
response.addCookie(cookie);
return ResponseEntity.ok().build();
}
}

파일 보기

@ -0,0 +1,70 @@
package gc.mda.kcg.auth;
import io.jsonwebtoken.Claims;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.Arrays;
@Slf4j
@Component
@RequiredArgsConstructor
public class AuthFilter extends OncePerRequestFilter {
private static final String JWT_COOKIE_NAME = "kcg_token";
private static final String AUTH_PATH_PREFIX = "/api/auth/";
private final JwtProvider jwtProvider;
@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
String path = request.getRequestURI();
return path.startsWith(AUTH_PATH_PREFIX);
}
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String token = extractTokenFromCookies(request);
if (token == null || token.isBlank()) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("{\"error\":\"인증이 필요합니다.\"}");
return;
}
try {
Claims claims = jwtProvider.validateToken(token);
request.setAttribute("userEmail", claims.getSubject());
request.setAttribute("userName", claims.get("name", String.class));
filterChain.doFilter(request, response);
} catch (IllegalArgumentException e) {
log.warn("인증 실패: {}", e.getMessage());
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("{\"error\":\"유효하지 않은 토큰입니다.\"}");
}
}
private String extractTokenFromCookies(HttpServletRequest request) {
Cookie[] cookies = request.getCookies();
if (cookies == null) {
return null;
}
return Arrays.stream(cookies)
.filter(c -> JWT_COOKIE_NAME.equals(c.getName()))
.findFirst()
.map(Cookie::getValue)
.orElse(null);
}
}

파일 보기

@ -0,0 +1,130 @@
package gc.mda.kcg.auth;
import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken;
import com.google.api.client.googleapis.auth.oauth2.GoogleIdTokenVerifier;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.client.json.gson.GsonFactory;
import gc.mda.kcg.auth.dto.AuthResponse;
import gc.mda.kcg.auth.entity.LoginHistory;
import gc.mda.kcg.auth.entity.User;
import gc.mda.kcg.auth.repository.LoginHistoryRepository;
import gc.mda.kcg.auth.repository.UserRepository;
import gc.mda.kcg.config.AppProperties;
import io.jsonwebtoken.Claims;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.Collections;
@Slf4j
@Service
@RequiredArgsConstructor
public class AuthService {
private final AppProperties appProperties;
private final JwtProvider jwtProvider;
private final UserRepository userRepository;
private final LoginHistoryRepository loginHistoryRepository;
/**
* Google id_token 검증 사용자 upsert 로그인 이력 기록
*/
@Transactional
public AuthResponse authenticateWithGoogle(String credential) {
GoogleIdToken idToken = verifyGoogleToken(credential);
GoogleIdToken.Payload payload = idToken.getPayload();
String email = payload.getEmail();
String name = (String) payload.get("name");
String picture = (String) payload.get("picture");
validateEmailDomain(email);
User user = upsertUser(email, name, picture);
recordLoginHistory(user);
return AuthResponse.builder()
.email(user.getEmail())
.name(user.getName())
.picture(user.getPicture())
.build();
}
/**
* JWT 토큰에서 사용자 정보 조회
*/
@Transactional(readOnly = true)
public AuthResponse getUserFromToken(String token) {
Claims claims = jwtProvider.validateToken(token);
String email = claims.getSubject();
User user = userRepository.findByEmail(email)
.orElseThrow(() -> new IllegalArgumentException("사용자를 찾을 수 없습니다: " + email));
return AuthResponse.builder()
.email(user.getEmail())
.name(user.getName())
.picture(user.getPicture())
.build();
}
private GoogleIdToken verifyGoogleToken(String credential) {
try {
GoogleIdTokenVerifier verifier = new GoogleIdTokenVerifier.Builder(
new NetHttpTransport(), GsonFactory.getDefaultInstance())
.setAudience(Collections.singletonList(appProperties.getGoogle().getClientId()))
.build();
GoogleIdToken idToken = verifier.verify(credential);
if (idToken == null) {
throw new IllegalArgumentException("유효하지 않은 Google 토큰입니다.");
}
return idToken;
} catch (IllegalArgumentException e) {
throw e;
} catch (Exception e) {
log.error("Google 토큰 검증 실패", e);
throw new IllegalArgumentException("Google 토큰 검증에 실패했습니다.", e);
}
}
private void validateEmailDomain(String email) {
String allowedDomain = appProperties.getAuth().getAllowedDomain();
if (allowedDomain != null && !allowedDomain.isBlank()) {
String domain = email.substring(email.indexOf('@') + 1);
if (!domain.equals(allowedDomain)) {
throw new IllegalArgumentException("허용되지 않은 이메일 도메인입니다: " + domain);
}
}
}
private User upsertUser(String email, String name, String picture) {
return userRepository.findByEmail(email)
.map(existing -> {
existing.setName(name);
existing.setPicture(picture);
existing.setLastLoginAt(LocalDateTime.now());
return userRepository.save(existing);
})
.orElseGet(() -> {
User newUser = User.builder()
.email(email)
.name(name)
.picture(picture)
.lastLoginAt(LocalDateTime.now())
.build();
return userRepository.save(newUser);
});
}
private void recordLoginHistory(User user) {
LoginHistory history = LoginHistory.builder()
.user(user)
.loginAt(LocalDateTime.now())
.build();
loginHistoryRepository.save(history);
}
}

파일 보기

@ -0,0 +1,68 @@
package gc.mda.kcg.auth;
import gc.mda.kcg.config.AppProperties;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.Date;
@Slf4j
@Component
public class JwtProvider {
private final SecretKey secretKey;
@Getter
private final long expirationMs;
public JwtProvider(AppProperties appProperties) {
this.secretKey = Keys.hmacShaKeyFor(
appProperties.getJwt().getSecret().getBytes(StandardCharsets.UTF_8));
this.expirationMs = appProperties.getJwt().getExpirationMs();
}
/**
* JWT 토큰 생성
*/
public String generateToken(String email, String name) {
Date now = new Date();
Date expiry = new Date(now.getTime() + expirationMs);
return Jwts.builder()
.subject(email)
.claim("name", name)
.issuedAt(now)
.expiration(expiry)
.signWith(secretKey)
.compact();
}
/**
* JWT 토큰 검증 Claims 반환
*/
public Claims validateToken(String token) {
try {
return Jwts.parser()
.verifyWith(secretKey)
.build()
.parseSignedClaims(token)
.getPayload();
} catch (JwtException e) {
log.warn("JWT 토큰 검증 실패: {}", e.getMessage());
throw new IllegalArgumentException("유효하지 않은 토큰입니다.", e);
}
}
/**
* 토큰에서 이메일 추출
*/
public String getEmailFromToken(String token) {
return validateToken(token).getSubject();
}
}

파일 보기

@ -0,0 +1,17 @@
package gc.mda.kcg.auth.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class AuthResponse {
private String email;
private String name;
private String picture;
}

파일 보기

@ -0,0 +1,15 @@
package gc.mda.kcg.auth.dto;
import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class GoogleAuthRequest {
@NotBlank(message = "credential은 필수입니다.")
private String credential;
}

파일 보기

@ -0,0 +1,38 @@
package gc.mda.kcg.auth.entity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Entity
@Table(name = "login_history", schema = "kcg")
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class LoginHistory {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private User user;
@Column(name = "login_at", nullable = false)
@Builder.Default
private LocalDateTime loginAt = LocalDateTime.now();
}

파일 보기

@ -0,0 +1,44 @@
package gc.mda.kcg.auth.entity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.time.LocalDateTime;
@Entity
@Table(name = "users", schema = "kcg")
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String email;
@Column(nullable = false)
private String name;
private String picture;
@Column(name = "last_login_at")
private LocalDateTime lastLoginAt;
@Column(name = "created_at", nullable = false, updatable = false)
@Builder.Default
private LocalDateTime createdAt = LocalDateTime.now();
}

파일 보기

@ -0,0 +1,7 @@
package gc.mda.kcg.auth.repository;
import gc.mda.kcg.auth.entity.LoginHistory;
import org.springframework.data.jpa.repository.JpaRepository;
public interface LoginHistoryRepository extends JpaRepository<LoginHistory, Long> {
}

파일 보기

@ -0,0 +1,11 @@
package gc.mda.kcg.auth.repository;
import gc.mda.kcg.auth.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
}

파일 보기

@ -0,0 +1,39 @@
package gc.mda.kcg.collector;
import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.regex.Pattern;
/**
* 기사 분류기 (정규식 기반)
* TODO: 프론트엔드 classifyArticle() 로직 이식
*/
@Component
public class ArticleClassifier {
private static final Map<String, Pattern> CATEGORY_PATTERNS = Map.of(
"military", Pattern.compile("(?i)(strike|attack|military|weapon|missile|drone|bomb|combat|war|force)"),
"political", Pattern.compile("(?i)(sanction|diplomat|negotiat|treaty|nuclear|iran|deal|policy)"),
"intelligence", Pattern.compile("(?i)(intel|spy|surveillance|intercept|sigint|osint|recon)")
);
/**
* 기사 제목/본문을 분석하여 카테고리를 반환
*
* @param title 기사 제목
* @param content 기사 본문
* @return 분류된 카테고리 (military, political, intelligence, general)
*/
public String classifyArticle(String title, String content) {
String text = (title + " " + content).toLowerCase();
for (Map.Entry<String, Pattern> entry : CATEGORY_PATTERNS.entrySet()) {
if (entry.getValue().matcher(text).find()) {
return entry.getKey();
}
}
return "general";
}
}

파일 보기

@ -0,0 +1,11 @@
package gc.mda.kcg.collector;
import org.springframework.stereotype.Component;
/**
* CENTCOM 보도자료 수집기
* TODO: CENTCOM 웹사이트에서 보도자료 수집 구현
*/
@Component
public class CentcomCollector {
}

파일 보기

@ -0,0 +1,11 @@
package gc.mda.kcg.collector;
import org.springframework.stereotype.Component;
/**
* GDELT 데이터 수집기
* TODO: GDELT API를 통한 이벤트/뉴스 데이터 수집 구현
*/
@Component
public class GdeltCollector {
}

파일 보기

@ -0,0 +1,11 @@
package gc.mda.kcg.collector;
import org.springframework.stereotype.Component;
/**
* Google News RSS 수집기
* TODO: Google News RSS 피드를 통한 뉴스 데이터 수집 구현
*/
@Component
public class GoogleNewsCollector {
}

파일 보기

@ -0,0 +1,36 @@
package gc.mda.kcg.config;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Getter
@Setter
@Configuration
@ConfigurationProperties(prefix = "app")
public class AppProperties {
private Jwt jwt = new Jwt();
private Google google = new Google();
private Auth auth = new Auth();
@Getter
@Setter
public static class Jwt {
private String secret;
private long expirationMs;
}
@Getter
@Setter
public static class Google {
private String clientId;
}
@Getter
@Setter
public static class Auth {
private String allowedDomain;
}
}

파일 보기

@ -0,0 +1,19 @@
package gc.mda.kcg.config;
import gc.mda.kcg.auth.AuthFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class SecurityConfig {
@Bean
public FilterRegistrationBean<AuthFilter> authFilterRegistration(AuthFilter authFilter) {
FilterRegistrationBean<AuthFilter> registration = new FilterRegistrationBean<>();
registration.setFilter(authFilter);
registration.addUrlPatterns("/api/*");
registration.setOrder(1);
return registration;
}
}

파일 보기

@ -0,0 +1,18 @@
package gc.mda.kcg.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("http://localhost:5173")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true);
}
}

파일 보기

@ -0,0 +1,4 @@
/**
* 항공기 도메인 (향후 구현)
*/
package gc.mda.kcg.domain.aircraft;

파일 보기

@ -0,0 +1,4 @@
/**
* 이벤트 도메인 (향후 구현)
*/
package gc.mda.kcg.domain.event;

파일 보기

@ -0,0 +1,4 @@
/**
* 뉴스 도메인 (향후 구현)
*/
package gc.mda.kcg.domain.news;

파일 보기

@ -0,0 +1,4 @@
/**
* OSINT 도메인 (향후 구현)
*/
package gc.mda.kcg.domain.osint;

파일 보기

@ -0,0 +1,13 @@
spring:
datasource:
url: jdbc:postgresql://localhost:5432/kcgdb?currentSchema=kcg
username: kcg_user
password: kcg_pass
app:
jwt:
secret: local-dev-secret-key-32chars-minimum!!
expiration-ms: 86400000
google:
client-id: YOUR_GOOGLE_CLIENT_ID
auth:
allowed-domain: gcsc.co.kr

파일 보기

@ -0,0 +1,13 @@
spring:
datasource:
url: ${DB_URL}
username: ${DB_USERNAME}
password: ${DB_PASSWORD}
app:
jwt:
secret: ${JWT_SECRET}
expiration-ms: ${JWT_EXPIRATION_MS:86400000}
google:
client-id: ${GOOGLE_CLIENT_ID}
auth:
allowed-domain: ${AUTH_ALLOWED_DOMAIN:gcsc.co.kr}

파일 보기

@ -0,0 +1,11 @@
spring:
profiles:
active: ${SPRING_PROFILES_ACTIVE:local}
jpa:
hibernate:
ddl-auto: validate
properties:
hibernate:
default_schema: kcg
server:
port: 8080

2
database/init.sql Normal file
파일 보기

@ -0,0 +1,2 @@
-- KCG 데이터베이스 초기화
CREATE SCHEMA IF NOT EXISTS kcg;

파일 보기

@ -0,0 +1,83 @@
-- 001: 초기 스키마 생성
-- events, news, osint, users, login_history
SET search_path TO kcg;
-- 이벤트 테이블 (GDELT 등 이벤트 데이터)
CREATE TABLE IF NOT EXISTS events (
id BIGSERIAL PRIMARY KEY,
event_id VARCHAR(64) UNIQUE,
title TEXT NOT NULL,
description TEXT,
source VARCHAR(128),
source_url TEXT,
category VARCHAR(64),
latitude DOUBLE PRECISION,
longitude DOUBLE PRECISION,
timestamp TIMESTAMP NOT NULL,
raw_data JSONB,
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_events_timestamp ON events (timestamp);
-- 뉴스 테이블
CREATE TABLE IF NOT EXISTS news (
id BIGSERIAL PRIMARY KEY,
title TEXT NOT NULL,
summary TEXT,
source VARCHAR(128),
source_url TEXT UNIQUE,
category VARCHAR(64),
language VARCHAR(8),
timestamp TIMESTAMP NOT NULL,
raw_data JSONB,
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_news_timestamp ON news (timestamp);
-- OSINT 테이블
CREATE TABLE IF NOT EXISTS osint (
id BIGSERIAL PRIMARY KEY,
title TEXT NOT NULL,
content TEXT,
source VARCHAR(128),
source_url TEXT UNIQUE,
category VARCHAR(64),
credibility SMALLINT,
latitude DOUBLE PRECISION,
longitude DOUBLE PRECISION,
timestamp TIMESTAMP NOT NULL,
raw_data JSONB,
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_osint_timestamp ON osint (timestamp);
CREATE INDEX IF NOT EXISTS idx_osint_category ON osint (category);
-- 사용자 테이블
CREATE TABLE IF NOT EXISTS users (
id BIGSERIAL PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
name VARCHAR(128) NOT NULL,
picture TEXT,
last_login_at TIMESTAMP,
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_users_email ON users (email);
-- 로그인 이력 테이블
CREATE TABLE IF NOT EXISTS login_history (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id),
login_at TIMESTAMP NOT NULL DEFAULT NOW(),
ip_address VARCHAR(45),
user_agent TEXT,
success BOOLEAN NOT NULL DEFAULT TRUE,
failure_reason VARCHAR(100)
);
CREATE INDEX IF NOT EXISTS idx_login_history_user_id ON login_history (user_id);
CREATE INDEX IF NOT EXISTS idx_login_history_login_at ON login_history (login_at);

17
prediction/README.md Normal file
파일 보기

@ -0,0 +1,17 @@
# KCG Prediction Service
FastAPI 기반 예측 서비스 (향후 구현 예정)
## 실행
```bash
cd prediction
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
uvicorn main:app --reload --port 8000
```
## 엔드포인트
- `GET /health` - 헬스 체크

8
prediction/main.py Normal file
파일 보기

@ -0,0 +1,8 @@
from fastapi import FastAPI
app = FastAPI(title="KCG Prediction Service", version="0.1.0")
@app.get("/health")
def health_check():
return {"status": "ok"}

파일 보기

@ -0,0 +1,2 @@
fastapi==0.115.0
uvicorn==0.30.6