feat(frontend): UI 브랜딩 개선 + 배포 설정 정리

- 로그인 화면: kcg.svg 로고 적용 (이모지 교체)
- 헤더 우측: 사용자 프로필/이름 + 로그아웃 버튼 추가
- 브라우저 탭: favicon → kcg.svg, 제목 → kcg-dashboard-demo
- 프로덕션 빌드: console/debugger 자동 제거
- CORS: CorsFilter 최우선 순위 등록 (AuthFilter 이전)
- deploy.yml: secrets → .env 파일로 배포
- systemd/nginx: 경로 /devdata/services/kcg/ 반영
This commit is contained in:
htlee 2026-03-17 15:50:05 +09:00
부모 dd57ba59d2
커밋 7cde0c57d8
10개의 변경된 파일1088개의 추가작업 그리고 21개의 파일을 삭제

파일 보기

@ -43,6 +43,9 @@ jobs:
run: mvn -B clean package -DskipTests
- name: Deploy backend
env:
GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }}
JWT_SECRET: ${{ secrets.JWT_SECRET }}
run: |
DEPLOY_DIR=/deploy/kcg-backend
mkdir -p $DEPLOY_DIR/backup
@ -53,6 +56,12 @@ jobs:
ls -t $DEPLOY_DIR/backup/*.jar | tail -n +6 | xargs -r rm
fi
# Secrets → 환경변수 파일 (systemd EnvironmentFile)
cat > $DEPLOY_DIR/.env << ENVEOF
GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID}
JWT_SECRET=${JWT_SECRET}
ENVEOF
# JAR 교체 + 재시작 트리거
cp backend/target/kcg.jar $DEPLOY_DIR/kcg.jar
date '+%s' > $DEPLOY_DIR/.deploy-trigger

파일 보기

@ -1,18 +1,36 @@
package gc.mda.kcg.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.core.Ordered;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import java.util.List;
@Configuration
public class WebConfig implements WebMvcConfigurer {
public class WebConfig {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("http://localhost:5173")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true);
@Value("${app.cors.allowed-origins:http://localhost:5173}")
private List<String> allowedOrigins;
@Bean
public FilterRegistrationBean<CorsFilter> corsFilterRegistration() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(allowedOrigins);
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
config.setAllowedHeaders(List.of("*"));
config.setAllowCredentials(true);
config.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/api/**", config);
FilterRegistrationBean<CorsFilter> bean = new FilterRegistrationBean<>(new CorsFilter(source));
bean.setOrder(Ordered.HIGHEST_PRECEDENCE);
return bean;
}
}

파일 보기

@ -6,12 +6,13 @@ After=network.target
Type=simple
User=root
Group=root
WorkingDirectory=/deploy/kcg-backend
WorkingDirectory=/devdata/services/kcg/backend
EnvironmentFile=-/devdata/services/kcg/backend/.env
ExecStart=/usr/lib/jvm/java-17-openjdk-17.0.18.0.8-1.el9.x86_64/bin/java \
-Xms2g -Xmx4g \
-Dspring.profiles.active=prod \
-Dspring.config.additional-location=file:/deploy/kcg-backend/ \
-jar /deploy/kcg-backend/kcg.jar
-Dspring.config.additional-location=file:/devdata/services/kcg/backend/ \
-jar /devdata/services/kcg/backend/kcg.jar
Restart=on-failure
RestartSec=10

파일 보기

@ -8,7 +8,7 @@ server {
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
# ── Frontend SPA ──
root /deploy/kcg;
root /devdata/services/kcg/dist;
# Static cache
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ {
expires 1y;

파일 보기

@ -2,9 +2,9 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<link rel="icon" type="image/svg+xml" href="/kcg.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>iran-airstrike-replay</title>
<title>kcg-dashboard-demo</title>
</head>
<body>
<div id="root"></div>

1004
frontend/public/kcg.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  크기: 72 KiB

파일 보기

@ -146,6 +146,29 @@
50% { opacity: 0.4; }
}
.header-user {
display: flex;
align-items: center;
gap: 6px;
padding-left: 8px;
border-left: 1px solid var(--kcg-border);
}
.header-user-avatar {
width: 22px;
height: 22px;
border-radius: 50%;
}
.header-user-name {
font-size: 11px;
color: var(--kcg-text);
max-width: 80px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* Cheonghae Unit pulsing beacon */
@keyframes cheonghae-pulse {
0% { transform: scale(1); opacity: 0.8; }

파일 보기

@ -103,7 +103,7 @@ interface AuthenticatedAppProps {
onLogout: () => Promise<void>;
}
function AuthenticatedApp(_props: AuthenticatedAppProps) {
function AuthenticatedApp({ user, onLogout }: AuthenticatedAppProps) {
const [appMode, setAppMode] = useState<AppMode>('live');
const [events, setEvents] = useState<GeoEvent[]>([]);
const [sensorData, setSensorData] = useState<SensorLog[]>([]);
@ -965,6 +965,17 @@ function AuthenticatedApp(_props: AuthenticatedAppProps) {
<span className={`status-dot ${isLive ? 'live' : replay.state.isPlaying ? 'live' : ''}`} />
{isLive ? t('header.live') : replay.state.isPlaying ? t('header.replaying') : t('header.paused')}
</div>
{user && (
<div className="header-user">
{user.picture && (
<img src={user.picture} alt="" className="header-user-avatar" referrerPolicy="no-referrer" />
)}
<span className="header-user-name">{user.name}</span>
<button type="button" className="header-toggle-btn" onClick={onLogout} title="Logout">
</button>
</div>
)}
</div>
</header>

파일 보기

@ -104,7 +104,7 @@ const LoginPage = ({ onGoogleLogin, onDevLogin }: LoginPageProps) => {
>
{/* Title */}
<div className="flex flex-col items-center gap-2">
<div className="text-3xl">&#x1f6e1;&#xfe0f;</div>
<img src="/kcg.svg" alt="KCG" style={{ width: 120, height: 120 }} />
<h1
className="text-xl font-bold"
style={{ color: 'var(--kcg-text)' }}

파일 보기

@ -1,10 +1,11 @@
import { defineConfig } from 'vite'
import { defineConfig, type UserConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
// https://vite.dev/config/
export default defineConfig({
export default defineConfig(({ mode }): UserConfig => ({
plugins: [tailwindcss(), react()],
esbuild: mode === 'production' ? { drop: ['console', 'debugger'] } : {},
server: {
proxy: {
'/api/ais': {
@ -117,4 +118,4 @@ export default defineConfig({
},
},
},
})
}))