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:
부모
dd57ba59d2
커밋
7cde0c57d8
@ -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
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">🛡️</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({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
}))
|
||||||
|
|||||||
불러오는 중...
Reference in New Issue
Block a user