diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 2c88106..c29507d 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -11,6 +11,9 @@ "@deck.gl/core": "^9.2.11", "@deck.gl/layers": "^9.2.11", "@deck.gl/mapbox": "^9.2.11", + "@fontsource-variable/fira-code": "^5.2.7", + "@fontsource-variable/inter": "^5.2.8", + "@fontsource-variable/noto-sans-kr": "^5.2.10", "@tailwindcss/vite": "^4.2.1", "@turf/boolean-point-in-polygon": "^7.3.4", "@turf/helpers": "^7.3.4", @@ -915,6 +918,33 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@fontsource-variable/fira-code": { + "version": "5.2.7", + "resolved": "https://registry.npmjs.org/@fontsource-variable/fira-code/-/fira-code-5.2.7.tgz", + "integrity": "sha512-J2bxN7fz5rd8WpQYyau4o19WqTzxoTqaNj9jhsv4p21GSu1Rf34tbqsxqjyDCR+wDMHM3SajyFqtq+5uvRUQ7w==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@fontsource-variable/inter": { + "version": "5.2.8", + "resolved": "https://registry.npmjs.org/@fontsource-variable/inter/-/inter-5.2.8.tgz", + "integrity": "sha512-kOfP2D+ykbcX/P3IFnokOhVRNoTozo5/JxhAIVYLpea/UBmCQ/YWPBfWIDuBImXX/15KH+eKh4xpEUyS2sQQGQ==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@fontsource-variable/noto-sans-kr": { + "version": "5.2.10", + "resolved": "https://registry.npmjs.org/@fontsource-variable/noto-sans-kr/-/noto-sans-kr-5.2.10.tgz", + "integrity": "sha512-UZOO7HF44Rt5+7SCeFMHYVgbKu36Jet6IxrAd7jjEkQMVDmeefwd0H8V5pSZydvBOOxClk3V5cQsujJqGHhQMw==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "dev": true, diff --git a/frontend/package.json b/frontend/package.json index 10d7a99..40a475d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -13,6 +13,9 @@ "@deck.gl/core": "^9.2.11", "@deck.gl/layers": "^9.2.11", "@deck.gl/mapbox": "^9.2.11", + "@fontsource-variable/fira-code": "^5.2.7", + "@fontsource-variable/inter": "^5.2.8", + "@fontsource-variable/noto-sans-kr": "^5.2.10", "@tailwindcss/vite": "^4.2.1", "@turf/boolean-point-in-polygon": "^7.3.4", "@turf/helpers": "^7.3.4", diff --git a/frontend/src/App.css b/frontend/src/App.css index 259b72c..6cce3fa 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -20,7 +20,7 @@ font-size: 14px; font-weight: 700; letter-spacing: 1.5px; - font-family: 'Courier New', monospace; + font-family: 'Fira Code Variable', 'Noto Sans KR Variable', monospace; } /* Map mode toggle */ @@ -47,7 +47,7 @@ color: var(--kcg-dim); cursor: pointer; transition: all 0.15s; - font-family: 'Courier New', monospace; + font-family: 'Fira Code Variable', 'Noto Sans KR Variable', monospace; } .map-mode-btn:hover { @@ -79,7 +79,7 @@ .count-item { font-size: 11px; font-weight: 700; - font-family: 'Courier New', monospace; + font-family: 'Fira Code Variable', 'Noto Sans KR Variable', monospace; letter-spacing: 0.5px; padding: 2px 8px; border-radius: 3px; @@ -103,7 +103,7 @@ color: var(--kcg-text-secondary); font-size: 10px; font-weight: 700; - font-family: 'Courier New', monospace; + font-family: 'Fira Code Variable', 'Noto Sans KR Variable', monospace; letter-spacing: 0.5px; padding: 2px 8px; border-radius: 4px; @@ -126,7 +126,7 @@ font-weight: 700; letter-spacing: 1px; color: var(--text-secondary); - font-family: 'Courier New', monospace; + font-family: 'Fira Code Variable', 'Noto Sans KR Variable', monospace; } .status-dot { @@ -240,7 +240,7 @@ letter-spacing: 1.5px; color: var(--text-secondary); margin-bottom: 8px; - font-family: 'Courier New', monospace; + font-family: 'Fira Code Variable', 'Noto Sans KR Variable', monospace; } .layer-items { @@ -299,7 +299,7 @@ align-items: center; padding: 2px 8px; font-size: 10px; - font-family: 'Courier New', monospace; + font-family: 'Fira Code Variable', 'Noto Sans KR Variable', monospace; } .stat-cat { @@ -318,7 +318,7 @@ letter-spacing: 1px; color: var(--text-secondary); padding: 2px 8px; - font-family: 'Courier New', monospace; + font-family: 'Fira Code Variable', 'Noto Sans KR Variable', monospace; } /* Layer tree */ @@ -434,7 +434,7 @@ border-radius: 4px; cursor: pointer; transition: all 0.2s; - font-family: 'Courier New', monospace; + font-family: 'Fira Code Variable', 'Noto Sans KR Variable', monospace; white-space: nowrap; } .dash-tab:hover { @@ -465,7 +465,7 @@ text-transform: uppercase; color: var(--text-secondary); border-bottom: 1px solid var(--kcg-border); - font-family: 'Courier New', monospace; + font-family: 'Fira Code Variable', 'Noto Sans KR Variable', monospace; } .event-list { @@ -504,7 +504,7 @@ white-space: nowrap; height: fit-content; margin-top: 2px; - font-family: 'Courier New', monospace; + font-family: 'Fira Code Variable', 'Noto Sans KR Variable', monospace; } .event-content { @@ -521,7 +521,7 @@ font-size: 10px; color: var(--text-secondary); margin-top: 1px; - font-family: 'Courier New', monospace; + font-family: 'Fira Code Variable', 'Noto Sans KR Variable', monospace; } .event-desc { @@ -559,7 +559,7 @@ background: var(--kcg-danger); padding: 2px 6px; border-radius: 2px; - font-family: 'Courier New', monospace; + font-family: 'Fira Code Variable', 'Noto Sans KR Variable', monospace; animation: flash-pulse 1.5s ease-in-out infinite; } @@ -573,7 +573,7 @@ font-weight: 700; color: var(--text-secondary); letter-spacing: 0.5px; - font-family: 'Courier New', monospace; + font-family: 'Fira Code Variable', 'Noto Sans KR Variable', monospace; } .breaking-news-list { @@ -616,7 +616,7 @@ .breaking-news-time { font-size: 9px; color: var(--kcg-dim); - font-family: 'Courier New', monospace; + font-family: 'Fira Code Variable', 'Noto Sans KR Variable', monospace; } .breaking-news-headline { @@ -658,13 +658,13 @@ font-weight: 700; letter-spacing: 1.5px; color: var(--kcg-danger); - font-family: 'Courier New', monospace; + font-family: 'Fira Code Variable', 'Noto Sans KR Variable', monospace; } .osint-count { margin-left: auto; font-size: 10px; color: var(--kcg-muted); - font-family: 'Courier New', monospace; + font-family: 'Fira Code Variable', 'Noto Sans KR Variable', monospace; } .osint-loading { margin-left: auto; @@ -722,7 +722,7 @@ font-size: 9px; color: var(--kcg-dim); white-space: nowrap; - font-family: 'Courier New', monospace; + font-family: 'Fira Code Variable', 'Noto Sans KR Variable', monospace; } .osint-item-title { font-size: 11px; @@ -757,10 +757,17 @@ .area-ship-header { display: flex; align-items: center; - gap: 8px; + gap: 6px; margin-bottom: 6px; padding-bottom: 6px; border-bottom: 1px solid var(--kcg-hover); + flex-wrap: nowrap; +} + +.area-ship-header:last-child { + margin-bottom: 0; + padding-bottom: 0; + border-bottom: none; } .korean-highlight-toggle { @@ -768,7 +775,7 @@ padding: 1px 8px; font-size: 9px; font-weight: 700; - font-family: 'Courier New', monospace; + font-family: 'Fira Code Variable', 'Noto Sans KR Variable', monospace; border-radius: 3px; border: 1px solid var(--kcg-border); background: transparent; @@ -793,7 +800,11 @@ font-weight: 700; letter-spacing: 0.5px; color: var(--text-secondary); - font-family: 'Courier New', monospace; + font-family: 'Fira Code Variable', 'Noto Sans KR Variable', monospace; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + min-width: 0; } .area-ship-total { @@ -801,7 +812,9 @@ font-size: 16px; font-weight: 700; color: #fb923c; - font-family: 'Courier New', monospace; + font-family: 'Fira Code Variable', 'Noto Sans KR Variable', monospace; + white-space: nowrap; + flex-shrink: 0; } .kr-ship-header { @@ -820,7 +833,7 @@ font-weight: 700; letter-spacing: 0.5px; color: var(--text-primary); - font-family: 'Courier New', monospace; + font-family: 'Fira Code Variable', 'Noto Sans KR Variable', monospace; } .kr-total { @@ -828,7 +841,7 @@ font-size: 14px; font-weight: 700; color: var(--kcg-accent); - font-family: 'Courier New', monospace; + font-family: 'Fira Code Variable', 'Noto Sans KR Variable', monospace; } .kr-ship-breakdown { @@ -859,7 +872,7 @@ flex: 1; font-size: 10px; color: var(--text-secondary); - font-family: 'Courier New', monospace; + font-family: 'Fira Code Variable', 'Noto Sans KR Variable', monospace; } .kr-count { @@ -867,7 +880,7 @@ font-weight: 700; color: var(--text-primary); text-align: right; - font-family: 'Courier New', monospace; + font-family: 'Fira Code Variable', 'Noto Sans KR Variable', monospace; } .kr-ship-list { @@ -881,7 +894,7 @@ gap: 6px; padding: 2px 0; font-size: 9px; - font-family: 'Courier New', monospace; + font-family: 'Fira Code Variable', 'Noto Sans KR Variable', monospace; } .kr-ship-name { @@ -1026,7 +1039,7 @@ .korea-stat-num { font-size: 20px; font-weight: 800; - font-family: 'Courier New', monospace; + font-family: 'Fira Code Variable', 'Noto Sans KR Variable', monospace; } .korea-stat-card.total .korea-stat-num { color: var(--kcg-accent); } .korea-stat-card.anchored .korea-stat-num { color: var(--kcg-danger); } @@ -1049,7 +1062,7 @@ letter-spacing: 1px; text-transform: uppercase; margin-bottom: 4px; - font-family: 'Courier New', monospace; + font-family: 'Fira Code Variable', 'Noto Sans KR Variable', monospace; } .korea-ship-section { flex: 1; @@ -1067,7 +1080,7 @@ color: var(--kcg-muted); border-bottom: 1px solid var(--kcg-border); flex-shrink: 0; - font-family: 'Courier New', monospace; + font-family: 'Fira Code Variable', 'Noto Sans KR Variable', monospace; } .korea-ship-section-count { color: var(--kcg-accent); @@ -1131,7 +1144,7 @@ .korea-ship-card-speed { margin-left: auto; color: var(--kcg-muted); - font-family: 'Courier New', monospace; + font-family: 'Fira Code Variable', 'Noto Sans KR Variable', monospace; } .korea-ship-card-dest { font-size: 9px; @@ -1161,7 +1174,7 @@ letter-spacing: 1.5px; text-transform: uppercase; color: var(--text-secondary); - font-family: 'Courier New', monospace; + font-family: 'Fira Code Variable', 'Noto Sans KR Variable', monospace; } .chart-grid { @@ -1177,7 +1190,7 @@ color: var(--text-secondary); margin-bottom: 0; padding-left: 4px; - font-family: 'Courier New', monospace; + font-family: 'Fira Code Variable', 'Noto Sans KR Variable', monospace; } .chart-demo-label { @@ -1193,7 +1206,7 @@ .ship-popup-body { width: 300px; - font-family: 'Courier New', monospace; + font-family: 'Fira Code Variable', 'Noto Sans KR Variable', monospace; font-size: 11px; line-height: 1.4; } @@ -1347,12 +1360,12 @@ } .popup-body { - font-family: 'Courier New', monospace; + font-family: 'Fira Code Variable', 'Noto Sans KR Variable', monospace; font-size: 12px; } .popup-body-sm { - font-family: 'Courier New', monospace; + font-family: 'Fira Code Variable', 'Noto Sans KR Variable', monospace; font-size: 11px; min-width: 220px; } @@ -1446,7 +1459,7 @@ color: var(--text-secondary); cursor: pointer; transition: all 0.15s; - font-family: 'Courier New', monospace; + font-family: 'Fira Code Variable', 'Noto Sans KR Variable', monospace; } .speed-btn:hover { @@ -1485,7 +1498,7 @@ color: var(--text-secondary); cursor: pointer; transition: all 0.15s; - font-family: 'Courier New', monospace; + font-family: 'Fira Code Variable', 'Noto Sans KR Variable', monospace; } .range-btn:hover { @@ -1537,7 +1550,7 @@ font-weight: 700; letter-spacing: 1px; color: var(--text-secondary); - font-family: 'Courier New', monospace; + font-family: 'Fira Code Variable', 'Noto Sans KR Variable', monospace; } .range-picker input[type="datetime-local"] { @@ -1547,7 +1560,7 @@ color: var(--text-primary); padding: 4px 8px; font-size: 11px; - font-family: 'Courier New', monospace; + font-family: 'Fira Code Variable', 'Noto Sans KR Variable', monospace; outline: none; transition: border-color 0.15s; } @@ -1571,7 +1584,7 @@ background: rgba(34, 197, 94, 0.15); color: var(--kcg-success); cursor: pointer; - font-family: 'Courier New', monospace; + font-family: 'Fira Code Variable', 'Noto Sans KR Variable', monospace; transition: all 0.15s; white-space: nowrap; } @@ -1591,7 +1604,7 @@ font-size: 10px; color: var(--text-secondary); margin-bottom: 2px; - font-family: 'Courier New', monospace; + font-family: 'Fira Code Variable', 'Noto Sans KR Variable', monospace; } .timeline-current { @@ -1727,7 +1740,7 @@ font-size: 9px; font-weight: 700; color: var(--kcg-dim); - font-family: 'Courier New', monospace; + font-family: 'Fira Code Variable', 'Noto Sans KR Variable', monospace; flex-shrink: 0; } @@ -1774,7 +1787,7 @@ color: var(--text-secondary); margin-bottom: 2px; padding-left: 8px; - font-family: 'Courier New', monospace; + font-family: 'Fira Code Variable', 'Noto Sans KR Variable', monospace; } /* Aircraft, Satellite, Ship & Oil Tooltips - override Leaflet */ @@ -1788,7 +1801,7 @@ border-radius: 3px !important; padding: 2px 6px !important; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.5) !important; - font-family: 'Courier New', monospace !important; + font-family: 'Fira Code Variable', 'Noto Sans KR Variable', monospace !important; } .aircraft-tooltip::before, @@ -1862,7 +1875,7 @@ border-radius: 3px !important; padding: 2px 6px !important; box-shadow: 0 2px 12px rgba(255, 0, 0, 0.4) !important; - font-family: 'Courier New', monospace !important; + font-family: 'Fira Code Variable', 'Noto Sans KR Variable', monospace !important; } .impact-tooltip::before { @@ -1924,7 +1937,7 @@ pointer-events: none; font-weight: 600; font-size: 11px; - font-family: 'Courier New', monospace; + font-family: 'Fira Code Variable', 'Noto Sans KR Variable', monospace; text-shadow: var(--kcg-map-label-shadow); background: var(--kcg-glass); border: 1px solid var(--kcg-border); @@ -1942,7 +1955,7 @@ color: var(--kcg-event-impact); font-weight: 700; font-size: 10px; - font-family: 'Courier New', monospace; + font-family: 'Fira Code Variable', 'Noto Sans KR Variable', monospace; text-shadow: var(--kcg-map-impact-shadow); background: rgba(40, 0, 0, 0.85); border: 1px solid var(--kcg-event-impact); @@ -2038,7 +2051,7 @@ color: var(--kcg-dim); cursor: pointer; transition: all 0.15s; - font-family: 'Courier New', monospace; + font-family: 'Fira Code Variable', 'Noto Sans KR Variable', monospace; } .mode-btn:hover { @@ -2100,14 +2113,14 @@ font-weight: 700; letter-spacing: 2px; color: var(--kcg-danger); - font-family: 'Courier New', monospace; + font-family: 'Fira Code Variable', 'Noto Sans KR Variable', monospace; } .live-clock { font-size: 14px; font-weight: 700; color: var(--text-primary); - font-family: 'Courier New', monospace; + font-family: 'Fira Code Variable', 'Noto Sans KR Variable', monospace; letter-spacing: 1px; padding: 4px 12px; background: var(--kcg-hover); @@ -2126,7 +2139,7 @@ font-weight: 700; letter-spacing: 1.5px; color: var(--text-secondary); - font-family: 'Courier New', monospace; + font-family: 'Fira Code Variable', 'Noto Sans KR Variable', monospace; } .history-presets { @@ -2144,7 +2157,7 @@ color: var(--text-secondary); cursor: pointer; transition: all 0.15s; - font-family: 'Courier New', monospace; + font-family: 'Fira Code Variable', 'Noto Sans KR Variable', monospace; } .history-btn:hover { @@ -2169,7 +2182,7 @@ padding: 1px 6px; font-size: 10px; font-weight: 700; - font-family: 'Courier New', monospace; + font-family: 'Fira Code Variable', 'Noto Sans KR Variable', monospace; border: none; background: transparent; color: var(--text-secondary); @@ -2335,7 +2348,7 @@ max-height: 80vh; overflow: auto; color: var(--kcg-text, #e2e8f0); - font-family: monospace; + font-family: 'Fira Code Variable', 'Noto Sans KR Variable', monospace; font-size: 13px; box-shadow: 0 25px 50px rgba(0, 0, 0, 0.5); } diff --git a/frontend/src/components/iran/AirportLayer.ts b/frontend/src/components/iran/AirportLayer.ts index fdf9fb4..0a091e7 100644 --- a/frontend/src/components/iran/AirportLayer.ts +++ b/frontend/src/components/iran/AirportLayer.ts @@ -1,6 +1,7 @@ import { IconLayer, TextLayer } from '@deck.gl/layers'; import type { PickingInfo, Layer } from '@deck.gl/core'; import { svgToDataUri } from '../../utils/svgToDataUri'; +import { FONT_MONO } from '../../styles/fonts'; import { middleEastAirports } from '../../data/airports'; import type { Airport } from '../../data/airports'; @@ -42,9 +43,9 @@ export const IRAN_AIRPORT_COUNT = DEDUPLICATED_AIRPORTS.length; // ─── Colors ────────────────────────────────────────────────────────────────── function getAirportColor(airport: Airport): string { - if (isUSBase(airport)) return '#3b82f6'; - if (airport.type === 'military') return '#ef4444'; - return '#f59e0b'; + if (isUSBase(airport)) return '#60a5fa'; + if (airport.type === 'military') return '#f87171'; + return '#38bdf8'; } // ─── SVG generators ────────────────────────────────────────────────────────── @@ -140,7 +141,7 @@ export function createIranAirportLayers(config: AirportLayerConfig): Layer[] { getTextAnchor: 'middle', getAlignmentBaseline: 'top', getPixelOffset: [0, 10], - fontFamily: 'monospace', + fontFamily: FONT_MONO, fontWeight: 700, outlineWidth: 2, outlineColor: [0, 0, 0, 200], diff --git a/frontend/src/components/iran/IranDashboard.tsx b/frontend/src/components/iran/IranDashboard.tsx index 421bfe3..85dc2db 100644 --- a/frontend/src/components/iran/IranDashboard.tsx +++ b/frontend/src/components/iran/IranDashboard.tsx @@ -141,23 +141,23 @@ const IranDashboard = ({ }, { key: 'events', label: t('layers.events'), color: '#a855f7' }, { key: 'koreanShips', label: `\u{1F1F0}\u{1F1F7} ${t('layers.koreanShips')}`, color: '#00e5ff', count: iranData.koreanShips.length }, - { key: 'airports', label: t('layers.airports'), color: '#f59e0b', count: IRAN_AIRPORT_COUNT }, - { key: 'oilFacilities', label: t('layers.oilFacilities'), color: '#d97706', count: IRAN_OIL_COUNT }, - { key: 'meFacilities', label: '주요시설/군사', color: '#ef4444', count: ME_FACILITY_COUNT }, + { key: 'airports', label: t('layers.airports'), color: '#38bdf8', count: IRAN_AIRPORT_COUNT }, + { key: 'oilFacilities', label: t('layers.oilFacilities'), color: '#fb7185', count: IRAN_OIL_COUNT }, + { key: 'meFacilities', label: '주요시설/군사', color: '#f87171', count: ME_FACILITY_COUNT }, { key: 'sensorCharts', label: t('layers.sensorCharts'), color: '#22c55e' }, { - key: 'overseas', label: '해외시설', color: '#f97316', + key: 'overseas', label: '해외시설', color: '#c084fc', children: [ - { key: 'overseasUS', label: '🇺🇸 미국', color: '#3b82f6', count: meCountByCountry('us') }, - { key: 'overseasIsrael', label: '🇮🇱 이스라엘', color: '#dc2626', count: meCountByCountry('il') }, - { key: 'overseasIran', label: '🇮🇷 이란', color: '#22c55e', count: meCountByCountry('ir') }, - { key: 'overseasUAE', label: '🇦🇪 UAE', color: '#f59e0b', count: meCountByCountry('ae') }, - { key: 'overseasSaudi', label: '🇸🇦 사우디아라비아', color: '#84cc16', count: meCountByCountry('sa') }, - { key: 'overseasOman', label: '🇴🇲 오만', color: '#e11d48', count: meCountByCountry('om') }, - { key: 'overseasQatar', label: '🇶🇦 카타르', color: '#8b5cf6', count: meCountByCountry('qa') }, - { key: 'overseasKuwait', label: '🇰🇼 쿠웨이트', color: '#f97316', count: meCountByCountry('kw') }, - { key: 'overseasIraq', label: '🇮🇶 이라크', color: '#65a30d', count: meCountByCountry('iq') }, - { key: 'overseasBahrain', label: '🇧🇭 바레인', color: '#e11d48', count: meCountByCountry('bh') }, + { key: 'overseasUS', label: '🇺🇸 미국', color: '#60a5fa', count: meCountByCountry('us') }, + { key: 'overseasIsrael', label: '🇮🇱 이스라엘', color: '#ef4444', count: meCountByCountry('il') }, + { key: 'overseasIran', label: '🇮🇷 이란', color: '#34d399', count: meCountByCountry('ir') }, + { key: 'overseasUAE', label: '🇦🇪 UAE', color: '#38bdf8', count: meCountByCountry('ae') }, + { key: 'overseasSaudi', label: '🇸🇦 사우디아라비아', color: '#a3e635', count: meCountByCountry('sa') }, + { key: 'overseasOman', label: '🇴🇲 오만', color: '#fb7185', count: meCountByCountry('om') }, + { key: 'overseasQatar', label: '🇶🇦 카타르', color: '#a78bfa', count: meCountByCountry('qa') }, + { key: 'overseasKuwait', label: '🇰🇼 쿠웨이트', color: '#f472b6', count: meCountByCountry('kw') }, + { key: 'overseasIraq', label: '🇮🇶 이라크', color: '#86efac', count: meCountByCountry('iq') }, + { key: 'overseasBahrain', label: '🇧🇭 바레인', color: '#f87171', count: meCountByCountry('bh') }, ], }, ], [iranData, t, meCountByCountry]); diff --git a/frontend/src/components/iran/MEEnergyHazardLayer.tsx b/frontend/src/components/iran/MEEnergyHazardLayer.tsx index 97294b0..3113bee 100644 --- a/frontend/src/components/iran/MEEnergyHazardLayer.tsx +++ b/frontend/src/components/iran/MEEnergyHazardLayer.tsx @@ -1,6 +1,7 @@ import { IconLayer, TextLayer } from '@deck.gl/layers'; import type { PickingInfo, Layer } from '@deck.gl/core'; import { svgToDataUri } from '../../utils/svgToDataUri'; +import { FONT_MONO } from '../../styles/fonts'; import { ME_ENERGY_HAZARD_FACILITIES, SUB_TYPE_META, @@ -204,12 +205,12 @@ export function createMEEnergyHazardLayers(config: MELayerConfig): Layer[] { getText: (d) => d.nameKo.length > 12 ? d.nameKo.slice(0, 12) + '..' : d.nameKo, getSize: 12 * sc * fs, updateTriggers: { getSize: [sc] }, - getColor: (d) => hexToRgba(SUB_TYPE_META[d.subType].color, 200), + getColor: (d) => hexToRgba(SUB_TYPE_META[d.subType].color), getTextAnchor: 'middle', getAlignmentBaseline: 'top', getPixelOffset: [0, 12], - fontFamily: 'monospace', - fontWeight: 600, + fontFamily: FONT_MONO, + fontWeight: 700, fontSettings: { sdf: true }, outlineWidth: 3, outlineColor: [0, 0, 0, 255], diff --git a/frontend/src/components/iran/MEFacilityLayer.ts b/frontend/src/components/iran/MEFacilityLayer.ts index 626e529..ab2d876 100644 --- a/frontend/src/components/iran/MEFacilityLayer.ts +++ b/frontend/src/components/iran/MEFacilityLayer.ts @@ -1,6 +1,7 @@ import { IconLayer, TextLayer } from '@deck.gl/layers'; import type { PickingInfo, Layer } from '@deck.gl/core'; import { svgToDataUri } from '../../utils/svgToDataUri'; +import { FONT_MONO } from '../../styles/fonts'; import { ME_FACILITIES } from '../../data/middleEastFacilities'; import type { MEFacility } from '../../data/middleEastFacilities'; @@ -9,12 +10,12 @@ export const ME_FACILITY_COUNT = ME_FACILITIES.length; // ─── Type colors ────────────────────────────────────────────────────────────── const TYPE_COLORS: Record = { - naval: '#3b82f6', - military_hq: '#ef4444', - missile: '#dc2626', - intelligence: '#8b5cf6', - government: '#f59e0b', - radar: '#06b6d4', + naval: '#60a5fa', + military_hq: '#f87171', + missile: '#ef4444', + intelligence: '#a78bfa', + government: '#c084fc', + radar: '#22d3ee', }; // ─── SVG generators ────────────────────────────────────────────────────────── @@ -168,7 +169,7 @@ export function createMEFacilityLayers(config: MEFacilityLayerConfig): Layer[] { getTextAnchor: 'middle', getAlignmentBaseline: 'top', getPixelOffset: [0, 10], - fontFamily: 'monospace', + fontFamily: FONT_MONO, fontWeight: 700, outlineWidth: 2, outlineColor: [0, 0, 0, 200], diff --git a/frontend/src/components/iran/OilFacilityLayer.ts b/frontend/src/components/iran/OilFacilityLayer.ts index 1ab4d1f..bac3f88 100644 --- a/frontend/src/components/iran/OilFacilityLayer.ts +++ b/frontend/src/components/iran/OilFacilityLayer.ts @@ -1,6 +1,7 @@ import { IconLayer, TextLayer } from '@deck.gl/layers'; import type { PickingInfo, Layer } from '@deck.gl/core'; import { svgToDataUri } from '../../utils/svgToDataUri'; +import { FONT_MONO } from '../../styles/fonts'; import { iranOilFacilities } from '../../data/oilFacilities'; import type { OilFacility, OilFacilityType } from '../../types'; @@ -9,12 +10,12 @@ export const IRAN_OIL_COUNT = iranOilFacilities.length; // ─── Type colors ────────────────────────────────────────────────────────────── const TYPE_COLORS: Record = { - refinery: '#f59e0b', - oilfield: '#10b981', - gasfield: '#6366f1', - terminal: '#ec4899', - petrochemical: '#8b5cf6', - desalination: '#06b6d4', + refinery: '#fb7185', + oilfield: '#34d399', + gasfield: '#818cf8', + terminal: '#c084fc', + petrochemical: '#f472b6', + desalination: '#22d3ee', }; // ─── SVG generators ────────────────────────────────────────────────────────── @@ -241,7 +242,7 @@ export function createIranOilLayers(config: OilLayerConfig): Layer[] { getTextAnchor: 'middle', getAlignmentBaseline: 'top', getPixelOffset: [0, 10], - fontFamily: 'monospace', + fontFamily: FONT_MONO, fontWeight: 700, outlineWidth: 2, outlineColor: [0, 0, 0, 200], diff --git a/frontend/src/components/iran/ReplayMap.tsx b/frontend/src/components/iran/ReplayMap.tsx index 76e350d..abe311d 100644 --- a/frontend/src/components/iran/ReplayMap.tsx +++ b/frontend/src/components/iran/ReplayMap.tsx @@ -529,8 +529,8 @@ export function ReplayMap({ events, currentTime, aircraft, satellites, ships, la const { kind, data } = iranPickedFacility; if (kind === 'oil') { const OIL_TYPE_COLORS: Record = { - refinery: '#f59e0b', oilfield: '#10b981', gasfield: '#6366f1', - terminal: '#ec4899', petrochemical: '#8b5cf6', desalination: '#06b6d4', + refinery: '#fb7185', oilfield: '#34d399', gasfield: '#818cf8', + terminal: '#c084fc', petrochemical: '#f472b6', desalination: '#22d3ee', }; const color = OIL_TYPE_COLORS[data.type] ?? '#888'; return ( @@ -568,13 +568,13 @@ export function ReplayMap({ events, currentTime, aircraft, satellites, ships, la } if (kind === 'airport') { const isMil = data.type === 'military'; - const color = isMil ? '#ef4444' : '#f59e0b'; + const color = isMil ? '#f87171' : '#38bdf8'; return ( setIranPickedFacility(null)} closeOnClick={false} anchor="bottom" maxWidth="300px" className="gl-popup">
-
+
{data.nameKo ?? data.name}
@@ -598,7 +598,7 @@ export function ReplayMap({ events, currentTime, aircraft, satellites, ships, la ); } if (kind === 'meFacility') { - const meta = { naval: { label: 'Naval Base', color: '#3b82f6', icon: '⚓' }, military_hq: { label: 'Military HQ', color: '#ef4444', icon: '⭐' }, missile: { label: 'Missile/Air Defense', color: '#dc2626', icon: '🚀' }, intelligence: { label: 'Intelligence', color: '#8b5cf6', icon: '🔍' }, government: { label: 'Government', color: '#f59e0b', icon: '🏛️' }, radar: { label: 'Radar/C2', color: '#06b6d4', icon: '📡' } }[data.type] ?? { label: data.type, color: '#888', icon: '📍' }; + const meta = { naval: { label: 'Naval Base', color: '#60a5fa', icon: '⚓' }, military_hq: { label: 'Military HQ', color: '#f87171', icon: '⭐' }, missile: { label: 'Missile/Air Defense', color: '#ef4444', icon: '🚀' }, intelligence: { label: 'Intelligence', color: '#a78bfa', icon: '🔍' }, government: { label: 'Government', color: '#c084fc', icon: '🏛️' }, radar: { label: 'Radar/C2', color: '#22d3ee', icon: '📡' } }[data.type] ?? { label: data.type, color: '#888', icon: '📍' }; return ( setIranPickedFacility(null)} closeOnClick={false} diff --git a/frontend/src/components/iran/SatelliteMap.tsx b/frontend/src/components/iran/SatelliteMap.tsx index 49a0182..2f0f0bc 100644 --- a/frontend/src/components/iran/SatelliteMap.tsx +++ b/frontend/src/components/iran/SatelliteMap.tsx @@ -359,8 +359,8 @@ export function SatelliteMap({ events, currentTime, aircraft, satellites, ships, const { kind, data } = iranPickedFacility; if (kind === 'oil') { const OIL_TYPE_COLORS: Record = { - refinery: '#f59e0b', oilfield: '#10b981', gasfield: '#6366f1', - terminal: '#ec4899', petrochemical: '#8b5cf6', desalination: '#06b6d4', + refinery: '#fb7185', oilfield: '#34d399', gasfield: '#818cf8', + terminal: '#c084fc', petrochemical: '#f472b6', desalination: '#22d3ee', }; const color = OIL_TYPE_COLORS[data.type] ?? '#888'; return ( @@ -398,13 +398,13 @@ export function SatelliteMap({ events, currentTime, aircraft, satellites, ships, } if (kind === 'airport') { const isMil = data.type === 'military'; - const color = isMil ? '#ef4444' : '#f59e0b'; + const color = isMil ? '#f87171' : '#38bdf8'; return ( setIranPickedFacility(null)} closeOnClick={false} anchor="bottom" maxWidth="300px" className="gl-popup">
-
+
{data.nameKo ?? data.name}
@@ -428,7 +428,7 @@ export function SatelliteMap({ events, currentTime, aircraft, satellites, ships, ); } if (kind === 'meFacility') { - const meta = { naval: { label: 'Naval Base', color: '#3b82f6', icon: '⚓' }, military_hq: { label: 'Military HQ', color: '#ef4444', icon: '⭐' }, missile: { label: 'Missile/Air Defense', color: '#dc2626', icon: '🚀' }, intelligence: { label: 'Intelligence', color: '#8b5cf6', icon: '🔍' }, government: { label: 'Government', color: '#f59e0b', icon: '🏛️' }, radar: { label: 'Radar/C2', color: '#06b6d4', icon: '📡' } }[data.type] ?? { label: data.type, color: '#888', icon: '📍' }; + const meta = { naval: { label: 'Naval Base', color: '#60a5fa', icon: '⚓' }, military_hq: { label: 'Military HQ', color: '#f87171', icon: '⭐' }, missile: { label: 'Missile/Air Defense', color: '#ef4444', icon: '🚀' }, intelligence: { label: 'Intelligence', color: '#a78bfa', icon: '🔍' }, government: { label: 'Government', color: '#c084fc', icon: '🏛️' }, radar: { label: 'Radar/C2', color: '#22d3ee', icon: '📡' } }[data.type] ?? { label: data.type, color: '#888', icon: '📍' }; return ( setIranPickedFacility(null)} closeOnClick={false} diff --git a/frontend/src/components/iran/createIranAirportLayers.ts b/frontend/src/components/iran/createIranAirportLayers.ts index 2ff899c..99d279c 100644 --- a/frontend/src/components/iran/createIranAirportLayers.ts +++ b/frontend/src/components/iran/createIranAirportLayers.ts @@ -3,6 +3,7 @@ import type { Layer, PickingInfo } from '@deck.gl/core'; import { svgToDataUri } from '../../utils/svgToDataUri'; import { middleEastAirports } from '../../data/airports'; import type { Airport } from '../../data/airports'; +import { FONT_MONO } from '../../styles/fonts'; export { type Airport }; @@ -15,10 +16,10 @@ const US_BASE_ICAOS = new Set([ function getAirportColor(airport: Airport): string { const isMil = airport.type === 'military'; const isUS = isMil && US_BASE_ICAOS.has(airport.icao); - if (isUS) return '#3b82f6'; - if (isMil) return '#ef4444'; - if (airport.type === 'international') return '#f59e0b'; - return '#7c8aaa'; + if (isUS) return '#60a5fa'; // blue-400 + if (isMil) return '#f87171'; // red-400 + if (airport.type === 'international') return '#38bdf8'; // sky-400 (was amber) + return '#a5b4fc'; // indigo-200 (was gray) } function airportSvg(color: string, size: number): string { @@ -92,8 +93,8 @@ export function createIranAirportLayers(config: IranAirportLayerConfig): Layer[] getTextAnchor: 'middle', getAlignmentBaseline: 'top', getPixelOffset: [0, 10], - fontFamily: 'monospace', - fontWeight: 600, + fontFamily: FONT_MONO, + fontWeight: 700, fontSettings: { sdf: true }, outlineWidth: 3, outlineColor: [0, 0, 0, 255], diff --git a/frontend/src/components/iran/createIranOilLayers.ts b/frontend/src/components/iran/createIranOilLayers.ts index d28798a..e238437 100644 --- a/frontend/src/components/iran/createIranOilLayers.ts +++ b/frontend/src/components/iran/createIranOilLayers.ts @@ -3,18 +3,19 @@ import type { Layer, PickingInfo } from '@deck.gl/core'; import { svgToDataUri } from '../../utils/svgToDataUri'; import { iranOilFacilities } from '../../data/oilFacilities'; import type { OilFacility, OilFacilityType } from '../../types'; +import { FONT_MONO } from '../../styles/fonts'; export { type OilFacility }; export const IRAN_OIL_COUNT = iranOilFacilities.length; const TYPE_COLORS: Record = { - refinery: '#f59e0b', - oilfield: '#10b981', - gasfield: '#6366f1', - terminal: '#ec4899', - petrochemical: '#8b5cf6', - desalination: '#06b6d4', + refinery: '#fb7185', // rose-400 (was amber — desert에 묻힘) + oilfield: '#34d399', // emerald-400 + gasfield: '#818cf8', // indigo-400 + terminal: '#c084fc', // purple-400 + petrochemical: '#f472b6', // pink-400 + desalination: '#22d3ee', // cyan-400 }; function refinerySvg(color: string, size: number): string { @@ -142,8 +143,8 @@ export function createIranOilLayers(config: IranOilLayerConfig): Layer[] { getTextAnchor: 'middle', getAlignmentBaseline: 'top', getPixelOffset: [0, 10], - fontFamily: 'monospace', - fontWeight: 600, + fontFamily: FONT_MONO, + fontWeight: 700, fontSettings: { sdf: true }, outlineWidth: 3, outlineColor: [0, 0, 0, 255], diff --git a/frontend/src/components/iran/createMEFacilityLayers.ts b/frontend/src/components/iran/createMEFacilityLayers.ts index 1943b92..63de4d0 100644 --- a/frontend/src/components/iran/createMEFacilityLayers.ts +++ b/frontend/src/components/iran/createMEFacilityLayers.ts @@ -3,6 +3,7 @@ import type { Layer, PickingInfo } from '@deck.gl/core'; import { svgToDataUri } from '../../utils/svgToDataUri'; import { ME_FACILITIES, ME_FACILITY_TYPE_META } from '../../data/middleEastFacilities'; import type { MEFacility } from '../../data/middleEastFacilities'; +import { FONT_MONO } from '../../styles/fonts'; export { type MEFacility }; @@ -133,12 +134,12 @@ export function createMEFacilityLayers(config: MEFacilityLayerConfig): Layer[] { getText: (d) => d.nameKo.length > 12 ? d.nameKo.slice(0, 12) + '..' : d.nameKo, getSize: 12 * sc * fs, updateTriggers: { getSize: [sc] }, - getColor: (d) => hexToRgba(ME_FACILITY_TYPE_META[d.type].color, 200), + getColor: (d) => hexToRgba(ME_FACILITY_TYPE_META[d.type].color), getTextAnchor: 'middle', getAlignmentBaseline: 'top', getPixelOffset: [0, 12], - fontFamily: 'monospace', - fontWeight: 600, + fontFamily: FONT_MONO, + fontWeight: 700, fontSettings: { sdf: true }, outlineWidth: 3, outlineColor: [0, 0, 0, 255], diff --git a/frontend/src/components/korea/AnalysisStatsPanel.tsx b/frontend/src/components/korea/AnalysisStatsPanel.tsx index 836d8bc..a9b80b9 100644 --- a/frontend/src/components/korea/AnalysisStatsPanel.tsx +++ b/frontend/src/components/korea/AnalysisStatsPanel.tsx @@ -1,5 +1,6 @@ import { useState, useMemo, useEffect } from 'react'; import type { VesselAnalysisDto, RiskLevel, Ship } from '../../types'; +import { FONT_MONO } from '../../styles/fonts'; import type { AnalysisStats } from '../../hooks/useVesselAnalysis'; import { useLocalStorage } from '../../hooks/useLocalStorage'; import { fetchVesselTrack } from '../../services/vesselTrack'; @@ -143,7 +144,7 @@ export function AnalysisStatsPanel({ stats, lastUpdated, isLoading, analysisMap, border: '1px solid rgba(99, 179, 237, 0.25)', borderRadius: 8, color: '#e2e8f0', - fontFamily: 'monospace, sans-serif', + fontFamily: FONT_MONO, fontSize: 11, boxShadow: '0 4px 16px rgba(0, 0, 0, 0.5)', overflow: 'hidden', @@ -311,7 +312,7 @@ export function AnalysisStatsPanel({ stats, lastUpdated, isLoading, analysisMap, fontSize: 10, cursor: 'pointer', padding: '2px 4px', - fontFamily: 'monospace, sans-serif', + fontFamily: FONT_MONO, }} > {RISK_EMOJI[level]} diff --git a/frontend/src/components/korea/FieldAnalysisModal.tsx b/frontend/src/components/korea/FieldAnalysisModal.tsx index 7343949..d2eb546 100644 --- a/frontend/src/components/korea/FieldAnalysisModal.tsx +++ b/frontend/src/components/korea/FieldAnalysisModal.tsx @@ -1,5 +1,6 @@ import { useState, useMemo, useEffect, useCallback } from 'react'; import type { Ship, ChnPrmShipInfo, VesselAnalysisDto } from '../../types'; +import { FONT_MONO } from '../../styles/fonts'; import { classifyFishingZone } from '../../utils/fishingAnalysis'; import { getMarineTrafficCategory } from '../../utils/marineTraffic'; import { lookupPermittedShip } from '../../services/chnPrmShip'; @@ -330,7 +331,7 @@ export function FieldAnalysisModal({ ships, vesselAnalysis, onClose }: Props) { position: 'absolute', inset: 0, zIndex: 2000, background: 'rgba(2,6,14,0.96)', display: 'flex', flexDirection: 'column', - fontFamily: "'IBM Plex Mono', 'Noto Sans KR', monospace", + fontFamily: FONT_MONO, }}> {/* ── 헤더 */}
-
+
{company?.nameCn || `선단 #${cid}`}
@@ -628,7 +629,7 @@ export function FleetClusterLayer({ ships, analysisMap: analysisMapProp, cluster closeButton={false} closeOnClick={false} anchor="bottom" className="gl-popup" maxWidth="220px" > -
+
{name} 어구 {entry.gears.length}개
diff --git a/frontend/src/components/korea/KoreaMap.tsx b/frontend/src/components/korea/KoreaMap.tsx index 8d7621b..080517a 100644 --- a/frontend/src/components/korea/KoreaMap.tsx +++ b/frontend/src/components/korea/KoreaMap.tsx @@ -4,6 +4,7 @@ import { Map, NavigationControl, Marker, Source, Layer } from 'react-map-gl/mapl import type { MapRef } from 'react-map-gl/maplibre'; import { ScatterplotLayer, TextLayer } from '@deck.gl/layers'; import { useFontScale } from '../../hooks/useFontScale'; +import { FONT_MONO } from '../../styles/fonts'; import { DeckGLOverlay } from '../layers/DeckGLOverlay'; import { useAnalysisDeckLayers } from '../../hooks/useAnalysisDeckLayers'; import { useStaticDeckLayers } from '../../hooks/useStaticDeckLayers'; @@ -249,7 +250,7 @@ export function KoreaMap({ ships, allShips, aircraft, satellites, layers, osintF getTextAnchor: 'middle', getAlignmentBaseline: 'top', getPixelOffset: [0, 14], - fontFamily: 'monospace', + fontFamily: FONT_MONO, fontSettings: { sdf: true }, outlineWidth: 3, outlineColor: [0, 0, 0, 255], @@ -287,7 +288,7 @@ export function KoreaMap({ ships, allShips, aircraft, satellites, layers, osintF getColor: [255, 255, 255, 220], getTextAnchor: 'middle', getAlignmentBaseline: 'center', - fontFamily: 'monospace', + fontFamily: FONT_MONO, fontWeight: 700, fontSettings: { sdf: true }, outlineWidth: 3, @@ -364,7 +365,7 @@ export function KoreaMap({ ships, allShips, aircraft, satellites, layers, osintF getTextAnchor: 'middle' as const, getAlignmentBaseline: 'top' as const, getPixelOffset: [0, 10], - fontFamily: 'monospace', + fontFamily: FONT_MONO, fontSettings: { sdf: true }, outlineWidth: 3, outlineColor: [0, 0, 0, 220], @@ -399,7 +400,7 @@ export function KoreaMap({ ships, allShips, aircraft, satellites, layers, osintF getTextAnchor: 'middle' as const, getAlignmentBaseline: 'top' as const, getPixelOffset: [0, 18], - fontFamily: 'monospace', + fontFamily: FONT_MONO, fontWeight: 700, fontSettings: { sdf: true }, outlineWidth: 3, @@ -464,7 +465,7 @@ export function KoreaMap({ ships, allShips, aircraft, satellites, layers, osintF getTextAnchor: 'middle' as const, getAlignmentBaseline: 'top' as const, getPixelOffset: [0, 12], - fontFamily: 'monospace', + fontFamily: FONT_MONO, fontWeight: 600, fontSettings: { sdf: true }, outlineWidth: 3, diff --git a/frontend/src/components/korea/SubmarineCableLayer.tsx b/frontend/src/components/korea/SubmarineCableLayer.tsx index 1bb58b8..d0f10d1 100644 --- a/frontend/src/components/korea/SubmarineCableLayer.tsx +++ b/frontend/src/components/korea/SubmarineCableLayer.tsx @@ -1,5 +1,6 @@ import { useState } from 'react'; import { Marker, Popup, Source, Layer } from 'react-map-gl/maplibre'; +import { FONT_MONO } from '../../styles/fonts'; import { KOREA_SUBMARINE_CABLES, KOREA_LANDING_POINTS } from '../../services/submarineCable'; import type { SubmarineCable } from '../../services/submarineCable'; @@ -90,7 +91,7 @@ export function SubmarineCableLayer() { { e.originalEvent.stopPropagation(); setSelectedCable(cable); setSelectedPoint(null); }}>
= { - power: { label: '발전소', color: '#a855f7', icon: '⚡' }, - wind: { label: '풍력단지', color: '#22d3ee', icon: '🌬' }, - nuclear: { label: '원자력발전소', color: '#f59e0b', icon: '☢' }, - thermal: { label: '화력발전소', color: '#64748b', icon: '🏭' }, - petrochem: { label: '석유화학단지', color: '#f97316', icon: '🛢' }, - lng: { label: 'LNG저장기지', color: '#0ea5e9', icon: '❄' }, - oil_tank: { label: '유류저장탱크', color: '#eab308', icon: '🛢' }, - haz_port: { label: '위험물항만하역시설', color: '#dc2626', icon: '⚠' }, + power: { label: '발전소', color: '#c084fc', icon: '⚡' }, // purple-400 + wind: { label: '풍력단지', color: '#22d3ee', icon: '🌬' }, // cyan-400 + nuclear: { label: '원자력발전소', color: '#f472b6', icon: '☢' }, // pink-400 (was amber) + thermal: { label: '화력발전소', color: '#94a3b8', icon: '🏭' }, // slate-400 (was 500) + petrochem: { label: '석유화학단지', color: '#fb7185', icon: '🛢' }, // rose-400 (was orange) + lng: { label: 'LNG저장기지', color: '#38bdf8', icon: '❄' }, // sky-400 + oil_tank: { label: '유류저장탱크', color: '#a3e635', icon: '🛢' }, // lime-400 (was yellow) + haz_port: { label: '위험물항만하역시설', color: '#f87171', icon: '⚠' }, // red-400 }; // layer key -> subType mapping diff --git a/frontend/src/data/middleEastFacilities.ts b/frontend/src/data/middleEastFacilities.ts index 6bf9e61..81a4677 100644 --- a/frontend/src/data/middleEastFacilities.ts +++ b/frontend/src/data/middleEastFacilities.ts @@ -14,12 +14,12 @@ export interface MEFacility { } const TYPE_META: Record = { - naval: { label: 'Naval Base', color: '#3b82f6', icon: '⚓' }, - military_hq: { label: 'Military HQ', color: '#ef4444', icon: '⭐' }, - missile: { label: 'Missile/Air Defense', color: '#dc2626', icon: '🚀' }, - intelligence: { label: 'Intelligence', color: '#8b5cf6', icon: '🔍' }, - government: { label: 'Government', color: '#f59e0b', icon: '🏛️' }, - radar: { label: 'Radar/C2', color: '#06b6d4', icon: '📡' }, + naval: { label: 'Naval Base', color: '#60a5fa', icon: '⚓' }, // blue-400 + military_hq: { label: 'Military HQ', color: '#f87171', icon: '⭐' }, // red-400 + missile: { label: 'Missile/Air Defense', color: '#ef4444', icon: '🚀' }, // red-500 + intelligence: { label: 'Intelligence', color: '#a78bfa', icon: '🔍' }, // violet-400 + government: { label: 'Government', color: '#c084fc', icon: '🏛️' }, // purple-400 (was amber) + radar: { label: 'Radar/C2', color: '#22d3ee', icon: '📡' }, // cyan-400 }; export { TYPE_META as ME_FACILITY_TYPE_META }; diff --git a/frontend/src/env.d.ts b/frontend/src/env.d.ts index d1719dc..2e02f3d 100644 --- a/frontend/src/env.d.ts +++ b/frontend/src/env.d.ts @@ -1,5 +1,9 @@ /// +declare module '@fontsource-variable/inter'; +declare module '@fontsource-variable/noto-sans-kr'; +declare module '@fontsource-variable/fira-code'; + interface ImportMetaEnv { readonly VITE_SPG_API_KEY?: string; readonly VITE_GOOGLE_CLIENT_ID?: string; diff --git a/frontend/src/hooks/layers/createFacilityLayers.ts b/frontend/src/hooks/layers/createFacilityLayers.ts index b510f77..7ce1578 100644 --- a/frontend/src/hooks/layers/createFacilityLayers.ts +++ b/frontend/src/hooks/layers/createFacilityLayers.ts @@ -14,6 +14,7 @@ import { type CnFacility, type JpFacility, } from './types'; +import { FONT_MONO } from '../../styles/fonts'; // ─── Infra SVG ──────────────────────────────────────────────────────────────── @@ -359,8 +360,8 @@ export function createFacilityLayers( getTextAnchor: 'middle', getAlignmentBaseline: 'top', getPixelOffset: [0, 8], - fontFamily: 'monospace', - fontWeight: 600, + fontFamily: FONT_MONO, + fontWeight: 700, fontSettings: { sdf: true }, outlineWidth: 3, outlineColor: [0, 0, 0, 255], @@ -415,8 +416,8 @@ export function createFacilityLayers( getTextAnchor: 'middle', getAlignmentBaseline: 'top', getPixelOffset: [0, 12], - fontFamily: 'monospace', - fontWeight: 600, + fontFamily: FONT_MONO, + fontWeight: 700, fontSettings: { sdf: true }, outlineWidth: 3, outlineColor: [0, 0, 0, 255], @@ -469,8 +470,8 @@ export function createFacilityLayers( getTextAnchor: 'middle', getAlignmentBaseline: 'top', getPixelOffset: [0, 12], - fontFamily: 'monospace', - fontWeight: 600, + fontFamily: FONT_MONO, + fontWeight: 700, fontSettings: { sdf: true }, outlineWidth: 3, outlineColor: [0, 0, 0, 255], @@ -522,8 +523,8 @@ export function createFacilityLayers( getTextAnchor: 'middle', getAlignmentBaseline: 'top', getPixelOffset: [0, 12], - fontFamily: 'monospace', - fontWeight: 600, + fontFamily: FONT_MONO, + fontWeight: 700, fontSettings: { sdf: true }, outlineWidth: 3, outlineColor: [0, 0, 0, 255], diff --git a/frontend/src/hooks/layers/createMilitaryLayers.ts b/frontend/src/hooks/layers/createMilitaryLayers.ts index b3ae0d1..211755d 100644 --- a/frontend/src/hooks/layers/createMilitaryLayers.ts +++ b/frontend/src/hooks/layers/createMilitaryLayers.ts @@ -11,6 +11,7 @@ import { NK_MISSILE_EVENTS } from '../../data/nkMissileEvents'; import type { NKMissileEvent } from '../../data/nkMissileEvents'; import { hexToRgb } from './types'; import type { LayerFactoryConfig } from './types'; +import { FONT_MONO } from '../../styles/fonts'; // ─── NKMissile SVG ──────────────────────────────────────────────────────────── @@ -318,7 +319,7 @@ export function createMilitaryLayers( getTextAnchor: 'middle', getAlignmentBaseline: 'top', getPixelOffset: [0, 9], - fontFamily: 'monospace', + fontFamily: FONT_MONO, fontWeight: 700, fontSettings: { sdf: true }, outlineWidth: 3, @@ -356,7 +357,7 @@ export function createMilitaryLayers( getTextAnchor: 'middle', getAlignmentBaseline: 'top', getPixelOffset: [0, 8], - fontFamily: 'monospace', + fontFamily: FONT_MONO, fontWeight: 700, fontSettings: { sdf: true }, outlineWidth: 3, @@ -394,7 +395,7 @@ export function createMilitaryLayers( getTextAnchor: 'middle', getAlignmentBaseline: 'top', getPixelOffset: [0, 8], - fontFamily: 'monospace', + fontFamily: FONT_MONO, fontWeight: 700, fontSettings: { sdf: true }, outlineWidth: 3, @@ -486,7 +487,7 @@ export function createMilitaryLayers( getTextAnchor: 'middle', getAlignmentBaseline: 'top', getPixelOffset: [0, 10], - fontFamily: 'monospace', + fontFamily: FONT_MONO, fontWeight: 700, fontSettings: { sdf: true }, outlineWidth: 3, diff --git a/frontend/src/hooks/layers/createNavigationLayers.ts b/frontend/src/hooks/layers/createNavigationLayers.ts index 5d8de84..9283f80 100644 --- a/frontend/src/hooks/layers/createNavigationLayers.ts +++ b/frontend/src/hooks/layers/createNavigationLayers.ts @@ -11,6 +11,7 @@ import { PIRACY_ZONES, PIRACY_LEVEL_COLOR } from '../../services/piracy'; import type { PiracyZone } from '../../services/piracy'; import { hexToRgb } from './types'; import type { LayerFactoryConfig } from './types'; +import { FONT_MONO } from '../../styles/fonts'; // ─── CoastGuard ─────────────────────────────────────────────────────────────── @@ -180,7 +181,7 @@ export function createNavigationLayers( getTextAnchor: 'middle', getAlignmentBaseline: 'top', getPixelOffset: [0, 8], - fontFamily: 'monospace', + fontFamily: FONT_MONO, fontWeight: 700, fontSettings: { sdf: true }, outlineWidth: 3, @@ -231,7 +232,7 @@ export function createNavigationLayers( getTextAnchor: 'middle', getAlignmentBaseline: 'top', getPixelOffset: [0, 10], - fontFamily: 'monospace', + fontFamily: FONT_MONO, fontWeight: 700, fontSettings: { sdf: true }, outlineWidth: 3, @@ -281,7 +282,7 @@ export function createNavigationLayers( getTextAnchor: 'middle', getAlignmentBaseline: 'top', getPixelOffset: [0, 9], - fontFamily: 'monospace', + fontFamily: FONT_MONO, fontWeight: 700, fontSettings: { sdf: true }, outlineWidth: 3, @@ -332,7 +333,7 @@ export function createNavigationLayers( getTextAnchor: 'middle', getAlignmentBaseline: 'top', getPixelOffset: [0, 14], - fontFamily: 'monospace', + fontFamily: FONT_MONO, fontWeight: 700, fontSettings: { sdf: true }, outlineWidth: 3, diff --git a/frontend/src/hooks/layers/createPortLayers.ts b/frontend/src/hooks/layers/createPortLayers.ts index 4ff363f..05f8b61 100644 --- a/frontend/src/hooks/layers/createPortLayers.ts +++ b/frontend/src/hooks/layers/createPortLayers.ts @@ -7,6 +7,7 @@ import { KOREA_WIND_FARMS } from '../../data/windFarms'; import type { WindFarm } from '../../data/windFarms'; import { hexToRgb } from './types'; import type { LayerFactoryConfig } from './types'; +import { FONT_MONO } from '../../styles/fonts'; // ─── Port colors ────────────────────────────────────────────────────────────── @@ -101,7 +102,7 @@ export function createPortLayers( getTextAnchor: 'middle', getAlignmentBaseline: 'top', getPixelOffset: [0, 8], - fontFamily: 'monospace', + fontFamily: FONT_MONO, fontWeight: 700, fontSettings: { sdf: true }, outlineWidth: 3, @@ -139,7 +140,7 @@ export function createPortLayers( getTextAnchor: 'middle', getAlignmentBaseline: 'top', getPixelOffset: [0, 10], - fontFamily: 'monospace', + fontFamily: FONT_MONO, fontWeight: 700, fontSettings: { sdf: true }, outlineWidth: 3, diff --git a/frontend/src/hooks/useAnalysisDeckLayers.ts b/frontend/src/hooks/useAnalysisDeckLayers.ts index 8f48ddb..7726fe8 100644 --- a/frontend/src/hooks/useAnalysisDeckLayers.ts +++ b/frontend/src/hooks/useAnalysisDeckLayers.ts @@ -3,6 +3,7 @@ import { ScatterplotLayer, TextLayer } from '@deck.gl/layers'; import type { Layer } from '@deck.gl/core'; import type { Ship, VesselAnalysisDto } from '../types'; import { useFontScale } from './useFontScale'; +import { FONT_MONO } from '../styles/fonts'; interface AnalyzedShip { ship: Ship; @@ -132,7 +133,7 @@ export function useAnalysisDeckLayers( getTextAnchor: 'middle', getAlignmentBaseline: 'top', getPixelOffset: [0, 16], - fontFamily: 'monospace', + fontFamily: FONT_MONO, fontSettings: { sdf: true }, outlineWidth: 3, outlineColor: [0, 0, 0, 255], @@ -176,7 +177,7 @@ export function useAnalysisDeckLayers( getTextAnchor: 'middle', getAlignmentBaseline: 'top', getPixelOffset: [0, 14], - fontFamily: 'monospace', + fontFamily: FONT_MONO, fontSettings: { sdf: true }, outlineWidth: 3, outlineColor: [0, 0, 0, 255], @@ -198,7 +199,7 @@ export function useAnalysisDeckLayers( getColor: [239, 68, 68, 255], getTextAnchor: 'start', getPixelOffset: [12, -8], - fontFamily: 'monospace', + fontFamily: FONT_MONO, fontWeight: 700, fontSettings: { sdf: true }, outlineWidth: 3, diff --git a/frontend/src/index.css b/frontend/src/index.css index 5536b3f..4322265 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -17,7 +17,7 @@ } body { - font-family: 'Inter', system-ui, -apple-system, sans-serif; + font-family: 'Inter Variable', 'Noto Sans KR Variable', system-ui, -apple-system, sans-serif; background: var(--bg-primary); color: var(--text-primary); min-height: 100vh; diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 81dce04..c931ae6 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -1,5 +1,8 @@ import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' +import '@fontsource-variable/inter' +import '@fontsource-variable/noto-sans-kr' +import '@fontsource-variable/fira-code' import './i18n' import './styles/tailwind.css' import './index.css' diff --git a/frontend/src/styles/fonts.ts b/frontend/src/styles/fonts.ts new file mode 100644 index 0000000..18844c4 --- /dev/null +++ b/frontend/src/styles/fonts.ts @@ -0,0 +1,3 @@ +/** Self-hosted web font stacks — @fontsource-variable 패키지 기반 */ +export const FONT_SANS = "'Inter Variable', 'Noto Sans KR Variable', system-ui, sans-serif"; +export const FONT_MONO = "'Fira Code Variable', 'Noto Sans KR Variable', monospace"; diff --git a/frontend/src/styles/tokens.css b/frontend/src/styles/tokens.css index 0253a92..24693c5 100644 --- a/frontend/src/styles/tokens.css +++ b/frontend/src/styles/tokens.css @@ -184,6 +184,6 @@ --color-kcg-danger-bg: var(--kcg-danger-bg); /* 폰트 */ - --font-mono: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace; - --font-sans: 'Inter', system-ui, -apple-system, sans-serif; + --font-mono: 'Fira Code Variable', 'Noto Sans KR Variable', monospace; + --font-sans: 'Inter Variable', 'Noto Sans KR Variable', system-ui, -apple-system, sans-serif; }