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

파일 보기

@ -1,18 +1,36 @@
package gc.mda.kcg.config; 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.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.core.Ordered;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import java.util.List;
@Configuration @Configuration
public class WebConfig implements WebMvcConfigurer { public class WebConfig {
@Override @Value("${app.cors.allowed-origins:http://localhost:5173}")
public void addCorsMappings(CorsRegistry registry) { private List<String> allowedOrigins;
registry.addMapping("/api/**")
.allowedOrigins("http://localhost:5173") @Bean
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") public FilterRegistrationBean<CorsFilter> corsFilterRegistration() {
.allowedHeaders("*") CorsConfiguration config = new CorsConfiguration();
.allowCredentials(true); 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 Type=simple
User=root User=root
Group=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 \ ExecStart=/usr/lib/jvm/java-17-openjdk-17.0.18.0.8-1.el9.x86_64/bin/java \
-Xms2g -Xmx4g \ -Xms2g -Xmx4g \
-Dspring.profiles.active=prod \ -Dspring.profiles.active=prod \
-Dspring.config.additional-location=file:/deploy/kcg-backend/ \ -Dspring.config.additional-location=file:/devdata/services/kcg/backend/ \
-jar /deploy/kcg-backend/kcg.jar -jar /devdata/services/kcg/backend/kcg.jar
Restart=on-failure Restart=on-failure
RestartSec=10 RestartSec=10

파일 보기

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

파일 보기

@ -2,9 +2,9 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <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" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>iran-airstrike-replay</title> <title>kcg-dashboard-demo</title>
</head> </head>
<body> <body>
<div id="root"></div> <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; } 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 */ /* Cheonghae Unit pulsing beacon */
@keyframes cheonghae-pulse { @keyframes cheonghae-pulse {
0% { transform: scale(1); opacity: 0.8; } 0% { transform: scale(1); opacity: 0.8; }

파일 보기

@ -103,7 +103,7 @@ interface AuthenticatedAppProps {
onLogout: () => Promise<void>; onLogout: () => Promise<void>;
} }
function AuthenticatedApp(_props: AuthenticatedAppProps) { function AuthenticatedApp({ user, onLogout }: AuthenticatedAppProps) {
const [appMode, setAppMode] = useState<AppMode>('live'); const [appMode, setAppMode] = useState<AppMode>('live');
const [events, setEvents] = useState<GeoEvent[]>([]); const [events, setEvents] = useState<GeoEvent[]>([]);
const [sensorData, setSensorData] = useState<SensorLog[]>([]); const [sensorData, setSensorData] = useState<SensorLog[]>([]);
@ -965,6 +965,17 @@ function AuthenticatedApp(_props: AuthenticatedAppProps) {
<span className={`status-dot ${isLive ? 'live' : replay.state.isPlaying ? 'live' : ''}`} /> <span className={`status-dot ${isLive ? 'live' : replay.state.isPlaying ? 'live' : ''}`} />
{isLive ? t('header.live') : replay.state.isPlaying ? t('header.replaying') : t('header.paused')} {isLive ? t('header.live') : replay.state.isPlaying ? t('header.replaying') : t('header.paused')}
</div> </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> </div>
</header> </header>

파일 보기

@ -104,7 +104,7 @@ const LoginPage = ({ onGoogleLogin, onDevLogin }: LoginPageProps) => {
> >
{/* Title */} {/* Title */}
<div className="flex flex-col items-center gap-2"> <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 <h1
className="text-xl font-bold" className="text-xl font-bold"
style={{ color: 'var(--kcg-text)' }} 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 react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite' import tailwindcss from '@tailwindcss/vite'
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig(({ mode }): UserConfig => ({
plugins: [tailwindcss(), react()], plugins: [tailwindcss(), react()],
esbuild: mode === 'production' ? { drop: ['console', 'debugger'] } : {},
server: { server: {
proxy: { proxy: {
'/api/ais': { '/api/ais': {
@ -117,4 +118,4 @@ export default defineConfig({
}, },
}, },
}, },
}) }))