From 4dd1597111d65b7fdac10ce293798f16167a810b Mon Sep 17 00:00:00 2001 From: htlee Date: Wed, 18 Mar 2026 14:23:09 +0900 Subject: [PATCH] =?UTF-8?q?refactor:=20=EC=9D=B8=EB=9D=BC=EC=9D=B8=20CSS?= =?UTF-8?q?=20=EC=A0=95=EB=A6=AC=20=E2=80=94=20=EA=B3=B5=ED=86=B5=20?= =?UTF-8?q?=ED=81=B4=EB=9E=98=EC=8A=A4=20=EC=B6=94=EC=B6=9C=20+=20Tailwind?= =?UTF-8?q?=20=EC=A0=84=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CollectorMonitor: 29건 인라인 → CSS 클래스 (~3건 동적만 잔존) - 팝업 공통 CSS: .popup-header, .popup-body, .popup-grid, .popup-label 추출 - AirportLayer, DamagedShipLayer, InfraLayer, SubmarineCableLayer 적용 - LoginPage: var(--kcg-*) 인라인 → Tailwind 유틸리티 전환 (hover 포함) Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend/src/App.css | 186 ++++++++++++++++++ frontend/src/components/auth/LoginPage.tsx | 62 +----- .../components/common/CollectorMonitor.tsx | 115 ++++------- frontend/src/components/iran/AirportLayer.tsx | 21 +- frontend/src/components/korea/InfraLayer.tsx | 20 +- .../components/korea/SubmarineCableLayer.tsx | 29 +-- .../components/layers/DamagedShipLayer.tsx | 21 +- 7 files changed, 266 insertions(+), 188 deletions(-) diff --git a/frontend/src/App.css b/frontend/src/App.css index bc45c80..d6d0a08 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -1317,6 +1317,48 @@ text-decoration: none; } +/* ═══ 범용 팝업 스타일 ═══ */ +.popup-header { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 10px; + border-radius: 4px 4px 0 0; + margin: -10px -10px 8px -10px; + color: #fff; + font-weight: 700; + font-size: 13px; +} + +.popup-body { + font-family: 'Courier New', monospace; + font-size: 12px; +} + +.popup-body-sm { + font-family: 'Courier New', monospace; + font-size: 11px; + min-width: 220px; +} + +.popup-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 3px 12px; + font-size: 11px; +} + +.popup-label { + color: var(--kcg-muted, #888); +} + +.popup-desc { + margin-top: 6px; + font-size: 11px; + color: var(--kcg-text-secondary, #ccc); + line-height: 1.4; +} + .ship-popup-link:hover { text-decoration: underline; } @@ -2234,3 +2276,147 @@ color: var(--kcg-border-light); margin-right: 6px; } + +/* ===== CollectorMonitor ===== */ +.collector-modal { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + z-index: 10000; + background: var(--kcg-panel-bg, rgba(15, 23, 42, 0.95)); + border: 1px solid var(--kcg-border, rgba(100, 116, 139, 0.3)); + border-radius: 12px; + padding: 20px; + min-width: 600px; + max-width: 800px; + max-height: 80vh; + overflow: auto; + color: var(--kcg-text, #e2e8f0); + font-family: monospace; + font-size: 13px; + box-shadow: 0 25px 50px rgba(0, 0, 0, 0.5); +} + +.collector-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16px; +} + +.collector-title { + margin: 0; + font-size: 15px; + font-weight: 600; +} + +.collector-header-actions { + display: flex; + align-items: center; + gap: 12px; +} + +.collector-server-time { + font-size: 11px; + opacity: 0.6; +} + +.collector-refresh-btn { + background: none; + border: 1px solid var(--kcg-border, rgba(100, 116, 139, 0.3)); + color: var(--kcg-text, #e2e8f0); + border-radius: 4px; + padding: 2px 8px; + cursor: pointer; + font-size: 12px; +} + +.collector-close-btn { + background: none; + border: none; + color: var(--kcg-text, #e2e8f0); + cursor: pointer; + font-size: 18px; + line-height: 1; + padding: 0 4px; +} + +.collector-error { + padding: 8px 12px; + background: rgba(239, 68, 68, 0.2); + border-radius: 6px; + margin-bottom: 12px; +} + +.collector-table { + width: 100%; + border-collapse: collapse; +} + +.collector-table thead tr { + border-bottom: 1px solid var(--kcg-border, rgba(100, 116, 139, 0.3)); +} + +.collector-th { + text-align: left; + padding: 6px 8px; + font-size: 11px; + opacity: 0.7; +} + +.collector-th-right { + text-align: right; + padding: 6px 8px; + font-size: 11px; + opacity: 0.7; +} + +.collector-table tbody tr { + border-bottom: 1px solid var(--kcg-border, rgba(100, 116, 139, 0.15)); +} + +.collector-td { + padding: 8px; +} + +.collector-td-right { + padding: 8px; + text-align: right; +} + +.collector-td-name { + padding: 8px; + font-weight: 500; +} + +.collector-status-dot { + display: inline-block; + width: 8px; + height: 8px; + border-radius: 50%; +} + +.collector-success-count { + color: #22c55e; +} + +.collector-td-last-success { + padding: 8px; + opacity: 0.8; +} + +.collector-td-error { + padding: 8px; + max-width: 200px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-size: 11px; +} + +.collector-td-empty { + padding: 20px; + text-align: center; + opacity: 0.5; +} diff --git a/frontend/src/components/auth/LoginPage.tsx b/frontend/src/components/auth/LoginPage.tsx index 0703a47..26fc61c 100644 --- a/frontend/src/components/auth/LoginPage.tsx +++ b/frontend/src/components/auth/LoginPage.tsx @@ -91,28 +91,18 @@ const LoginPage = ({ onGoogleLogin, onDevLogin }: LoginPageProps) => { const googleBtnRef = useGoogleIdentity(handleGoogleCredential); return ( -
-
+
+
{/* Title */}
KCG { DEMO
-

+

{t('auth.title')}

-

+

{t('auth.subtitle')}

{/* Error */} {error && ( -
+
{error}
)} @@ -151,10 +129,7 @@ const LoginPage = ({ onGoogleLogin, onDevLogin }: LoginPageProps) => { {GOOGLE_CLIENT_ID && ( <>
-

+

{t('auth.domainNotice')}

@@ -163,32 +138,15 @@ const LoginPage = ({ onGoogleLogin, onDevLogin }: LoginPageProps) => { {/* Dev Login */} {IS_DEV && ( <> -
- +
+ {t('auth.devNotice')}
diff --git a/frontend/src/components/common/CollectorMonitor.tsx b/frontend/src/components/common/CollectorMonitor.tsx index e1d8ee7..308248d 100644 --- a/frontend/src/components/common/CollectorMonitor.tsx +++ b/frontend/src/components/common/CollectorMonitor.tsx @@ -50,61 +50,27 @@ const CollectorMonitor = ({ onClose }: CollectorMonitorProps) => { }, [refresh]); return ( -
+
{/* Header */} -
-

+
+

수집기 모니터링

-
+
{serverTime && ( - + 서버: {new Date(serverTime).toLocaleTimeString('ko-KR')} )} @@ -112,62 +78,57 @@ const CollectorMonitor = ({ onClose }: CollectorMonitorProps) => {
{error && ( -
+
{error}
)} {/* Table */} - +
- - - - - - - - + + + + + + + + {collectors.map((c) => ( - - + - - - + + - - - + + ))} {collectors.length === 0 && !error && ( - diff --git a/frontend/src/components/iran/AirportLayer.tsx b/frontend/src/components/iran/AirportLayer.tsx index bc0cc96..34a243b 100644 --- a/frontend/src/components/iran/AirportLayer.tsx +++ b/frontend/src/components/iran/AirportLayer.tsx @@ -91,16 +91,11 @@ function AirportMarker({ airport }: { airport: Airport }) { setShowPopup(false)} closeOnClick={false} anchor="bottom" offset={[0, -size / 2]} maxWidth="280px" className="gl-popup"> -
-
+
+
{isUS ? {'\u{1F1FA}\u{1F1F8}'} : flag ? {flag} : null} - {airport.name} + {airport.name}
{airport.nameKo && (
{airport.nameKo}
@@ -113,11 +108,11 @@ function AirportMarker({ airport }: { airport: Airport }) { {isUS ? 'US Military Base' : TYPE_LABELS[airport.type]}
-
- {airport.iata &&
IATA : {airport.iata}
} -
ICAO : {airport.icao}
- {airport.city &&
City : {airport.city}
} -
Country : {airport.country}
+
+ {airport.iata &&
IATA : {airport.iata}
} +
ICAO : {airport.icao}
+ {airport.city &&
City : {airport.city}
} +
Country : {airport.country}
{airport.lat.toFixed(4)}°{airport.lat >= 0 ? 'N' : 'S'}, {airport.lng.toFixed(4)}°{airport.lng >= 0 ? 'E' : 'W'} diff --git a/frontend/src/components/korea/InfraLayer.tsx b/frontend/src/components/korea/InfraLayer.tsx index f4795b9..34881b3 100644 --- a/frontend/src/components/korea/InfraLayer.tsx +++ b/frontend/src/components/korea/InfraLayer.tsx @@ -121,14 +121,8 @@ export function InfraLayer({ facilities }: Props) { setSelectedId(null)} closeOnClick={false} anchor="bottom" maxWidth="280px" className="gl-popup"> -
-
+
+
{getStyle(selected).icon} {selected.name}
@@ -146,18 +140,18 @@ export function InfraLayer({ facilities }: Props) { {selected.type === 'plant' ? '발전소' : '변전소'}
-
+
{selected.output && ( -
출력: {selected.output}
+
출력: {selected.output}
)} {selected.voltage && ( -
전압: {formatVoltage(selected.voltage)}
+
전압: {formatVoltage(selected.voltage)}
)} {selected.operator && ( -
운영: {selected.operator}
+
운영: {selected.operator}
)} {selected.source && ( -
연료: {selected.source}
+
연료: {selected.source}
)}
{selected.lat.toFixed(4)}°N, {selected.lng.toFixed(4)}°E diff --git a/frontend/src/components/korea/SubmarineCableLayer.tsx b/frontend/src/components/korea/SubmarineCableLayer.tsx index 5be99a8..0c9fed2 100644 --- a/frontend/src/components/korea/SubmarineCableLayer.tsx +++ b/frontend/src/components/korea/SubmarineCableLayer.tsx @@ -86,14 +86,8 @@ export function SubmarineCableLayer() { setSelectedPoint(null)} closeOnClick={false} anchor="bottom" maxWidth="260px" className="gl-popup"> -
-
+
+
📡 {selectedPoint.name} 해저케이블 기지
@@ -122,28 +116,23 @@ export function SubmarineCableLayer() { latitude={selectedCable.route[0][1]} onClose={() => setSelectedCable(null)} closeOnClick={false} anchor="bottom" maxWidth="280px" className="gl-popup"> -
-
+
+
🔌 {selectedCable.name}
-
+
- 경유지: + 경유지: {selectedCable.landingPoints.join(' → ')}
{selectedCable.rfsYear && ( -
개통: {selectedCable.rfsYear}년
+
개통: {selectedCable.rfsYear}년
)} {selectedCable.length && ( -
총 길이: {selectedCable.length}
+
총 길이: {selectedCable.length}
)} {selectedCable.owners && ( -
운영: {selectedCable.owners}
+
운영: {selectedCable.owners}
)}
diff --git a/frontend/src/components/layers/DamagedShipLayer.tsx b/frontend/src/components/layers/DamagedShipLayer.tsx index 5dfefa9..cccb28a 100644 --- a/frontend/src/components/layers/DamagedShipLayer.tsx +++ b/frontend/src/components/layers/DamagedShipLayer.tsx @@ -120,13 +120,8 @@ export function DamagedShipLayer({ currentTime }: Props) { setSelectedId(null)} closeOnClick={false} anchor="bottom" maxWidth="320px" className="gl-popup"> -
-
+
+
{FLAG_EMOJI[selected.flag] && {FLAG_EMOJI[selected.flag]}} {selected.name} {DAMAGE_LABELS[selected.damage]}
-
-
선종 : {selected.type}
-
국적 : {selected.flag}
-
원인 : {selected.cause}
-
피격 : {formatKST(selected.damagedAt)}
+
+
선종 : {selected.type}
+
국적 : {selected.flag}
+
원인 : {selected.cause}
+
피격 : {formatKST(selected.damagedAt)}
-
+
{selected.description}
상태수집기최근 건수성공/실패총 수집마지막 성공에러
상태수집기최근 건수성공/실패총 수집마지막 성공에러
- +
+ {c.name}{c.lastCount} - {c.totalSuccess} + {c.name}{c.lastCount} + {c.totalSuccess} {' / '} 0 ? '#ef4444' : 'inherit' }}>{c.totalFailure} {c.totalItems.toLocaleString()}{formatRelativeTime(c.lastSuccess)} + {c.totalItems.toLocaleString()}{formatRelativeTime(c.lastSuccess)} {c.lastError || '-'}
+ 수집기 데이터 로딩 중...