Compare commits

...

5 커밋

작성자 SHA1 메시지 날짜
a33dd09485 Merge pull request 'feat(design): 디자인 시스템 토큰 적용 및 Float 카탈로그 추가' (#161) from feature/design-system-refactoring into develop 2026-04-07 18:04:52 +09:00
f375ecc3ab chore: 임시 MR 스크립트 제거 2026-04-07 18:03:34 +09:00
9e51651fc7 Merge remote-tracking branch 'origin/develop' into feature/design-system-refactoring
# Conflicts:
#	docs/RELEASE-NOTES.md
#	frontend/src/common/components/map/MapView.tsx
#	frontend/src/tabs/incidents/components/DischargeZonePanel.tsx
#	frontend/src/tabs/incidents/components/IncidentsView.tsx
2026-04-07 18:02:57 +09:00
4065ec76ef docs: 릴리즈 노트 업데이트 2026-04-07 17:34:15 +09:00
109c0d2480 feat(design): 디자인 시스템 토큰 적용 및 Float 카탈로그 추가 2026-04-07 17:30:42 +09:00
113개의 변경된 파일24662개의 추가작업 그리고 49919개의 파일을 삭제

파일 보기

@ -125,6 +125,25 @@ wing/
- API 인터페이스 변경 시 `memory/api-types.md` 갱신
- 개별 탭 개발자는 공통 가이드를 참조하여 연동 구현
## 진행 중 작업 (완료 후 삭제)
### 디자인 시스템 폰트+색상 통일 작업
compact 후 반드시 `memory/design-system-work.md`를 읽고 작업 상태(완료/미완료 컴포넌트)를 확인할 것.
**색상 규칙:**
- 하드코딩 색상(`#ef4444`, `#a855f7` 등) → CSS 변수 전환
- `rgba(59,130,246,...)` 등 비-accent 계열 → `rgba(6,182,212,...)` (accent cyan)
- 시맨틱 컬러(`color-accent`, `color-info`, `color-caution` 등)는 다양하게 사용 가능하되, 강조 색상은 **최대 2가지**로 제한
- `linear-gradient` → 단색으로 단순화
- 장식용 `border-top`, `border-left` → 제거 여부를 유저에게 확인 후 진행
**폰트 규칙:**
- 하드코딩 `fontSize`/`fontWeight` → Tailwind 토큰 (`text-title-2`, `text-caption` 등)
- `fontFamily: monospace``var(--font-mono)`
- `fontFamily: sans-serif` / `'Noto Sans KR'``var(--font-korean)`
- 인라인 `style={{ fontSize, padding }}` → Tailwind 클래스 전환 (가능한 범위)
## 환경 설정
- Node.js 20 (`.node-version`, fnm 사용)

파일 보기

@ -5,6 +5,8 @@
## [Unreleased]
### 추가
- 디자인 시스템 Float 카탈로그 추가 (Modal / Dropdown / Overlay / Toast)
- 디자인 시스템 폰트/색상 토큰을 전 탭 컴포넌트에 전면 적용 (admin, aerial, assets, board, hns, incidents, prediction, reports, rescue, scat, weather)
- SR 민감자원 벡터타일 오버레이 컴포넌트 및 백엔드 프록시 엔드포인트 추가
- 해양 오염물질 배출규정 구역 판별 기능 추가

파일 보기

@ -2,7 +2,7 @@
<html lang="ko">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다. Load Diff

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다. Load Diff

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다. Load Diff

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다. Load Diff

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다. Load Diff

파일 보기

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
<path d="M4 12 Q16 0 28 12 Q22 15 16 13 Q10 15 4 12 Z" fill="#06b6d4"/>
<path d="M4 19 Q10 15 16 19 T28 19 L28 22 Q22 26 16 22 T4 22 Z" fill="#06b6d4"/>
<path d="M4 25 Q10 21 16 25 T28 25 L28 28 Q22 32 16 28 T4 28 Z" fill="#06b6d4"/>
</svg>

After

Width:  |  Height:  |  크기: 320 B

파일 보기

@ -113,7 +113,7 @@ export function LoginPage() {
{/* User ID */}
<div className="mb-4">
<label
className="block text-[10px] font-semibold text-fg-disabled mb-1.5"
className="block text-caption font-semibold text-fg-disabled mb-1.5"
style={{ letterSpacing: '0.3px' }}
>
@ -147,7 +147,7 @@ export function LoginPage() {
placeholder="사용자 아이디 입력"
autoComplete="username"
autoFocus
className="w-full bg-bg-elevated border border-stroke rounded-md text-[13px] outline-none"
className="w-full bg-bg-elevated border border-stroke rounded-md text-title-4 outline-none"
style={{
padding: '11px 14px 11px 38px',
transition: 'border-color 0.2s, box-shadow 0.2s',
@ -167,7 +167,7 @@ export function LoginPage() {
{/* Password */}
<div className="mb-5">
<label
className="block text-[10px] font-semibold text-fg-disabled mb-1.5"
className="block text-caption font-semibold text-fg-disabled mb-1.5"
style={{ letterSpacing: '0.3px' }}
>
@ -200,7 +200,7 @@ export function LoginPage() {
}}
placeholder="비밀번호 입력"
autoComplete="current-password"
className="w-full bg-bg-elevated border border-stroke rounded-md text-[13px] outline-none"
className="w-full bg-bg-elevated border border-stroke rounded-md text-title-4 outline-none"
style={{
padding: '11px 14px 11px 38px',
transition: 'border-color 0.2s, box-shadow 0.2s',
@ -219,7 +219,7 @@ export function LoginPage() {
{/* Remember + Forgot */}
<div className="flex items-center justify-between mb-5">
<label className="flex items-center gap-1.5 text-[11px] text-fg-disabled cursor-pointer">
<label className="flex items-center gap-1.5 text-label-2 text-fg-disabled cursor-pointer">
<input
type="checkbox"
checked={remember}
@ -230,7 +230,7 @@ export function LoginPage() {
</label>
<button
type="button"
className="text-[11px] text-color-accent cursor-pointer bg-transparent border-none"
className="text-label-2 text-color-accent cursor-pointer bg-transparent border-none"
onMouseEnter={(e) => (e.currentTarget.style.textDecoration = 'underline')}
onMouseLeave={(e) => (e.currentTarget.style.textDecoration = 'none')}
>
@ -241,7 +241,7 @@ export function LoginPage() {
{/* Pending approval */}
{pendingMessage && (
<div
className="flex items-start gap-2 text-[11px] rounded-sm mb-4"
className="flex items-start gap-2 text-label-2 rounded-sm mb-4"
style={{
padding: '10px 12px',
background: 'rgba(6,182,212,0.08)',
@ -271,7 +271,7 @@ export function LoginPage() {
{/* Error */}
{error && (
<div
className="flex items-center gap-1.5 text-[11px] rounded-sm mb-4"
className="flex items-center gap-1.5 text-label-2 rounded-sm mb-4"
style={{
padding: '8px 12px',
background: 'rgba(239,68,68,0.08)',
@ -279,7 +279,7 @@ export function LoginPage() {
color: '#f87171',
}}
>
<span className="text-[13px]">
<span className="text-title-4">
<svg
width="13"
height="13"
@ -353,7 +353,7 @@ export function LoginPage() {
{/* Divider */}
<div className="flex items-center gap-3 my-6">
<div className="flex-1 bg-border h-px" />
<span className="text-[9px] text-fg-disabled"></span>
<span className="text-caption text-fg-disabled"></span>
<div className="flex-1 bg-border h-px" />
</div>
@ -375,7 +375,7 @@ export function LoginPage() {
)}
<button
type="button"
className="w-full rounded-md bg-bg-card border border-stroke text-fg-sub text-[11px] font-semibold cursor-pointer flex items-center justify-center gap-1.5 px-[10px] py-[10px]"
className="w-full rounded-md bg-bg-card border border-stroke text-fg-sub text-label-2 font-semibold cursor-pointer flex items-center justify-center gap-1.5 px-[10px] py-[10px]"
style={{ transition: 'background 0.15s' }}
onMouseEnter={(e) => (e.currentTarget.style.background = 'var(--bg-surface-hover)')}
onMouseLeave={(e) => (e.currentTarget.style.background = 'var(--bg-card)')}
@ -407,7 +407,7 @@ export function LoginPage() {
border: '1px solid rgba(6,182,212,0.08)',
}}
>
<div className="text-[9px] font-bold text-color-accent mb-1.5"> </div>
<div className="text-caption font-bold text-color-accent mb-1.5"> </div>
<div className="flex flex-col gap-[3px]">
{DEMO_ACCOUNTS.map((acc) => (
<div
@ -427,10 +427,10 @@ export function LoginPage() {
}
onMouseLeave={(e) => (e.currentTarget.style.background = 'transparent')}
>
<span className="text-[9px] text-fg-sub font-mono">
<span className="text-caption text-fg-sub font-mono">
{acc.id} / {acc.password}
</span>
<span className="text-[8px] text-fg-disabled">{acc.label}</span>
<span className="text-caption text-fg-disabled">{acc.label}</span>
</div>
))}
</div>
@ -440,7 +440,7 @@ export function LoginPage() {
{/* end form card */}
{/* Footer */}
<div className="text-center text-[9px] text-fg-disabled mt-6 leading-[1.6]">
<div className="text-center text-caption text-fg-disabled mt-6 leading-[1.6]">
<div>WING V2.0 | </div>
<div className="mt-0.5" style={{ color: 'rgba(134,144,166,0.6)' }}>
&copy; 2026 Korea Coast Guard. All rights reserved.

파일 보기

@ -42,7 +42,7 @@ export function LayerTree({
return (
<div className="px-1">
<div className="flex items-center justify-between px-2 pt-1 pb-2 mb-1 border-b border-stroke">
<span className="text-[10px] font-semibold text-fg-disabled"> </span>
<span className="text-caption font-semibold text-fg-disabled"> </span>
<div
className={`lyr-sw ${allEnabled ? 'on' : ''} cursor-pointer`}
onClick={handleToggleAll}
@ -260,7 +260,7 @@ function LayerNode({
<div>
<div className="lyr-t gap-1.5">
<span
className={`lyr-arr ${expanded ? 'open' : ''} cursor-pointer text-[7px] w-[10px] text-center`}
className={`lyr-arr ${expanded ? 'open' : ''} cursor-pointer text-caption w-[10px] text-center`}
onClick={handleHeaderClick}
>

파일 보기

@ -216,7 +216,7 @@ export function BacktrackReplayBar({
{/* Collision marker */}
{collisionEvent && (
<div
className="absolute text-[10px] cursor-pointer"
className="absolute text-caption cursor-pointer"
style={{
top: '-14px',
left: `${collisionEvent.progressPercent}%`,
@ -244,7 +244,7 @@ export function BacktrackReplayBar({
</div>
{/* Time labels */}
<div className="flex justify-between text-[9px] font-mono">
<div className="flex justify-between text-caption font-mono">
<span className="text-fg-disabled">{startLabel}</span>
<span className="font-semibold text-color-tertiary">{currentTimeLabel}</span>
<span className="text-fg-disabled">{endLabel}</span>
@ -257,13 +257,13 @@ export function BacktrackReplayBar({
{replayShips.map((ship) => (
<div key={ship.vesselName} className="flex items-center gap-1.5">
<div className="w-4 h-[3px]" style={{ background: ship.color, borderRadius: '1px' }} />
<span className="text-[9px] text-fg-sub font-mono">{ship.vesselName}</span>
<span className="text-caption text-fg-sub font-mono">{ship.vesselName}</span>
</div>
))}
{hasBackwardParticles && (
<div className="flex items-center gap-1.5">
<div className="w-3 h-3 rounded-full" style={{ background: '#a855f7', opacity: 0.8 }} />
<span className="text-[9px] text-fg-sub font-mono"> </span>
<span className="text-caption text-fg-sub font-mono"> </span>
</div>
)}
</div>

파일 보기

@ -25,9 +25,7 @@ interface Particle {
age: number;
}
export default function HydrParticleOverlay({
hydrStep,
}: HydrParticleOverlayProps) {
export default function HydrParticleOverlay({ hydrStep }: HydrParticleOverlayProps) {
const lightMode = useThemeStore((s) => s.theme) === 'light';
const { current: map } = useMap();
const animRef = useRef<number>();

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다. Load Diff

파일 보기

@ -26,7 +26,7 @@ export function MeasureOverlay() {
e.stopPropagation();
removeMeasurement(mk.id);
}}
className="px-2 py-0.5 text-[11px] font-semibold text-white bg-[rgba(239,68,68,0.85)] hover:bg-[rgba(239,68,68,1)] rounded shadow-lg border border-[rgba(255,255,255,0.2)] cursor-pointer font-korean"
className="px-2 py-0.5 text-label-2 font-semibold text-white bg-[rgba(239,68,68,0.85)] hover:bg-[rgba(239,68,68,1)] rounded shadow-lg border border-[rgba(255,255,255,0.2)] cursor-pointer font-korean"
>
</button>

파일 보기

@ -53,9 +53,9 @@ function extractCode(wmsLayer: string): string | null {
// ─── layerTree → SR 매핑 구축 ───
interface SrMapping {
layerCd: string; // DB LAYER_CD (예: 'LYR001002001004005')
code: string; // mpc: 뒤 코드 (예: '468', 'kcg', '3')
name: string; // DB 레이어명 (예: '갯벌', '경찰청', '군산')
layerCd: string; // DB LAYER_CD (예: 'LYR001002001004005')
code: string; // mpc: 뒤 코드 (예: '468', 'kcg', '3')
name: string; // DB 레이어명 (예: '갯벌', '경찰청', '군산')
}
// ─── source-layer → DB layerCd 매칭 ───
@ -65,19 +65,19 @@ function matchSourceLayer(sourceLayer: string, mappings: SrMapping[]): string[]
const numMatch = sourceLayer.match(/^(\d+)/);
if (numMatch) {
const code = numMatch[1];
const matched = mappings.filter(m => m.code === code);
if (matched.length > 0) return matched.map(m => m.layerCd);
const matched = mappings.filter((m) => m.code === code);
if (matched.length > 0) return matched.map((m) => m.layerCd);
}
// 2차: 이름 정확 일치 (경찰청 = 경찰청)
const exactMatch = mappings.filter(m => sourceLayer === m.name);
if (exactMatch.length > 0) return exactMatch.map(m => m.layerCd);
const exactMatch = mappings.filter((m) => sourceLayer === m.name);
if (exactMatch.length > 0) return exactMatch.map((m) => m.layerCd);
// 3차: 접미사 일치 (해경관할구역-군산 → name '군산')
const suffixMatch = mappings.filter(m =>
sourceLayer.endsWith(`-${m.name}`) || sourceLayer.endsWith(`_${m.name}`)
const suffixMatch = mappings.filter(
(m) => sourceLayer.endsWith(`-${m.name}`) || sourceLayer.endsWith(`_${m.name}`),
);
if (suffixMatch.length > 0) return suffixMatch.map(m => m.layerCd);
if (suffixMatch.length > 0) return suffixMatch.map((m) => m.layerCd);
return [];
}
@ -177,9 +177,9 @@ export function SrOverlay({ enabledLayers, opacity = 100, layerColors }: SrOverl
};
// style JSON 레이어 중 활성화된 DB 레이어에 해당하는 스타일 레이어 필터
const enabledStyleLayers = style.layers.filter(sl => {
const enabledStyleLayers = style.layers.filter((sl) => {
const ids = sourceLayerToIds.get(sl['source-layer']);
return ids && ids.some(id => enabledLayers.has(id));
return ids && ids.some((id) => enabledLayers.has(id));
});
const syncLayers = () => {
@ -252,7 +252,9 @@ export function SrOverlay({ enabledLayers, opacity = 100, layerColors }: SrOverl
syncLayers();
} else {
map.once('style.load', syncLayers);
return () => { map.off('style.load', syncLayers); };
return () => {
map.off('style.load', syncLayers);
};
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [enabledLayers, layerTree, style, mapRef, layerColors]);
@ -321,7 +323,9 @@ export function SrOverlay({ enabledLayers, opacity = 100, layerColors }: SrOverl
useEffect(() => {
const map = mapRef?.getMap();
if (!map) return;
return () => { removeAll(map); };
return () => {
removeAll(map);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [mapRef]);

파일 보기

@ -4,17 +4,23 @@
/** opacity 속성 키를 레이어 타입에 따라 반환 */
export function getOpacityProp(type: 'fill' | 'line' | 'circle'): string {
switch (type) {
case 'fill': return 'fill-opacity';
case 'line': return 'line-opacity';
case 'circle': return 'circle-opacity';
case 'fill':
return 'fill-opacity';
case 'line':
return 'line-opacity';
case 'circle':
return 'circle-opacity';
}
}
/** color 속성 키를 레이어 타입에 따라 반환 */
export function getColorProp(type: 'fill' | 'line' | 'circle'): string {
switch (type) {
case 'fill': return 'fill-color';
case 'line': return 'line-color';
case 'circle': return 'circle-color';
case 'fill':
return 'fill-color';
case 'line':
return 'line-color';
case 'circle':
return 'circle-color';
}
}

파일 보기

@ -39,7 +39,7 @@ export function ComboBox({ value, onChange, options, placeholder, className }: C
>
<span>{displayText}</span>
<span
className="text-[8px] text-fg-disabled"
className="text-caption text-fg-disabled"
style={{
transition: 'transform 0.2s',
transform: isOpen ? 'rotate(180deg)' : 'rotate(0deg)',
@ -67,7 +67,7 @@ export function ComboBox({ value, onChange, options, placeholder, className }: C
onChange(option.value);
setIsOpen(false);
}}
className="text-[11px] cursor-pointer"
className="text-label-2 cursor-pointer"
style={{
padding: '8px 10px',
color: option.value === String(value) ? 'var(--color-accent)' : 'var(--fg-sub)',

파일 보기

@ -1125,11 +1125,11 @@ const UserManualPopup = ({ isOpen, onClose }: UserManualPopupProps) => {
}}
>
<div className="flex items-center gap-3">
<span className="font-bold text-[15px]" style={{ color: '#e2e8f0' }}>
<span className="font-bold text-subtitle" style={{ color: '#e2e8f0' }}>
Wing
</span>
<span
className="text-[11px] px-2 py-0.5 rounded font-mono"
className="text-label-2 px-2 py-0.5 rounded font-mono"
style={{
background: 'rgba(6,182,212,0.12)',
color: '#06b6d4',
@ -1141,7 +1141,7 @@ const UserManualPopup = ({ isOpen, onClose }: UserManualPopupProps) => {
</div>
<button
onClick={onClose}
className="flex items-center justify-center w-7 h-7 rounded text-[13px] font-semibold transition-colors"
className="flex items-center justify-center w-7 h-7 rounded text-title-4 font-semibold transition-colors"
style={{ color: '#94a3b8', background: 'transparent' }}
onMouseEnter={(e) => {
e.currentTarget.style.background = '#1a2540';
@ -1194,7 +1194,7 @@ const UserManualPopup = ({ isOpen, onClose }: UserManualPopupProps) => {
>
<div className="flex items-center gap-2.5">
<span
className="flex-shrink-0 w-7 h-7 rounded flex items-center justify-center text-[10px] font-bold font-mono"
className="flex-shrink-0 w-7 h-7 rounded flex items-center justify-center text-caption font-bold font-mono"
style={{
background: isActive ? 'rgba(6,182,212,0.18)' : 'rgba(255,255,255,0.05)',
color: isActive ? '#06b6d4' : '#64748b',
@ -1205,13 +1205,13 @@ const UserManualPopup = ({ isOpen, onClose }: UserManualPopupProps) => {
</span>
<div className="min-w-0">
<div
className="text-[12px] font-medium leading-tight truncate"
className="text-label-1 font-medium leading-tight truncate"
style={{ color: isActive ? '#06b6d4' : '#cbd5e1' }}
>
{chapter.title}
</div>
<div
className="text-[10px] leading-tight mt-0.5 truncate"
className="text-caption leading-tight mt-0.5 truncate"
style={{ color: '#475569' }}
>
{chapter.subtitle}
@ -1230,7 +1230,7 @@ const UserManualPopup = ({ isOpen, onClose }: UserManualPopupProps) => {
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<span
className="text-[11px] font-mono px-2 py-0.5 rounded font-bold"
className="text-label-2 font-mono px-2 py-0.5 rounded font-bold"
style={{
background: 'rgba(6,182,212,0.12)',
color: '#06b6d4',
@ -1239,20 +1239,20 @@ const UserManualPopup = ({ isOpen, onClose }: UserManualPopupProps) => {
>
CH {selectedChapter.number}
</span>
<h2 className="text-[16px] font-semibold" style={{ color: '#e2e8f0' }}>
<h2 className="text-title-2 font-semibold" style={{ color: '#e2e8f0' }}>
{selectedChapter.title}
</h2>
<span className="text-[12px]" style={{ color: '#475569' }}>
<span className="text-label-1" style={{ color: '#475569' }}>
{selectedChapter.subtitle}
</span>
</div>
<div className="flex items-center gap-2">
<span className="text-[11px] mr-1" style={{ color: '#64748b' }}>
<span className="text-label-2 mr-1" style={{ color: '#64748b' }}>
{selectedChapter.screens.length}
</span>
<button
onClick={allExpanded ? collapseAll : expandAll}
className="text-[11px] px-3 py-1 rounded transition-colors"
className="text-label-2 px-3 py-1 rounded transition-colors"
style={{
background: 'rgba(6,182,212,0.08)',
color: '#06b6d4',
@ -1298,7 +1298,7 @@ const UserManualPopup = ({ isOpen, onClose }: UserManualPopupProps) => {
}}
>
<span
className="flex-shrink-0 text-[10px] font-mono font-bold px-1.5 py-0.5 rounded"
className="flex-shrink-0 text-caption font-mono font-bold px-1.5 py-0.5 rounded"
style={{
background: 'rgba(6,182,212,0.1)',
color: '#06b6d4',
@ -1310,13 +1310,13 @@ const UserManualPopup = ({ isOpen, onClose }: UserManualPopupProps) => {
{screen.id}
</span>
<span
className="flex-1 text-[13px] font-medium"
className="flex-1 text-title-4 font-medium"
style={{ color: '#cbd5e1' }}
>
{screen.name}
</span>
<span
className="flex-shrink-0 text-[10px] font-mono"
className="flex-shrink-0 text-caption font-mono"
style={{
color: '#475569',
transition: 'transform 0.2s',
@ -1346,14 +1346,17 @@ const UserManualPopup = ({ isOpen, onClose }: UserManualPopupProps) => {
display: 'block',
}}
/>
<p className="mt-1 text-[10px] text-right" style={{ color: '#475569' }}>
<p
className="mt-1 text-caption text-right"
style={{ color: '#475569' }}
>
</p>
</div>
{/* Menu path breadcrumb */}
<div
className="mb-3 text-[11px] font-mono px-2 py-1 rounded inline-block"
className="mb-3 text-label-2 font-mono px-2 py-1 rounded inline-block"
style={{
background: 'rgba(71,85,105,0.15)',
color: '#64748b',
@ -1365,7 +1368,10 @@ const UserManualPopup = ({ isOpen, onClose }: UserManualPopupProps) => {
{/* Overview */}
<div className="mt-2">
<p className="text-[12px] leading-relaxed" style={{ color: '#94a3b8' }}>
<p
className="text-label-1 leading-relaxed"
style={{ color: '#94a3b8' }}
>
{screen.overview}
</p>
</div>
@ -1380,13 +1386,13 @@ const UserManualPopup = ({ isOpen, onClose }: UserManualPopupProps) => {
}}
>
<div
className="text-[11px] font-semibold mb-1.5 uppercase tracking-wide"
className="text-label-2 font-semibold mb-1.5 uppercase tracking-wide"
style={{ color: '#475569' }}
>
</div>
<p
className="text-[12px] leading-relaxed"
className="text-label-1 leading-relaxed"
style={{ color: '#7f8ea3' }}
>
{screen.description}
@ -1398,7 +1404,7 @@ const UserManualPopup = ({ isOpen, onClose }: UserManualPopupProps) => {
{screen.procedure && screen.procedure.length > 0 && (
<div className="mt-4">
<div
className="text-[11px] font-semibold mb-2 uppercase tracking-wide"
className="text-label-2 font-semibold mb-2 uppercase tracking-wide"
style={{ color: '#475569' }}
>
@ -1407,7 +1413,7 @@ const UserManualPopup = ({ isOpen, onClose }: UserManualPopupProps) => {
{screen.procedure.map((step, idx) => (
<li key={idx} className="flex items-start gap-2.5">
<span
className="flex-shrink-0 w-5 h-5 rounded-full flex items-center justify-center text-[10px] font-bold mt-0.5"
className="flex-shrink-0 w-5 h-5 rounded-full flex items-center justify-center text-caption font-bold mt-0.5"
style={{
background: 'rgba(6,182,212,0.12)',
color: '#06b6d4',
@ -1417,7 +1423,7 @@ const UserManualPopup = ({ isOpen, onClose }: UserManualPopupProps) => {
{idx + 1}
</span>
<span
className="text-[12px] leading-relaxed"
className="text-label-1 leading-relaxed"
style={{ color: '#94a3b8' }}
>
{step}
@ -1432,7 +1438,7 @@ const UserManualPopup = ({ isOpen, onClose }: UserManualPopupProps) => {
{screen.inputs && screen.inputs.length > 0 && (
<div className="mt-4">
<div
className="text-[11px] font-semibold mb-2 uppercase tracking-wide"
className="text-label-2 font-semibold mb-2 uppercase tracking-wide"
style={{ color: '#475569' }}
>
@ -1441,7 +1447,7 @@ const UserManualPopup = ({ isOpen, onClose }: UserManualPopupProps) => {
className="rounded overflow-hidden"
style={{ border: '1px solid #1e2a45' }}
>
<table className="w-full text-[12px]">
<table className="w-full text-label-1">
<thead>
<tr style={{ background: '#0f1729' }}>
<th
@ -1494,7 +1500,7 @@ const UserManualPopup = ({ isOpen, onClose }: UserManualPopupProps) => {
<td className="px-3 py-2">
{input.required ? (
<span
className="text-[10px] font-bold px-1.5 py-0.5 rounded"
className="text-caption font-bold px-1.5 py-0.5 rounded"
style={{
background: 'rgba(239,68,68,0.1)',
color: '#f87171',
@ -1505,7 +1511,7 @@ const UserManualPopup = ({ isOpen, onClose }: UserManualPopupProps) => {
</span>
) : (
<span
className="text-[10px] px-1.5 py-0.5 rounded"
className="text-caption px-1.5 py-0.5 rounded"
style={{
background: 'rgba(100,116,139,0.1)',
color: '#64748b',
@ -1531,7 +1537,7 @@ const UserManualPopup = ({ isOpen, onClose }: UserManualPopupProps) => {
{screen.notes && screen.notes.length > 0 && (
<div className="mt-4">
<div
className="text-[11px] font-semibold mb-2 uppercase tracking-wide"
className="text-label-2 font-semibold mb-2 uppercase tracking-wide"
style={{ color: '#475569' }}
>
@ -1544,7 +1550,7 @@ const UserManualPopup = ({ isOpen, onClose }: UserManualPopupProps) => {
style={{ background: '#f59e0b' }}
/>
<span
className="text-[12px] leading-relaxed"
className="text-label-1 leading-relaxed"
style={{ color: '#94a3b8' }}
>
{note}
@ -1590,7 +1596,7 @@ const UserManualPopup = ({ isOpen, onClose }: UserManualPopupProps) => {
/>
<button
onClick={() => setLightboxSrc(null)}
className="absolute top-2 right-2 w-8 h-8 rounded flex items-center justify-center text-[13px] font-bold"
className="absolute top-2 right-2 w-8 h-8 rounded flex items-center justify-center text-title-4 font-bold"
style={{
background: 'rgba(15,23,41,0.85)',
color: '#94a3b8',

파일 보기

@ -1,10 +1,6 @@
import type { StyleSpecification } from 'maplibre-gl';
import { useMapStore } from '@common/store/mapStore';
import {
LIGHT_STYLE,
SATELLITE_3D_STYLE,
ENC_EMPTY_STYLE,
} from '@common/components/map/mapStyles';
import { LIGHT_STYLE, SATELLITE_3D_STYLE, ENC_EMPTY_STYLE } from '@common/components/map/mapStyles';
export function useBaseMapStyle(): StyleSpecification {
const mapToggles = useMapStore((s) => s.mapToggles);

파일 보기

@ -1,6 +1,6 @@
import { create } from 'zustand'
import { api } from '../services/api'
import { haversineDistance, polygonAreaKm2 } from '../utils/geo'
import { create } from 'zustand';
import { api } from '../services/api';
import { haversineDistance, polygonAreaKm2 } from '../utils/geo';
export interface MapTypeItem {
mapKey: string;
@ -46,11 +46,11 @@ interface MapState {
}
const DEFAULT_MAP_TYPES: MapTypeItem[] = [
{ mapKey: 's57', mapNm: 'S-57 전자해도', mapLevelCd: 'S-57' },
{ mapKey: 's101', mapNm: 'S-101 전자해도', mapLevelCd: 'S-101' },
{ mapKey: 'threeD', mapNm: '3D 지도', mapLevelCd: '3D' },
{ mapKey: 'satellite', mapNm: '위성 영상', mapLevelCd: 'SAT' },
]
{ mapKey: 's57', mapNm: 'S-57 전자해도', mapLevelCd: 'S-57' },
{ mapKey: 's101', mapNm: 'S-101 전자해도', mapLevelCd: 'S-101' },
{ mapKey: 'threeD', mapNm: '3D 지도', mapLevelCd: '3D' },
{ mapKey: 'satellite', mapNm: '위성 영상', mapLevelCd: 'SAT' },
];
let measureIdCounter = 0;
@ -67,17 +67,17 @@ export const useMapStore = create<MapState>((set, get) => ({
}),
loadMapTypes: async () => {
try {
const res = await api.get<MapTypeItem[]>('/map-base/active')
const types = res.data
const current = get().mapToggles
const newToggles: Partial<MapToggles> = {}
const res = await api.get<MapTypeItem[]>('/map-base/active');
const types = res.data;
const current = get().mapToggles;
const newToggles: Partial<MapToggles> = {};
for (const t of types) {
if (t.mapKey in current) {
newToggles[t.mapKey as keyof MapToggles] = current[t.mapKey as keyof MapToggles] ?? false
newToggles[t.mapKey as keyof MapToggles] = current[t.mapKey as keyof MapToggles] ?? false;
}
}
// 모든 토글 기본 off (기본지도 표시)
set({ mapTypes: types, mapToggles: { ...current, ...newToggles } })
set({ mapTypes: types, mapToggles: { ...current, ...newToggles } });
} catch {
// API 실패 시 fallback 유지
}
@ -88,8 +88,7 @@ export const useMapStore = create<MapState>((set, get) => ({
measureInProgress: [],
measurements: [],
setMeasureMode: (mode) =>
set({ measureMode: mode, measureInProgress: [] }),
setMeasureMode: (mode) => set({ measureMode: mode, measureInProgress: [] }),
addMeasurePoint: (pt) => {
const { measureMode, measureInProgress } = get();
@ -99,7 +98,10 @@ export const useMapStore = create<MapState>((set, get) => ({
const dist = haversineDistance(next[0], next[1]);
const id = `measure-${++measureIdCounter}`;
set((s) => ({
measurements: [...s.measurements, { id, mode: 'distance', points: [next[0], next[1]], value: dist }],
measurements: [
...s.measurements,
{ id, mode: 'distance', points: [next[0], next[1]], value: dist },
],
measureInProgress: [],
}));
} else {
@ -116,7 +118,10 @@ export const useMapStore = create<MapState>((set, get) => ({
const area = polygonAreaKm2(measureInProgress);
const id = `measure-${++measureIdCounter}`;
set((s) => ({
measurements: [...s.measurements, { id, mode: 'area', points: [...measureInProgress], value: area }],
measurements: [
...s.measurements,
{ id, mode: 'area', points: [...measureInProgress], value: area },
],
measureInProgress: [],
}));
},
@ -124,6 +129,5 @@ export const useMapStore = create<MapState>((set, get) => ({
removeMeasurement: (id) =>
set((s) => ({ measurements: s.measurements.filter((m) => m.id !== id) })),
clearAllMeasurements: () =>
set({ measurements: [], measureInProgress: [], measureMode: null }),
}))
clearAllMeasurements: () => set({ measurements: [], measureInProgress: [], measureMode: null }),
}));

파일 보기

@ -77,6 +77,7 @@
--font-size-heading-2: 1.5rem;
--font-size-heading-3: 1.375rem;
--font-size-title-1: 1.125rem;
--font-size-subtitle: 0.9375rem;
--font-size-title-2: 1rem;
--font-size-title-3: 0.875rem;
--font-size-title-4: 0.8125rem;

파일 보기

@ -41,34 +41,34 @@
}
.wing-section-header {
@apply text-[13px] font-bold font-korean mb-2;
@apply text-title-4 font-bold font-korean mb-2;
}
.wing-section-desc {
@apply text-[10px] font-korean leading-relaxed;
@apply text-caption font-korean leading-relaxed;
color: var(--fg-disabled);
}
/* ── Typography ── */
.wing-title {
@apply text-[15px] font-bold font-korean;
@apply text-subtitle font-bold font-korean;
}
.wing-subtitle {
@apply text-[10px] font-korean mt-0.5;
@apply text-caption font-korean mt-0.5;
color: var(--fg-disabled);
}
.wing-label {
@apply text-[11px] font-semibold font-korean;
@apply text-label-2 font-semibold font-korean;
}
.wing-value {
@apply text-[11px] font-mono font-semibold;
@apply text-label-2 font-mono font-semibold;
}
.wing-meta {
@apply text-[9px] font-korean;
@apply text-caption font-korean;
color: var(--fg-disabled);
}
@ -83,12 +83,12 @@
/* ── Badge ── */
.wing-badge {
@apply inline-flex items-center px-2 py-0.5 rounded text-[9px] font-bold font-korean;
@apply inline-flex items-center px-2 py-0.5 rounded text-caption font-bold font-korean;
}
/* ── Button ── */
.wing-btn {
@apply px-3 py-1.5 rounded-sm text-[11px] font-semibold cursor-pointer font-korean border-none;
@apply px-3 py-1.5 rounded-sm text-label-2 font-semibold cursor-pointer font-korean border-none;
transition: all 0.15s;
}
@ -134,7 +134,7 @@
/* ── Input ── */
.wing-input {
@apply w-full rounded-sm text-[11px] font-korean outline-none;
@apply w-full rounded-sm text-label-2 font-korean outline-none;
padding: 6px 10px;
background: var(--bg-base);
border: 1px solid var(--stroke-default);
@ -151,7 +151,7 @@
/* ── Table ── */
.wing-table {
@apply w-full text-[10px] font-korean;
@apply w-full text-caption font-korean;
border-collapse: collapse;
}
@ -232,11 +232,11 @@
}
.wing-kv-label {
@apply text-[10px] font-korean;
@apply text-caption font-korean;
color: var(--fg-disabled);
}
.wing-kv-value {
@apply text-[11px] font-semibold font-mono;
@apply text-label-2 font-semibold font-mono;
}
}

파일 보기

@ -30,13 +30,13 @@ export const ComponentsContent = () => {
style={{ opacity: 0.4 }}
>
<span
className="text-[#64748b] font-sans text-[10px] leading-[15px] font-bold uppercase"
className="text-[#64748b] font-sans text-caption leading-[15px] font-bold uppercase"
style={{ letterSpacing: '1px' }}
>
© 2024 WING-OPS
</span>
<span
className="text-[#22d3ee] font-korean text-[10px] leading-[15px] font-medium uppercase"
className="text-[#22d3ee] font-korean text-caption leading-[15px] font-medium uppercase"
style={{ letterSpacing: '1px' }}
>
v2.4

파일 보기

@ -29,13 +29,12 @@ const ButtonsThumbnail = ({ isDark }: { isDark: boolean }) => {
{buttons.map(({ label, bg, border, color }) => (
<div
key={label}
className="w-full rounded flex items-center justify-center"
className="w-full rounded flex items-center justify-center text-label-1"
style={{
height: '32px',
backgroundColor: bg,
border: `1.5px solid ${border}`,
color,
fontSize: '12px',
fontWeight: 600,
}}
>
@ -112,6 +111,109 @@ const TextInputsThumbnail = ({ isDark }: { isDark: boolean }) => {
);
};
const FloatThumbnail = ({ isDark }: { isDark: boolean }) => {
const backdropBg = isDark ? 'rgba(0,0,0,0.40)' : 'rgba(0,0,0,0.18)';
const dialogBg = isDark ? '#1a2236' : '#ffffff';
const dialogBorder = isDark ? 'rgba(66,71,84,0.30)' : '#e2e8f0';
const accent = isDark ? '#4cd7f6' : '#06b6d4';
const lineBg = isDark ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.06)';
const successGreen = '#22c55e';
return (
<div className="w-full h-full flex flex-col items-center justify-center gap-3 px-6">
{/* 미니 모달 */}
<div
className="w-full rounded-md flex items-center justify-center"
style={{ height: '60px', backgroundColor: backdropBg }}
>
<div
className="rounded"
style={{
width: '80%',
height: '40px',
backgroundColor: dialogBg,
border: `1px solid ${dialogBorder}`,
borderTop: `2px solid ${accent}`,
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
gap: '4px',
padding: '6px 8px',
}}
>
<div
className="rounded"
style={{ height: '5px', width: '50%', backgroundColor: lineBg }}
/>
<div
className="rounded"
style={{ height: '5px', width: '70%', backgroundColor: lineBg }}
/>
</div>
</div>
{/* 미니 드롭다운 */}
<div className="w-full flex flex-col items-start gap-0.5" style={{ paddingLeft: '8px' }}>
<div
className="rounded"
style={{
width: '60%',
height: '16px',
backgroundColor: isDark ? 'rgba(255,255,255,0.07)' : '#e2e8f0',
border: `1px solid ${dialogBorder}`,
}}
/>
<div
className="rounded"
style={{
width: '60%',
height: '28px',
backgroundColor: dialogBg,
border: `1px solid ${dialogBorder}`,
display: 'flex',
flexDirection: 'column',
gap: '2px',
padding: '3px 6px',
}}
>
<div
className="rounded"
style={{ height: '4px', width: '80%', backgroundColor: accent, opacity: 0.5 }}
/>
<div
className="rounded"
style={{ height: '4px', width: '60%', backgroundColor: lineBg }}
/>
<div
className="rounded"
style={{ height: '4px', width: '70%', backgroundColor: lineBg }}
/>
</div>
</div>
{/* 미니 토스트 */}
<div className="w-full flex justify-end" style={{ paddingRight: '8px' }}>
<div
className="rounded"
style={{
width: '55%',
height: '16px',
backgroundColor: dialogBg,
border: `1px solid ${dialogBorder}`,
borderLeft: `3px solid ${successGreen}`,
display: 'flex',
alignItems: 'center',
paddingLeft: '6px',
}}
>
<div
className="rounded"
style={{ height: '4px', width: '60%', backgroundColor: lineBg }}
/>
</div>
</div>
</div>
);
};
// ---------- 카드 정의 ----------
const OVERVIEW_CARDS: OverviewCard[] = [
@ -125,6 +227,11 @@ const OVERVIEW_CARDS: OverviewCard[] = [
label: 'Text Field',
thumbnail: (isDark) => <TextInputsThumbnail isDark={isDark} />,
},
{
id: 'float',
label: 'Float',
thumbnail: (isDark) => <FloatThumbnail isDark={isDark} />,
},
];
// ---------- Props ----------

파일 보기

@ -32,7 +32,7 @@ const SectionTitle = ({ num, title, sub, rightNode, theme }: SectionTitleProps)
</div>
{sub && (
<p
className="font-mono text-[10px] leading-[15px] uppercase"
className="font-mono text-caption leading-[15px] uppercase"
style={{ letterSpacing: theme.sectionSubSpacing, color: theme.sectionSub }}
>
{sub}
@ -46,7 +46,7 @@ const TYPO_ROWS: TypoRow[] = [
{
size: '9px / Meta',
sampleNode: (t) => (
<span className="font-korean text-[9px]" style={{ color: t.typoSampleText }}>
<span className="font-korean text-caption" style={{ color: t.typoSampleText }}>
Meta info
</span>
),
@ -55,7 +55,7 @@ const TYPO_ROWS: TypoRow[] = [
{
size: '10px / Table',
sampleNode: (t) => (
<span className="font-korean text-[10px]" style={{ color: t.typoSampleText }}>
<span className="font-korean text-caption" style={{ color: t.typoSampleText }}>
Table data
</span>
),
@ -68,7 +68,7 @@ const TYPO_ROWS: TypoRow[] = [
className="inline-flex items-center rounded-md border border-solid py-2 px-4"
style={{ backgroundColor: t.typoActionBg, borderColor: t.typoActionBorder }}
>
<span className="font-korean text-[11px] font-medium" style={{ color: t.typoActionText }}>
<span className="font-korean text-label-2 font-medium" style={{ color: t.typoActionText }}>
/ Input/Button text
</span>
</span>
@ -78,7 +78,7 @@ const TYPO_ROWS: TypoRow[] = [
{
size: '13px / Header',
sampleNode: (t) => (
<span className="font-korean text-[13px] font-bold" style={{ color: t.textPrimary }}>
<span className="font-korean text-title-4 font-bold" style={{ color: t.textPrimary }}>
Section Header
</span>
),
@ -87,7 +87,7 @@ const TYPO_ROWS: TypoRow[] = [
{
size: '15px / Title',
sampleNode: (t) => (
<span className="font-korean text-[15px] font-bold" style={{ color: t.textPrimary }}>
<span className="font-korean text-subtitle font-bold" style={{ color: t.textPrimary }}>
Panel Title
</span>
),
@ -97,10 +97,10 @@ const TYPO_ROWS: TypoRow[] = [
size: 'Data / Mono',
sampleNode: (t) => (
<div className="flex flex-col gap-1">
<span className="font-mono text-[11px]" style={{ color: t.typoDataText }}>
<span className="font-mono text-label-2" style={{ color: t.typoDataText }}>
1,234.56 km²
</span>
<span className="font-mono text-[11px]" style={{ color: t.typoCoordText }}>
<span className="font-mono text-label-2" style={{ color: t.typoCoordText }}>
35° 06' 12&quot; N
</span>
</div>
@ -202,7 +202,7 @@ export const DesignContent = ({ theme }: DesignContentProps) => {
{item.hex}
</span>
<span
className="font-korean text-[11px] leading-[16.5px]"
className="font-korean text-label-2 leading-[16.5px]"
style={{ color: t.textSecondary }}
>
{item.desc}
@ -229,7 +229,7 @@ export const DesignContent = ({ theme }: DesignContentProps) => {
boxShadow: t.borderCardShadow,
}}
>
<span className="font-mono text-[10px]" style={{ color: t.textAccent }}>
<span className="font-mono text-caption" style={{ color: t.textAccent }}>
{item.token}
</span>
<span
@ -259,7 +259,7 @@ export const DesignContent = ({ theme }: DesignContentProps) => {
>
{t.textTokens.map((item) => (
<div key={item.token} className="flex flex-col gap-[3px]">
<span className="font-mono text-[10px]" style={{ color: t.textAccent }}>
<span className="font-mono text-caption" style={{ color: t.textAccent }}>
{item.token}
</span>
<span className={item.sampleClass}>{item.sampleText}</span>
@ -302,7 +302,7 @@ export const DesignContent = ({ theme }: DesignContentProps) => {
>
{item.name}
</span>
<span className="font-mono text-[10px]" style={{ color: t.textMuted }}>
<span className="font-mono text-caption" style={{ color: t.textMuted }}>
{item.token} / {item.color}
</span>
</div>
@ -316,7 +316,7 @@ export const DesignContent = ({ theme }: DesignContentProps) => {
}}
>
<span
className="font-korean text-[11px] font-medium"
className="font-korean text-label-2 font-medium"
style={{ color: item.badgeText }}
>
{item.badge}
@ -348,12 +348,12 @@ export const DesignContent = ({ theme }: DesignContentProps) => {
}}
/>
<span
className="font-korean text-[13px] font-bold flex-1"
className="font-korean text-title-4 font-bold flex-1"
style={{ color: item.color }}
>
{item.label}
</span>
<span className="font-mono text-[10px] opacity-40" style={{ color: item.color }}>
<span className="font-mono text-caption opacity-40" style={{ color: item.color }}>
{item.hex}
</span>
</div>
@ -370,7 +370,7 @@ export const DesignContent = ({ theme }: DesignContentProps) => {
rightNode={
<div className="flex flex-row gap-2 items-center">
<span
className="rounded-sm py-0.5 px-2 font-korean text-[10px] font-bold"
className="rounded-sm py-0.5 px-2 font-korean text-caption font-bold"
style={{ backgroundColor: t.fontBadgePrimaryBg, color: t.fontBadgePrimaryText }}
>
PretendardGOV
@ -391,7 +391,7 @@ export const DesignContent = ({ theme }: DesignContentProps) => {
style={{ textAlign: i === 2 ? 'right' : 'left', borderColor: t.tableRowBorder }}
>
<span
className="font-mono text-[10px] font-medium uppercase"
className="font-mono text-caption font-medium uppercase"
style={{ letterSpacing: '1px', color: t.textMuted }}
>
{col}
@ -412,7 +412,7 @@ export const DesignContent = ({ theme }: DesignContentProps) => {
>
{/* Size */}
<div className="flex-1 py-4 px-8">
<span className="font-mono text-[10px]" style={{ color: t.typoSizeText }}>
<span className="font-mono text-caption" style={{ color: t.typoSizeText }}>
{row.size}
</span>
</div>
@ -420,7 +420,7 @@ export const DesignContent = ({ theme }: DesignContentProps) => {
<div className="flex-1 py-4 px-8">{row.sampleNode(t)}</div>
{/* Properties */}
<div className="flex-1 py-4 px-8 text-right" style={{ opacity: 0.5 }}>
<span className="font-mono text-[10px]" style={{ color: t.typoPropertiesText }}>
<span className="font-mono text-caption" style={{ color: t.typoPropertiesText }}>
{row.properties}
</span>
</div>
@ -447,7 +447,7 @@ export const DesignContent = ({ theme }: DesignContentProps) => {
}}
>
<span
className="font-korean text-[10px] font-bold uppercase"
className="font-korean text-caption font-bold uppercase"
style={{ letterSpacing: '1px', color: t.textAccent }}
>
Small Elements
@ -475,7 +475,7 @@ export const DesignContent = ({ theme }: DesignContentProps) => {
}}
>
<span
className="font-korean text-[10px] font-bold uppercase"
className="font-korean text-caption font-bold uppercase"
style={{ letterSpacing: '1px', color: t.textAccent }}
>
Structural Panels
@ -503,7 +503,7 @@ export const DesignContent = ({ theme }: DesignContentProps) => {
{['Precision Engineering', 'Safety Compliant', 'Optimized v8.42'].map((label) => (
<span
key={label}
className="font-mono text-[10px] uppercase"
className="font-mono text-caption uppercase"
style={{ letterSpacing: '1px', color: t.footerText }}
>
{label}
@ -513,13 +513,13 @@ export const DesignContent = ({ theme }: DesignContentProps) => {
{/* 우측 */}
<div className="flex flex-row gap-2 items-center">
<span
className="font-mono text-[10px] uppercase"
className="font-mono text-caption uppercase"
style={{ letterSpacing: '1px', color: t.footerText }}
>
Generated for Terminal:
</span>
<span
className="font-mono text-[10px] font-medium uppercase"
className="font-mono text-caption font-medium uppercase"
style={{ letterSpacing: '1px', color: t.footerAccent }}
>
1440x900_PR_MKT

파일 보기

@ -50,10 +50,10 @@ export const DesignHeader = ({
}}
>
<span
className="font-sans text-[10px] leading-[15px] uppercase"
className="font-sans text-caption leading-[15px] uppercase"
style={{ letterSpacing: '2px', color: theme.textMuted }}
>
Design System v1.0
Design System v1.1
</span>
</div>
</div>

파일 보기

@ -13,6 +13,7 @@ import FoundationsOverview from './FoundationsOverview';
import ComponentsOverview from './ComponentsOverview';
import { ButtonContent } from './ButtonContent';
import { TextFieldContent } from './TextFieldContent';
import { FloatContent } from './FloatContent';
import { getTheme } from './designTheme';
import type { ThemeMode } from './designTheme';
@ -69,6 +70,8 @@ export const DesignPage = () => {
return <ButtonContent theme={theme} />;
case 'text-field':
return <TextFieldContent theme={theme} />;
case 'float':
return <FloatContent theme={theme} />;
default:
return <ComponentsContent />;
}

파일 보기

@ -2,7 +2,7 @@ import type { DesignTheme } from './designTheme';
import type { DesignTab } from './DesignHeader';
export type FoundationsMenuItemId = 'overview' | 'color' | 'typography' | 'radius' | 'layout';
export type ComponentsMenuItemId = 'overview' | 'buttons' | 'text-field';
export type ComponentsMenuItemId = 'overview' | 'buttons' | 'text-field' | 'float';
export type MenuItemId = FoundationsMenuItemId | ComponentsMenuItemId;
interface MenuItem {
@ -22,6 +22,7 @@ const COMPONENTS_MENU: MenuItem[] = [
{ id: 'overview', label: 'Overview' },
{ id: 'buttons', label: 'Buttons' },
{ id: 'text-field', label: 'Text Field' },
{ id: 'float', label: 'Float' },
];
const SIDEBAR_CONFIG: Record<DesignTab, { title: string; subtitle: string; menu: MenuItem[] }> = {

파일 보기

@ -0,0 +1,100 @@
// FloatContent.tsx — Float 서브탭 래퍼
import { useState } from 'react';
import type { DesignTheme } from './designTheme';
import { FloatModalContent } from './float/FloatModalContent';
import { FloatDropdownContent } from './float/FloatDropdownContent';
import { FloatOverlayContent } from './float/FloatOverlayContent';
import { FloatToastContent } from './float/FloatToastContent';
type FloatSubTab = 'modal' | 'dropdown' | 'overlay' | 'toast';
const SUB_TABS: { id: FloatSubTab; label: string; desc: string }[] = [
{ id: 'modal', label: 'Modal', desc: 'Dialog · Confirm' },
{ id: 'dropdown', label: 'Dropdown', desc: 'ComboBox · Select' },
{ id: 'overlay', label: 'Overlay', desc: 'Map Layer · Popup' },
{ id: 'toast', label: 'Toast', desc: 'Notification · Alert' },
];
interface FloatContentProps {
theme: DesignTheme;
}
export const FloatContent = ({ theme }: FloatContentProps) => {
const [activeSubTab, setActiveSubTab] = useState<FloatSubTab>('modal');
const t = theme;
const isDark = t.mode === 'dark';
const renderSubContent = () => {
switch (activeSubTab) {
case 'modal':
return <FloatModalContent theme={t} />;
case 'dropdown':
return <FloatDropdownContent theme={t} />;
case 'overlay':
return <FloatOverlayContent theme={t} />;
case 'toast':
return <FloatToastContent theme={t} />;
}
};
return (
<div className="flex flex-col h-full">
{/* 서브탭 헤더 */}
<div
className="px-8 pt-6 pb-0 border-b border-solid shrink-0"
style={{ borderColor: isDark ? 'rgba(66,71,84,0.20)' : '#e2e8f0' }}
>
<div className="flex flex-col gap-4">
<div>
<h1 className="font-sans text-2xl leading-8 font-bold" style={{ color: t.textPrimary }}>
Float
</h1>
<p className="font-korean text-sm leading-5 mt-1" style={{ color: t.textSecondary }}>
UI Modal, Dropdown, Overlay, Toast
</p>
</div>
{/* 서브탭 바 */}
<nav className="flex flex-row gap-1">
{SUB_TABS.map(({ id, label, desc }) => {
const isActive = activeSubTab === id;
return (
<button
key={id}
type="button"
onClick={() => setActiveSubTab(id)}
className="flex flex-col items-start px-4 pb-3 pt-1 cursor-pointer bg-transparent relative"
style={{
borderBottom: isActive ? `2px solid ${t.textAccent}` : '2px solid transparent',
}}
>
<span
className="font-sans text-sm font-bold leading-5"
style={{ color: isActive ? t.textAccent : t.textMuted }}
>
{label}
</span>
<span
className="font-mono text-caption leading-4"
style={{
color: isActive ? t.textAccent : t.textMuted,
opacity: isActive ? 0.7 : 0.5,
}}
>
{desc}
</span>
</button>
);
})}
</nav>
</div>
</div>
{/* 서브탭 콘텐츠 */}
<div className="flex-1 overflow-y-auto">{renderSubContent()}</div>
</div>
);
};
export default FloatContent;

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다. Load Diff

파일 보기

@ -118,7 +118,7 @@ export const RadiusContent = ({ theme }: RadiusContentProps) => {
{(['이름', '값', 'Preview'] as const).map((col) => (
<div key={col} className="py-3 px-4">
<span
className="font-mono text-[10px] font-medium uppercase"
className="font-mono text-caption font-medium uppercase"
style={{ letterSpacing: '1px', color: t.textMuted }}
>
{col}
@ -153,7 +153,7 @@ export const RadiusContent = ({ theme }: RadiusContentProps) => {
</span>
{token.isCustom && (
<span
className="font-mono text-[9px] rounded px-1.5 py-0.5"
className="font-mono text-caption rounded px-1.5 py-0.5"
style={{
color: isDark ? '#f97316' : '#c2410c',
backgroundColor: isDark ? 'rgba(249,115,22,0.10)' : 'rgba(249,115,22,0.08)',
@ -166,7 +166,7 @@ export const RadiusContent = ({ theme }: RadiusContentProps) => {
{/* 값 */}
<div className="py-4 px-4">
<span className="font-mono text-[11px]" style={{ color: t.textPrimary }}>
<span className="font-mono text-label-2" style={{ color: t.textPrimary }}>
{token.value}
</span>
</div>

파일 보기

@ -378,7 +378,7 @@ export const TypographyContent = ({ theme }: TypographyContentProps) => {
>
<span className="font-sans text-lg font-bold" style={{ color: t.textPrimary }}>{font.name}</span>
<span
className="font-mono text-[11px] rounded border border-solid px-2 py-0.5"
className="font-mono text-label-2 rounded border border-solid px-2 py-0.5"
style={{
color: t.textAccent,
backgroundColor: isDark ? 'rgba(6,182,212,0.05)' : 'rgba(6,182,212,0.08)',
@ -401,11 +401,11 @@ export const TypographyContent = ({ theme }: TypographyContentProps) => {
<p className="font-korean text-xs leading-5" style={{ color: t.textSecondary }}>{font.usage}</p>
<div className="flex flex-col gap-3 pt-2">
<div className="flex flex-col gap-1">
<span className="font-mono text-[9px] uppercase" style={{ letterSpacing: '1px', color: t.textMuted }}>Regular</span>
<span className="font-mono text-caption uppercase" style={{ letterSpacing: '1px', color: t.textMuted }}>Regular</span>
<span className="text-xl leading-7" style={{ color: t.textPrimary, fontWeight: 400 }}>{font.sampleText}</span>
</div>
<div className="flex flex-col gap-1">
<span className="font-mono text-[9px] uppercase" style={{ letterSpacing: '1px', color: t.textMuted }}>Bold</span>
<span className="font-mono text-caption uppercase" style={{ letterSpacing: '1px', color: t.textMuted }}>Bold</span>
<span className="text-xl leading-7 font-bold" style={{ color: t.textPrimary }}>{font.sampleText}</span>
</div>
</div>
@ -484,7 +484,7 @@ export const TypographyContent = ({ theme }: TypographyContentProps) => {
{row.letterSpacing}
</span>
<span
className="font-mono text-[10px] rounded border border-solid px-1.5 py-0.5 w-fit"
className="font-mono text-caption rounded border border-solid px-1.5 py-0.5 w-fit"
style={{
color: t.textAccent,
backgroundColor: isDark ? 'rgba(6,182,212,0.05)' : 'rgba(6,182,212,0.08)',
@ -515,7 +515,7 @@ export const TypographyContent = ({ theme }: TypographyContentProps) => {
>
{row.token}
</div>
<div className="font-mono text-[10px] mt-0.5" style={{ color: t.textMuted }}>
<div className="font-mono text-caption mt-0.5" style={{ color: t.textMuted }}>
{row.size} · {row.weight} · {row.lineHeight}
</div>
</div>
@ -767,7 +767,7 @@ export const TypographyContent = ({ theme }: TypographyContentProps) => {
{row.value}
</span>
<span
className="font-mono text-[10px] rounded border border-solid px-1.5 py-0.5 w-fit"
className="font-mono text-caption rounded border border-solid px-1.5 py-0.5 w-fit"
style={{
color: t.textAccent,
backgroundColor: isDark ? 'rgba(6,182,212,0.05)' : 'rgba(6,182,212,0.08)',

파일 보기

@ -19,7 +19,7 @@ const buttonRows: ButtonRow[] = [
'linear-gradient(120.41deg, rgba(6, 182, 212, 1) 0%, rgba(59, 130, 246, 1) 100%)',
}}
>
<div className="text-white text-center font-korean text-[11px] font-medium relative flex items-center justify-center">
<div className="text-white text-center font-korean text-label-2 font-medium relative flex items-center justify-center">
</div>
</div>
@ -33,7 +33,7 @@ const buttonRows: ButtonRow[] = [
boxShadow: '0px 0px 12px 0px rgba(6, 182, 212, 0.4)',
}}
>
<div className="text-white text-center font-korean text-[11px] font-medium relative flex items-center justify-center">
<div className="text-white text-center font-korean text-label-2 font-medium relative flex items-center justify-center">
</div>
</div>
@ -43,7 +43,7 @@ const buttonRows: ButtonRow[] = [
className="bg-[#334155] rounded-md pt-1.5 pr-3 pb-[7px] pl-3 flex flex-row gap-0 items-center justify-center shrink-0 relative"
style={{ opacity: 0.5 }}
>
<div className="text-[#64748b] text-center font-korean text-[11px] font-medium relative flex items-center justify-center">
<div className="text-[#64748b] text-center font-korean text-label-2 font-medium relative flex items-center justify-center">
</div>
</div>
@ -53,21 +53,21 @@ const buttonRows: ButtonRow[] = [
label: '세컨더리 (솔리드)',
defaultBtn: (
<div className="bg-[#1a2236] rounded-md border border-solid border-[#1e2a42] pt-1.5 pr-3 pb-[7px] pl-3 flex flex-row gap-0 items-center justify-center shrink-0 relative">
<div className="text-[#c0c8dc] text-center font-korean text-[11px] font-medium relative flex items-center justify-center">
<div className="text-[#c0c8dc] text-center font-korean text-label-2 font-medium relative flex items-center justify-center">
</div>
</div>
),
hoverBtn: (
<div className="bg-[#1e2844] rounded-md border border-solid border-[#1e2a42] pt-1.5 pr-3 pb-[7px] pl-3 flex flex-row gap-0 items-center justify-center shrink-0 relative">
<div className="text-[#c0c8dc] text-center font-korean text-[11px] font-medium relative flex items-center justify-center">
<div className="text-[#c0c8dc] text-center font-korean text-label-2 font-medium relative flex items-center justify-center">
</div>
</div>
),
disabledBtn: (
<div className="bg-[rgba(26,34,54,0.50)] rounded-md border border-solid border-[rgba(30,42,66,0.30)] pt-1.5 pr-3 pb-[7px] pl-3 flex flex-row gap-0 items-center justify-center shrink-0 relative">
<div className="text-[rgba(192,200,220,0.30)] text-center font-korean text-[11px] font-medium relative flex items-center justify-center">
<div className="text-[rgba(192,200,220,0.30)] text-center font-korean text-label-2 font-medium relative flex items-center justify-center">
</div>
</div>
@ -77,21 +77,21 @@ const buttonRows: ButtonRow[] = [
label: '아웃라인 (고스트)',
defaultBtn: (
<div className="rounded-md border border-solid border-[#1e2a42] pt-1.5 pr-3 pb-[7px] pl-3 flex flex-row gap-0 items-center justify-center shrink-0 relative">
<div className="text-[#c0c8dc] text-center font-korean text-[11px] font-medium relative flex items-center justify-center">
<div className="text-[#c0c8dc] text-center font-korean text-label-2 font-medium relative flex items-center justify-center">
</div>
</div>
),
hoverBtn: (
<div className="bg-[#1e2844] rounded-md border border-solid border-[#1e2a42] pt-1.5 pr-3 pb-[7px] pl-3 flex flex-row gap-0 items-center justify-center shrink-0 relative">
<div className="text-[#c0c8dc] text-center font-korean text-[11px] font-medium relative flex items-center justify-center">
<div className="text-[#c0c8dc] text-center font-korean text-label-2 font-medium relative flex items-center justify-center">
</div>
</div>
),
disabledBtn: (
<div className="rounded-md border border-solid border-[rgba(30,42,66,0.30)] pt-1.5 pr-3 pb-[7px] pl-3 flex flex-row gap-0 items-center justify-center shrink-0 relative">
<div className="text-[rgba(192,200,220,0.30)] text-center font-korean text-[11px] font-medium relative flex items-center justify-center">
<div className="text-[rgba(192,200,220,0.30)] text-center font-korean text-label-2 font-medium relative flex items-center justify-center">
</div>
</div>
@ -102,7 +102,7 @@ const buttonRows: ButtonRow[] = [
defaultBtn: (
<div className="bg-[rgba(59,130,246,0.08)] rounded-md border border-solid border-[rgba(59,130,246,0.30)] pt-1.5 pr-3 pb-1.5 pl-3 flex flex-row gap-2 items-center justify-start shrink-0 relative">
<img className="shrink-0 relative overflow-visible" src={pdfFileIcon} alt="PDF 아이콘" />
<div className="text-[#3b82f6] text-center font-korean text-[11px] font-medium relative flex items-center justify-center">
<div className="text-[#3b82f6] text-center font-korean text-label-2 font-medium relative flex items-center justify-center">
PDF
</div>
</div>
@ -113,7 +113,7 @@ const buttonRows: ButtonRow[] = [
style={{ boxShadow: '0px 0px 8px 0px rgba(59, 130, 246, 0.2)' }}
>
<img className="shrink-0 relative overflow-visible" src={pdfFileIcon} alt="PDF 아이콘" />
<div className="text-[#3b82f6] text-center font-korean text-[11px] font-medium relative flex items-center justify-center">
<div className="text-[#3b82f6] text-center font-korean text-label-2 font-medium relative flex items-center justify-center">
PDF
</div>
</div>
@ -125,7 +125,7 @@ const buttonRows: ButtonRow[] = [
src={pdfFileDisabledIcon}
alt="PDF 아이콘 (비활성)"
/>
<div className="text-[rgba(59,130,246,0.40)] text-center font-korean text-[11px] font-medium relative flex items-center justify-center">
<div className="text-[rgba(59,130,246,0.40)] text-center font-korean text-label-2 font-medium relative flex items-center justify-center">
PDF
</div>
</div>
@ -135,7 +135,7 @@ const buttonRows: ButtonRow[] = [
label: '경고 (삭제)',
defaultBtn: (
<div className="bg-[rgba(239,68,68,0.10)] rounded-md border border-solid border-[rgba(239,68,68,0.30)] pt-1.5 pr-3 pb-[7px] pl-3 flex flex-row gap-0 items-center justify-center shrink-0 relative">
<div className="text-[#ef4444] text-center font-korean text-[11px] font-medium relative flex items-center justify-center">
<div className="text-[#ef4444] text-center font-korean text-label-2 font-medium relative flex items-center justify-center">
</div>
</div>
@ -145,14 +145,14 @@ const buttonRows: ButtonRow[] = [
className="bg-[rgba(239,68,68,0.20)] rounded-md border border-solid border-[rgba(239,68,68,0.50)] pt-1.5 pr-3 pb-[7px] pl-3 flex flex-row gap-0 items-center justify-center shrink-0 relative"
style={{ boxShadow: '0px 0px 8px 0px rgba(239, 68, 68, 0.15)' }}
>
<div className="text-[#ef4444] text-center font-korean text-[11px] font-medium relative flex items-center justify-center">
<div className="text-[#ef4444] text-center font-korean text-label-2 font-medium relative flex items-center justify-center">
</div>
</div>
),
disabledBtn: (
<div className="bg-[rgba(239,68,68,0.05)] rounded-md border border-solid border-[rgba(239,68,68,0.15)] pt-1.5 pr-3 pb-[7px] pl-3 flex flex-row gap-0 items-center justify-center shrink-0 relative">
<div className="text-[rgba(239,68,68,0.40)] text-center font-korean text-[11px] font-medium relative flex items-center justify-center">
<div className="text-[rgba(239,68,68,0.40)] text-center font-korean text-label-2 font-medium relative flex items-center justify-center">
</div>
</div>

파일 보기

@ -32,7 +32,7 @@ export const CardSection = () => {
{/* 카드 헤더 */}
<div className="border-l-2 border-[#06b6d4] pl-3 flex flex-col gap-0 items-start justify-start self-stretch shrink-0 relative">
<div
className="text-[#64748b] text-left font-korean text-[10px] leading-[15px] font-medium uppercase relative flex items-center justify-start"
className="text-[#64748b] text-left font-korean text-caption leading-[15px] font-medium uppercase relative flex items-center justify-start"
style={{ letterSpacing: '1px' }}
>
@ -55,12 +55,12 @@ export const CardSection = () => {
</div>
<div className="flex flex-col gap-0 items-start justify-start flex-1 min-w-0 relative">
<div className="flex flex-col gap-0 items-start justify-start self-stretch shrink-0 relative">
<div className="text-[#dfe2f3] text-left font-korean text-[11px] leading-[16.5px] font-medium relative flex items-center justify-start">
<div className="text-[#dfe2f3] text-left font-korean text-label-2 leading-[16.5px] font-medium relative flex items-center justify-start">
{item.label}
</div>
</div>
<div className="flex flex-col gap-0 items-start justify-start self-stretch shrink-0 relative">
<div className="text-[#64748b] text-left font-sans text-[10px] leading-[15px] font-normal relative flex items-center justify-start">
<div className="text-[#64748b] text-left font-sans text-caption leading-[15px] font-normal relative flex items-center justify-start">
{item.progress}
</div>
</div>
@ -78,7 +78,7 @@ export const CardSection = () => {
'linear-gradient(97.29deg, rgba(6, 182, 212, 1) 0%, rgba(59, 130, 246, 1) 100%)',
}}
>
<div className="text-white text-center font-korean text-[11px] leading-[16.5px] font-medium relative flex items-center justify-center">
<div className="text-white text-center font-korean text-label-2 leading-[16.5px] font-medium relative flex items-center justify-center">
</div>
</div>
@ -108,7 +108,7 @@ export const CardSection = () => {
<div className="flex flex-row items-start justify-between self-stretch shrink-0 relative">
<div className="flex flex-col gap-[4.5px] items-start justify-start shrink-0 relative">
<div
className="text-[#22d3ee] text-left font-korean text-[10px] leading-[15px] font-medium uppercase relative flex items-center justify-start"
className="text-[#22d3ee] text-left font-korean text-caption leading-[15px] font-medium uppercase relative flex items-center justify-start"
style={{ letterSpacing: '1px' }}
>
@ -139,14 +139,14 @@ export const CardSection = () => {
<div className="flex flex-row items-center justify-between self-stretch shrink-0 relative">
{/* 정상 가동중 뱃지 */}
<div className="bg-[rgba(34,197,94,0.10)] rounded-xl pt-0.5 pr-2 pb-0.5 pl-2 flex flex-col gap-0 items-start justify-start shrink-0 relative">
<div className="text-[#22c55e] text-left font-korean text-[9px] leading-[13.5px] font-medium relative flex items-center justify-start">
<div className="text-[#22c55e] text-left font-korean text-caption leading-[13.5px] font-medium relative flex items-center justify-start">
</div>
</div>
{/* 대응팀 배치 아웃라인 버튼 */}
<div className="rounded-md border border-[#1e2a42] pt-1 pr-3 pb-1 pl-3 flex flex-col gap-0 items-center justify-center shrink-0 relative">
<div className="text-[#c0c8dc] text-center font-korean text-[10px] leading-[15px] font-medium relative flex items-center justify-center">
<div className="text-[#c0c8dc] text-center font-korean text-caption leading-[15px] font-medium relative flex items-center justify-center">
</div>
</div>

파일 보기

@ -85,7 +85,7 @@ export const IconBadgeSection = () => {
</div>
<div className="flex flex-col gap-0 items-start justify-start shrink-0 relative">
<div
className="text-[#64748b] text-left font-sans font-bold text-[10px] leading-[15px] uppercase relative flex items-center justify-start"
className="text-[#64748b] text-left font-sans font-bold text-caption leading-[15px] uppercase relative flex items-center justify-start"
style={{ letterSpacing: '0.9px' }}
>
{btn.label}
@ -97,7 +97,7 @@ export const IconBadgeSection = () => {
{/* 카드 푸터 */}
<div className="bg-[rgba(15,23,42,0.30)] pt-4 pr-6 pb-4 pl-6 flex flex-col gap-0 items-start justify-start self-stretch shrink-0 relative">
<div className="text-[#64748b] text-left font-sans text-[10px] leading-[15px] font-normal relative flex items-center justify-start">
<div className="text-[#64748b] text-left font-sans text-caption leading-[15px] font-normal relative flex items-center justify-start">
Standard dimensions: 36x36px with radius-md (6px)
</div>
</div>
@ -127,7 +127,7 @@ export const IconBadgeSection = () => {
<div className="flex flex-col gap-4 items-start justify-start self-stretch shrink-0 relative">
<div className="border-l-2 border-[#0e7490] pl-3 flex flex-col gap-0 items-start justify-start self-stretch shrink-0 relative">
<div
className="text-[#64748b] text-left font-sans font-bold text-[10px] leading-[15px] uppercase relative flex items-center justify-start"
className="text-[#64748b] text-left font-sans font-bold text-caption leading-[15px] uppercase relative flex items-center justify-start"
style={{ letterSpacing: '1px' }}
>
Operational Status
@ -141,7 +141,7 @@ export const IconBadgeSection = () => {
style={{ backgroundColor: badge.bg }}
>
<div
className="text-left font-korean text-[10px] leading-[15px] font-medium relative flex items-center justify-start"
className="text-left font-korean text-caption leading-[15px] font-medium relative flex items-center justify-start"
style={{ color: badge.color }}
>
{badge.label}
@ -155,7 +155,7 @@ export const IconBadgeSection = () => {
<div className="flex flex-col gap-4 items-start justify-start self-stretch shrink-0 relative">
<div className="border-l-2 border-[#0e7490] pl-3 flex flex-col gap-0 items-start justify-start self-stretch shrink-0 relative">
<div
className="text-[#64748b] text-left font-sans font-bold text-[10px] leading-[15px] uppercase relative flex items-center justify-start"
className="text-[#64748b] text-left font-sans font-bold text-caption leading-[15px] uppercase relative flex items-center justify-start"
style={{ letterSpacing: '1px' }}
>
Data Classification
@ -174,7 +174,7 @@ export const IconBadgeSection = () => {
></div>
<div className="flex flex-col gap-0 items-start justify-start shrink-0 relative">
<div
className="text-left font-sans font-bold text-[10px] leading-[15px] relative flex items-center justify-start"
className="text-left font-sans font-bold text-caption leading-[15px] relative flex items-center justify-start"
style={{ color: tag.color }}
>
{tag.label}

파일 보기

@ -321,21 +321,21 @@ export const DARK_THEME: DesignTheme = {
{
token: 'text-1',
sampleText: '주요 텍스트 Primary Text',
sampleClass: 'text-[#edf0f7] font-korean text-[15px] font-bold',
sampleClass: 'text-[#edf0f7] font-korean text-subtitle font-bold',
desc: 'Headings, active values, and primary labels.',
descColor: 'rgba(237,240,247,0.60)',
},
{
token: 'text-2',
sampleText: '보조 텍스트 Secondary Text',
sampleClass: 'text-[#c0c8dc] font-korean text-[15px] font-medium',
sampleClass: 'text-[#c0c8dc] font-korean text-subtitle font-medium',
desc: 'Supporting labels and secondary information.',
descColor: 'rgba(192,200,220,0.60)',
},
{
token: 'text-3',
sampleText: '비활성 텍스트 Muted Text',
sampleClass: 'text-[#9ba3b8] font-korean text-[15px]',
sampleClass: 'text-[#9ba3b8] font-korean text-subtitle',
desc: 'Disabled states, placeholders, and captions.',
descColor: 'rgba(155,163,184,0.60)',
},
@ -353,7 +353,7 @@ export const LIGHT_THEME: DesignTheme = {
headerBg: '#ffffff',
headerBorder: '#e2e8f0',
textPrimary: '#0f172a',
textPrimary: '#000000',
textSecondary: '#64748b',
textMuted: '#94a3b8',
textAccent: '#06b6d4',
@ -498,21 +498,21 @@ export const LIGHT_THEME: DesignTheme = {
{
token: 'text-1',
sampleText: '주요 텍스트 Primary Text',
sampleClass: 'text-[#0f172a] font-korean text-[15px] font-bold',
sampleClass: 'text-[#0f172a] font-korean text-subtitle font-bold',
desc: 'Headings, active values, and primary labels.',
descColor: '#64748b',
},
{
token: 'text-2',
sampleText: '보조 텍스트 Secondary Text',
sampleClass: 'text-[#475569] font-korean text-[15px] font-medium',
sampleClass: 'text-[#475569] font-korean text-subtitle font-medium',
desc: 'Supporting labels and secondary information.',
descColor: '#64748b',
},
{
token: 'text-3',
sampleText: '비활성 텍스트 Muted Text',
sampleClass: 'text-[#94a3b8] font-korean text-[15px]',
sampleClass: 'text-[#94a3b8] font-korean text-subtitle',
desc: 'Disabled states, placeholders, and captions.',
descColor: '#94a3b8',
},

파일 보기

@ -0,0 +1,440 @@
// FloatDropdownContent.tsx — Dropdown/ComboBox 카탈로그
import { useState } from 'react';
import type { DesignTheme } from '../designTheme';
import { ComboBox } from '@common/components/ui/ComboBox';
interface FloatDropdownContentProps {
theme: DesignTheme;
}
const DEMO_OPTIONS = [
{ value: 'option1', label: '연속 유출 (Continuous)' },
{ value: 'option2', label: '순간 유출 (Instantaneous)' },
{ value: 'option3', label: '밀도가스 유출 (Dense Gas)' },
{ value: 'option4', label: '수중 유출 (Subsurface)' },
{ value: 'option5', label: '증발 유출 (Evaporative)' },
];
const ALGORITHM_OPTIONS = [
{ value: 'slick', label: 'Slick Formation Model' },
{ value: 'gnome', label: 'GNOME (NOAA)' },
{ value: 'medslik', label: 'MEDSLIK-II' },
];
export const FloatDropdownContent = ({ theme }: FloatDropdownContentProps) => {
const t = theme;
const isDark = t.mode === 'dark';
const [demoValue, setDemoValue] = useState('option1');
const [algoValue, setAlgoValue] = useState('slick');
return (
<div className="px-8 py-10 flex flex-col gap-12 max-w-[1200px]">
{/* ── 개요 ── */}
<div className="flex flex-col gap-3">
<h2 className="font-sans text-xl font-bold" style={{ color: t.textPrimary }}>
Dropdown
</h2>
<p className="font-korean text-sm leading-6" style={{ color: t.textSecondary }}>
{' '}
<code
className="font-mono text-xs px-1.5 py-0.5 rounded"
style={{
backgroundColor: isDark ? 'rgba(76,215,246,0.08)' : 'rgba(6,182,212,0.06)',
color: t.textAccent,
}}
>
position: absolute
</code>
. 5 .
{' '}
<code
className="font-mono text-xs px-1.5 py-0.5 rounded"
style={{
backgroundColor: isDark ? 'rgba(76,215,246,0.08)' : 'rgba(6,182,212,0.06)',
color: t.textAccent,
}}
>
ComboBox
</code>
.
</p>
</div>
{/* ── Live Preview ── */}
<div className="flex flex-col gap-4">
<div className="flex items-center gap-2">
<h3 className="font-sans text-base font-bold" style={{ color: t.textPrimary }}>
Live Preview
</h3>
<span
className="font-mono text-caption px-2 py-0.5 rounded"
style={{
backgroundColor: isDark ? 'rgba(34,197,94,0.10)' : 'rgba(34,197,94,0.08)',
color: '#22c55e',
}}
>
interactive
</span>
</div>
<div
className="rounded-lg border border-solid p-6 flex flex-col gap-6"
style={{ backgroundColor: t.cardBg, borderColor: t.cardBorder }}
>
<div className="grid grid-cols-2 gap-6">
<div className="flex flex-col gap-2">
<span className="font-korean text-xs" style={{ color: t.textMuted }}>
</span>
<ComboBox
value={demoValue}
onChange={setDemoValue}
options={DEMO_OPTIONS}
placeholder="유형 선택"
/>
<span className="font-mono text-caption" style={{ color: t.textAccent }}>
: {DEMO_OPTIONS.find((o) => o.value === demoValue)?.label}
</span>
</div>
<div className="flex flex-col gap-2">
<span className="font-korean text-xs" style={{ color: t.textMuted }}>
</span>
<ComboBox
value={algoValue}
onChange={setAlgoValue}
options={ALGORITHM_OPTIONS}
placeholder="알고리즘 선택"
/>
<span className="font-mono text-caption" style={{ color: t.textAccent }}>
: {ALGORITHM_OPTIONS.find((o) => o.value === algoValue)?.label}
</span>
</div>
</div>
<p className="font-korean text-xs" style={{ color: t.textMuted }}>
{' '}
<code className="font-mono" style={{ color: t.textAccent }}>
@common/components/ui/ComboBox
</code>
. .
</p>
</div>
</div>
{/* ── Anatomy ── */}
<div className="flex flex-col gap-4">
<h3 className="font-sans text-base font-bold" style={{ color: t.textPrimary }}>
Anatomy
</h3>
<div className="grid grid-cols-2 gap-6">
{/* 구조 다이어그램 */}
<div
className="rounded-lg border border-solid p-4 flex flex-col gap-3"
style={{ backgroundColor: t.cardBg, borderColor: t.cardBorder }}
>
<span
className="font-mono text-caption uppercase"
style={{ letterSpacing: '1px', color: t.textMuted }}
>
Structure
</span>
<div className="flex flex-col gap-0.5 p-4">
{/* 트리거 */}
<div
className="rounded border border-solid flex items-center justify-between px-3 py-2"
style={{
backgroundColor: isDark ? '#1b1f2c' : '#ffffff',
borderColor: isDark ? 'rgba(66,71,84,0.40)' : '#e2e8f0',
}}
>
<span className="font-korean text-caption" style={{ color: t.textSecondary }}>
</span>
<span className="font-mono text-caption" style={{ color: t.textMuted }}>
</span>
</div>
{/* 리스트 */}
<div
className="rounded border border-solid flex flex-col overflow-hidden mt-0.5"
style={{
backgroundColor: isDark ? '#1b1f2c' : '#ffffff',
borderColor: isDark ? 'rgba(66,71,84,0.40)' : '#e2e8f0',
boxShadow: '0 4px 12px rgba(0,0,0,0.25)',
}}
>
{['옵션 A (선택됨)', '옵션 B', '옵션 C', '옵션 D'].map((opt, i) => (
<div
key={opt}
className="px-3 py-1.5"
style={{
backgroundColor:
i === 0
? isDark
? 'rgba(76,215,246,0.10)'
: 'rgba(6,182,212,0.07)'
: 'transparent',
borderTop: i === 0 ? 'none' : `1px solid ${t.tableRowBorder}`,
borderLeft: i === 0 ? `2px solid ${t.textAccent}` : '2px solid transparent',
}}
>
<span
className="font-korean text-caption"
style={{ color: i === 0 ? t.textAccent : t.textSecondary }}
>
{opt}
</span>
</div>
))}
</div>
</div>
</div>
{/* 위치 지정 규칙 */}
<div
className="rounded-lg border border-solid p-4 flex flex-col gap-3"
style={{ backgroundColor: t.cardBg, borderColor: t.cardBorder }}
>
<span
className="font-mono text-caption uppercase"
style={{ letterSpacing: '1px', color: t.textMuted }}
>
Position Rules
</span>
<div className="flex flex-col gap-2">
{[
{ label: 'trigger', rule: 'position: relative', desc: '드롭다운 기준점' },
{
label: 'list',
rule: 'position: absolute, top: calc(100% + 2px)',
desc: '트리거 바로 아래',
},
{ label: 'z-index', rule: 'z-[1000]', desc: '모달(9999) 아래, 일반 UI 위' },
{ label: 'max-height', rule: '200px + overflow-y: auto', desc: '스크롤 한계' },
{ label: 'animation', rule: 'fadeSlideDown 0.15s ease-out', desc: '부드러운 등장' },
].map((item) => (
<div key={item.label} className="flex items-start gap-2">
<span
className="font-mono text-caption rounded px-1.5 py-0.5 shrink-0"
style={{
backgroundColor: isDark ? 'rgba(76,215,246,0.08)' : 'rgba(6,182,212,0.06)',
color: t.textAccent,
}}
>
{item.label}
</span>
<div className="flex flex-col gap-0.5">
<span className="font-mono text-caption" style={{ color: t.textPrimary }}>
{item.rule}
</span>
<span className="font-korean text-caption" style={{ color: t.textMuted }}>
{item.desc}
</span>
</div>
</div>
))}
</div>
</div>
</div>
</div>
{/* ── 상태 ── */}
<div className="flex flex-col gap-4">
<h3 className="font-sans text-base font-bold" style={{ color: t.textPrimary }}>
</h3>
<div className="grid grid-cols-4 gap-3">
{[
{
label: 'Default',
desc: '닫힘, 값 미선택',
borderColor: isDark ? 'rgba(66,71,84,0.40)' : '#e2e8f0',
textColor: t.textMuted,
},
{
label: 'Open',
desc: '리스트 표시 중',
borderColor: t.textAccent,
textColor: t.textAccent,
},
{
label: 'Selected',
desc: '값 선택됨',
borderColor: isDark ? 'rgba(66,71,84,0.40)' : '#e2e8f0',
textColor: t.textPrimary,
},
{
label: 'Disabled',
desc: '비활성',
borderColor: isDark ? 'rgba(66,71,84,0.20)' : '#f1f5f9',
textColor: isDark ? 'rgba(140,144,159,0.40)' : '#cbd5e1',
},
].map((state) => (
<div
key={state.label}
className="rounded-lg border border-solid p-3 flex flex-col gap-2"
style={{ backgroundColor: t.cardBg, borderColor: t.cardBorder }}
>
<span className="font-mono text-caption font-bold" style={{ color: t.textPrimary }}>
{state.label}
</span>
<div
className="rounded border border-solid flex items-center justify-between px-2 py-1.5"
style={{
borderColor: state.borderColor,
opacity: state.label === 'Disabled' ? 0.45 : 1,
}}
>
<span className="font-korean text-caption" style={{ color: state.textColor }}>
{state.label === 'Selected' ? '연속 유출' : '선택하세요'}
</span>
<span className="font-mono text-caption" style={{ color: state.textColor }}>
{state.label === 'Open' ? '▲' : '▼'}
</span>
</div>
<span className="font-korean text-caption" style={{ color: t.textMuted }}>
{state.desc}
</span>
</div>
))}
</div>
</div>
{/* ── Props 테이블 ── */}
<div className="flex flex-col gap-4">
<h3 className="font-sans text-base font-bold" style={{ color: t.textPrimary }}>
Props (ComboBox)
</h3>
<div
className="rounded-lg border border-solid overflow-hidden"
style={{ backgroundColor: t.tableContainerBg, borderColor: t.cardBorder }}
>
<div
className="grid"
style={{
gridTemplateColumns: '140px 160px 80px 1fr',
backgroundColor: t.tableHeaderBg,
borderBottom: `1px solid ${t.tableRowBorder}`,
}}
>
{['Prop', 'Type', 'Required', 'Description'].map((col) => (
<div key={col} className="py-2.5 px-4">
<span
className="font-mono text-caption uppercase"
style={{ letterSpacing: '1px', color: t.textMuted }}
>
{col}
</span>
</div>
))}
</div>
{[
{ prop: 'value', type: 'string | number', required: 'Y', desc: '현재 선택값' },
{
prop: 'onChange',
type: '(value: string) => void',
required: 'Y',
desc: '선택 변경 콜백',
},
{
prop: 'options',
type: 'ComboBoxOption[]',
required: 'Y',
desc: '{ value, label } 배열',
},
{ prop: 'placeholder', type: 'string', required: 'N', desc: '미선택 상태 표시 텍스트' },
{ prop: 'className', type: 'string', required: 'N', desc: '트리거 추가 스타일' },
].map((row, idx) => (
<div
key={row.prop}
className="grid items-center"
style={{
gridTemplateColumns: '140px 160px 80px 1fr',
borderTop: idx === 0 ? 'none' : `1px solid ${t.tableRowBorder}`,
}}
>
<div className="py-2.5 px-4">
<span className="font-mono text-caption" style={{ color: t.textAccent }}>
{row.prop}
</span>
</div>
<div className="py-2.5 px-4">
<span className="font-mono text-caption" style={{ color: t.textSecondary }}>
{row.type}
</span>
</div>
<div className="py-2.5 px-4">
<span
className="font-mono text-caption"
style={{ color: row.required === 'Y' ? '#22c55e' : t.textMuted }}
>
{row.required}
</span>
</div>
<div className="py-2.5 px-4">
<span className="font-korean text-xs" style={{ color: t.textSecondary }}>
{row.desc}
</span>
</div>
</div>
))}
</div>
</div>
{/* ── 사용 가이드라인 ── */}
<div className="flex flex-col gap-4">
<h3 className="font-sans text-base font-bold" style={{ color: t.textPrimary }}>
</h3>
<div className="grid grid-cols-2 gap-3">
{[
{
title: 'clickOutside 처리 필수',
desc: 'useEffect + mousedown 이벤트로 외부 클릭 감지. ComboBox 내부에 구현됨.',
type: 'rule',
},
{
title: '5개 이상 선택지',
desc: '4개 이하는 Radio 버튼 또는 버튼 그룹으로 대체. 너무 많은 옵션은 검색 필터 추가 고려.',
type: 'rule',
},
{
title: '모달 내부 사용 시',
desc: '모달 z-index(9999) 내부에 있으면 드롭다운 z-[1000]이 자연스럽게 모달 위에 렌더링됨.',
type: 'info',
},
{
title: '너비 상속',
desc: '드롭다운 리스트는 트리거와 동일한 너비. left: 0, right: 0으로 너비 상속.',
type: 'info',
},
].map((item) => (
<div
key={item.title}
className="rounded border border-solid px-4 py-3 flex flex-col gap-1"
style={{
backgroundColor: t.cardBg,
borderColor:
item.type === 'rule'
? isDark
? 'rgba(76,215,246,0.20)'
: 'rgba(6,182,212,0.20)'
: t.cardBorder,
}}
>
<span className="font-korean text-sm font-medium" style={{ color: t.textPrimary }}>
{item.title}
</span>
<span className="font-korean text-xs leading-5" style={{ color: t.textSecondary }}>
{item.desc}
</span>
</div>
))}
</div>
</div>
</div>
);
};
export default FloatDropdownContent;

파일 보기

@ -0,0 +1,668 @@
// FloatModalContent.tsx — Modal + Confirm 카탈로그
import { useState } from 'react';
import type { DesignTheme } from '../designTheme';
interface FloatModalContentProps {
theme: DesignTheme;
}
type ModalSize = 'sm' | 'md' | 'lg' | 'full';
const SIZE_CONFIG: Record<ModalSize, { label: string; width: string; desc: string }> = {
sm: { label: 'Small', width: '380px', desc: '입력 폼, 간단한 확인' },
md: { label: 'Medium', width: '520px', desc: '상세 파라미터, 재계산' },
lg: { label: 'Large', width: '720px', desc: '복잡한 폼, 미디어 뷰어' },
full: { label: 'Full', width: '95vw', desc: '매뉴얼, 전체 화면 콘텐츠' },
};
const MODAL_INVENTORY = [
{
component: 'HNSRecalcModal',
zIndex: 'z-[9999]',
trigger: '버튼 클릭',
source: 'tabs/hns/components/',
},
{
component: 'RecalcModal',
zIndex: 'z-[9999]',
trigger: '재계산 버튼',
source: 'tabs/prediction/components/',
},
{
component: 'BacktrackModal',
zIndex: 'z-[9999]',
trigger: '역추적 분석',
source: 'tabs/prediction/components/',
},
{
component: 'MediaModal',
zIndex: 'z-[10000]',
trigger: '미디어 클릭',
source: 'tabs/incidents/components/',
},
{
component: 'SimulationErrorModal',
zIndex: 'z-50 ⚠️',
trigger: '오류 발생',
source: 'tabs/prediction/components/',
},
{
component: 'TemplateFormEditor',
zIndex: 'z-50 ⚠️',
trigger: '템플릿 편집',
source: 'tabs/reports/components/',
},
{
component: 'Admin 모달 (Layer/Map/Perm)',
zIndex: 'z-50 ⚠️',
trigger: '관리 작업',
source: 'tabs/admin/components/',
},
{
component: 'UserManualPopup',
zIndex: 'z-[9999]',
trigger: '도움말 버튼',
source: 'common/components/ui/',
},
];
export const FloatModalContent = ({ theme }: FloatModalContentProps) => {
const t = theme;
const isDark = t.mode === 'dark';
const [isModalOpen, setIsModalOpen] = useState(false);
const [activeSize, setActiveSize] = useState<ModalSize>('md');
const [isConfirmOpen, setIsConfirmOpen] = useState(false);
const overlayBg = isDark ? 'rgba(0,0,0,0.65)' : 'rgba(0,0,0,0.45)';
const modalBg = isDark ? '#1b1f2c' : '#ffffff';
const modalBorder = isDark ? 'rgba(66,71,84,0.30)' : '#e2e8f0';
const modalWidth = activeSize === 'full' ? '95vw' : SIZE_CONFIG[activeSize].width;
const modalHeight = activeSize === 'full' ? '92vh' : 'auto';
return (
<div className="px-8 py-10 flex flex-col gap-12 max-w-[1200px]">
{/* ── 개요 ── */}
<div className="flex flex-col gap-3">
<h2 className="font-sans text-xl font-bold" style={{ color: t.textPrimary }}>
Modal
</h2>
<p className="font-korean text-sm leading-6" style={{ color: t.textSecondary }}>
<code
className="font-mono text-xs px-1.5 py-0.5 rounded"
style={{
backgroundColor: isDark ? 'rgba(76,215,246,0.08)' : 'rgba(6,182,212,0.06)',
color: t.textAccent,
}}
>
fixed inset-0
</code>{' '}
. · .
(size) (variant) , <strong>Confirm</strong> Modal의 .
</p>
</div>
{/* ── Live Preview ── */}
<div className="flex flex-col gap-4">
<div className="flex items-center gap-2">
<h3 className="font-sans text-base font-bold" style={{ color: t.textPrimary }}>
Live Preview
</h3>
<span
className="font-mono text-caption px-2 py-0.5 rounded"
style={{
backgroundColor: isDark ? 'rgba(34,197,94,0.10)' : 'rgba(34,197,94,0.08)',
color: '#22c55e',
}}
>
interactive
</span>
</div>
{/* 컨트롤 */}
<div
className="rounded-lg border border-solid p-5 flex flex-col gap-4"
style={{ backgroundColor: t.cardBg, borderColor: t.cardBorder }}
>
{/* 사이즈 선택 */}
<div className="flex flex-col gap-2">
<span
className="font-mono text-caption uppercase"
style={{ letterSpacing: '1px', color: t.textMuted }}
>
Size Variant
</span>
<div className="flex gap-2 flex-wrap">
{(Object.keys(SIZE_CONFIG) as ModalSize[]).map((size) => (
<button
key={size}
type="button"
onClick={() => setActiveSize(size)}
className="px-3 py-1.5 rounded border border-solid font-mono text-caption transition-colors"
style={{
backgroundColor:
activeSize === size
? isDark
? 'rgba(76,215,246,0.15)'
: 'rgba(6,182,212,0.10)'
: 'transparent',
borderColor: activeSize === size ? t.textAccent : t.cardBorder,
color: activeSize === size ? t.textAccent : t.textMuted,
}}
>
{SIZE_CONFIG[size].label}
<span className="ml-1.5 opacity-60">{SIZE_CONFIG[size].width}</span>
</button>
))}
</div>
<span className="font-korean text-xs" style={{ color: t.textMuted }}>
{SIZE_CONFIG[activeSize].desc}
</span>
</div>
{/* 버튼 그룹 */}
<div className="flex gap-3 flex-wrap">
<button
type="button"
onClick={() => setIsModalOpen(true)}
className="px-4 py-2 rounded border border-solid font-korean text-sm font-medium transition-opacity hover:opacity-80"
style={{
backgroundColor: isDark ? 'rgba(76,215,246,0.15)' : 'rgba(6,182,212,0.12)',
borderColor: t.textAccent,
color: t.textAccent,
}}
>
</button>
<button
type="button"
onClick={() => setIsConfirmOpen(true)}
className="px-4 py-2 rounded border border-solid font-korean text-sm font-medium transition-opacity hover:opacity-80"
style={{
backgroundColor: isDark ? 'rgba(239,68,68,0.10)' : 'rgba(239,68,68,0.06)',
borderColor: 'rgba(239,68,68,0.40)',
color: '#ef4444',
}}
>
Confirm ( )
</button>
</div>
</div>
</div>
{/* ── Confirm 서브컴포넌트 ── */}
<div className="flex flex-col gap-4">
<h3 className="font-sans text-base font-bold" style={{ color: t.textPrimary }}>
Confirm Modal
</h3>
<div
className="rounded-lg border border-solid p-5 flex flex-col gap-3"
style={{ backgroundColor: t.cardBg, borderColor: t.cardBorder }}
>
<p className="font-korean text-sm leading-6" style={{ color: t.textSecondary }}>
Confirm은 <strong>Modal의 variant</strong>. +
+ / 2 . (, ) .
</p>
<div
className="rounded border border-solid px-4 py-3"
style={{
backgroundColor: isDark ? 'rgba(239,68,68,0.06)' : 'rgba(239,68,68,0.04)',
borderColor: 'rgba(239,68,68,0.20)',
}}
>
<p className="font-mono text-caption" style={{ color: '#ef4444' }}>
window.confirm admin, board 4 OS confirm
ConfirmDialog로
</p>
</div>
<div className="flex flex-col gap-1.5">
{[
{ label: 'variant', value: '"confirm"', desc: 'Modal 컴포넌트의 prop으로 전달' },
{ label: 'title', value: '"항목을 삭제하시겠습니까?"', desc: '액션을 명확히 서술' },
{
label: 'message',
value: '"삭제된 데이터는 복구할 수 없습니다."',
desc: '부가 설명 (선택)',
},
{ label: 'onConfirm', value: '() => handleDelete()', desc: '확인 버튼 콜백' },
].map((row) => (
<div key={row.label} className="flex items-start gap-3">
<span
className="font-mono text-caption rounded px-1.5 py-0.5 shrink-0"
style={{
backgroundColor: isDark ? 'rgba(76,215,246,0.08)' : 'rgba(6,182,212,0.06)',
color: t.textAccent,
}}
>
{row.label}
</span>
<span className="font-mono text-caption" style={{ color: t.textPrimary }}>
{row.value}
</span>
<span className="font-korean text-caption ml-auto" style={{ color: t.textMuted }}>
{row.desc}
</span>
</div>
))}
</div>
</div>
</div>
{/* ── Anatomy ── */}
<div className="flex flex-col gap-4">
<h3 className="font-sans text-base font-bold" style={{ color: t.textPrimary }}>
Anatomy
</h3>
<div className="grid grid-cols-2 gap-6">
{/* 구조 다이어그램 */}
<div
className="rounded-lg border border-solid p-4 flex flex-col gap-3"
style={{ backgroundColor: t.cardBg, borderColor: t.cardBorder }}
>
<span
className="font-mono text-caption uppercase"
style={{ letterSpacing: '1px', color: t.textMuted }}
>
Structure
</span>
<div
className="rounded flex items-center justify-center p-4"
style={{
backgroundColor: isDark ? 'rgba(0,0,0,0.50)' : 'rgba(0,0,0,0.06)',
minHeight: '200px',
}}
>
<div
className="rounded-lg border border-solid flex flex-col overflow-hidden w-full"
style={{ backgroundColor: modalBg, borderColor: modalBorder }}
>
<div
className="flex items-center justify-between px-3 py-2 border-b border-solid"
style={{ borderColor: modalBorder }}
>
<span className="font-korean text-caption" style={{ color: t.textPrimary }}>
</span>
<div
className="w-4 h-4 rounded flex items-center justify-center"
style={{ backgroundColor: isDark ? 'rgba(66,71,84,0.25)' : '#f1f5f9' }}
>
<span className="font-mono text-caption" style={{ color: t.textMuted }}>
</span>
</div>
</div>
<div className="flex flex-col gap-1.5 px-3 py-3">
{[75, 100, 60].map((w, i) => (
<div
key={i}
className="rounded"
style={{
height: '7px',
width: `${w}%`,
backgroundColor: isDark ? 'rgba(66,71,84,0.30)' : '#e2e8f0',
}}
/>
))}
</div>
<div
className="flex items-center justify-end gap-2 px-3 py-2 border-t border-solid"
style={{ borderColor: modalBorder }}
>
<div
className="rounded px-2 py-1"
style={{ backgroundColor: isDark ? 'rgba(66,71,84,0.20)' : '#f1f5f9' }}
>
<span className="font-korean text-caption" style={{ color: t.textMuted }}>
</span>
</div>
<div
className="rounded px-2 py-1"
style={{
backgroundColor: isDark ? 'rgba(76,215,246,0.18)' : 'rgba(6,182,212,0.12)',
}}
>
<span className="font-korean text-caption" style={{ color: t.textAccent }}>
</span>
</div>
</div>
</div>
</div>
</div>
{/* CSS 클래스 레퍼런스 */}
<div
className="rounded-lg border border-solid p-4 flex flex-col gap-3"
style={{ backgroundColor: t.cardBg, borderColor: t.cardBorder }}
>
<span
className="font-mono text-caption uppercase"
style={{ letterSpacing: '1px', color: t.textMuted }}
>
CSS Classes
</span>
<div className="flex flex-col gap-2">
{[
{
cls: '.wing-overlay',
styles: 'fixed inset-0, z-index: 10000',
desc: '백드롭 오버레이',
},
{
cls: '.wing-modal',
styles: 'rounded-xl, bg-surface, border + shadow',
desc: '다이얼로그 컨테이너',
},
{
cls: '.wing-modal-header',
styles: 'flex justify-between, px-5, py-[14px], border-b',
desc: '헤더 (타이틀 + 닫기)',
},
].map((item) => (
<div
key={item.cls}
className="rounded border border-solid px-3 py-2"
style={{ borderColor: t.cardBorder }}
>
<div className="flex items-center gap-2 mb-1">
<span
className="font-mono text-caption rounded px-1.5 py-0.5"
style={{
backgroundColor: isDark ? 'rgba(76,215,246,0.08)' : 'rgba(6,182,212,0.06)',
color: t.textAccent,
}}
>
{item.cls}
</span>
<span className="font-korean text-caption" style={{ color: t.textMuted }}>
{item.desc}
</span>
</div>
<span className="font-mono text-caption" style={{ color: t.textSecondary }}>
{item.styles}
</span>
</div>
))}
</div>
</div>
</div>
</div>
{/* ── Z-Index 규칙 ── */}
<div className="flex flex-col gap-4">
<h3 className="font-sans text-base font-bold" style={{ color: t.textPrimary }}>
Z-Index
</h3>
<div className="flex flex-col gap-2">
{[
{
range: 'z-[9999]',
status: '표준',
desc: '일반 Modal — 표준값, 신규 Modal은 이 값 사용',
color: '#22c55e',
},
{
range: 'z-[10000]',
status: '허용',
desc: '모달 위 모달 (MediaModal, IncidentsView) — 필요 시만',
color: '#eab308',
},
{
range: 'z-50',
status: '비표준 ⚠️',
desc: 'SimulationErrorModal, Admin 모달, TemplateFormEditor — z-[9999]로 통일 필요',
color: '#ef4444',
},
].map((row) => (
<div
key={row.range}
className="flex items-start gap-4 rounded border border-solid px-4 py-3"
style={{ backgroundColor: t.cardBg, borderColor: t.cardBorder }}
>
<span
className="font-mono text-sm rounded border border-solid px-2 py-0.5 shrink-0"
style={{ color: t.textAccent, borderColor: t.cardBorder }}
>
{row.range}
</span>
<span
className="font-mono text-caption rounded px-1.5 py-0.5 shrink-0"
style={{ color: row.color, backgroundColor: `${row.color}15` }}
>
{row.status}
</span>
<span className="font-korean text-xs leading-5" style={{ color: t.textSecondary }}>
{row.desc}
</span>
</div>
))}
</div>
</div>
{/* ── 인벤토리 ── */}
<div className="flex flex-col gap-4">
<h3 className="font-sans text-base font-bold" style={{ color: t.textPrimary }}>
</h3>
<div
className="rounded-lg border border-solid overflow-hidden"
style={{ backgroundColor: t.tableContainerBg, borderColor: t.cardBorder }}
>
<div
className="grid"
style={{
gridTemplateColumns: '1fr 120px 130px 1fr',
backgroundColor: t.tableHeaderBg,
borderBottom: `1px solid ${t.tableRowBorder}`,
}}
>
{['Component', 'Z-Index', 'Trigger', 'Source'].map((col) => (
<div key={col} className="py-2.5 px-4">
<span
className="font-mono text-caption uppercase"
style={{ letterSpacing: '1px', color: t.textMuted }}
>
{col}
</span>
</div>
))}
</div>
{MODAL_INVENTORY.map((item, idx) => (
<div
key={item.component}
className="grid items-center"
style={{
gridTemplateColumns: '1fr 120px 130px 1fr',
borderTop: idx === 0 ? 'none' : `1px solid ${t.tableRowBorder}`,
}}
>
<div className="py-2.5 px-4">
<span className="font-mono text-xs" style={{ color: t.textPrimary }}>
{item.component}
</span>
</div>
<div className="py-2.5 px-4">
<span
className="font-mono text-caption rounded border border-solid px-1.5 py-0.5"
style={{
color: item.zIndex.includes('⚠️') ? '#ef4444' : t.textAccent,
borderColor: item.zIndex.includes('⚠️') ? 'rgba(239,68,68,0.30)' : t.cardBorder,
}}
>
{item.zIndex}
</span>
</div>
<div className="py-2.5 px-4">
<span className="font-korean text-xs" style={{ color: t.textSecondary }}>
{item.trigger}
</span>
</div>
<div className="py-2.5 px-4">
<span className="font-mono text-caption" style={{ color: t.textMuted }}>
{item.source}
</span>
</div>
</div>
))}
</div>
</div>
{/* ── 실제 Modal 렌더링 ── */}
{isModalOpen && (
<div
className="fixed inset-0 flex items-center justify-center z-[9999]"
style={{ backgroundColor: overlayBg, backdropFilter: 'blur(4px)' }}
onClick={(e) => {
if (e.target === e.currentTarget) setIsModalOpen(false);
}}
>
<div
className="flex flex-col rounded-xl border border-solid overflow-hidden"
style={{
width: modalWidth,
maxHeight: modalHeight,
backgroundColor: modalBg,
borderColor: modalBorder,
boxShadow: '0 20px 60px rgba(0,0,0,0.5)',
}}
>
<div
className="flex items-center justify-between px-5 py-4 border-b border-solid shrink-0"
style={{ borderColor: modalBorder }}
>
<span className="font-korean text-sm font-medium" style={{ color: t.textPrimary }}>
Modal Preview {SIZE_CONFIG[activeSize].label} ({SIZE_CONFIG[activeSize].width})
</span>
<button
type="button"
onClick={() => setIsModalOpen(false)}
className="w-7 h-7 rounded flex items-center justify-center hover:opacity-70 transition-opacity"
style={{ backgroundColor: isDark ? 'rgba(66,71,84,0.25)' : '#f1f5f9' }}
>
<span className="font-mono text-sm" style={{ color: t.textMuted }}>
</span>
</button>
</div>
<div className="px-5 py-5 flex flex-col gap-3 overflow-y-auto">
<p className="font-korean text-sm leading-6" style={{ color: t.textSecondary }}>
{' '}
<code
className="font-mono text-xs px-1 rounded"
style={{
backgroundColor: isDark ? 'rgba(76,215,246,0.08)' : 'rgba(6,182,212,0.06)',
color: t.textAccent,
}}
>
fixed inset-0, z-[9999]
</code>{' '}
. .
</p>
<div className="flex flex-col gap-2">
{['파라미터 입력 필드', '선택 항목', '추가 설정값'].map((label) => (
<div
key={label}
className="rounded border border-solid px-3 py-2.5"
style={{ borderColor: t.cardBorder }}
>
<span className="font-korean text-xs" style={{ color: t.textMuted }}>
{label}
</span>
</div>
))}
</div>
</div>
<div
className="flex items-center justify-end gap-2 px-5 py-4 border-t border-solid shrink-0"
style={{ borderColor: modalBorder }}
>
<button
type="button"
onClick={() => setIsModalOpen(false)}
className="px-4 py-2 rounded border border-solid font-korean text-sm transition-opacity hover:opacity-70"
style={{ borderColor: t.cardBorder, color: t.textMuted }}
>
</button>
<button
type="button"
onClick={() => setIsModalOpen(false)}
className="px-4 py-2 rounded font-korean text-sm font-medium transition-opacity hover:opacity-80"
style={{
backgroundColor: isDark ? 'rgba(76,215,246,0.18)' : 'rgba(6,182,212,0.14)',
color: t.textAccent,
}}
>
</button>
</div>
</div>
</div>
)}
{/* ── 실제 Confirm 렌더링 ── */}
{isConfirmOpen && (
<div
className="fixed inset-0 flex items-center justify-center z-[9999]"
style={{ backgroundColor: overlayBg, backdropFilter: 'blur(4px)' }}
>
<div
className="flex flex-col rounded-xl border border-solid overflow-hidden"
style={{
width: '360px',
backgroundColor: modalBg,
borderColor: modalBorder,
boxShadow: '0 20px 60px rgba(0,0,0,0.5)',
}}
>
<div
className="flex items-center justify-between px-5 py-4 border-b border-solid"
style={{ borderColor: modalBorder }}
>
<div className="flex items-center gap-2">
<span style={{ color: '#ef4444', fontSize: '16px' }}></span>
<span className="font-korean text-sm font-medium" style={{ color: t.textPrimary }}>
?
</span>
</div>
</div>
<div className="px-5 py-4">
<p className="font-korean text-sm leading-6" style={{ color: t.textSecondary }}>
. ?
</p>
</div>
<div
className="flex items-center justify-end gap-2 px-5 py-4 border-t border-solid"
style={{ borderColor: modalBorder }}
>
<button
type="button"
onClick={() => setIsConfirmOpen(false)}
className="px-4 py-2 rounded border border-solid font-korean text-sm transition-opacity hover:opacity-70"
style={{ borderColor: t.cardBorder, color: t.textMuted }}
>
</button>
<button
type="button"
onClick={() => setIsConfirmOpen(false)}
className="px-4 py-2 rounded font-korean text-sm font-medium transition-opacity hover:opacity-80"
style={{
backgroundColor: isDark ? 'rgba(239,68,68,0.18)' : 'rgba(239,68,68,0.12)',
color: '#ef4444',
}}
>
</button>
</div>
</div>
</div>
)}
</div>
);
};
export default FloatModalContent;

파일 보기

@ -0,0 +1,428 @@
// FloatOverlayContent.tsx — Map Overlay + Map Popup 카탈로그
import type { DesignTheme } from '../designTheme';
interface FloatOverlayContentProps {
theme: DesignTheme;
}
const OVERLAY_CASES = [
{
component: 'BacktrackReplayBar',
position: '하단 중앙',
zIndex: 'z-40',
pointerEvents: 'auto',
desc: '역추적 재생 컨트롤 바. 재생/일시정지/슬라이더.',
source: 'common/components/map/BacktrackReplayBar.tsx',
},
{
component: 'MeasureOverlay',
position: '마커 위치',
zIndex: 'z-40',
pointerEvents: 'auto',
desc: '거리 측정 마커 "지우기" 버튼. MapLibre Marker 컴포넌트 활용.',
source: 'common/components/map/MeasureOverlay.tsx',
},
{
component: 'OilDetectionOverlay',
position: 'inset-0 + 우하단 정보',
zIndex: 'z-[15]',
pointerEvents: 'none',
desc: '유류 탐지 결과 마스크 렌더링. OffscreenCanvas 기반. 정보 패널만 클릭 가능.',
source: 'tabs/aerial/components/OilDetectionOverlay.tsx',
},
{
component: 'WeatherMapOverlay',
position: 'absolute inset-0',
zIndex: 'map layer',
pointerEvents: 'none',
desc: '기상 데이터 레이어 오버레이.',
source: 'tabs/weather/components/WeatherMapOverlay.tsx',
},
{
component: 'OceanForecastOverlay',
position: 'absolute inset-0',
zIndex: 'map layer',
pointerEvents: 'none',
desc: '해양 예측 레이어 오버레이.',
source: 'tabs/weather/components/OceanForecastOverlay.tsx',
},
];
export const FloatOverlayContent = ({ theme }: FloatOverlayContentProps) => {
const t = theme;
const isDark = t.mode === 'dark';
const mapMockBg = isDark ? '#0f1a2e' : '#c8d8e8';
const mapGridColor = isDark ? 'rgba(255,255,255,0.04)' : 'rgba(0,0,0,0.06)';
return (
<div className="px-8 py-10 flex flex-col gap-12 max-w-[1200px]">
{/* ── 개요 ── */}
<div className="flex flex-col gap-3">
<h2 className="font-sans text-xl font-bold" style={{ color: t.textPrimary }}>
Overlay
</h2>
<p className="font-korean text-sm leading-6" style={{ color: t.textSecondary }}>
<code
className="font-mono text-xs px-1.5 py-0.5 mx-1 rounded"
style={{
backgroundColor: isDark ? 'rgba(76,215,246,0.08)' : 'rgba(6,182,212,0.06)',
color: t.textAccent,
}}
>
position: absolute
</code>
UI. UI를 . Modal과
, .
</p>
</div>
{/* ── Overlay vs Modal 비교 ── */}
<div className="flex flex-col gap-4">
<h3 className="font-sans text-base font-bold" style={{ color: t.textPrimary }}>
Overlay vs Modal
</h3>
<div
className="rounded-lg border border-solid overflow-hidden"
style={{ backgroundColor: t.tableContainerBg, borderColor: t.cardBorder }}
>
<div
className="grid"
style={{
gridTemplateColumns: '160px 1fr 1fr',
backgroundColor: t.tableHeaderBg,
borderBottom: `1px solid ${t.tableRowBorder}`,
}}
>
{['속성', 'Overlay', 'Modal'].map((col) => (
<div key={col} className="py-2.5 px-4">
<span
className="font-mono text-caption uppercase"
style={{ letterSpacing: '1px', color: t.textMuted }}
>
{col}
</span>
</div>
))}
</div>
{[
{ attr: 'position', overlay: 'absolute (지도 기준)', modal: 'fixed (뷰포트 기준)' },
{ attr: '백드롭', overlay: '없음', modal: 'rgba(0,0,0,0.65) + blur' },
{ attr: '클릭 차단', overlay: 'pointer-events: none (일반)', modal: '전체 화면 차단' },
{ attr: 'z-index', overlay: 'z-40 (지도 UI 위)', modal: 'z-[9999] (최상위)' },
{ attr: '크기 기준', overlay: '지도 컨테이너 = 100%', modal: '고정 너비 (380~720px)' },
{
attr: '닫기 방식',
overlay: '기능 비활성화 시 사라짐',
modal: '닫기 버튼 / 백드롭 클릭',
},
].map((row, idx) => (
<div
key={row.attr}
className="grid items-center"
style={{
gridTemplateColumns: '160px 1fr 1fr',
borderTop: idx === 0 ? 'none' : `1px solid ${t.tableRowBorder}`,
}}
>
<div className="py-2.5 px-4">
<span className="font-mono text-caption" style={{ color: t.textMuted }}>
{row.attr}
</span>
</div>
<div className="py-2.5 px-4">
<span className="font-korean text-xs" style={{ color: t.textSecondary }}>
{row.overlay}
</span>
</div>
<div className="py-2.5 px-4">
<span className="font-korean text-xs" style={{ color: t.textSecondary }}>
{row.modal}
</span>
</div>
</div>
))}
</div>
</div>
{/* ── 지도 목업 다이어그램 ── */}
<div className="flex flex-col gap-4">
<h3 className="font-sans text-base font-bold" style={{ color: t.textPrimary }}>
Overlay
</h3>
<div
className="rounded-lg border border-solid p-4 flex flex-col gap-3"
style={{ backgroundColor: t.cardBg, borderColor: t.cardBorder }}
>
<span
className="font-mono text-caption uppercase"
style={{ letterSpacing: '1px', color: t.textMuted }}
>
</span>
{/* 지도 목업 */}
<div
className="relative rounded overflow-hidden"
style={{ backgroundColor: mapMockBg, minHeight: '280px' }}
>
{/* 격자 배경 (지도 모사) */}
<div
className="absolute inset-0"
style={{
backgroundImage: `linear-gradient(${mapGridColor} 1px, transparent 1px), linear-gradient(90deg, ${mapGridColor} 1px, transparent 1px)`,
backgroundSize: '40px 40px',
}}
/>
{/* 지도 레이블 */}
<div className="absolute top-3 left-3">
<span
className="font-mono text-caption"
style={{ color: isDark ? 'rgba(255,255,255,0.25)' : 'rgba(0,0,0,0.25)' }}
>
MapView (position: relative)
</span>
</div>
{/* OilDetectionOverlay — 전체 영역 */}
<div
className="absolute inset-0 pointer-events-none"
style={{ border: `1.5px dashed rgba(6,182,212,0.35)`, borderRadius: '4px' }}
>
<div className="absolute top-10 left-3">
<span
className="font-mono text-caption rounded px-1.5 py-0.5"
style={{ backgroundColor: 'rgba(6,182,212,0.15)', color: t.textAccent }}
>
OilDetectionOverlay inset-0, z-[15], pointer-events:none
</span>
</div>
</div>
{/* MeasureOverlay 마커 */}
<div className="absolute" style={{ top: '80px', left: '120px' }}>
<div
className="rounded-full w-3 h-3 border-2 border-solid"
style={{ backgroundColor: '#ef4444', borderColor: '#ffffff' }}
/>
<div
className="rounded border border-solid px-2 py-0.5 mt-1"
style={{
backgroundColor: isDark ? 'rgba(239,68,68,0.85)' : 'rgba(239,68,68,0.90)',
borderColor: 'transparent',
}}
>
<span className="font-korean text-caption" style={{ color: '#ffffff' }}>
</span>
</div>
<div className="absolute -top-4 left-8">
<span
className="font-mono text-caption rounded px-1 py-0.5"
style={{
backgroundColor: 'rgba(239,68,68,0.15)',
color: '#ef4444',
whiteSpace: 'nowrap',
}}
>
MeasureOverlay Marker
</span>
</div>
</div>
{/* BacktrackReplayBar — 하단 중앙 */}
<div
className="absolute bottom-3 left-1/2 rounded border border-solid px-4 py-2 flex items-center gap-3"
style={{
transform: 'translateX(-50%)',
backgroundColor: isDark ? 'rgba(23,27,40,0.92)' : 'rgba(255,255,255,0.92)',
borderColor: isDark ? 'rgba(66,71,84,0.40)' : '#e2e8f0',
backdropFilter: 'blur(6px)',
}}
>
<span className="font-mono text-caption" style={{ color: t.textAccent }}>
</span>
<span className="font-mono text-caption" style={{ color: t.textAccent }}>
</span>
<div
className="w-24 h-1 rounded"
style={{ backgroundColor: isDark ? 'rgba(66,71,84,0.40)' : '#e2e8f0' }}
>
<div className="h-1 w-10 rounded" style={{ backgroundColor: t.textAccent }} />
</div>
<span className="font-mono text-caption" style={{ color: t.textMuted }}>
</span>
</div>
<div className="absolute bottom-14 left-1/2" style={{ transform: 'translateX(-50%)' }}>
<span
className="font-mono text-caption rounded px-1.5 py-0.5"
style={{
backgroundColor: isDark ? 'rgba(76,215,246,0.12)' : 'rgba(6,182,212,0.10)',
color: t.textAccent,
whiteSpace: 'nowrap',
}}
>
BacktrackReplayBar bottom-3 center, z-40
</span>
</div>
</div>
</div>
</div>
{/* ── Map Popup 서브패턴 ── */}
<div className="flex flex-col gap-4">
<h3 className="font-sans text-base font-bold" style={{ color: t.textPrimary }}>
Map Popup
</h3>
<div
className="rounded-lg border border-solid p-5 flex flex-col gap-4"
style={{ backgroundColor: t.cardBg, borderColor: t.cardBorder }}
>
<p className="font-korean text-sm leading-6" style={{ color: t.textSecondary }}>
<strong>ScatPopup</strong> . Modal(fixed
) , ·
.
</p>
<div className="grid grid-cols-3 gap-3">
{[
{
label: '위치 계산',
value: 'map.project(lngLat)',
desc: '지도 좌표 → 픽셀 좌표 변환',
},
{ label: '위치 업데이트', value: 'map.on("move")', desc: '패닝/줌 시 재계산' },
{ label: 'z-index', value: 'z-[9999]', desc: '다른 오버레이 위' },
].map((item) => (
<div
key={item.label}
className="rounded border border-solid px-3 py-2.5 flex flex-col gap-1"
style={{ borderColor: t.cardBorder }}
>
<span className="font-mono text-caption" style={{ color: t.textMuted }}>
{item.label}
</span>
<span className="font-mono text-xs" style={{ color: t.textAccent }}>
{item.value}
</span>
<span className="font-korean text-caption" style={{ color: t.textSecondary }}>
{item.desc}
</span>
</div>
))}
</div>
<div
className="rounded border border-solid px-3 py-2"
style={{
backgroundColor: isDark ? 'rgba(234,179,8,0.06)' : 'rgba(234,179,8,0.04)',
borderColor: 'rgba(234,179,8,0.25)',
}}
>
<span className="font-korean text-xs" style={{ color: '#eab308' }}>
주의: ScatPopup은 MapLibre GL JS의 Popup/Marker React DOM으로 .
position: absolute로 .
</span>
</div>
<div className="flex items-center gap-2">
<span className="font-mono text-caption" style={{ color: t.textMuted }}>
Source:
</span>
<span className="font-mono text-caption" style={{ color: t.textAccent }}>
tabs/scat/components/ScatPopup.tsx
</span>
</div>
</div>
</div>
{/* ── 사용 사례 목록 ── */}
<div className="flex flex-col gap-4">
<h3 className="font-sans text-base font-bold" style={{ color: t.textPrimary }}>
</h3>
<div
className="rounded-lg border border-solid overflow-hidden"
style={{ backgroundColor: t.tableContainerBg, borderColor: t.cardBorder }}
>
<div
className="grid"
style={{
gridTemplateColumns: '200px 120px 80px 100px 1fr',
backgroundColor: t.tableHeaderBg,
borderBottom: `1px solid ${t.tableRowBorder}`,
}}
>
{['Component', 'Position', 'Z-Index', 'Events', 'Description'].map((col) => (
<div key={col} className="py-2.5 px-3">
<span
className="font-mono text-caption uppercase"
style={{ letterSpacing: '1px', color: t.textMuted }}
>
{col}
</span>
</div>
))}
</div>
{OVERLAY_CASES.map((item, idx) => (
<div
key={item.component}
className="grid items-start"
style={{
gridTemplateColumns: '200px 120px 80px 100px 1fr',
borderTop: idx === 0 ? 'none' : `1px solid ${t.tableRowBorder}`,
}}
>
<div className="py-2.5 px-3">
<span className="font-mono text-xs" style={{ color: t.textPrimary }}>
{item.component}
</span>
</div>
<div className="py-2.5 px-3">
<span className="font-korean text-caption" style={{ color: t.textSecondary }}>
{item.position}
</span>
</div>
<div className="py-2.5 px-3">
<span
className="font-mono text-caption rounded border border-solid px-1.5 py-0.5"
style={{ color: t.textAccent, borderColor: t.cardBorder }}
>
{item.zIndex}
</span>
</div>
<div className="py-2.5 px-3">
<span
className="font-mono text-caption rounded px-1.5 py-0.5"
style={{
color: item.pointerEvents === 'none' ? t.textMuted : '#22c55e',
backgroundColor:
item.pointerEvents === 'none'
? isDark
? 'rgba(140,144,159,0.10)'
: 'rgba(148,163,184,0.10)'
: isDark
? 'rgba(34,197,94,0.10)'
: 'rgba(34,197,94,0.08)',
}}
>
{item.pointerEvents}
</span>
</div>
<div className="py-2.5 px-3">
<span className="font-korean text-xs leading-5" style={{ color: t.textSecondary }}>
{item.desc}
</span>
</div>
</div>
))}
</div>
</div>
</div>
);
};
export default FloatOverlayContent;

파일 보기

@ -0,0 +1,427 @@
// FloatToastContent.tsx — Toast 컴포넌트 카탈로그
import { useState, useEffect } from 'react';
import type { DesignTheme } from '../designTheme';
interface FloatToastContentProps {
theme: DesignTheme;
}
type ToastType = 'success' | 'error' | 'info' | 'warning';
interface ToastItem {
id: number;
type: ToastType;
message: string;
progress: number;
}
const TOAST_CONFIG: Record<ToastType, { color: string; bg: string; icon: string; label: string }> =
{
success: { color: '#22c55e', bg: 'rgba(34,197,94,0.12)', icon: '✓', label: 'Success' },
error: { color: '#ef4444', bg: 'rgba(239,68,68,0.12)', icon: '✕', label: 'Error' },
info: { color: '#06b6d4', bg: 'rgba(6,182,212,0.12)', icon: '', label: 'Info' },
warning: { color: '#eab308', bg: 'rgba(234,179,8,0.12)', icon: '⚠', label: 'Warning' },
};
const DEMO_MESSAGES: Record<ToastType, string> = {
success: '저장이 완료되었습니다.',
error: '요청 처리 중 오류가 발생했습니다.',
info: '시뮬레이션이 시작되었습니다.',
warning: '미저장 변경사항이 있습니다.',
};
const TOAST_DURATION = 3000;
let toastIdCounter = 0;
export const FloatToastContent = ({ theme }: FloatToastContentProps) => {
const t = theme;
const isDark = t.mode === 'dark';
const [toasts, setToasts] = useState<ToastItem[]>([]);
const addToast = (type: ToastType) => {
const id = ++toastIdCounter;
setToasts((prev) => [...prev, { id, type, message: DEMO_MESSAGES[type], progress: 100 }]);
};
const removeToast = (id: number) => {
setToasts((prev) => prev.filter((t) => t.id !== id));
};
useEffect(() => {
if (toasts.length === 0) return;
const interval = setInterval(() => {
setToasts((prev) =>
prev
.map((toast) => ({ ...toast, progress: toast.progress - 100 / (TOAST_DURATION / 100) }))
.filter((toast) => toast.progress > 0),
);
}, 100);
return () => clearInterval(interval);
}, [toasts.length]);
const toastBg = isDark ? '#1b1f2c' : '#ffffff';
const toastBorder = isDark ? 'rgba(66,71,84,0.30)' : '#e2e8f0';
return (
<div className="px-8 py-10 flex flex-col gap-12 max-w-[1200px]">
{/* ── 개요 ── */}
<div className="flex flex-col gap-3">
<div className="flex items-center gap-3">
<h2 className="font-sans text-xl font-bold" style={{ color: t.textPrimary }}>
Toast
</h2>
<span
className="font-mono text-caption rounded px-2 py-0.5"
style={{
backgroundColor: isDark ? 'rgba(234,179,8,0.10)' : 'rgba(234,179,8,0.08)',
color: '#eab308',
}}
>
</span>
</div>
<p className="font-korean text-sm leading-6" style={{ color: t.textSecondary }}>
.
<code
className="font-mono text-xs px-1.5 py-0.5 mx-1 rounded"
style={{
backgroundColor: isDark ? 'rgba(76,215,246,0.08)' : 'rgba(6,182,212,0.06)',
color: t.textAccent,
}}
>
fixed bottom-right
</code>
. {' '}
<code
className="font-mono text-xs px-1.5 py-0.5 mx-1 rounded"
style={{
backgroundColor: isDark ? 'rgba(239,68,68,0.08)' : 'rgba(239,68,68,0.05)',
color: '#ef4444',
}}
>
window.alert
</code>
console.log로 .
</p>
</div>
{/* ── Live Preview ── */}
<div className="flex flex-col gap-4">
<div className="flex items-center gap-2">
<h3 className="font-sans text-base font-bold" style={{ color: t.textPrimary }}>
Live Preview
</h3>
<span
className="font-mono text-caption px-2 py-0.5 rounded"
style={{
backgroundColor: isDark ? 'rgba(34,197,94,0.10)' : 'rgba(34,197,94,0.08)',
color: '#22c55e',
}}
>
interactive
</span>
</div>
<div
className="rounded-lg border border-solid p-5 flex flex-col gap-4"
style={{ backgroundColor: t.cardBg, borderColor: t.cardBorder }}
>
<p className="font-korean text-xs" style={{ color: t.textMuted }}>
Toast가 . 3 .
</p>
<div className="flex gap-2 flex-wrap">
{(Object.keys(TOAST_CONFIG) as ToastType[]).map((type) => {
const cfg = TOAST_CONFIG[type];
return (
<button
key={type}
type="button"
onClick={() => addToast(type)}
className="px-4 py-2 rounded border border-solid font-mono text-caption font-medium transition-opacity hover:opacity-80"
style={{
backgroundColor: cfg.bg,
borderColor: `${cfg.color}40`,
color: cfg.color,
}}
>
{cfg.icon} {cfg.label}
</button>
);
})}
</div>
{toasts.length > 0 && (
<p className="font-mono text-caption" style={{ color: t.textMuted }}>
Toast: {toasts.length}
</p>
)}
</div>
</div>
{/* ── Anatomy ── */}
<div className="flex flex-col gap-4">
<h3 className="font-sans text-base font-bold" style={{ color: t.textPrimary }}>
Anatomy
</h3>
<div className="grid grid-cols-2 gap-6">
{/* 구조 목업 */}
<div
className="rounded-lg border border-solid p-4 flex flex-col gap-3"
style={{ backgroundColor: t.cardBg, borderColor: t.cardBorder }}
>
<span
className="font-mono text-caption uppercase"
style={{ letterSpacing: '1px', color: t.textMuted }}
>
Structure
</span>
<div
className="rounded relative flex items-end justify-end"
style={{
backgroundColor: isDark ? 'rgba(0,0,0,0.25)' : 'rgba(0,0,0,0.04)',
padding: '16px',
minHeight: '160px',
}}
>
{/* 성공 Toast 목업 */}
<div className="flex flex-col gap-1.5 w-full max-w-[220px]">
{(['success', 'info', 'error'] as ToastType[]).map((type, i) => {
const cfg = TOAST_CONFIG[type];
return (
<div
key={type}
className="rounded border border-solid flex items-center gap-2 px-3 py-2"
style={{
backgroundColor: toastBg,
borderColor: toastBorder,
borderLeft: `3px solid ${cfg.color}`,
opacity: 1 - i * 0.2,
}}
>
<span
className="font-mono text-caption shrink-0"
style={{ color: cfg.color }}
>
{cfg.icon}
</span>
<span className="font-korean text-caption" style={{ color: t.textSecondary }}>
{DEMO_MESSAGES[type].slice(0, 14)}
</span>
</div>
);
})}
</div>
</div>
</div>
{/* 위치 규칙 */}
<div
className="rounded-lg border border-solid p-4 flex flex-col gap-3"
style={{ backgroundColor: t.cardBg, borderColor: t.cardBorder }}
>
<span
className="font-mono text-caption uppercase"
style={{ letterSpacing: '1px', color: t.textMuted }}
>
Position Rules
</span>
<div className="flex flex-col gap-2">
{[
{ label: 'position', value: 'fixed', desc: '뷰포트 기준 고정' },
{ label: 'bottom', value: '24px', desc: '화면 하단에서 24px' },
{ label: 'right', value: '24px', desc: '화면 우측에서 24px' },
{ label: 'z-index', value: 'z-60', desc: '콘텐츠 위, Modal(9999) 아래' },
{ label: 'width', value: '320px (고정)', desc: '일정한 너비 유지' },
{ label: 'gap', value: '8px (스택)', desc: '복수 Toast 간격' },
].map((item) => (
<div key={item.label} className="flex items-center gap-2">
<span
className="font-mono text-caption rounded px-1.5 py-0.5 shrink-0 w-24 text-right"
style={{
backgroundColor: isDark ? 'rgba(76,215,246,0.08)' : 'rgba(6,182,212,0.06)',
color: t.textAccent,
}}
>
{item.label}
</span>
<span className="font-mono text-caption" style={{ color: t.textPrimary }}>
{item.value}
</span>
<span className="font-korean text-caption ml-auto" style={{ color: t.textMuted }}>
{item.desc}
</span>
</div>
))}
</div>
</div>
</div>
</div>
{/* ── 타입별 색상 ── */}
<div className="flex flex-col gap-4">
<h3 className="font-sans text-base font-bold" style={{ color: t.textPrimary }}>
</h3>
<div className="grid grid-cols-4 gap-3">
{(Object.entries(TOAST_CONFIG) as [ToastType, (typeof TOAST_CONFIG)[ToastType]][]).map(
([type, cfg]) => (
<div
key={type}
className="rounded-lg border border-solid p-4 flex flex-col gap-3"
style={{
backgroundColor: t.cardBg,
borderColor: t.cardBorder,
borderLeft: `3px solid ${cfg.color}`,
}}
>
<div className="flex items-center gap-2">
<span className="font-mono text-lg" style={{ color: cfg.color }}>
{cfg.icon}
</span>
<span className="font-mono text-sm font-bold" style={{ color: cfg.color }}>
{cfg.label}
</span>
</div>
<span className="font-mono text-caption" style={{ color: t.textMuted }}>
{cfg.color}
</span>
<span
className="font-korean text-caption leading-5"
style={{ color: t.textSecondary }}
>
{type === 'success' && '저장 완료, 복사 완료, 전송 성공'}
{type === 'error' && 'API 오류, 저장 실패, 권한 없음'}
{type === 'info' && '작업 시작, 업데이트 알림'}
{type === 'warning' && '미저장 변경, 만료 임박'}
</span>
</div>
),
)}
</div>
</div>
{/* ── 구현 패턴 제안 ── */}
<div className="flex flex-col gap-4">
<h3 className="font-sans text-base font-bold" style={{ color: t.textPrimary }}>
useToast Hook
</h3>
<div
className="rounded-lg border border-solid p-5 flex flex-col gap-4"
style={{ backgroundColor: t.cardBg, borderColor: t.cardBorder }}
>
<p className="font-korean text-sm leading-6" style={{ color: t.textSecondary }}>
Toast는 <strong>Zustand store + useToast hook</strong>{' '}
. ToastContainer는 App.tsx .
</p>
<div className="flex flex-col gap-3">
{[
{
title: 'toastStore.ts',
code: 'const useToastStore = create<ToastStore>()\naddToast(type, message, duration?)\nremoveToast(id)',
desc: 'Zustand store — Toast 큐 관리',
},
{
title: 'useToast.ts',
code: 'const { success, error, info, warning } = useToast()\nsuccess("저장 완료") // duration 기본값 3000ms',
desc: '컴포넌트에서 호출하는 hook',
},
{
title: 'ToastContainer.tsx',
code: '<div className="fixed bottom-6 right-6 z-[60] flex flex-col gap-2">\n {toasts.map(t => <ToastItem key={t.id} {...t} />)}\n</div>',
desc: 'App.tsx 최상위에 배치',
},
].map((item) => (
<div
key={item.title}
className="rounded border border-solid p-3 flex flex-col gap-1.5"
style={{ borderColor: t.cardBorder }}
>
<div className="flex items-center justify-between">
<span
className="font-mono text-caption font-bold"
style={{ color: t.textPrimary }}
>
{item.title}
</span>
<span className="font-korean text-caption" style={{ color: t.textMuted }}>
{item.desc}
</span>
</div>
<pre
className="font-mono text-caption leading-5 rounded p-2 overflow-x-auto"
style={{
backgroundColor: isDark ? 'rgba(0,0,0,0.30)' : 'rgba(0,0,0,0.04)',
color: t.textSecondary,
}}
>
{item.code}
</pre>
</div>
))}
</div>
</div>
</div>
{/* ── 실제 Toast 렌더링 (fixed 위치) ── */}
{toasts.length > 0 && (
<div
className="fixed bottom-6 right-6 z-[60] flex flex-col gap-2"
style={{ width: '300px' }}
>
{toasts.map((toast) => {
const cfg = TOAST_CONFIG[toast.type];
return (
<div
key={toast.id}
className="rounded border border-solid flex flex-col overflow-hidden"
style={{
backgroundColor: toastBg,
borderColor: toastBorder,
borderLeft: `3px solid ${cfg.color}`,
boxShadow: '0 4px 16px rgba(0,0,0,0.30)',
}}
>
<div className="flex items-center gap-2.5 px-3 py-2.5">
<span className="font-mono text-base shrink-0" style={{ color: cfg.color }}>
{cfg.icon}
</span>
<span className="font-korean text-sm flex-1" style={{ color: t.textPrimary }}>
{toast.message}
</span>
<button
type="button"
onClick={() => removeToast(toast.id)}
className="w-5 h-5 rounded flex items-center justify-center shrink-0 hover:opacity-70 transition-opacity"
style={{ color: t.textMuted }}
>
<span className="font-mono text-caption"></span>
</button>
</div>
{/* Progress bar */}
<div
style={{
height: '2px',
backgroundColor: isDark ? 'rgba(66,71,84,0.30)' : '#f1f5f9',
}}
>
<div
style={{
height: '2px',
width: `${toast.progress}%`,
backgroundColor: cfg.color,
transition: 'width 0.1s linear',
}}
/>
</div>
</div>
);
})}
</div>
)}
</div>
);
};
export default FloatToastContent;

파일 보기

@ -7,7 +7,7 @@ const AdminPlaceholder = ({ label }: AdminPlaceholderProps) => (
<div className="flex flex-col items-center justify-center h-full gap-3">
<div className="text-4xl opacity-20">🚧</div>
<div className="text-sm font-korean text-fg-sub font-semibold">{label}</div>
<div className="text-[11px] font-korean text-fg-disabled"> .</div>
<div className="text-label-2 font-korean text-fg-disabled"> .</div>
</div>
);

파일 보기

@ -37,7 +37,7 @@ const AdminSidebar = ({ activeMenu, onSelect }: AdminSidebarProps) => {
<button
key={item.id}
onClick={() => onSelect(item.id)}
className="w-full text-left px-3 py-1.5 text-[11px] font-korean transition-colors cursor-pointer rounded-[3px]"
className="w-full text-left px-3 py-1.5 text-label-2 font-korean transition-colors cursor-pointer rounded-[3px]"
style={{
paddingLeft: `${12 + depth * 14}px`,
background: isActive ? 'rgba(6,182,212,.12)' : 'transparent',
@ -65,7 +65,7 @@ const AdminSidebar = ({ activeMenu, onSelect }: AdminSidebarProps) => {
if (firstLeaf) onSelect(firstLeaf.id);
}
}}
className="w-full flex items-center justify-between px-3 py-1.5 text-[11px] font-korean transition-colors cursor-pointer rounded-[3px]"
className="w-full flex items-center justify-between px-3 py-1.5 text-label-2 font-korean transition-colors cursor-pointer rounded-[3px]"
style={{
paddingLeft: `${12 + depth * 14}px`,
color: hasActiveChild ? 'var(--color-accent)' : 'var(--fg-sub)',
@ -74,7 +74,7 @@ const AdminSidebar = ({ activeMenu, onSelect }: AdminSidebarProps) => {
>
<span>{item.label}</span>
<span
className="text-[9px] text-fg-disabled transition-transform"
className="text-caption text-fg-disabled transition-transform"
style={{ transform: isOpen ? 'rotate(90deg)' : 'rotate(0)' }}
>
@ -123,7 +123,7 @@ const AdminSidebar = ({ activeMenu, onSelect }: AdminSidebarProps) => {
{/* 섹션 헤더 */}
<button
onClick={() => toggle(section.id)}
className="w-full flex items-center gap-2 px-3 py-2 rounded-md text-[11px] font-bold font-korean transition-colors cursor-pointer"
className="w-full flex items-center gap-2 px-3 py-2 rounded-md text-label-2 font-bold font-korean transition-colors cursor-pointer"
style={{
background: hasActiveChild ? 'rgba(6,182,212,.08)' : 'transparent',
color: hasActiveChild ? 'var(--color-accent)' : 'var(--fg-default)',
@ -132,7 +132,7 @@ const AdminSidebar = ({ activeMenu, onSelect }: AdminSidebarProps) => {
<span className="text-sm">{section.icon}</span>
<span className="flex-1 text-left">{section.label}</span>
<span
className="text-[9px] text-fg-disabled transition-transform"
className="text-caption text-fg-disabled transition-transform"
style={{ transform: isOpen ? 'rotate(90deg)' : 'rotate(0)' }}
>

파일 보기

@ -130,7 +130,7 @@ function AssetUploadPanel() {
className={`rounded-lg border-2 border-dashed py-8 text-center cursor-pointer transition-colors ${
dragging
? 'border-color-accent bg-[rgba(6,182,212,0.05)]'
: 'border-stroke hover:border-color-accent/50 bg-bg-elevated'
: 'border-stroke hover:border-[rgba(6,182,212,0.5)] bg-bg-elevated'
}`}
>
<div className="text-3xl mb-2 opacity-40">📁</div>
@ -143,7 +143,7 @@ function AssetUploadPanel() {
<div className="text-xs font-semibold text-fg-sub font-korean mb-1">
</div>
<div className="text-[10px] text-fg-disabled font-korean mb-3">
<div className="text-caption text-fg-disabled font-korean mb-3">
(.xlsx), CSV · 10MB
</div>
<button
@ -170,7 +170,7 @@ function AssetUploadPanel() {
{/* 자산 분류 */}
<div>
<label className="block text-[11px] font-semibold text-fg-sub font-korean mb-1.5">
<label className="block text-label-2 font-semibold text-fg-sub font-korean mb-1.5">
</label>
<select
@ -189,7 +189,7 @@ function AssetUploadPanel() {
{/* 대상 관할 */}
<div>
<label className="block text-[11px] font-semibold text-fg-sub font-korean mb-1.5">
<label className="block text-label-2 font-semibold text-fg-sub font-korean mb-1.5">
</label>
<select
@ -208,7 +208,7 @@ function AssetUploadPanel() {
{/* 업로드 방식 */}
<div>
<label className="block text-[11px] font-semibold text-fg-sub font-korean mb-1.5">
<label className="block text-label-2 font-semibold text-fg-sub font-korean mb-1.5">
</label>
<div className="flex gap-4">
@ -271,7 +271,7 @@ function AssetUploadPanel() {
</div>
<div>
<div className={`text-xs font-bold font-korean ${p.color}`}>{p.role}</div>
<div className="text-[10px] text-fg-disabled font-korean mt-0.5">
<div className="text-caption text-fg-disabled font-korean mt-0.5">
{p.desc}
</div>
</div>
@ -287,7 +287,7 @@ function AssetUploadPanel() {
</div>
<div className="px-5 py-4 space-y-2">
{uploadHistory.length === 0 ? (
<div className="text-[11px] text-fg-disabled font-korean text-center py-4">
<div className="text-label-2 text-fg-disabled font-korean text-center py-4">
.
</div>
) : (
@ -298,12 +298,12 @@ function AssetUploadPanel() {
>
<div>
<div className="text-xs font-semibold text-fg font-korean">{h.fileNm}</div>
<div className="text-[10px] text-fg-disabled mt-0.5 font-korean">
<div className="text-caption text-fg-disabled mt-0.5 font-korean">
{formatDate(h.regDtm)} · {h.uploaderNm} · {h.uploadCnt.toLocaleString()}
</div>
</div>
<span
className="px-2 py-0.5 rounded-full text-[10px] font-semibold
className="px-2 py-0.5 rounded-full text-caption font-semibold
bg-[rgba(34,197,94,0.15)] text-color-success flex-shrink-0"
>

파일 보기

@ -273,7 +273,7 @@ function PostRow({ post, checked, onToggle }: PostRowProps) {
<td className="py-2 text-center text-fg-disabled">{post.sn}</td>
<td className="py-2 text-center">
<span
className={`inline-block px-2 py-0.5 rounded-full text-[10px] font-medium ${
className={`inline-block px-2 py-0.5 rounded-full text-caption font-medium ${
post.categoryCd === 'NOTICE'
? 'bg-red-500/15 text-red-400'
: post.categoryCd === 'QNA'
@ -285,7 +285,7 @@ function PostRow({ post, checked, onToggle }: PostRowProps) {
</span>
</td>
<td className="py-2 pl-3 text-fg truncate max-w-[300px]">
{post.pinnedYn === 'Y' && <span className="text-[10px] text-orange-400 mr-1">[]</span>}
{post.pinnedYn === 'Y' && <span className="text-caption text-orange-400 mr-1">[]</span>}
{post.title}
</td>
<td className="py-2 text-center text-fg-sub">{post.authorName}</td>

파일 보기

@ -167,47 +167,47 @@ function CleanupEquipPanel() {
<table className="w-full">
<thead>
<tr className="border-b border-stroke bg-bg-surface">
<th className="px-4 py-3 text-left text-[11px] font-semibold text-fg-disabled font-korean w-10 whitespace-nowrap">
<th className="px-4 py-3 text-left text-label-2 font-semibold text-fg-disabled font-korean w-10 whitespace-nowrap">
</th>
<th className="px-4 py-3 text-left text-[11px] font-semibold text-fg-disabled font-korean">
<th className="px-4 py-3 text-left text-label-2 font-semibold text-fg-disabled font-korean">
</th>
<th className="px-4 py-3 text-left text-[11px] font-semibold text-fg-disabled font-korean">
<th className="px-4 py-3 text-left text-label-2 font-semibold text-fg-disabled font-korean">
</th>
<th className="px-4 py-3 text-left text-[11px] font-semibold text-fg-disabled font-korean">
<th className="px-4 py-3 text-left text-label-2 font-semibold text-fg-disabled font-korean">
</th>
<th className="px-4 py-3 text-left text-[11px] font-semibold text-fg-disabled font-korean">
<th className="px-4 py-3 text-left text-label-2 font-semibold text-fg-disabled font-korean">
</th>
<th
className={`px-4 py-3 text-center text-[11px] font-semibold font-korean ${equipFilter === '방제선' ? 'text-color-accent bg-color-accent/5' : 'text-fg-disabled'}`}
className={`px-4 py-3 text-center text-label-2 font-semibold font-korean ${equipFilter === '방제선' ? 'text-color-accent bg-[rgba(6,182,212,0.05)]' : 'text-fg-disabled'}`}
>
</th>
<th
className={`px-4 py-3 text-center text-[11px] font-semibold font-korean ${equipFilter === '유회수기' ? 'text-color-accent bg-color-accent/5' : 'text-fg-disabled'}`}
className={`px-4 py-3 text-center text-label-2 font-semibold font-korean ${equipFilter === '유회수기' ? 'text-color-accent bg-[rgba(6,182,212,0.05)]' : 'text-fg-disabled'}`}
>
</th>
<th
className={`px-4 py-3 text-center text-[11px] font-semibold font-korean ${equipFilter === '이송펌프' ? 'text-color-accent bg-color-accent/5' : 'text-fg-disabled'}`}
className={`px-4 py-3 text-center text-label-2 font-semibold font-korean ${equipFilter === '이송펌프' ? 'text-color-accent bg-[rgba(6,182,212,0.05)]' : 'text-fg-disabled'}`}
>
</th>
<th
className={`px-4 py-3 text-center text-[11px] font-semibold font-korean ${equipFilter === '방제차량' ? 'text-color-accent bg-color-accent/5' : 'text-fg-disabled'}`}
className={`px-4 py-3 text-center text-label-2 font-semibold font-korean ${equipFilter === '방제차량' ? 'text-color-accent bg-[rgba(6,182,212,0.05)]' : 'text-fg-disabled'}`}
>
</th>
<th
className={`px-4 py-3 text-center text-[11px] font-semibold font-korean ${equipFilter === '살포장치' ? 'text-color-accent bg-color-accent/5' : 'text-fg-disabled'}`}
className={`px-4 py-3 text-center text-label-2 font-semibold font-korean ${equipFilter === '살포장치' ? 'text-color-accent bg-[rgba(6,182,212,0.05)]' : 'text-fg-disabled'}`}
>
</th>
<th className="px-4 py-3 text-center text-[11px] font-semibold text-fg-disabled font-korean">
<th className="px-4 py-3 text-center text-label-2 font-semibold text-fg-disabled font-korean">
</th>
</tr>
@ -228,51 +228,51 @@ function CleanupEquipPanel() {
key={org.id}
className="border-b border-stroke hover:bg-[rgba(255,255,255,0.02)] transition-colors"
>
<td className="px-4 py-3 text-[11px] text-fg-disabled font-mono text-center">
<td className="px-4 py-3 text-label-2 text-fg-disabled font-mono text-center">
{(safePage - 1) * PAGE_SIZE + idx + 1}
</td>
<td className="px-4 py-3">
<span
className={`text-[10px] px-1.5 py-0.5 rounded font-bold font-korean ${typeTagCls(org.type)}`}
className={`text-caption px-1.5 py-0.5 rounded font-bold font-korean ${typeTagCls(org.type)}`}
>
{org.type}
</span>
</td>
<td className="px-4 py-3 text-[11px] text-fg-sub font-korean">
<td className="px-4 py-3 text-label-2 text-fg-sub font-korean">
{regionShort(org.jurisdiction)}
</td>
<td className="px-4 py-3 text-[11px] text-fg font-korean font-semibold">
<td className="px-4 py-3 text-label-2 text-fg font-korean font-semibold">
{org.name}
</td>
<td className="px-4 py-3 text-[11px] text-fg-disabled font-korean max-w-[200px] truncate">
<td className="px-4 py-3 text-label-2 text-fg-disabled font-korean max-w-[200px] truncate">
{org.address}
</td>
<td
className={`px-4 py-3 text-[11px] font-mono text-center ${equipFilter === '방제선' ? 'text-color-accent font-semibold bg-color-accent/5' : 'text-fg-sub'}`}
className={`px-4 py-3 text-label-2 font-mono text-center ${equipFilter === '방제선' ? 'text-color-accent font-semibold bg-[rgba(6,182,212,0.05)]' : 'text-fg-sub'}`}
>
{org.vessel > 0 ? org.vessel : <span className="text-fg-disabled"></span>}
</td>
<td
className={`px-4 py-3 text-[11px] font-mono text-center ${equipFilter === '유회수기' ? 'text-color-accent font-semibold bg-color-accent/5' : 'text-fg-sub'}`}
className={`px-4 py-3 text-label-2 font-mono text-center ${equipFilter === '유회수기' ? 'text-color-accent font-semibold bg-[rgba(6,182,212,0.05)]' : 'text-fg-sub'}`}
>
{org.skimmer > 0 ? org.skimmer : <span className="text-fg-disabled"></span>}
</td>
<td
className={`px-4 py-3 text-[11px] font-mono text-center ${equipFilter === '이송펌프' ? 'text-color-accent font-semibold bg-color-accent/5' : 'text-fg-sub'}`}
className={`px-4 py-3 text-label-2 font-mono text-center ${equipFilter === '이송펌프' ? 'text-color-accent font-semibold bg-[rgba(6,182,212,0.05)]' : 'text-fg-sub'}`}
>
{org.pump > 0 ? org.pump : <span className="text-fg-disabled"></span>}
</td>
<td
className={`px-4 py-3 text-[11px] font-mono text-center ${equipFilter === '방제차량' ? 'text-color-accent font-semibold bg-color-accent/5' : 'text-fg-sub'}`}
className={`px-4 py-3 text-label-2 font-mono text-center ${equipFilter === '방제차량' ? 'text-color-accent font-semibold bg-[rgba(6,182,212,0.05)]' : 'text-fg-sub'}`}
>
{org.vehicle > 0 ? org.vehicle : <span className="text-fg-disabled"></span>}
</td>
<td
className={`px-4 py-3 text-[11px] font-mono text-center ${equipFilter === '살포장치' ? 'text-color-accent font-semibold bg-color-accent/5' : 'text-fg-sub'}`}
className={`px-4 py-3 text-label-2 font-mono text-center ${equipFilter === '살포장치' ? 'text-color-accent font-semibold bg-[rgba(6,182,212,0.05)]' : 'text-fg-sub'}`}
>
{org.sprayer > 0 ? org.sprayer : <span className="text-fg-disabled"></span>}
</td>
<td className="px-4 py-3 text-[11px] font-mono text-center font-bold text-color-accent">
<td className="px-4 py-3 text-label-2 font-mono text-center font-bold text-color-accent">
{org.totalAssets.toLocaleString()}
</td>
</tr>
@ -286,7 +286,7 @@ function CleanupEquipPanel() {
{/* 합계 */}
{!loading && filtered.length > 0 && (
<div className="flex items-center gap-4 px-6 py-2 border-t border-stroke bg-bg-base/80">
<span className="text-[10px] text-fg-disabled font-korean font-semibold mr-auto">
<span className="text-caption text-fg-disabled font-korean font-semibold mr-auto">
({filtered.length} )
</span>
{[
@ -301,15 +301,15 @@ function CleanupEquipPanel() {
return (
<div
key={t.label}
className={`flex items-center gap-1 px-1.5 py-0.5 rounded ${isActive ? 'bg-color-accent/10' : ''}`}
className={`flex items-center gap-1 px-1.5 py-0.5 rounded ${isActive ? 'bg-[rgba(6,182,212,0.1)]' : ''}`}
>
<span
className={`text-[9px] font-korean ${isActive ? 'text-color-accent' : 'text-fg-disabled'}`}
className={`text-caption font-korean ${isActive ? 'text-color-accent' : 'text-fg-disabled'}`}
>
{t.label}
</span>
<span
className={`text-[10px] font-mono font-bold ${isActive ? 'text-color-accent' : 'text-fg'}`}
className={`text-caption font-mono font-bold ${isActive ? 'text-color-accent' : 'text-fg'}`}
>
{t.value.toLocaleString()}
{t.unit}
@ -323,7 +323,7 @@ function CleanupEquipPanel() {
{/* 페이지네이션 */}
{!loading && filtered.length > 0 && (
<div className="flex items-center justify-between px-6 py-3 border-t border-stroke">
<span className="text-[11px] text-fg-disabled font-korean">
<span className="text-label-2 text-fg-disabled font-korean">
{(safePage - 1) * PAGE_SIZE + 1}{Math.min(safePage * PAGE_SIZE, filtered.length)} /
{filtered.length}
</span>
@ -331,7 +331,7 @@ function CleanupEquipPanel() {
<button
onClick={() => setCurrentPage((p) => Math.max(1, p - 1))}
disabled={safePage === 1}
className="px-2.5 py-1 text-[11px] border border-stroke rounded text-fg-sub hover:border-color-accent hover:text-color-accent disabled:opacity-40 transition-colors"
className="px-2.5 py-1 text-label-2 border border-stroke rounded text-fg-sub hover:border-color-accent hover:text-color-accent disabled:opacity-40 transition-colors"
>
&lt;
</button>
@ -339,7 +339,7 @@ function CleanupEquipPanel() {
<button
key={p}
onClick={() => setCurrentPage(p)}
className="px-2.5 py-1 text-[11px] border rounded transition-colors"
className="px-2.5 py-1 text-label-2 border rounded transition-colors"
style={
p === safePage
? {
@ -356,7 +356,7 @@ function CleanupEquipPanel() {
<button
onClick={() => setCurrentPage((p) => Math.min(totalPages, p + 1))}
disabled={safePage === totalPages}
className="px-2.5 py-1 text-[11px] border border-stroke rounded text-fg-sub hover:border-color-accent hover:text-color-accent disabled:opacity-40 transition-colors"
className="px-2.5 py-1 text-label-2 border border-stroke rounded text-fg-sub hover:border-color-accent hover:text-color-accent disabled:opacity-40 transition-colors"
>
&gt;
</button>

파일 보기

@ -256,7 +256,7 @@ function HrTable({ rows, loading }: { rows: HrCollectItem[]; loading: boolean })
<td className="px-3 py-2 text-t2 font-mono">{row.jobName}</td>
<td className="px-3 py-2 text-center">
<span
className={`inline-block px-1.5 py-0.5 rounded text-[11px] font-medium ${
className={`inline-block px-1.5 py-0.5 rounded text-label-2 font-medium ${
row.activeYn === 'Y'
? 'text-emerald-400 bg-emerald-500/10'
: 'text-t3 bg-bg-elevated'
@ -269,7 +269,7 @@ function HrTable({ rows, loading }: { rows: HrCollectItem[]; loading: boolean })
<td className="px-3 py-2 text-t2 whitespace-nowrap">{row.clctDate ?? '-'}</td>
<td className="px-3 py-2">
<span
className={`inline-block px-2 py-0.5 rounded text-[11px] font-medium ${status.color}`}
className={`inline-block px-2 py-0.5 rounded text-label-2 font-medium ${status.color}`}
>
{status.label}
</span>

파일 보기

@ -149,7 +149,7 @@ const DispersingZonePanel = () => {
/>
</button>
{/* 펼침 화살표 */}
<span className="text-fg-disabled text-[10px] shrink-0">{isExpanded ? '▲' : '▼'}</span>
<span className="text-fg-disabled text-caption shrink-0">{isExpanded ? '▲' : '▼'}</span>
</div>
{/* 펼침 영역 */}
@ -159,10 +159,10 @@ const DispersingZonePanel = () => {
<tbody>
{info.rows.map((row) => (
<tr key={row.key} className="border-b border-stroke last:border-0">
<td className="py-2 pr-2 text-[11px] text-fg-disabled font-korean whitespace-nowrap align-top w-24">
<td className="py-2 pr-2 text-label-2 text-fg-disabled font-korean whitespace-nowrap align-top w-24">
{row.key}
</td>
<td className="py-2 text-[11px] text-fg-sub font-korean leading-relaxed">
<td className="py-2 text-label-2 text-fg-sub font-korean leading-relaxed">
{row.value}
</td>
</tr>
@ -196,11 +196,11 @@ const DispersingZonePanel = () => {
<div className="absolute bottom-4 left-4 bg-bg-surface border border-stroke rounded-lg px-3 py-2 flex flex-col gap-1.5">
<div className="flex items-center gap-2">
<span className="w-3 h-3 rounded-sm bg-blue-500 opacity-80" />
<span className="text-[11px] text-fg-sub font-korean"></span>
<span className="text-label-2 text-fg-sub font-korean"></span>
</div>
<div className="flex items-center gap-2">
<span className="w-3 h-3 rounded-sm bg-red-500 opacity-80" />
<span className="text-[11px] text-fg-sub font-korean"></span>
<span className="text-label-2 text-fg-sub font-korean"></span>
</div>
</div>
</div>
@ -210,7 +210,7 @@ const DispersingZonePanel = () => {
{/* 헤더 */}
<div className="px-4 py-4 border-b border-stroke shrink-0">
<h1 className="text-sm font-bold text-fg font-korean"> </h1>
<p className="text-[11px] text-fg-disabled mt-0.5 font-korean"> </p>
<p className="text-label-2 text-fg-disabled mt-0.5 font-korean"> </p>
</div>
{/* 구역 카드 목록 */}

파일 보기

@ -187,7 +187,7 @@ const LayerFormModal = ({ mode, initialData, onClose, onSaved }: LayerFormModalP
const inputCls =
'w-full px-3 py-2 text-xs bg-bg-elevated border border-stroke rounded-md text-fg placeholder-fg-disabled focus:border-color-accent focus:outline-none';
const labelCls = 'block text-[11px] font-semibold text-fg-sub font-korean mb-1.5';
const labelCls = 'block text-label-2 font-semibold text-fg-sub font-korean mb-1.5';
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm">
@ -321,7 +321,7 @@ const LayerFormModal = ({ mode, initialData, onClose, onSaved }: LayerFormModalP
{/* 에러 */}
{formError && (
<div className="px-6 pb-2">
<p className="text-[11px] text-red-400 font-korean">{formError}</p>
<p className="text-label-2 text-red-400 font-korean">{formError}</p>
</div>
)}
{/* 버튼 */}
@ -502,34 +502,34 @@ const LayerPanel = () => {
<table className="w-full">
<thead>
<tr className="border-b border-stroke bg-bg-surface sticky top-0 z-10">
<th className="px-4 py-3 text-left text-[11px] font-semibold text-fg-disabled font-korean w-10 whitespace-nowrap">
<th className="px-4 py-3 text-left text-label-2 font-semibold text-fg-disabled font-korean w-10 whitespace-nowrap">
</th>
<th className="px-4 py-3 text-left text-[11px] font-semibold text-fg-disabled font-mono">
<th className="px-4 py-3 text-left text-label-2 font-semibold text-fg-disabled font-mono">
</th>
<th className="px-4 py-3 text-left text-[11px] font-semibold text-fg-disabled font-korean">
<th className="px-4 py-3 text-left text-label-2 font-semibold text-fg-disabled font-korean">
</th>
<th className="px-4 py-3 text-left text-[11px] font-semibold text-fg-disabled font-korean">
<th className="px-4 py-3 text-left text-label-2 font-semibold text-fg-disabled font-korean">
</th>
<th className="px-4 py-3 text-center text-[11px] font-semibold text-fg-disabled font-korean w-12 whitespace-nowrap">
<th className="px-4 py-3 text-center text-label-2 font-semibold text-fg-disabled font-korean w-12 whitespace-nowrap">
</th>
<th className="px-4 py-3 text-left text-[11px] font-semibold text-fg-disabled font-mono">
<th className="px-4 py-3 text-left text-label-2 font-semibold text-fg-disabled font-mono">
WMS레이어명
</th>
<th className="px-4 py-3 text-center text-[11px] font-semibold text-fg-disabled font-korean w-16">
<th className="px-4 py-3 text-center text-label-2 font-semibold text-fg-disabled font-korean w-16">
</th>
<th className="px-4 py-3 text-left text-[11px] font-semibold text-fg-disabled font-korean w-28">
<th className="px-4 py-3 text-left text-label-2 font-semibold text-fg-disabled font-korean w-28">
</th>
<th className="px-4 py-3 text-center text-[11px] font-semibold text-fg-disabled font-korean w-20">
<th className="px-4 py-3 text-center text-label-2 font-semibold text-fg-disabled font-korean w-20">
</th>
<th className="px-4 py-3 text-center text-[11px] font-semibold text-fg-disabled font-korean w-28 whitespace-nowrap">
<th className="px-4 py-3 text-center text-label-2 font-semibold text-fg-disabled font-korean w-28 whitespace-nowrap">
</th>
</tr>
@ -555,7 +555,7 @@ const LayerPanel = () => {
{(page - 1) * PAGE_SIZE + idx + 1}
</td>
{/* 레이어코드 */}
<td className="px-4 py-3 text-[11px] text-fg-sub font-mono">{item.layerCd}</td>
<td className="px-4 py-3 text-label-2 text-fg-sub font-mono">{item.layerCd}</td>
{/* 레이어명 */}
<td className="px-4 py-3 text-xs text-fg font-korean">{item.layerNm}</td>
{/* 레이어전체명 */}
@ -566,12 +566,12 @@ const LayerPanel = () => {
</td>
{/* 레벨 */}
<td className="px-4 py-3 text-center">
<span className="inline-flex items-center justify-center w-5 h-5 rounded text-[10px] font-semibold bg-[rgba(6,182,212,0.1)] text-color-accent border border-[rgba(6,182,212,0.3)]">
<span className="inline-flex items-center justify-center w-5 h-5 rounded text-caption font-semibold bg-[rgba(6,182,212,0.1)] text-color-accent border border-[rgba(6,182,212,0.3)]">
{item.layerLevel}
</span>
</td>
{/* WMS레이어명 */}
<td className="px-4 py-3 text-[11px] text-fg-sub font-mono">
<td className="px-4 py-3 text-label-2 text-fg-sub font-mono">
{item.wmsLayerNm ?? <span className="text-fg-disabled">-</span>}
</td>
{/* 정렬순서 */}
@ -579,7 +579,7 @@ const LayerPanel = () => {
{item.sortOrd}
</td>
{/* 등록일시 */}
<td className="px-4 py-3 text-[11px] text-fg-disabled font-mono">
<td className="px-4 py-3 text-label-2 text-fg-disabled font-mono">
{item.regDtm ?? '-'}
</td>
{/* 사용여부 토글 */}
@ -598,7 +598,7 @@ const LayerPanel = () => {
item.useYn === 'Y' && item.parentUseYn !== 'N'
? 'bg-color-accent'
: item.useYn === 'Y' && item.parentUseYn === 'N'
? 'bg-color-accent/40'
? 'bg-[rgba(6,182,212,0.4)]'
: 'bg-[rgba(255,255,255,0.08)] border border-stroke'
}`}
>
@ -637,27 +637,27 @@ const LayerPanel = () => {
{/* 페이지네이션 */}
{!loading && totalPages > 1 && (
<div className="flex items-center justify-between px-6 py-3 border-t border-stroke bg-bg-surface shrink-0">
<span className="text-[11px] text-fg-disabled font-korean">
<span className="text-label-2 text-fg-disabled font-korean">
{(page - 1) * PAGE_SIZE + 1}{Math.min(page * PAGE_SIZE, total)} / {total}
</span>
<div className="flex items-center gap-1">
<button
onClick={() => setPage((p) => Math.max(1, p - 1))}
disabled={page === 1}
className="px-2.5 py-1 text-[11px] border border-stroke text-fg-disabled rounded hover:bg-[rgba(255,255,255,0.04)] disabled:opacity-40 transition-all font-korean"
className="px-2.5 py-1 text-label-2 border border-stroke text-fg-disabled rounded hover:bg-[rgba(255,255,255,0.04)] disabled:opacity-40 transition-all font-korean"
>
</button>
{buildPageButtons().map((btn, i) =>
btn === 'ellipsis' ? (
<span key={`e${i}`} className="px-1.5 text-[11px] text-fg-disabled">
<span key={`e${i}`} className="px-1.5 text-label-2 text-fg-disabled">
</span>
) : (
<button
key={btn}
onClick={() => setPage(btn)}
className={`px-2.5 py-1 text-[11px] rounded transition-all ${
className={`px-2.5 py-1 text-label-2 rounded transition-all ${
page === btn
? 'bg-color-accent text-bg-0 font-semibold shadow-[0_0_8px_rgba(6,182,212,0.25)]'
: 'border border-stroke text-fg-disabled hover:bg-[rgba(255,255,255,0.04)]'
@ -670,7 +670,7 @@ const LayerPanel = () => {
<button
onClick={() => setPage((p) => Math.min(totalPages, p + 1))}
disabled={page === totalPages}
className="px-2.5 py-1 text-[11px] border border-stroke text-fg-disabled rounded hover:bg-[rgba(255,255,255,0.04)] disabled:opacity-40 transition-all font-korean"
className="px-2.5 py-1 text-label-2 border border-stroke text-fg-disabled rounded hover:bg-[rgba(255,255,255,0.04)] disabled:opacity-40 transition-all font-korean"
>
</button>

파일 보기

@ -100,7 +100,7 @@ function MapBaseModal({
<div className="px-6 py-4 space-y-4">
{/* 지도 이름 */}
<div>
<label className="block text-[11px] font-semibold text-fg-sub font-korean mb-1.5">
<label className="block text-label-2 font-semibold text-fg-sub font-korean mb-1.5">
<span className="text-red-400">*</span>
</label>
<input
@ -114,7 +114,7 @@ function MapBaseModal({
{/* 지도 키 */}
<div>
<label className="block text-[11px] font-semibold text-fg-sub font-korean mb-1.5">
<label className="block text-label-2 font-semibold text-fg-sub font-korean mb-1.5">
<span className="text-red-400">*</span>
</label>
<input
@ -129,7 +129,7 @@ function MapBaseModal({
{/* 지도 레벨 */}
<div>
<label className="block text-[11px] font-semibold text-fg-sub font-korean mb-1.5">
<label className="block text-label-2 font-semibold text-fg-sub font-korean mb-1.5">
</label>
<select
@ -148,7 +148,7 @@ function MapBaseModal({
{/* 파일 소스 */}
<div>
<label className="block text-[11px] font-semibold text-fg-sub font-korean mb-1.5">
<label className="block text-label-2 font-semibold text-fg-sub font-korean mb-1.5">
</label>
<input
@ -162,7 +162,7 @@ function MapBaseModal({
{/* 상세 설명 */}
<div>
<label className="block text-[11px] font-semibold text-fg-sub font-korean mb-1.5">
<label className="block text-label-2 font-semibold text-fg-sub font-korean mb-1.5">
</label>
<textarea
@ -176,7 +176,7 @@ function MapBaseModal({
{/* 사용여부 */}
<div>
<label className="block text-[11px] font-semibold text-fg-sub font-korean mb-1.5">
<label className="block text-label-2 font-semibold text-fg-sub font-korean mb-1.5">
</label>
<div className="flex items-center gap-3">
@ -200,7 +200,7 @@ function MapBaseModal({
</div>
{/* 에러 */}
{modalError && <p className="text-[11px] text-red-400 font-korean">{modalError}</p>}
{modalError && <p className="text-label-2 text-red-400 font-korean">{modalError}</p>}
</div>
{/* 모달 푸터 */}
@ -363,7 +363,7 @@ function MapBasePanel() {
{/* 메시지 */}
{message && (
<div
className={`mx-6 mt-2 px-3 py-2 text-[11px] rounded-md font-korean ${
className={`mx-6 mt-2 px-3 py-2 text-label-2 rounded-md font-korean ${
message.type === 'success'
? 'text-green-400 bg-[rgba(74,222,128,0.08)] border border-[rgba(74,222,128,0.2)]'
: 'text-red-400 bg-[rgba(239,68,68,0.08)] border border-[rgba(239,68,68,0.2)]'
@ -404,7 +404,7 @@ function MapBasePanel() {
<td className="py-3 text-center text-fg-disabled">{(page - 1) * 10 + idx + 1}</td>
<td className="py-3 pl-4">
<span className="text-fg font-korean">{item.mapNm}</span>
<span className="ml-2 text-[10px] text-fg-disabled font-mono">
<span className="ml-2 text-caption text-fg-disabled font-mono">
{item.mapKey}
</span>
</td>

파일 보기

@ -189,8 +189,8 @@ function MenusPanel() {
{activeMenu ? (
<div className="flex items-center gap-3 px-4 py-3 rounded-md border border-color-accent bg-bg-surface shadow-lg opacity-90 max-w-[700px]">
<span className="text-fg-disabled text-xs"></span>
<span className="text-[16px]">{activeMenu.icon}</span>
<span className="text-[13px] font-semibold text-fg font-korean">
<span className="text-title-2">{activeMenu.icon}</span>
<span className="text-title-4 font-semibold text-fg font-korean">
{activeMenu.label}
</span>
</div>

파일 보기

@ -342,19 +342,19 @@ function ConnectionBadge({
if (isNormal) {
return (
<div className="flex flex-col items-start gap-0.5">
<span className="inline-flex items-center px-2 py-0.5 rounded text-[11px] font-semibold bg-blue-600 text-white">
<span className="inline-flex items-center px-2 py-0.5 rounded text-label-2 font-semibold bg-blue-600 text-white">
ON
</span>
{lastMessageTime && <span className="text-[10px] text-t3">{lastMessageTime}</span>}
{lastMessageTime && <span className="text-caption text-t3">{lastMessageTime}</span>}
</div>
);
}
return (
<div className="flex flex-col items-start gap-0.5">
<span className="inline-flex items-center px-2 py-0.5 rounded text-[11px] font-semibold bg-orange-500 text-white">
<span className="inline-flex items-center px-2 py-0.5 rounded text-label-2 font-semibold bg-orange-500 text-white">
OFF
</span>
{lastMessageTime && <span className="text-[10px] text-t3">{lastMessageTime}</span>}
{lastMessageTime && <span className="text-caption text-t3">{lastMessageTime}</span>}
</div>
);
}

파일 보기

@ -133,7 +133,7 @@ function PermCell({ state, onToggle, label, readOnly = false }: PermCellProps) {
const isDisabled = state === 'forced-denied' || readOnly;
const baseClasses =
'w-5 h-5 rounded border text-[10px] font-bold transition-all flex items-center justify-center';
'w-5 h-5 rounded border text-caption font-bold transition-all flex items-center justify-center';
let classes: string;
let icon: string;
@ -240,14 +240,14 @@ function TreeRow({
</svg>
</button>
) : (
<span className="w-4 mr-1 flex-shrink-0 text-center text-fg-disabled text-[9px]">
<span className="w-4 mr-1 flex-shrink-0 text-center text-fg-disabled text-caption">
{node.level > 0 ? '├' : ''}
</span>
)}
{node.icon && <span className="mr-1 flex-shrink-0 text-[11px]">{node.icon}</span>}
{node.icon && <span className="mr-1 flex-shrink-0 text-label-2">{node.icon}</span>}
<div className="min-w-0">
<div
className={`text-[11px] font-korean truncate ${node.level === 0 ? 'font-bold text-fg' : 'font-medium text-fg-sub'}`}
className={`text-label-2 font-korean truncate ${node.level === 0 ? 'font-bold text-fg' : 'font-medium text-fg-sub'}`}
>
{node.name}
</div>
@ -295,29 +295,29 @@ function TreeRow({
function PermLegend() {
return (
<div
className="flex items-center gap-3 px-4 py-1.5 border-b border-stroke bg-bg-surface text-[9px] text-fg-disabled font-korean"
className="flex items-center gap-3 px-4 py-1.5 border-b border-stroke bg-bg-surface text-caption text-fg-disabled font-korean"
style={{ flexShrink: 0 }}
>
<span className="flex items-center gap-1">
<span className="inline-block w-3 h-3 rounded border bg-[rgba(6,182,212,0.2)] border-color-accent text-color-accent text-center text-[8px] leading-3">
<span className="inline-block w-3 h-3 rounded border bg-[rgba(6,182,212,0.2)] border-color-accent text-color-accent text-center text-caption leading-3">
</span>
</span>
<span className="flex items-center gap-1">
<span className="inline-block w-3 h-3 rounded border bg-[rgba(6,182,212,0.08)] border-[rgba(6,182,212,0.3)] text-[rgba(6,182,212,0.5)] text-center text-[8px] leading-3">
<span className="inline-block w-3 h-3 rounded border bg-[rgba(6,182,212,0.08)] border-[rgba(6,182,212,0.3)] text-[rgba(6,182,212,0.5)] text-center text-caption leading-3">
</span>
</span>
<span className="flex items-center gap-1">
<span className="inline-block w-3 h-3 rounded border bg-[rgba(239,68,68,0.08)] border-[rgba(239,68,68,0.3)] text-red-400 text-center text-[8px] leading-3">
<span className="inline-block w-3 h-3 rounded border bg-[rgba(239,68,68,0.08)] border-[rgba(239,68,68,0.3)] text-red-400 text-center text-caption leading-3">
</span>
</span>
<span className="flex items-center gap-1">
<span className="inline-block w-3 h-3 rounded border bg-bg-elevated border-stroke text-fg-disabled opacity-40 text-center text-[8px] leading-3">
<span className="inline-block w-3 h-3 rounded border bg-bg-elevated border-stroke text-fg-disabled opacity-40 text-center text-caption leading-3">
</span>
@ -418,14 +418,14 @@ function RolePermTab({
setShowCreateForm(true);
setCreateError('');
}}
className="px-3 py-1.5 text-[11px] font-semibold rounded-md border border-color-accent text-color-accent hover:bg-[rgba(6,182,212,0.08)] transition-all font-korean"
className="px-3 py-1.5 text-label-2 font-semibold rounded-md border border-color-accent text-color-accent hover:bg-[rgba(6,182,212,0.08)] transition-all font-korean"
>
+
</button>
<button
onClick={handleSave}
disabled={!dirty || saving}
className={`px-3 py-1.5 text-[11px] font-semibold rounded-md transition-all font-korean ${
className={`px-3 py-1.5 text-label-2 font-semibold rounded-md transition-all font-korean ${
dirty
? 'bg-color-accent text-bg-0 hover:shadow-[0_0_12px_rgba(6,182,212,0.3)]'
: 'bg-bg-card text-fg-disabled cursor-not-allowed'
@ -434,7 +434,7 @@ function RolePermTab({
{saving ? '저장 중...' : '변경사항 저장'}
</button>
{saveError && (
<span className="text-[11px] text-color-danger font-korean">{saveError}</span>
<span className="text-label-2 text-color-danger font-korean">{saveError}</span>
)}
</div>
@ -450,7 +450,7 @@ function RolePermTab({
<div key={role.sn} className="flex items-center gap-0.5 flex-shrink-0">
<button
onClick={() => setSelectedRoleSn(role.sn)}
className={`px-2.5 py-1 text-[11px] font-semibold rounded-md transition-all font-korean ${
className={`px-2.5 py-1 text-label-2 font-semibold rounded-md transition-all font-korean ${
isSelected
? 'border-2 shadow-[0_0_8px_rgba(6,182,212,0.2)]'
: 'border border-stroke text-fg-disabled hover:border-stroke'
@ -469,19 +469,21 @@ function RolePermTab({
onBlur={() => handleSaveRoleName(role.sn)}
onClick={(e) => e.stopPropagation()}
autoFocus
className="w-20 px-1 py-0 text-[11px] font-semibold bg-bg-elevated border border-color-accent rounded text-center text-fg focus:outline-none font-korean"
className="w-20 px-1 py-0 text-label-2 font-semibold bg-bg-elevated border border-color-accent rounded text-center text-fg focus:outline-none font-korean"
/>
) : (
<span onDoubleClick={() => handleStartEditName(role)}>{role.name}</span>
)}
<span className="ml-1 text-[9px] font-mono opacity-50">{role.code}</span>
{role.isDefault && <span className="ml-1 text-[9px] text-color-accent"></span>}
<span className="ml-1 text-caption font-mono opacity-50">{role.code}</span>
{role.isDefault && (
<span className="ml-1 text-caption text-color-accent"></span>
)}
</button>
{isSelected && (
<div className="flex items-center gap-0.5">
<button
onClick={() => toggleDefault(role.sn)}
className={`px-1.5 py-0.5 text-[9px] rounded transition-all font-korean ${
className={`px-1.5 py-0.5 text-caption rounded transition-all font-korean ${
role.isDefault
? 'bg-[rgba(6,182,212,0.15)] text-color-accent'
: 'text-fg-disabled hover:text-fg-sub'
@ -525,13 +527,15 @@ function RolePermTab({
<table className="w-full">
<thead>
<tr className="border-b border-stroke bg-bg-surface sticky top-0 z-10">
<th className="px-3 py-1.5 text-left text-[10px] font-semibold text-fg-disabled font-korean min-w-[200px]">
<th className="px-3 py-1.5 text-left text-caption font-semibold text-fg-disabled font-korean min-w-[200px]">
</th>
{OPER_CODES.map((oper) => (
<th key={oper} className="px-1 py-1.5 text-center w-12">
<div className="text-[10px] font-semibold text-fg-sub">{OPER_LABELS[oper]}</div>
<div className="text-[8px] text-fg-disabled font-korean">
<div className="text-caption font-semibold text-fg-sub">
{OPER_LABELS[oper]}
</div>
<div className="text-caption text-fg-disabled font-korean">
{OPER_FULL_LABELS[oper]}
</div>
</th>
@ -567,7 +571,7 @@ function RolePermTab({
</div>
<div className="px-5 py-4 flex flex-col gap-3">
<div>
<label className="text-[11px] text-fg-disabled font-korean block mb-1">
<label className="text-label-2 text-fg-disabled font-korean block mb-1">
</label>
<input
@ -579,12 +583,12 @@ function RolePermTab({
placeholder="CUSTOM_ROLE"
className="w-full px-3 py-2 text-xs bg-bg-elevated border border-stroke rounded-md text-fg placeholder-fg-disabled focus:border-color-accent focus:outline-none font-mono"
/>
<p className="text-[10px] text-fg-disabled mt-1 font-korean">
<p className="text-caption text-fg-disabled mt-1 font-korean">
, , ( )
</p>
</div>
<div>
<label className="text-[11px] text-fg-disabled font-korean block mb-1">
<label className="text-label-2 text-fg-disabled font-korean block mb-1">
</label>
<input
@ -596,7 +600,7 @@ function RolePermTab({
/>
</div>
<div>
<label className="text-[11px] text-fg-disabled font-korean block mb-1">
<label className="text-label-2 text-fg-disabled font-korean block mb-1">
()
</label>
<input
@ -608,7 +612,7 @@ function RolePermTab({
/>
</div>
{createError && (
<div className="px-3 py-2 text-[11px] text-red-400 bg-[rgba(239,68,68,0.08)] border border-[rgba(239,68,68,0.2)] rounded-md font-korean">
<div className="px-3 py-2 text-label-2 text-red-400 bg-[rgba(239,68,68,0.08)] border border-[rgba(239,68,68,0.2)] rounded-md font-korean">
{createError}
</div>
)}
@ -792,7 +796,9 @@ function UserPermTab({ roles, permTree, rolePerms }: UserPermTabProps) {
<div className="flex flex-col flex-1 min-h-0">
{/* 사용자 검색/선택 */}
<div className="px-4 py-2.5 border-b border-stroke" style={{ flexShrink: 0 }}>
<label className="text-[10px] text-fg-disabled font-korean block mb-1.5"> </label>
<label className="text-caption text-fg-disabled font-korean block mb-1.5">
</label>
<div className="relative" ref={dropdownRef}>
<input
type="text"
@ -823,17 +829,17 @@ function UserPermTab({ roles, permTree, rolePerms }: UserPermTabProps) {
<div className="text-xs font-semibold text-fg font-korean truncate">
{user.name}
{user.rank && (
<span className="ml-1 text-[10px] text-fg-disabled font-korean">
<span className="ml-1 text-caption text-fg-disabled font-korean">
{user.rank}
</span>
)}
</div>
<div className="text-[10px] text-fg-disabled font-mono truncate">
<div className="text-caption text-fg-disabled font-mono truncate">
{user.account}
</div>
</div>
{user.orgName && (
<span className="text-[10px] text-fg-disabled font-korean flex-shrink-0 truncate max-w-[100px]">
<span className="text-caption text-fg-disabled font-korean flex-shrink-0 truncate max-w-[100px]">
{user.orgName}
</span>
)}
@ -857,11 +863,11 @@ function UserPermTab({ roles, permTree, rolePerms }: UserPermTabProps) {
style={{ flexShrink: 0 }}
>
<div className="flex items-center justify-between mb-2">
<span className="text-[10px] font-semibold text-fg-sub font-korean"> </span>
<span className="text-caption font-semibold text-fg-sub font-korean"> </span>
<button
onClick={handleSaveRoles}
disabled={!rolesDirty || savingRoles}
className={`px-3 py-1.5 text-[11px] font-semibold rounded-md transition-all font-korean ${
className={`px-3 py-1.5 text-label-2 font-semibold rounded-md transition-all font-korean ${
rolesDirty
? 'bg-color-accent text-bg-0 hover:shadow-[0_0_12px_rgba(6,182,212,0.3)]'
: 'bg-bg-card text-fg-disabled cursor-not-allowed'
@ -878,7 +884,7 @@ function UserPermTab({ roles, permTree, rolePerms }: UserPermTabProps) {
<label
key={role.sn}
className={[
'flex items-center gap-1 px-2 py-1 rounded-md border cursor-pointer transition-all font-korean text-[11px] select-none',
'flex items-center gap-1 px-2 py-1 rounded-md border cursor-pointer transition-all font-korean text-label-2 select-none',
isChecked ? '' : 'border-stroke text-fg-disabled hover:border-text-2',
].join(' ')}
style={
@ -894,7 +900,7 @@ function UserPermTab({ roles, permTree, rolePerms }: UserPermTabProps) {
className="w-3 h-3 accent-primary-cyan"
/>
<span>{role.name}</span>
<span className="text-[9px] font-mono opacity-60">{role.code}</span>
<span className="text-caption font-mono opacity-60">{role.code}</span>
</label>
);
})}
@ -903,7 +909,7 @@ function UserPermTab({ roles, permTree, rolePerms }: UserPermTabProps) {
{/* 유효 권한 매트릭스 (읽기 전용) */}
<div
className="px-4 py-1.5 border-b border-stroke bg-bg-surface text-[9px] text-fg-disabled font-korean"
className="px-4 py-1.5 border-b border-stroke bg-bg-surface text-caption text-fg-disabled font-korean"
style={{ flexShrink: 0 }}
>
<span className="font-semibold text-fg-sub"> ( )</span>
@ -917,15 +923,15 @@ function UserPermTab({ roles, permTree, rolePerms }: UserPermTabProps) {
<table className="w-full">
<thead>
<tr className="border-b border-stroke bg-bg-surface sticky top-0 z-10">
<th className="px-4 py-3 text-left text-[11px] font-semibold text-fg-disabled font-korean min-w-[240px]">
<th className="px-4 py-3 text-left text-label-2 font-semibold text-fg-disabled font-korean min-w-[240px]">
</th>
{OPER_CODES.map((oper) => (
<th key={oper} className="px-2 py-3 text-center w-16">
<div className="text-[11px] font-semibold text-fg-sub">
<div className="text-label-2 font-semibold text-fg-sub">
{OPER_LABELS[oper]}
</div>
<div className="text-[9px] text-fg-disabled font-korean">
<div className="text-caption text-fg-disabled font-korean">
{OPER_FULL_LABELS[oper]}
</div>
</th>
@ -1189,7 +1195,7 @@ function PermissionsPanel() {
>
<div>
<h1 className="text-sm font-bold text-fg font-korean"> </h1>
<p className="text-[10px] text-fg-disabled mt-0.5 font-korean">
<p className="text-caption text-fg-disabled mt-0.5 font-korean">
× CRUD
</p>
</div>

파일 보기

@ -183,31 +183,31 @@ const SensitiveLayerPanel = ({ categoryCode, title }: SensitiveLayerPanelProps)
<table className="w-full">
<thead>
<tr className="border-b border-stroke bg-bg-surface sticky top-0 z-10">
<th className="px-4 py-3 text-left text-[11px] font-semibold text-fg-disabled font-korean w-10 whitespace-nowrap">
<th className="px-4 py-3 text-left text-label-2 font-semibold text-fg-disabled font-korean w-10 whitespace-nowrap">
</th>
<th className="px-4 py-3 text-left text-[11px] font-semibold text-fg-disabled font-mono">
<th className="px-4 py-3 text-left text-label-2 font-semibold text-fg-disabled font-mono">
</th>
<th className="px-4 py-3 text-left text-[11px] font-semibold text-fg-disabled font-korean">
<th className="px-4 py-3 text-left text-label-2 font-semibold text-fg-disabled font-korean">
</th>
<th className="px-4 py-3 text-left text-[11px] font-semibold text-fg-disabled font-korean">
<th className="px-4 py-3 text-left text-label-2 font-semibold text-fg-disabled font-korean">
</th>
<th className="px-4 py-3 text-center text-[11px] font-semibold text-fg-disabled font-korean w-12 whitespace-nowrap">
<th className="px-4 py-3 text-center text-label-2 font-semibold text-fg-disabled font-korean w-12 whitespace-nowrap">
</th>
<th className="px-4 py-3 text-left text-[11px] font-semibold text-fg-disabled font-mono">
<th className="px-4 py-3 text-left text-label-2 font-semibold text-fg-disabled font-mono">
WMS레이어명
</th>
<th className="px-4 py-3 text-center text-[11px] font-semibold text-fg-disabled font-korean w-16">
<th className="px-4 py-3 text-center text-label-2 font-semibold text-fg-disabled font-korean w-16">
</th>
<th className="px-4 py-3 text-left text-[11px] font-semibold text-fg-disabled font-korean w-28">
<th className="px-4 py-3 text-left text-label-2 font-semibold text-fg-disabled font-korean w-28">
</th>
<th className="px-4 py-3 text-center text-[11px] font-semibold text-fg-disabled font-korean w-20">
<th className="px-4 py-3 text-center text-label-2 font-semibold text-fg-disabled font-korean w-20">
</th>
</tr>
@ -231,7 +231,7 @@ const SensitiveLayerPanel = ({ categoryCode, title }: SensitiveLayerPanelProps)
<td className="px-4 py-3 text-xs text-fg-disabled font-mono">
{(page - 1) * PAGE_SIZE + idx + 1}
</td>
<td className="px-4 py-3 text-[11px] text-fg-sub font-mono">{item.layerCd}</td>
<td className="px-4 py-3 text-label-2 text-fg-sub font-mono">{item.layerCd}</td>
<td className="px-4 py-3 text-xs text-fg font-korean">{item.layerNm}</td>
<td className="px-4 py-3 text-xs text-fg-sub font-korean max-w-[200px]">
<span className="block truncate" title={item.layerFullNm}>
@ -239,17 +239,17 @@ const SensitiveLayerPanel = ({ categoryCode, title }: SensitiveLayerPanelProps)
</span>
</td>
<td className="px-4 py-3 text-center">
<span className="inline-flex items-center justify-center w-5 h-5 rounded text-[10px] font-semibold bg-[rgba(6,182,212,0.1)] text-color-accent border border-[rgba(6,182,212,0.3)]">
<span className="inline-flex items-center justify-center w-5 h-5 rounded text-caption font-semibold bg-[rgba(6,182,212,0.1)] text-color-accent border border-[rgba(6,182,212,0.3)]">
{item.layerLevel}
</span>
</td>
<td className="px-4 py-3 text-[11px] text-fg-sub font-mono">
<td className="px-4 py-3 text-label-2 text-fg-sub font-mono">
{item.wmsLayerNm ?? <span className="text-fg-disabled">-</span>}
</td>
<td className="px-4 py-3 text-xs text-fg-disabled text-center font-mono">
{item.sortOrd}
</td>
<td className="px-4 py-3 text-[11px] text-fg-disabled font-mono">
<td className="px-4 py-3 text-label-2 text-fg-disabled font-mono">
{item.regDtm ?? '-'}
</td>
<td className="px-4 py-3 text-center">
@ -285,27 +285,27 @@ const SensitiveLayerPanel = ({ categoryCode, title }: SensitiveLayerPanelProps)
{/* 페이지네이션 */}
{!loading && totalPages > 1 && (
<div className="flex items-center justify-between px-6 py-3 border-t border-stroke bg-bg-surface shrink-0">
<span className="text-[11px] text-fg-disabled font-korean">
<span className="text-label-2 text-fg-disabled font-korean">
{(page - 1) * PAGE_SIZE + 1}{Math.min(page * PAGE_SIZE, total)} / {total}
</span>
<div className="flex items-center gap-1">
<button
onClick={() => setPage((p) => Math.max(1, p - 1))}
disabled={page === 1}
className="px-2.5 py-1 text-[11px] border border-stroke text-fg-disabled rounded hover:bg-[rgba(255,255,255,0.04)] disabled:opacity-40 transition-all font-korean"
className="px-2.5 py-1 text-label-2 border border-stroke text-fg-disabled rounded hover:bg-[rgba(255,255,255,0.04)] disabled:opacity-40 transition-all font-korean"
>
</button>
{buildPageButtons().map((btn, i) =>
btn === 'ellipsis' ? (
<span key={`e${i}`} className="px-1.5 text-[11px] text-fg-disabled">
<span key={`e${i}`} className="px-1.5 text-label-2 text-fg-disabled">
</span>
) : (
<button
key={btn}
onClick={() => setPage(btn)}
className={`px-2.5 py-1 text-[11px] rounded transition-all ${
className={`px-2.5 py-1 text-label-2 rounded transition-all ${
page === btn
? 'bg-color-accent text-bg-0 font-semibold shadow-[0_0_8px_rgba(6,182,212,0.25)]'
: 'border border-stroke text-fg-disabled hover:bg-[rgba(255,255,255,0.04)]'
@ -318,7 +318,7 @@ const SensitiveLayerPanel = ({ categoryCode, title }: SensitiveLayerPanelProps)
<button
onClick={() => setPage((p) => Math.min(totalPages, p + 1))}
disabled={page === totalPages}
className="px-2.5 py-1 text-[11px] border border-stroke text-fg-disabled rounded hover:bg-[rgba(255,255,255,0.04)] disabled:opacity-40 transition-all font-korean"
className="px-2.5 py-1 text-label-2 border border-stroke text-fg-disabled rounded hover:bg-[rgba(255,255,255,0.04)] disabled:opacity-40 transition-all font-korean"
>
</button>

파일 보기

@ -75,7 +75,7 @@ function SettingsPanel() {
<div className="rounded-lg border border-stroke bg-bg-surface overflow-hidden">
<div className="px-5 py-3 border-b border-stroke">
<h2 className="text-sm font-bold text-fg font-korean"> </h2>
<p className="text-[11px] text-fg-disabled mt-0.5 font-korean">
<p className="text-label-2 text-fg-disabled mt-0.5 font-korean">
</p>
</div>
@ -84,8 +84,8 @@ function SettingsPanel() {
{/* 자동 승인 */}
<div className="px-5 py-4 flex items-center justify-between">
<div className="flex-1 mr-4">
<div className="text-[13px] font-semibold text-fg font-korean"> </div>
<p className="text-[11px] text-fg-disabled mt-1 font-korean leading-relaxed">
<div className="text-title-4 font-semibold text-fg font-korean"> </div>
<p className="text-label-2 text-fg-disabled mt-1 font-korean leading-relaxed">
{' '}
<span className="text-green-400 font-semibold">ACTIVE</span> .
{' '}
@ -111,10 +111,10 @@ function SettingsPanel() {
{/* 기본 역할 자동 할당 */}
<div className="px-5 py-4 flex items-center justify-between">
<div className="flex-1 mr-4">
<div className="text-[13px] font-semibold text-fg font-korean">
<div className="text-title-4 font-semibold text-fg font-korean">
</div>
<p className="text-[11px] text-fg-disabled mt-1 font-korean leading-relaxed">
<p className="text-label-2 text-fg-disabled mt-1 font-korean leading-relaxed">
{' '}
<span className="text-color-accent font-semibold"> </span>
. .
@ -141,16 +141,16 @@ function SettingsPanel() {
<div className="rounded-lg border border-stroke bg-bg-surface overflow-hidden">
<div className="px-5 py-3 border-b border-stroke">
<h2 className="text-sm font-bold text-fg font-korean">Google OAuth </h2>
<p className="text-[11px] text-fg-disabled mt-0.5 font-korean">
<p className="text-label-2 text-fg-disabled mt-0.5 font-korean">
Google
</p>
</div>
<div className="px-5 py-4">
<div className="flex-1 mr-4 mb-3">
<div className="text-[13px] font-semibold text-fg font-korean mb-1">
<div className="text-title-4 font-semibold text-fg font-korean mb-1">
</div>
<p className="text-[11px] text-fg-disabled font-korean leading-relaxed mb-3">
<p className="text-label-2 text-fg-disabled font-korean leading-relaxed mb-3">
Google {' '}
<span className="text-green-400 font-semibold">ACTIVE</span> .
<span className="text-yellow-400 font-semibold">PENDING</span>
@ -202,7 +202,7 @@ function SettingsPanel() {
.map((domain) => (
<span
key={domain}
className="inline-flex items-center gap-1 px-2 py-1 text-[10px] font-mono rounded-md"
className="inline-flex items-center gap-1 px-2 py-1 text-caption font-mono rounded-md"
style={{
background: 'rgba(6,182,212,0.1)',
color: 'var(--color-accent)',
@ -223,7 +223,7 @@ function SettingsPanel() {
<h2 className="text-sm font-bold text-fg font-korean"> </h2>
</div>
<div className="px-5 py-4">
<div className="flex flex-col gap-3 text-[12px] font-korean">
<div className="flex flex-col gap-3 text-label-1 font-korean">
<div className="flex items-center gap-2">
<span
className={`w-2 h-2 rounded-full ${settings?.autoApprove ? 'bg-green-400' : 'bg-yellow-400'}`}

파일 보기

@ -103,31 +103,31 @@ function SortableMenuItem({
type="text"
value={menu.label}
onChange={(e) => onLabelChange(menu.id, e.target.value)}
className="w-full h-8 text-[13px] font-semibold font-korean bg-bg-elevated border border-stroke rounded px-2 text-fg focus:border-color-accent focus:outline-none"
className="w-full h-8 text-title-4 font-semibold font-korean bg-bg-elevated border border-stroke rounded px-2 text-fg focus:border-color-accent focus:outline-none"
/>
<div className="text-[10px] text-fg-disabled font-mono mt-0.5">{menu.id}</div>
<div className="text-caption text-fg-disabled font-mono mt-0.5">{menu.id}</div>
</div>
<button
onClick={onEditEnd}
className="shrink-0 px-2 py-1 text-[10px] font-semibold text-color-accent border border-color-accent rounded hover:bg-[rgba(6,182,212,0.1)] transition-all font-korean"
className="shrink-0 px-2 py-1 text-caption font-semibold text-color-accent border border-color-accent rounded hover:bg-[rgba(6,182,212,0.1)] transition-all font-korean"
>
</button>
</>
) : (
<>
<span className="text-[16px] shrink-0">{menu.icon}</span>
<span className="text-title-2 shrink-0">{menu.icon}</span>
<div className="flex-1 min-w-0">
<div
className={`text-[13px] font-semibold font-korean ${menu.enabled ? 'text-fg' : 'text-fg-disabled'}`}
className={`text-title-4 font-semibold font-korean ${menu.enabled ? 'text-fg' : 'text-fg-disabled'}`}
>
{menu.label}
</div>
<div className="text-[10px] text-fg-disabled font-mono">{menu.id}</div>
<div className="text-caption text-fg-disabled font-mono">{menu.id}</div>
</div>
<button
onClick={() => onEditStart(menu.id)}
className="shrink-0 w-7 h-7 rounded border border-stroke bg-bg-elevated text-fg-disabled text-[11px] flex items-center justify-center hover:bg-bg-surface-hover hover:text-fg transition-all"
className="shrink-0 w-7 h-7 rounded border border-stroke bg-bg-elevated text-fg-disabled text-label-2 flex items-center justify-center hover:bg-bg-surface-hover hover:text-fg transition-all"
title="라벨/아이콘 편집"
>

파일 보기

@ -107,7 +107,7 @@ function RegisterModal({ allRoles, allOrgs, onClose, onSuccess }: RegisterModalP
<div className="px-6 py-4 space-y-4">
{/* 계정 */}
<div>
<label className="block text-[11px] font-semibold text-fg-sub font-korean mb-1.5">
<label className="block text-label-2 font-semibold text-fg-sub font-korean mb-1.5">
<span className="text-red-400">*</span>
</label>
<input
@ -121,7 +121,7 @@ function RegisterModal({ allRoles, allOrgs, onClose, onSuccess }: RegisterModalP
{/* 비밀번호 */}
<div>
<label className="block text-[11px] font-semibold text-fg-sub font-korean mb-1.5">
<label className="block text-label-2 font-semibold text-fg-sub font-korean mb-1.5">
<span className="text-red-400">*</span>
</label>
<input
@ -135,7 +135,7 @@ function RegisterModal({ allRoles, allOrgs, onClose, onSuccess }: RegisterModalP
{/* 사용자명 */}
<div>
<label className="block text-[11px] font-semibold text-fg-sub font-korean mb-1.5">
<label className="block text-label-2 font-semibold text-fg-sub font-korean mb-1.5">
<span className="text-red-400">*</span>
</label>
<input
@ -149,7 +149,7 @@ function RegisterModal({ allRoles, allOrgs, onClose, onSuccess }: RegisterModalP
{/* 직급 */}
<div>
<label className="block text-[11px] font-semibold text-fg-sub font-korean mb-1.5">
<label className="block text-label-2 font-semibold text-fg-sub font-korean mb-1.5">
</label>
<input
@ -163,7 +163,7 @@ function RegisterModal({ allRoles, allOrgs, onClose, onSuccess }: RegisterModalP
{/* 소속 */}
<div>
<label className="block text-[11px] font-semibold text-fg-sub font-korean mb-1.5">
<label className="block text-label-2 font-semibold text-fg-sub font-korean mb-1.5">
</label>
<select
@ -183,7 +183,7 @@ function RegisterModal({ allRoles, allOrgs, onClose, onSuccess }: RegisterModalP
{/* 이메일 */}
<div>
<label className="block text-[11px] font-semibold text-fg-sub font-korean mb-1.5">
<label className="block text-label-2 font-semibold text-fg-sub font-korean mb-1.5">
</label>
<input
@ -197,12 +197,12 @@ function RegisterModal({ allRoles, allOrgs, onClose, onSuccess }: RegisterModalP
{/* 역할 */}
<div>
<label className="block text-[11px] font-semibold text-fg-sub font-korean mb-1.5">
<label className="block text-label-2 font-semibold text-fg-sub font-korean mb-1.5">
</label>
<div className="bg-bg-elevated border border-stroke rounded-md p-2 space-y-1 max-h-[120px] overflow-y-auto">
{allRoles.length === 0 ? (
<p className="text-[10px] text-fg-disabled font-korean px-1 py-1"> </p>
<p className="text-caption text-fg-disabled font-korean px-1 py-1"> </p>
) : (
allRoles.map((role, idx) => {
const color = getRoleColor(role.code, idx);
@ -220,7 +220,7 @@ function RegisterModal({ allRoles, allOrgs, onClose, onSuccess }: RegisterModalP
<span className="text-xs font-korean" style={{ color }}>
{role.name}
</span>
<span className="text-[10px] text-fg-disabled font-mono">{role.code}</span>
<span className="text-caption text-fg-disabled font-mono">{role.code}</span>
</label>
);
})
@ -229,7 +229,7 @@ function RegisterModal({ allRoles, allOrgs, onClose, onSuccess }: RegisterModalP
</div>
{/* 에러 메시지 */}
{error && <p className="text-[11px] text-red-400 font-korean">{error}</p>}
{error && <p className="text-label-2 text-red-400 font-korean">{error}</p>}
</div>
{/* 푸터 */}
@ -333,7 +333,7 @@ function UserDetailModal({ user, allOrgs, onClose, onUpdated }: UserDetailModalP
<div className="flex items-center justify-between px-6 py-4 border-b border-stroke">
<div>
<h2 className="text-sm font-bold text-fg font-korean"> </h2>
<p className="text-[10px] text-fg-disabled font-mono mt-0.5">{user.account}</p>
<p className="text-caption text-fg-disabled font-mono mt-0.5">{user.account}</p>
</div>
<button onClick={onClose} className="text-fg-disabled hover:text-fg transition-colors">
<svg
@ -352,12 +352,12 @@ function UserDetailModal({ user, allOrgs, onClose, onUpdated }: UserDetailModalP
<div className="flex-1 overflow-y-auto px-6 py-4 space-y-5">
{/* 기본 정보 수정 */}
<div>
<h3 className="text-[11px] font-semibold text-fg-sub font-korean mb-3">
<h3 className="text-label-2 font-semibold text-fg-sub font-korean mb-3">
</h3>
<div className="space-y-3">
<div>
<label className="block text-[10px] text-fg-disabled font-korean mb-1">
<label className="block text-caption text-fg-disabled font-korean mb-1">
</label>
<input
@ -369,7 +369,7 @@ function UserDetailModal({ user, allOrgs, onClose, onUpdated }: UserDetailModalP
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-[10px] text-fg-disabled font-korean mb-1">
<label className="block text-caption text-fg-disabled font-korean mb-1">
</label>
<input
@ -381,7 +381,7 @@ function UserDetailModal({ user, allOrgs, onClose, onUpdated }: UserDetailModalP
/>
</div>
<div>
<label className="block text-[10px] text-fg-disabled font-korean mb-1">
<label className="block text-caption text-fg-disabled font-korean mb-1">
</label>
<select
@ -402,7 +402,7 @@ function UserDetailModal({ user, allOrgs, onClose, onUpdated }: UserDetailModalP
<button
onClick={handleSaveInfo}
disabled={saving || !name.trim()}
className="px-4 py-1.5 text-[11px] font-semibold rounded-md bg-color-accent text-bg-0 hover:shadow-[0_0_12px_rgba(6,182,212,0.3)] transition-all disabled:opacity-50 font-korean"
className="px-4 py-1.5 text-label-2 font-semibold rounded-md bg-color-accent text-bg-0 hover:shadow-[0_0_12px_rgba(6,182,212,0.3)] transition-all disabled:opacity-50 font-korean"
>
{saving ? '저장 중...' : '정보 저장'}
</button>
@ -414,12 +414,12 @@ function UserDetailModal({ user, allOrgs, onClose, onUpdated }: UserDetailModalP
{/* 비밀번호 초기화 */}
<div>
<h3 className="text-[11px] font-semibold text-fg-sub font-korean mb-3">
<h3 className="text-label-2 font-semibold text-fg-sub font-korean mb-3">
</h3>
<div className="flex items-end gap-2">
<div className="flex-1">
<label className="block text-[10px] text-fg-disabled font-korean mb-1">
<label className="block text-caption text-fg-disabled font-korean mb-1">
</label>
<input
@ -433,20 +433,20 @@ function UserDetailModal({ user, allOrgs, onClose, onUpdated }: UserDetailModalP
<button
onClick={handleResetPassword}
disabled={resetPwLoading || !newPassword.trim()}
className="px-4 py-1.5 text-[11px] font-semibold rounded-md border border-yellow-400 text-yellow-400 hover:bg-[rgba(250,204,21,0.1)] transition-all disabled:opacity-50 font-korean flex-shrink-0"
className="px-4 py-1.5 text-label-2 font-semibold rounded-md border border-yellow-400 text-yellow-400 hover:bg-[rgba(250,204,21,0.1)] transition-all disabled:opacity-50 font-korean flex-shrink-0"
>
{resetPwLoading ? '초기화 중...' : resetPwDone ? '초기화 완료' : '비밀번호 초기화'}
</button>
<button
onClick={handleUnlock}
disabled={unlockLoading || user.status !== 'LOCKED'}
className="px-4 py-1.5 text-[11px] font-semibold rounded-md border border-green-400 text-green-400 hover:bg-[rgba(74,222,128,0.1)] transition-all disabled:opacity-50 font-korean flex-shrink-0"
className="px-4 py-1.5 text-label-2 font-semibold rounded-md border border-green-400 text-green-400 hover:bg-[rgba(74,222,128,0.1)] transition-all disabled:opacity-50 font-korean flex-shrink-0"
title={user.status !== 'LOCKED' ? '잠금 상태가 아닙니다' : ''}
>
{unlockLoading ? '해제 중...' : '패스워드잠금해제'}
</button>
</div>
<p className="text-[9px] text-fg-disabled font-korean mt-1.5">
<p className="text-caption text-fg-disabled font-korean mt-1.5">
.
</p>
</div>
@ -456,12 +456,12 @@ function UserDetailModal({ user, allOrgs, onClose, onUpdated }: UserDetailModalP
{/* 계정 잠금 해제 */}
<div>
<h3 className="text-[11px] font-semibold text-fg-sub font-korean mb-2"> </h3>
<h3 className="text-label-2 font-semibold text-fg-sub font-korean mb-2"> </h3>
<div className="flex items-center justify-between bg-bg-elevated border border-stroke rounded-md px-4 py-3">
<div>
<div className="flex items-center gap-2">
<span
className={`inline-flex items-center gap-1.5 text-[11px] font-semibold font-korean ${(statusLabels[user.status] || statusLabels.INACTIVE).color}`}
className={`inline-flex items-center gap-1.5 text-label-2 font-semibold font-korean ${(statusLabels[user.status] || statusLabels.INACTIVE).color}`}
>
<span
className={`w-1.5 h-1.5 rounded-full ${(statusLabels[user.status] || statusLabels.INACTIVE).dot}`}
@ -469,13 +469,13 @@ function UserDetailModal({ user, allOrgs, onClose, onUpdated }: UserDetailModalP
{(statusLabels[user.status] || statusLabels.INACTIVE).label}
</span>
{user.failCount > 0 && (
<span className="text-[10px] text-red-400 font-korean">
<span className="text-caption text-red-400 font-korean">
( {user.failCount})
</span>
)}
</div>
{user.status === 'LOCKED' && (
<p className="text-[9px] text-fg-disabled font-korean mt-1">
<p className="text-caption text-fg-disabled font-korean mt-1">
5
</p>
)}
@ -484,7 +484,7 @@ function UserDetailModal({ user, allOrgs, onClose, onUpdated }: UserDetailModalP
<button
onClick={handleUnlock}
disabled={unlockLoading}
className="px-4 py-1.5 text-[11px] font-semibold rounded-md border border-green-400 text-green-400 hover:bg-[rgba(74,222,128,0.1)] transition-all disabled:opacity-50 font-korean flex-shrink-0"
className="px-4 py-1.5 text-label-2 font-semibold rounded-md border border-green-400 text-green-400 hover:bg-[rgba(74,222,128,0.1)] transition-all disabled:opacity-50 font-korean flex-shrink-0"
>
{unlockLoading ? '해제 중...' : '잠금 해제'}
</button>
@ -494,8 +494,8 @@ function UserDetailModal({ user, allOrgs, onClose, onUpdated }: UserDetailModalP
{/* 기타 정보 (읽기 전용) */}
<div>
<h3 className="text-[11px] font-semibold text-fg-sub font-korean mb-2"> </h3>
<div className="grid grid-cols-2 gap-2 text-[10px] font-korean">
<h3 className="text-label-2 font-semibold text-fg-sub font-korean mb-2"> </h3>
<div className="grid grid-cols-2 gap-2 text-caption font-korean">
<div className="bg-bg-elevated border border-stroke rounded px-3 py-2">
<span className="text-fg-disabled">: </span>
<span className="text-fg-sub font-mono">{user.email || '-'}</span>
@ -520,7 +520,7 @@ function UserDetailModal({ user, allOrgs, onClose, onUpdated }: UserDetailModalP
{/* 메시지 */}
{message && (
<div
className={`px-3 py-2 text-[11px] rounded-md font-korean ${
className={`px-3 py-2 text-label-2 rounded-md font-korean ${
message.type === 'success'
? 'text-green-400 bg-[rgba(74,222,128,0.08)] border border-[rgba(74,222,128,0.2)]'
: 'text-red-400 bg-[rgba(239,68,68,0.08)] border border-[rgba(239,68,68,0.2)]'
@ -685,7 +685,7 @@ function UsersPanel() {
</p>
</div>
{pendingCount > 0 && (
<span className="px-2.5 py-1 text-[10px] font-bold rounded-full bg-[rgba(250,204,21,0.15)] text-yellow-400 border border-[rgba(250,204,21,0.3)] animate-pulse font-korean">
<span className="px-2.5 py-1 text-caption font-bold rounded-full bg-[rgba(250,204,21,0.15)] text-yellow-400 border border-[rgba(250,204,21,0.3)] animate-pulse font-korean">
{pendingCount}
</span>
)}
@ -747,31 +747,31 @@ function UsersPanel() {
<table className="w-full">
<thead>
<tr className="border-b border-stroke bg-bg-surface">
<th className="px-4 py-3 text-left text-[11px] font-semibold text-fg-disabled font-korean w-10">
<th className="px-4 py-3 text-left text-label-2 font-semibold text-fg-disabled font-korean w-10">
</th>
<th className="px-4 py-3 text-left text-[11px] font-semibold text-fg-disabled font-mono">
<th className="px-4 py-3 text-left text-label-2 font-semibold text-fg-disabled font-mono">
ID
</th>
<th className="px-4 py-3 text-left text-[11px] font-semibold text-fg-disabled font-korean">
<th className="px-4 py-3 text-left text-label-2 font-semibold text-fg-disabled font-korean">
</th>
<th className="px-4 py-3 text-left text-[11px] font-semibold text-fg-disabled font-korean">
<th className="px-4 py-3 text-left text-label-2 font-semibold text-fg-disabled font-korean">
</th>
<th className="px-4 py-3 text-left text-[11px] font-semibold text-fg-disabled font-korean">
<th className="px-4 py-3 text-left text-label-2 font-semibold text-fg-disabled font-korean">
</th>
<th className="px-4 py-3 text-left text-[11px] font-semibold text-fg-disabled font-korean">
<th className="px-4 py-3 text-left text-label-2 font-semibold text-fg-disabled font-korean">
</th>
<th className="px-4 py-3 text-left text-[11px] font-semibold text-fg-disabled font-korean">
<th className="px-4 py-3 text-left text-label-2 font-semibold text-fg-disabled font-korean">
</th>
<th className="px-4 py-3 text-left text-[11px] font-semibold text-fg-disabled font-korean">
<th className="px-4 py-3 text-left text-label-2 font-semibold text-fg-disabled font-korean">
</th>
<th className="px-4 py-3 text-right text-[11px] font-semibold text-fg-disabled font-korean">
<th className="px-4 py-3 text-right text-label-2 font-semibold text-fg-disabled font-korean">
</th>
</tr>
@ -796,12 +796,12 @@ function UsersPanel() {
className="border-b border-stroke hover:bg-[rgba(255,255,255,0.02)] transition-colors"
>
{/* 번호 */}
<td className="px-4 py-3 text-[11px] text-fg-disabled font-mono text-center">
<td className="px-4 py-3 text-label-2 text-fg-disabled font-mono text-center">
{rowNum}
</td>
{/* ID(account) */}
<td className="px-4 py-3 text-[12px] text-fg-sub font-mono">
<td className="px-4 py-3 text-label-1 text-fg-sub font-mono">
{user.account}
</td>
@ -809,24 +809,24 @@ function UsersPanel() {
<td className="px-4 py-3">
<button
onClick={() => setDetailUser(user)}
className="text-[12px] text-color-accent font-semibold font-korean hover:underline"
className="text-label-1 text-color-accent font-semibold font-korean hover:underline"
>
{user.name}
</button>
</td>
{/* 직급 */}
<td className="px-4 py-3 text-[12px] text-fg-sub font-korean">
<td className="px-4 py-3 text-label-1 text-fg-sub font-korean">
{user.rank || '-'}
</td>
{/* 소속 */}
<td className="px-4 py-3 text-[12px] text-fg-sub font-korean">
<td className="px-4 py-3 text-label-1 text-fg-sub font-korean">
{user.orgAbbr || user.orgName || '-'}
</td>
{/* 이메일 */}
<td className="px-4 py-3 text-[11px] text-fg-disabled font-mono">
<td className="px-4 py-3 text-label-2 text-fg-disabled font-mono">
{user.email || '-'}
</td>
@ -846,7 +846,7 @@ function UsersPanel() {
return (
<span
key={roleCode}
className="px-2 py-0.5 text-[10px] font-semibold rounded-md font-korean"
className="px-2 py-0.5 text-caption font-semibold rounded-md font-korean"
style={{
background: `${color}20`,
color: color,
@ -858,11 +858,11 @@ function UsersPanel() {
);
})
) : (
<span className="text-[10px] text-fg-disabled font-korean">
<span className="text-caption text-fg-disabled font-korean">
</span>
)}
<span className="text-[10px] text-fg-disabled ml-0.5">
<span className="text-caption text-fg-disabled ml-0.5">
<svg
width="10"
height="10"
@ -881,7 +881,7 @@ function UsersPanel() {
ref={roleDropdownRef}
className="absolute z-20 top-full left-0 mt-1 p-2 bg-bg-surface border border-stroke rounded-lg shadow-lg min-w-[200px]"
>
<div className="text-[10px] text-fg-disabled font-korean font-semibold mb-1.5 px-1">
<div className="text-caption text-fg-disabled font-korean font-semibold mb-1.5 px-1">
</div>
{allRoles.map((role, roleIdx) => {
@ -900,7 +900,7 @@ function UsersPanel() {
<span className="text-xs font-korean" style={{ color }}>
{role.name}
</span>
<span className="text-[10px] text-fg-disabled font-mono">
<span className="text-caption text-fg-disabled font-mono">
{role.code}
</span>
</label>
@ -909,14 +909,14 @@ function UsersPanel() {
<div className="flex justify-end gap-2 mt-2 pt-2 border-t border-stroke">
<button
onClick={() => setRoleEditUserId(null)}
className="px-3 py-1 text-[10px] text-fg-disabled border border-stroke rounded hover:bg-bg-surface-hover font-korean"
className="px-3 py-1 text-caption text-fg-disabled border border-stroke rounded hover:bg-bg-surface-hover font-korean"
>
</button>
<button
onClick={() => handleSaveRoles(user.id)}
disabled={selectedRoleSns.length === 0}
className="px-3 py-1 text-[10px] font-semibold rounded bg-color-accent text-bg-0 hover:shadow-[0_0_8px_rgba(6,182,212,0.3)] disabled:opacity-50 font-korean"
className="px-3 py-1 text-caption font-semibold rounded bg-color-accent text-bg-0 hover:shadow-[0_0_8px_rgba(6,182,212,0.3)] disabled:opacity-50 font-korean"
>
</button>
@ -929,7 +929,7 @@ function UsersPanel() {
{/* 승인상태 */}
<td className="px-4 py-3">
<span
className={`inline-flex items-center gap-1.5 text-[10px] font-semibold font-korean ${statusInfo.color}`}
className={`inline-flex items-center gap-1.5 text-caption font-semibold font-korean ${statusInfo.color}`}
>
<span className={`w-1.5 h-1.5 rounded-full ${statusInfo.dot}`} />
{statusInfo.label}
@ -943,13 +943,13 @@ function UsersPanel() {
<>
<button
onClick={() => handleApprove(user.id)}
className="px-2 py-1 text-[10px] font-semibold text-green-400 border border-green-400 rounded hover:bg-[rgba(74,222,128,0.1)] transition-all font-korean"
className="px-2 py-1 text-caption font-semibold text-green-400 border border-green-400 rounded hover:bg-[rgba(74,222,128,0.1)] transition-all font-korean"
>
</button>
<button
onClick={() => handleReject(user.id)}
className="px-2 py-1 text-[10px] font-semibold text-red-400 border border-red-400 rounded hover:bg-[rgba(248,113,113,0.1)] transition-all font-korean"
className="px-2 py-1 text-caption font-semibold text-red-400 border border-red-400 rounded hover:bg-[rgba(248,113,113,0.1)] transition-all font-korean"
>
</button>
@ -958,7 +958,7 @@ function UsersPanel() {
{user.status === 'LOCKED' && (
<button
onClick={() => handleUnlock(user.id)}
className="px-2 py-1 text-[10px] font-semibold text-yellow-400 border border-yellow-400 rounded hover:bg-[rgba(250,204,21,0.1)] transition-all font-korean"
className="px-2 py-1 text-caption font-semibold text-yellow-400 border border-yellow-400 rounded hover:bg-[rgba(250,204,21,0.1)] transition-all font-korean"
>
</button>
@ -966,7 +966,7 @@ function UsersPanel() {
{user.status === 'ACTIVE' && (
<button
onClick={() => handleDeactivate(user.id)}
className="px-2 py-1 text-[10px] font-semibold text-fg-disabled border border-stroke rounded hover:bg-[rgba(255,255,255,0.04)] transition-all font-korean"
className="px-2 py-1 text-caption font-semibold text-fg-disabled border border-stroke rounded hover:bg-[rgba(255,255,255,0.04)] transition-all font-korean"
>
</button>
@ -974,7 +974,7 @@ function UsersPanel() {
{(user.status === 'INACTIVE' || user.status === 'REJECTED') && (
<button
onClick={() => handleActivate(user.id)}
className="px-2 py-1 text-[10px] font-semibold text-green-400 border border-green-400 rounded hover:bg-[rgba(74,222,128,0.1)] transition-all font-korean"
className="px-2 py-1 text-caption font-semibold text-green-400 border border-green-400 rounded hover:bg-[rgba(74,222,128,0.1)] transition-all font-korean"
>
</button>
@ -993,7 +993,7 @@ function UsersPanel() {
{/* 페이지네이션 */}
{!loading && totalPages > 1 && (
<div className="flex items-center justify-between px-6 py-3 border-t border-stroke bg-bg-surface">
<span className="text-[11px] text-fg-disabled font-korean">
<span className="text-label-2 text-fg-disabled font-korean">
{(currentPage - 1) * PAGE_SIZE + 1}{Math.min(currentPage * PAGE_SIZE, totalCount)} /{' '}
{totalCount}
</span>
@ -1001,7 +1001,7 @@ function UsersPanel() {
<button
onClick={() => setCurrentPage((p) => Math.max(1, p - 1))}
disabled={currentPage === 1}
className="px-2.5 py-1 text-[11px] border border-stroke text-fg-disabled rounded hover:bg-[rgba(255,255,255,0.04)] disabled:opacity-40 transition-all font-korean"
className="px-2.5 py-1 text-label-2 border border-stroke text-fg-disabled rounded hover:bg-[rgba(255,255,255,0.04)] disabled:opacity-40 transition-all font-korean"
>
</button>
@ -1020,14 +1020,14 @@ function UsersPanel() {
}, [])
.map((item, i) =>
item === '...' ? (
<span key={`ellipsis-${i}`} className="px-2 text-[11px] text-fg-disabled">
<span key={`ellipsis-${i}`} className="px-2 text-label-2 text-fg-disabled">
</span>
) : (
<button
key={item}
onClick={() => setCurrentPage(item as number)}
className="px-2.5 py-1 text-[11px] border rounded transition-all font-mono"
className="px-2.5 py-1 text-label-2 border rounded transition-all font-mono"
style={
currentPage === item
? {
@ -1045,7 +1045,7 @@ function UsersPanel() {
<button
onClick={() => setCurrentPage((p) => Math.min(totalPages, p + 1))}
disabled={currentPage === totalPages}
className="px-2.5 py-1 text-[11px] border border-stroke text-fg-disabled rounded hover:bg-[rgba(255,255,255,0.04)] disabled:opacity-40 transition-all font-korean"
className="px-2.5 py-1 text-label-2 border border-stroke text-fg-disabled rounded hover:bg-[rgba(255,255,255,0.04)] disabled:opacity-40 transition-all font-korean"
>
</button>

파일 보기

@ -148,37 +148,37 @@ function VesselMaterialsPanel() {
<table className="w-full">
<thead>
<tr className="border-b border-stroke bg-bg-surface">
<th className="px-4 py-3 text-left text-[11px] font-semibold text-fg-disabled font-korean w-10 whitespace-nowrap">
<th className="px-4 py-3 text-left text-label-2 font-semibold text-fg-disabled font-korean w-10 whitespace-nowrap">
</th>
<th className="px-4 py-3 text-left text-[11px] font-semibold text-fg-disabled font-korean">
<th className="px-4 py-3 text-left text-label-2 font-semibold text-fg-disabled font-korean">
</th>
<th className="px-4 py-3 text-left text-[11px] font-semibold text-fg-disabled font-korean">
<th className="px-4 py-3 text-left text-label-2 font-semibold text-fg-disabled font-korean">
</th>
<th className="px-4 py-3 text-left text-[11px] font-semibold text-fg-disabled font-korean">
<th className="px-4 py-3 text-left text-label-2 font-semibold text-fg-disabled font-korean">
</th>
<th className="px-4 py-3 text-left text-[11px] font-semibold text-fg-disabled font-korean">
<th className="px-4 py-3 text-left text-label-2 font-semibold text-fg-disabled font-korean">
</th>
<th className="px-4 py-3 text-center text-[11px] font-semibold font-korean text-color-accent bg-color-accent/5">
<th className="px-4 py-3 text-center text-label-2 font-semibold font-korean text-color-accent bg-[rgba(6,182,212,0.05)]">
</th>
<th className="px-4 py-3 text-center text-[11px] font-semibold font-korean text-fg-disabled">
<th className="px-4 py-3 text-center text-label-2 font-semibold font-korean text-fg-disabled">
</th>
<th className="px-4 py-3 text-center text-[11px] font-semibold font-korean text-fg-disabled">
<th className="px-4 py-3 text-center text-label-2 font-semibold font-korean text-fg-disabled">
</th>
<th className="px-4 py-3 text-center text-[11px] font-semibold font-korean text-fg-disabled">
<th className="px-4 py-3 text-center text-label-2 font-semibold font-korean text-fg-disabled">
</th>
<th className="px-4 py-3 text-center text-[11px] font-semibold font-korean text-fg-disabled">
<th className="px-4 py-3 text-center text-label-2 font-semibold font-korean text-fg-disabled">
</th>
<th className="px-4 py-3 text-center text-[11px] font-semibold text-fg-disabled font-korean">
<th className="px-4 py-3 text-center text-label-2 font-semibold text-fg-disabled font-korean">
</th>
</tr>
@ -199,41 +199,41 @@ function VesselMaterialsPanel() {
key={org.id}
className="border-b border-stroke hover:bg-[rgba(255,255,255,0.02)] transition-colors"
>
<td className="px-4 py-3 text-[11px] text-fg-disabled font-mono text-center">
<td className="px-4 py-3 text-label-2 text-fg-disabled font-mono text-center">
{(safePage - 1) * PAGE_SIZE + idx + 1}
</td>
<td className="px-4 py-3">
<span
className={`text-[10px] px-1.5 py-0.5 rounded font-bold font-korean ${typeTagCls(org.type)}`}
className={`text-caption px-1.5 py-0.5 rounded font-bold font-korean ${typeTagCls(org.type)}`}
>
{org.type}
</span>
</td>
<td className="px-4 py-3 text-[11px] text-fg-sub font-korean">
<td className="px-4 py-3 text-label-2 text-fg-sub font-korean">
{regionShort(org.jurisdiction)}
</td>
<td className="px-4 py-3 text-[11px] text-fg font-korean font-semibold">
<td className="px-4 py-3 text-label-2 text-fg font-korean font-semibold">
{org.name}
</td>
<td className="px-4 py-3 text-[11px] text-fg-disabled font-korean max-w-[200px] truncate">
<td className="px-4 py-3 text-label-2 text-fg-disabled font-korean max-w-[200px] truncate">
{org.address}
</td>
<td className="px-4 py-3 text-[11px] font-mono text-center text-color-accent font-semibold bg-color-accent/5">
<td className="px-4 py-3 text-label-2 font-mono text-center text-color-accent font-semibold bg-[rgba(6,182,212,0.05)]">
{org.vessel > 0 ? org.vessel : <span className="text-fg-disabled"></span>}
</td>
<td className="px-4 py-3 text-[11px] font-mono text-center text-fg-sub">
<td className="px-4 py-3 text-label-2 font-mono text-center text-fg-sub">
{org.skimmer > 0 ? org.skimmer : <span className="text-fg-disabled"></span>}
</td>
<td className="px-4 py-3 text-[11px] font-mono text-center text-fg-sub">
<td className="px-4 py-3 text-label-2 font-mono text-center text-fg-sub">
{org.pump > 0 ? org.pump : <span className="text-fg-disabled"></span>}
</td>
<td className="px-4 py-3 text-[11px] font-mono text-center text-fg-sub">
<td className="px-4 py-3 text-label-2 font-mono text-center text-fg-sub">
{org.vehicle > 0 ? org.vehicle : <span className="text-fg-disabled"></span>}
</td>
<td className="px-4 py-3 text-[11px] font-mono text-center text-fg-sub">
<td className="px-4 py-3 text-label-2 font-mono text-center text-fg-sub">
{org.sprayer > 0 ? org.sprayer : <span className="text-fg-disabled"></span>}
</td>
<td className="px-4 py-3 text-[11px] font-mono text-center font-bold text-color-accent">
<td className="px-4 py-3 text-label-2 font-mono text-center font-bold text-color-accent">
{org.totalAssets.toLocaleString()}
</td>
</tr>
@ -247,7 +247,7 @@ function VesselMaterialsPanel() {
{/* 합계 */}
{!loading && filtered.length > 0 && (
<div className="flex items-center gap-4 px-6 py-2 border-t border-stroke bg-bg-base/80">
<span className="text-[10px] text-fg-disabled font-korean font-semibold mr-auto">
<span className="text-caption text-fg-disabled font-korean font-semibold mr-auto">
({filtered.length} )
</span>
{[
@ -290,15 +290,15 @@ function VesselMaterialsPanel() {
].map((t) => (
<div
key={t.label}
className={`flex items-center gap-1 px-1.5 py-0.5 rounded ${t.active ? 'bg-color-accent/10' : ''}`}
className={`flex items-center gap-1 px-1.5 py-0.5 rounded ${t.active ? 'bg-[rgba(6,182,212,0.1)]' : ''}`}
>
<span
className={`text-[9px] font-korean ${t.active ? 'text-color-accent' : 'text-fg-disabled'}`}
className={`text-caption font-korean ${t.active ? 'text-color-accent' : 'text-fg-disabled'}`}
>
{t.label}
</span>
<span
className={`text-[10px] font-mono font-bold ${t.active ? 'text-color-accent' : 'text-fg'}`}
className={`text-caption font-mono font-bold ${t.active ? 'text-color-accent' : 'text-fg'}`}
>
{t.value.toLocaleString()}
{t.unit}
@ -311,7 +311,7 @@ function VesselMaterialsPanel() {
{/* 페이지네이션 */}
{!loading && filtered.length > 0 && (
<div className="flex items-center justify-between px-6 py-3 border-t border-stroke">
<span className="text-[11px] text-fg-disabled font-korean">
<span className="text-label-2 text-fg-disabled font-korean">
{(safePage - 1) * PAGE_SIZE + 1}{Math.min(safePage * PAGE_SIZE, filtered.length)} /
{filtered.length}
</span>
@ -319,7 +319,7 @@ function VesselMaterialsPanel() {
<button
onClick={() => setCurrentPage((p) => Math.max(1, p - 1))}
disabled={safePage === 1}
className="px-2.5 py-1 text-[11px] border border-stroke rounded text-fg-sub hover:border-color-accent hover:text-color-accent disabled:opacity-40 transition-colors"
className="px-2.5 py-1 text-label-2 border border-stroke rounded text-fg-sub hover:border-color-accent hover:text-color-accent disabled:opacity-40 transition-colors"
>
&lt;
</button>
@ -327,7 +327,7 @@ function VesselMaterialsPanel() {
<button
key={p}
onClick={() => setCurrentPage(p)}
className="px-2.5 py-1 text-[11px] border rounded transition-colors"
className="px-2.5 py-1 text-label-2 border rounded transition-colors"
style={
p === safePage
? {
@ -344,7 +344,7 @@ function VesselMaterialsPanel() {
<button
onClick={() => setCurrentPage((p) => Math.min(totalPages, p + 1))}
disabled={safePage === totalPages}
className="px-2.5 py-1 text-[11px] border border-stroke rounded text-fg-sub hover:border-color-accent hover:text-color-accent disabled:opacity-40 transition-colors"
className="px-2.5 py-1 text-label-2 border border-stroke rounded text-fg-sub hover:border-color-accent hover:text-color-accent disabled:opacity-40 transition-colors"
>
&gt;
</button>

파일 보기

@ -180,10 +180,10 @@ export default function VesselSignalPanel() {
className="flex flex-col justify-center mb-4"
style={{ height: 20 }}
>
<span className="text-[12px] font-semibold leading-tight" style={{ color: c }}>
<span className="text-label-1 font-semibold leading-tight" style={{ color: c }}>
{src}
</span>
<span className="text-[10px] font-mono text-text-4 mt-0.5">{st.rate}%</span>
<span className="text-caption font-mono text-text-4 mt-0.5">{st.rate}%</span>
</div>
);
})}
@ -196,14 +196,14 @@ export default function VesselSignalPanel() {
{HOURS.map((h) => (
<span
key={h}
className="absolute text-[10px] text-fg-disabled font-mono"
className="absolute text-caption text-fg-disabled font-mono"
style={{ left: `${(h / 24) * 100}%`, transform: 'translateX(-50%)' }}
>
{String(h).padStart(2, '0')}
</span>
))}
<span
className="absolute text-[10px] text-fg-disabled font-mono"
className="absolute text-caption text-fg-disabled font-mono"
style={{ right: 0 }}
>
24

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다. Load Diff

파일 보기

@ -19,6 +19,7 @@ interface CCTVPlayerProps {
coordDc?: string | null;
sourceNm?: string | null;
cellIndex?: number;
onClose?: () => void;
oilDetectionEnabled?: boolean;
vesselDetectionEnabled?: boolean;
intrusionDetectionEnabled?: boolean;
@ -44,9 +45,8 @@ export const CCTVPlayer = forwardRef<CCTVPlayerHandle, CCTVPlayerProps>(
cameraNm,
streamUrl,
sttsCd,
coordDc,
sourceNm,
cellIndex = 0,
onClose,
oilDetectionEnabled = false,
vesselDetectionEnabled = false,
intrusionDetectionEnabled = false,
@ -251,10 +251,12 @@ export const CCTVPlayer = forwardRef<CCTVPlayerHandle, CCTVPlayerProps>(
return (
<div className="absolute inset-0 flex flex-col items-center justify-center bg-bg-base">
<div className="text-2xl opacity-30 mb-2">📹</div>
<div className="text-[11px] font-korean text-fg-disabled opacity-70">
<div className="text-label-2 font-korean text-fg-disabled opacity-70">
{sttsCd === 'MAINT' ? '점검중' : '오프라인'}
</div>
<div className="text-[9px] font-korean text-fg-disabled opacity-40 mt-1">{cameraNm}</div>
<div className="text-caption font-korean text-fg-disabled opacity-40 mt-1">
{cameraNm}
</div>
</div>
);
}
@ -264,10 +266,12 @@ export const CCTVPlayer = forwardRef<CCTVPlayerHandle, CCTVPlayerProps>(
return (
<div className="absolute inset-0 flex flex-col items-center justify-center bg-bg-base">
<div className="text-2xl opacity-20 mb-2">📹</div>
<div className="text-[10px] font-korean text-fg-disabled opacity-50">
<div className="text-caption font-korean text-fg-disabled opacity-50">
URL
</div>
<div className="text-[9px] font-korean text-fg-disabled opacity-30 mt-1">{cameraNm}</div>
<div className="text-caption font-korean text-fg-disabled opacity-30 mt-1">
{cameraNm}
</div>
</div>
);
}
@ -277,11 +281,13 @@ export const CCTVPlayer = forwardRef<CCTVPlayerHandle, CCTVPlayerProps>(
return (
<div className="absolute inset-0 flex flex-col items-center justify-center bg-bg-base">
<div className="text-2xl opacity-30 mb-2"></div>
<div className="text-[10px] font-korean text-color-danger opacity-70"> </div>
<div className="text-[9px] font-korean text-fg-disabled opacity-40 mt-1">{cameraNm}</div>
<div className="text-caption font-korean text-color-danger opacity-70"> </div>
<div className="text-caption font-korean text-fg-disabled opacity-40 mt-1">
{cameraNm}
</div>
<button
onClick={() => setRetryKey((k) => k + 1)}
className="mt-2 px-2.5 py-1 rounded text-[9px] font-korean bg-bg-card border border-stroke text-fg-sub cursor-pointer hover:bg-bg-surface-hover transition-colors"
className="mt-2 px-2.5 py-1 rounded text-caption font-korean bg-bg-card border border-stroke text-fg-sub cursor-pointer hover:bg-bg-surface-hover transition-colors"
>
</button>
@ -295,7 +301,7 @@ export const CCTVPlayer = forwardRef<CCTVPlayerHandle, CCTVPlayerProps>(
{playerState === 'loading' && (
<div className="absolute inset-0 flex flex-col items-center justify-center bg-bg-base z-10">
<div className="text-lg opacity-40 animate-pulse mb-2">📹</div>
<div className="text-[10px] font-korean text-fg-disabled opacity-50"> ...</div>
<div className="text-caption font-korean text-fg-disabled opacity-50"> ...</div>
</div>
)}
@ -343,16 +349,22 @@ export const CCTVPlayer = forwardRef<CCTVPlayerHandle, CCTVPlayerProps>(
<div className="absolute top-2 right-2 flex flex-col gap-1 z-20">
{vesselDetectionEnabled && (
<div
className="flex items-center gap-1 px-1.5 py-0.5 rounded text-[8px] font-bold"
style={{ background: 'rgba(59,130,246,.3)', color: '#93c5fd' }}
className="flex items-center gap-1 px-1.5 py-0.5 rounded text-caption font-bold"
style={{
background: 'color-mix(in srgb, var(--color-info) 30%, transparent)',
color: 'var(--color-info)',
}}
>
🚢
</div>
)}
{intrusionDetectionEnabled && (
<div
className="flex items-center gap-1 px-1.5 py-0.5 rounded text-[8px] font-bold"
style={{ background: 'rgba(249,115,22,.3)', color: '#fdba74' }}
className="flex items-center gap-1 px-1.5 py-0.5 rounded text-caption font-bold"
style={{
background: 'color-mix(in srgb, var(--color-warning) 30%, transparent)',
color: 'var(--color-warning)',
}}
>
🚨
</div>
@ -362,22 +374,45 @@ export const CCTVPlayer = forwardRef<CCTVPlayerHandle, CCTVPlayerProps>(
{/* OSD 오버레이 */}
<div className="absolute top-2 left-2 flex items-center gap-1.5 z-20">
<span className="text-[9px] font-bold px-1.5 py-0.5 rounded bg-black/70 text-white">
<span className="text-caption font-bold px-1.5 py-0.5 rounded bg-black/70 text-white">
{cameraNm}
</span>
{sttsCd === 'LIVE' && (
<span
className="text-[8px] font-bold px-1 py-0.5 rounded text-color-danger"
style={{ background: 'rgba(239,68,68,.3)' }}
className="text-caption font-bold px-1 py-0.5 rounded text-color-danger"
style={{ background: 'color-mix(in srgb, var(--color-danger) 30%, transparent)' }}
>
REC
</span>
)}
</div>
<div className="absolute bottom-2 left-2 text-[9px] font-mono px-1.5 py-0.5 rounded text-fg-disabled bg-black/70 z-20">
{/* 닫기 (지도로 돌아가기) */}
{onClose && (
<button
onClick={onClose}
className="absolute top-2 right-2 w-7 h-7 flex items-center justify-center rounded bg-black/60 hover:bg-black/80 text-white/70 hover:text-white cursor-pointer transition-colors z-20"
title="지도로 돌아가기"
>
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2.5"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M19 12H5" />
<path d="M12 19l-7-7 7-7" />
</svg>
</button>
)}
{/* <div className="absolute bottom-2 left-2 text-caption font-mono px-1.5 py-0.5 rounded text-fg-disabled bg-black/70 z-20">
{coordDc ?? ''}
{sourceNm ? ` · ${sourceNm}` : ''}
</div>
</div> */}
</div>
);
},

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다. Load Diff

파일 보기

@ -13,17 +13,9 @@ function formatDtm(dtm: string | null): string {
const equipIcon = (t: string) => (t === 'drone' ? '🛸' : t === 'plane' ? '✈' : '🛰');
const equipTagCls = (t: string) =>
t === 'drone'
? 'bg-[rgba(59,130,246,0.12)] text-color-info'
: t === 'plane'
? 'bg-[rgba(34,197,94,0.12)] text-color-success'
: 'bg-[rgba(168,85,247,0.12)] text-color-tertiary';
const equipTagCls = () => 'text-fg';
const mediaTagCls = (t: string) =>
t === '영상'
? 'bg-[rgba(239,68,68,0.12)] text-color-danger'
: 'bg-[rgba(234,179,8,0.12)] text-color-caution';
const mediaTagCls = () => 'text-fg';
const FilterBtn = ({
label,
@ -36,10 +28,10 @@ const FilterBtn = ({
}) => (
<button
onClick={onClick}
className={`px-2.5 py-1 text-[10px] font-semibold rounded font-korean transition-colors ${
className={`px-2.5 py-1 text-caption font-semibold rounded font-korean transition-colors ${
active
? 'bg-[rgba(6,182,212,0.15)] text-color-accent border border-color-accent/30'
: 'bg-bg-card border border-stroke text-fg-sub hover:bg-bg-surface-hover'
? 'bg-[rgba(6,182,212,0.15)] text-color-accent border border-[rgba(6,182,212,0.3)]'
: 'border border-stroke text-fg-sub hover:bg-bg-surface-hover'
}`}
>
{label}
@ -181,7 +173,7 @@ export function MediaManagement() {
{/* Filters */}
<div className="flex items-center justify-between mb-4">
<div className="flex gap-1.5 items-center">
<span className="text-[11px] text-fg-disabled font-korean"> :</span>
<span className="text-label-2 text-fg-disabled font-korean"> :</span>
<FilterBtn
label="전체"
active={equipFilter === 'all'}
@ -203,7 +195,7 @@ export function MediaManagement() {
onClick={() => setEquipFilter('satellite')}
/>
<span className="w-px h-4 bg-border mx-1" />
<span className="text-[11px] text-fg-disabled font-korean">:</span>
<span className="text-label-2 text-fg-disabled font-korean">:</span>
<FilterBtn
label="📷 사진"
active={typeFilter.has('photo')}
@ -221,7 +213,7 @@ export function MediaManagement() {
placeholder="파일명 검색..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="px-3 py-1.5 bg-bg-base border border-stroke rounded-sm text-fg font-korean text-[11px] outline-none w-40 focus:border-color-accent"
className="px-3 py-1.5 bg-bg-base border border-stroke rounded-sm text-fg font-korean text-label-2 outline-none w-40 focus:border-color-accent"
/>
<select
value={sortBy}
@ -242,7 +234,7 @@ export function MediaManagement() {
icon: '📸',
value: loading ? '…' : String(mediaItems.length),
label: '총 파일',
color: 'text-color-accent',
color: 'text-fg',
},
{
icon: '🛸',
@ -261,19 +253,19 @@ export function MediaManagement() {
].map((s, i) => (
<div
key={i}
className="flex-1 flex items-center gap-2.5 px-4 py-3 bg-bg-card border border-stroke rounded-sm"
className="flex-1 flex items-center gap-2.5 px-4 py-3 border border-stroke rounded-sm"
>
<span className="text-xl">{s.icon}</span>
<div>
<div className={`text-base font-bold font-mono ${s.color}`}>{s.value}</div>
<div className="text-[10px] text-fg-disabled font-korean">{s.label}</div>
<div className="text-caption text-fg-disabled font-korean">{s.label}</div>
</div>
</div>
))}
</div>
{/* File Table */}
<div className="flex-1 bg-bg-card border border-stroke rounded-md overflow-hidden flex flex-col">
<div className="flex-1 border border-stroke rounded-md overflow-hidden flex flex-col">
<div className="overflow-auto flex-1">
<table className="w-full text-left" style={{ tableLayout: 'fixed' }}>
<colgroup>
@ -287,7 +279,7 @@ export function MediaManagement() {
<col style={{ width: 145 }} />
<col style={{ width: 85 }} />
<col style={{ width: 95 }} />
<col style={{ width: 50 }} />
<col style={{ width: 60 }} />
</colgroup>
<thead>
<tr className="border-b border-stroke bg-bg-elevated">
@ -300,32 +292,32 @@ export function MediaManagement() {
/>
</th>
<th className="px-1 py-2.5" />
<th className="px-2 py-2.5 text-[10px] font-semibold text-fg-disabled font-korean whitespace-nowrap">
<th className="px-2 py-2.5 text-caption font-semibold text-fg-disabled font-korean whitespace-nowrap">
</th>
<th className="px-2 py-2.5 text-[10px] font-semibold text-fg-disabled font-korean whitespace-nowrap">
<th className="px-2 py-2.5 text-caption font-semibold text-fg-disabled font-korean whitespace-nowrap">
</th>
<th className="px-2 py-2.5 text-[10px] font-semibold text-fg-disabled font-korean">
<th className="px-2 py-2.5 text-caption font-semibold text-fg-disabled font-korean">
</th>
<th className="px-2 py-2.5 text-[10px] font-semibold text-fg-disabled font-korean">
<th className="px-2 py-2.5 text-caption font-semibold text-fg-disabled font-korean">
</th>
<th className="px-2 py-2.5 text-[10px] font-semibold text-fg-disabled font-korean">
<th className="px-2 py-2.5 text-caption font-semibold text-fg-disabled font-korean">
</th>
<th className="px-2 py-2.5 text-[10px] font-semibold text-fg-disabled font-korean whitespace-nowrap">
<th className="px-2 py-2.5 text-caption font-semibold text-fg-disabled font-korean whitespace-nowrap">
</th>
<th className="px-2 py-2.5 text-[10px] font-semibold text-fg-disabled font-korean whitespace-nowrap">
<th className="px-2 py-2.5 text-caption font-semibold text-fg-disabled font-korean whitespace-nowrap">
</th>
<th className="px-2 py-2.5 text-[10px] font-semibold text-fg-disabled font-korean whitespace-nowrap">
<th className="px-2 py-2.5 text-caption font-semibold text-fg-disabled font-korean whitespace-nowrap">
</th>
<th className="px-2 py-2.5 text-[10px] font-semibold text-fg-disabled text-center">
📥
<th className="px-2 py-2.5 text-caption font-semibold text-fg-disabled text-center">
</th>
</tr>
</thead>
@ -334,7 +326,7 @@ export function MediaManagement() {
<tr>
<td
colSpan={11}
className="px-4 py-8 text-center text-[11px] text-fg-disabled font-korean"
className="px-4 py-8 text-center text-label-2 text-fg-disabled font-korean"
>
...
</td>
@ -344,7 +336,7 @@ export function MediaManagement() {
<tr
key={f.aerialMediaSn}
onClick={() => toggleId(f.aerialMediaSn)}
className={`border-b border-stroke/50 cursor-pointer transition-colors hover:bg-[rgba(255,255,255,0.02)] ${
className={`border-b border-stroke cursor-pointer transition-colors hover:bg-[rgba(255,255,255,0.02)] ${
selectedIds.has(f.aerialMediaSn) ? 'bg-[rgba(6,182,212,0.06)]' : ''
}`}
>
@ -357,37 +349,33 @@ export function MediaManagement() {
/>
</td>
<td className="px-1 py-2 text-base">{equipIcon(f.equipTpCd)}</td>
<td className="px-2 py-2 text-[10px] font-semibold text-fg font-korean truncate">
<td className="px-2 py-2 text-caption font-semibold text-fg font-korean truncate">
{f.acdntSn != null ? String(f.acdntSn) : '—'}
</td>
<td className="px-2 py-2 text-[10px] text-color-accent font-mono truncate">
<td className="px-2 py-2 text-caption text-color-accent font-mono truncate">
{f.locDc ?? '—'}
</td>
<td className="px-2 py-2 text-[11px] font-semibold text-fg font-korean truncate">
<td className="px-2 py-2 text-label-2 font-semibold text-fg font-korean truncate">
{f.fileNm}
</td>
<td className="px-2 py-2">
<span
className={`px-1.5 py-0.5 rounded text-[9px] font-semibold font-korean ${equipTagCls(f.equipTpCd)}`}
>
<span className={`text-caption font-semibold font-korean ${equipTagCls()}`}>
{f.equipNm}
</span>
</td>
<td className="px-2 py-2">
<span
className={`px-1.5 py-0.5 rounded text-[9px] font-semibold font-korean ${mediaTagCls(f.mediaTpCd)}`}
>
<span className={`text-caption font-semibold font-korean ${mediaTagCls()}`}>
{f.mediaTpCd === '영상' ? '🎬' : '📷'} {f.mediaTpCd}
</span>
</td>
<td className="px-2 py-2 text-[11px] font-mono">{formatDtm(f.takngDtm)}</td>
<td className="px-2 py-2 text-[11px] font-mono">{f.fileSz ?? '—'}</td>
<td className="px-2 py-2 text-[11px] font-mono">{f.resolution ?? '—'}</td>
<td className="px-2 py-2 text-label-2 font-mono">{formatDtm(f.takngDtm)}</td>
<td className="px-2 py-2 text-label-2 font-mono">{f.fileSz ?? '—'}</td>
<td className="px-2 py-2 text-label-2 font-mono">{f.resolution ?? '—'}</td>
<td className="px-2 py-2 text-center" onClick={(e) => e.stopPropagation()}>
<button
onClick={(e) => handleDownload(e, f)}
disabled={downloadingId === f.aerialMediaSn}
className="px-2 py-1 text-[10px] rounded bg-[rgba(6,182,212,0.1)] text-color-accent border border-color-accent/20 hover:bg-[rgba(6,182,212,0.2)] transition-colors disabled:opacity-50"
className="px-2 py-1 text-caption rounded bg-[rgba(6,182,212,0.08)] text-color-accent border border-[rgba(6,182,212,0.3)] hover:bg-[rgba(6,182,212,0.15)] transition-colors disabled:opacity-50"
>
{downloadingId === f.aerialMediaSn ? '⏳' : '📥'}
</button>
@ -402,26 +390,26 @@ export function MediaManagement() {
{/* Bottom Actions */}
<div className="flex justify-between items-center mt-4 pt-3.5 border-t border-stroke">
<div className="text-[11px] text-fg-disabled font-korean">
<div className="text-label-2 text-fg-disabled font-korean">
: <span className="text-color-accent font-semibold">{selectedIds.size}</span>
</div>
<div className="flex gap-2">
<button
onClick={toggleAll}
className="px-3 py-1.5 text-[11px] font-semibold rounded bg-bg-card border border-stroke text-fg-sub hover:bg-bg-surface-hover transition-colors font-korean"
className="px-3 py-1.5 text-label-2 font-semibold rounded bg-bg-card border border-stroke text-fg-sub hover:bg-bg-surface-hover transition-colors font-korean"
>
</button>
<button
onClick={handleBulkDownload}
disabled={bulkDownloading || selectedIds.size === 0}
className="px-3 py-1.5 text-[11px] font-semibold rounded bg-[rgba(6,182,212,0.1)] text-color-accent border border-color-accent/30 hover:bg-[rgba(6,182,212,0.2)] transition-colors font-korean disabled:opacity-50"
className="px-3 py-1.5 text-label-2 font-semibold rounded bg-[rgba(6,182,212,0.08)] text-color-accent border border-[rgba(6,182,212,0.3)] hover:bg-[rgba(6,182,212,0.15)] transition-colors font-korean disabled:opacity-50"
>
{bulkDownloading ? '⏳ 다운로드 중...' : '📥 선택 다운로드'}
</button>
<button
onClick={() => navigateToTab('prediction', 'analysis')}
className="px-3 py-1.5 text-[11px] font-semibold rounded bg-[rgba(168,85,247,0.1)] text-color-tertiary border border-color-tertiary/30 hover:bg-[rgba(168,85,247,0.2)] transition-colors font-korean"
className="px-3 py-1.5 text-label-2 font-semibold rounded bg-[rgba(6,182,212,0.08)] text-color-accent border border-[rgba(6,182,212,0.3)] hover:bg-[rgba(6,182,212,0.15)] transition-colors font-korean"
>
🔬
</button>
@ -434,10 +422,10 @@ export function MediaManagement() {
<div className="bg-bg-surface border border-stroke rounded-md p-6 w-72 text-center">
<div className="text-2xl mb-3">📥</div>
<div className="text-sm font-bold font-korean mb-3"> </div>
<div className="text-[13px] font-korean text-fg-sub mb-1">
<div className="text-title-4 font-korean text-fg-sub mb-1">
<span className="text-color-accent font-bold">{downloadResult.total}</span>
</div>
<div className="text-[13px] font-korean text-fg-sub mb-4">
<div className="text-title-4 font-korean text-fg-sub mb-4">
<span className="text-color-success font-bold">{downloadResult.success}</span>
{downloadResult.total - downloadResult.success > 0 && (
@ -453,7 +441,7 @@ export function MediaManagement() {
</div>
<button
onClick={() => setDownloadResult(null)}
className="px-6 py-2 text-sm font-semibold rounded bg-[rgba(6,182,212,0.15)] text-color-accent border border-color-accent/30 hover:bg-[rgba(6,182,212,0.25)] transition-colors font-korean"
className="px-6 py-2 text-sm font-semibold rounded bg-[rgba(6,182,212,0.15)] text-color-accent border border-[rgba(6,182,212,0.3)] hover:bg-[rgba(6,182,212,0.25)] transition-colors font-korean"
>
</button>
@ -477,12 +465,12 @@ export function MediaManagement() {
</button>
</div>
<div className="border-2 border-dashed border-stroke-light rounded-md py-8 px-4 text-center mb-4 cursor-pointer hover:border-color-accent/40 transition-colors">
<div className="border-2 border-dashed border-stroke-light rounded-md py-8 px-4 text-center mb-4 cursor-pointer hover:border-[rgba(6,182,212,0.4)] transition-colors">
<div className="text-3xl mb-2 opacity-50">📁</div>
<div className="text-[13px] font-semibold mb-1 font-korean">
<div className="text-title-4 font-semibold mb-1 font-korean">
</div>
<div className="text-[11px] text-fg-disabled font-korean">
<div className="text-label-2 text-fg-disabled font-korean">
JPG, TIFF, GeoTIFF, MP4, MOV · 2GB
</div>
</div>
@ -520,9 +508,11 @@ export function MediaManagement() {
/>
</div>
<button
className="w-full py-3 rounded-sm text-sm font-bold font-korean text-white border-none cursor-pointer"
className="w-full py-3 rounded-sm text-sm font-bold font-korean cursor-pointer hover:brightness-125 transition-all"
style={{
background: 'linear-gradient(135deg, var(--color-accent), var(--color-info))',
background: 'rgba(6,182,212,0.15)',
border: '1px solid rgba(6,182,212,0.3)',
color: 'var(--color-accent)',
}}
>
📤

파일 보기

@ -240,7 +240,7 @@ export function OilAreaAnalysis() {
{/* ── Left Panel ── */}
<div className="w-[280px] min-w-[280px] flex flex-col overflow-y-auto scrollbar-thin">
<div className="text-sm font-bold mb-1 font-korean">🧩 </div>
<div className="text-[11px] text-fg-disabled mb-4 font-korean">
<div className="text-label-2 text-fg-disabled mb-4 font-korean">
.
</div>
@ -266,12 +266,12 @@ export function OilAreaAnalysis() {
{/* 선택된 이미지 목록 */}
{selectedFiles.length > 0 && (
<>
<div className="text-[11px] font-bold mb-1.5 font-korean"> </div>
<div className="text-label-2 font-bold mb-1.5 font-korean"> </div>
<div className="flex flex-col gap-1 mb-3">
{selectedFiles.map((file, i) => (
<div key={`${file.name}-${i}`}>
<div
className={`flex items-center gap-2 px-2 py-1.5 bg-bg-card border rounded-sm text-[11px] font-korean cursor-pointer transition-colors
className={`flex items-center gap-2 px-2 py-1.5 bg-bg-card border rounded-sm text-label-2 font-korean cursor-pointer transition-colors
${selectedImageIndex === i ? 'border-color-accent' : 'border-stroke'}`}
onClick={() => setSelectedImageIndex(i)}
>
@ -291,7 +291,7 @@ export function OilAreaAnalysis() {
</button>
</div>
{selectedImageIndex === i && imageExifs[i] !== undefined && (
<div className="mt-1 mb-1 px-2 py-1.5 bg-bg-base border border-stroke/60 rounded-sm text-[11px] font-korean">
<div className="mt-1 mb-1 px-2 py-1.5 bg-bg-base border border-stroke/60 rounded-sm text-label-2 font-korean">
<MetaRow label="파일 크기" value={formatFileSize(file.size)} />
<MetaRow
label="해상도"
@ -368,7 +368,7 @@ export function OilAreaAnalysis() {
{/* 에러 메시지 */}
{error && (
<div className="mb-3 px-2.5 py-2 bg-[rgba(239,68,68,0.08)] border border-[rgba(239,68,68,0.3)] rounded-sm text-[11px] text-color-danger font-korean">
<div className="mb-3 px-2.5 py-2 bg-[rgba(239,68,68,0.08)] border border-[rgba(239,68,68,0.3)] rounded-sm text-label-2 text-color-danger font-korean">
{error}
</div>
)}
@ -377,7 +377,7 @@ export function OilAreaAnalysis() {
<button
onClick={handleStitch}
disabled={!canStitch}
className="w-full py-2.5 mb-2 rounded-sm text-[12px] font-bold font-korean cursor-pointer transition-colors
className="w-full py-2.5 mb-2 rounded-sm text-label-1 font-bold font-korean cursor-pointer transition-colors
border border-color-accent text-color-accent bg-[rgba(6,182,212,0.06)]
hover:bg-[rgba(6,182,212,0.15)] disabled:opacity-40 disabled:cursor-not-allowed"
>
@ -388,7 +388,7 @@ export function OilAreaAnalysis() {
<button
onClick={handleAnalyze}
disabled={!canAnalyze}
className="w-full py-3 rounded-sm text-[13px] font-bold font-korean cursor-pointer border-none transition-colors
className="w-full py-3 rounded-sm text-title-4 font-bold font-korean cursor-pointer border-none transition-colors
disabled:opacity-40 disabled:cursor-not-allowed text-white"
style={
canAnalyze
@ -403,7 +403,7 @@ export function OilAreaAnalysis() {
{/* ── Right Panel ── */}
<div className="flex-1 flex flex-col overflow-hidden">
{/* 3×2 이미지 그리드 */}
<div className="text-[11px] font-bold mb-2 font-korean"> </div>
<div className="text-label-2 font-bold mb-2 font-korean"> </div>
<div className="grid grid-cols-3 gap-1.5 mb-3">
{Array.from({ length: MAX_IMAGES }).map((_, i) => (
<div
@ -426,21 +426,21 @@ export function OilAreaAnalysis() {
/>
</div>
<div className="px-2 py-1 bg-bg-base border-t border-stroke shrink-0 flex items-start justify-between gap-1">
<div className="text-[10px] text-fg-sub truncate font-korean flex-1 min-w-0">
<div className="text-caption text-fg-sub truncate font-korean flex-1 min-w-0">
{selectedFiles[i]?.name}
</div>
{imageExifs[i] === undefined ? (
<div className="text-[10px] text-fg-disabled font-korean shrink-0">
<div className="text-caption text-fg-disabled font-korean shrink-0">
GPS ...
</div>
) : imageExifs[i]?.lat !== null ? (
<div className="text-[10px] text-color-accent font-mono leading-tight text-right shrink-0">
<div className="text-caption text-color-accent font-mono leading-tight text-right shrink-0">
{decimalToDMS(imageExifs[i]!.lat!, true)}
<br />
{decimalToDMS(imageExifs[i]!.lon!, false)}
</div>
) : (
<div className="text-[10px] text-fg-disabled font-korean shrink-0">
<div className="text-caption text-fg-disabled font-korean shrink-0">
GPS
</div>
)}
@ -456,7 +456,7 @@ export function OilAreaAnalysis() {
</div>
{/* 합성 결과 */}
<div className="text-[11px] font-bold mb-2 font-korean"> </div>
<div className="text-label-2 font-bold mb-2 font-korean"> </div>
<div
className="relative bg-bg-base border border-stroke rounded-sm overflow-hidden flex items-center justify-center"
style={{ minHeight: '160px', flex: '1 1 0' }}
@ -468,7 +468,7 @@ export function OilAreaAnalysis() {
className="max-w-full max-h-full object-contain"
/>
) : (
<div className="text-[12px] text-fg-disabled font-korean text-center px-4">
<div className="text-label-1 text-fg-disabled font-korean text-center px-4">
{isStitching
? '⏳ 이미지를 합성하고 있습니다...'
: '이미지를 선택하고 합성 버튼을 클릭하면\n합성 결과가 여기에 표시됩니다.'}

파일 보기

@ -85,11 +85,11 @@ const OilDetectionOverlay = memo(
{/* 에러 표시 */}
{error && (
<div
className="px-2 py-0.5 rounded text-[10px] font-semibold font-korean"
className="px-2 py-0.5 rounded text-caption font-semibold font-korean"
style={{
background: 'rgba(239,68,68,0.2)',
border: '1px solid rgba(239,68,68,0.5)',
color: '#f87171',
background: 'color-mix(in srgb, var(--color-danger) 20%, transparent)',
border: '1px solid color-mix(in srgb, var(--color-danger) 50%, transparent)',
color: 'var(--color-danger)',
}}
>
@ -101,13 +101,13 @@ const OilDetectionOverlay = memo(
<>
{result.regions.map((region) => {
const oilClass = OIL_CLASSES.find((c) => c.classId === region.classId);
const color = oilClass ? `rgb(${oilClass.color.join(',')})` : '#f87171';
const color = oilClass ? `rgb(${oilClass.color.join(',')})` : 'var(--color-danger)';
const label = OIL_CLASS_NAMES[region.classId] || region.className;
return (
<div
key={region.classId}
className="px-2 py-0.5 rounded text-[10px] font-semibold font-korean"
className="px-2 py-0.5 rounded text-caption font-semibold font-korean"
style={{
background: `${color}33`,
border: `1px solid ${color}80`,
@ -120,11 +120,11 @@ const OilDetectionOverlay = memo(
})}
{/* 합계 */}
<div
className="px-2 py-0.5 rounded text-[10px] font-semibold font-korean"
className="px-2 py-0.5 rounded text-caption font-semibold font-korean"
style={{
background: 'rgba(239,68,68,0.2)',
border: '1px solid rgba(239,68,68,0.5)',
color: '#f87171',
background: 'color-mix(in srgb, var(--color-danger) 20%, transparent)',
border: '1px solid color-mix(in srgb, var(--color-danger) 50%, transparent)',
color: 'var(--color-danger)',
}}
>
: {formatArea(result.totalAreaM2)} ({result.totalPercentage.toFixed(1)}%)
@ -135,11 +135,11 @@ const OilDetectionOverlay = memo(
{/* 감지 없음 */}
{!hasRegions && !isAnalyzing && !error && (
<div
className="px-2 py-0.5 rounded text-[10px] font-semibold font-korean"
className="px-2 py-0.5 rounded text-caption font-semibold font-korean"
style={{
background: 'rgba(34,197,94,0.15)',
border: '1px solid rgba(34,197,94,0.35)',
color: '#4ade80',
background: 'color-mix(in srgb, var(--color-success) 15%, transparent)',
border: '1px solid color-mix(in srgb, var(--color-success) 35%, transparent)',
color: 'var(--color-success)',
}}
>
@ -148,7 +148,7 @@ const OilDetectionOverlay = memo(
{/* 분석 중 */}
{isAnalyzing && (
<span className="text-[9px] font-korean text-fg-disabled animate-pulse px-1">
<span className="text-caption font-korean text-fg-disabled animate-pulse px-1">
...
</span>
)}

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다. Load Diff

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다. Load Diff

파일 보기

@ -396,7 +396,7 @@ function Vessel3DModel({ viewMode, status }: { viewMode: string; status: string
</div>
<div className="w-24 h-0.5 bg-bg-card rounded-full mt-2 mx-auto overflow-hidden">
<div
className="h-full bg-color-accent/40 rounded-full"
className="h-full bg-[rgba(6,182,212,0.4)] rounded-full"
style={{ width: '64%', animation: 'pulse 2s infinite' }}
/>
</div>
@ -717,7 +717,7 @@ function Pollution3DModel({ viewMode, status }: { viewMode: string; status: stri
</div>
<div className="w-24 h-0.5 bg-bg-card rounded-full mt-2 mx-auto overflow-hidden">
<div
className="h-full bg-color-danger/40 rounded-full"
className="h-full bg-[rgba(239,68,68,0.4)] rounded-full"
style={{ width: '52%', animation: 'pulse 2s infinite' }}
/>
</div>
@ -746,15 +746,15 @@ export function SensorAnalysis() {
<div className="w-[280px] bg-bg-surface border-r border-stroke flex flex-col overflow-auto">
{/* 3D Reconstruction List */}
<div className="p-2.5 px-3 border-b border-stroke">
<div className="text-[10px] font-bold text-fg-disabled mb-1.5 uppercase tracking-wider">
<div className="text-caption font-bold text-fg-disabled mb-1.5 uppercase tracking-wider">
📋 3D
</div>
<div className="flex gap-1 mb-2">
<button
onClick={() => setSubTab('vessel')}
className={`flex-1 py-1.5 text-center text-[9px] font-semibold rounded cursor-pointer border transition-colors font-korean ${
className={`flex-1 py-1.5 text-center text-caption font-semibold rounded cursor-pointer border transition-colors font-korean ${
subTab === 'vessel'
? 'text-color-accent bg-[rgba(6,182,212,0.08)] border-color-accent/20'
? 'text-color-accent bg-[rgba(6,182,212,0.08)] border-[rgba(6,182,212,0.2)]'
: 'text-fg-disabled bg-bg-base border-stroke'
}`}
>
@ -762,9 +762,9 @@ export function SensorAnalysis() {
</button>
<button
onClick={() => setSubTab('pollution')}
className={`flex-1 py-1.5 text-center text-[9px] font-semibold rounded cursor-pointer border transition-colors font-korean ${
className={`flex-1 py-1.5 text-center text-caption font-semibold rounded cursor-pointer border transition-colors font-korean ${
subTab === 'pollution'
? 'text-color-accent bg-[rgba(6,182,212,0.08)] border-color-accent/20'
? 'text-color-accent bg-[rgba(6,182,212,0.08)] border-[rgba(6,182,212,0.2)]'
: 'text-fg-disabled bg-bg-base border-stroke'
}`}
>
@ -778,20 +778,20 @@ export function SensorAnalysis() {
onClick={() => setSelectedItem(item)}
className={`flex items-center gap-2 px-2 py-2 rounded-sm cursor-pointer transition-colors border ${
selectedItem.id === item.id
? 'bg-[rgba(6,182,212,0.08)] border-color-accent/20'
? 'bg-[rgba(6,182,212,0.08)] border-[rgba(6,182,212,0.2)]'
: 'border-transparent hover:bg-white/[0.02]'
}`}
>
<div className="flex-1 min-w-0">
<div className="text-[10px] font-bold text-fg font-korean">{item.name}</div>
<div className="text-[8px] text-fg-disabled font-mono">
<div className="text-caption font-bold text-fg font-korean">{item.name}</div>
<div className="text-caption text-fg-disabled font-mono">
{item.id} · {item.points} pts
</div>
</div>
<span
className={`text-[8px] font-semibold ${item.status === 'complete' ? 'text-color-success' : 'text-color-warning'}`}
className={`text-caption font-semibold ${item.status === 'complete' ? 'text-color-success' : 'text-color-warning'}`}
>
{item.status === 'complete' ? '완료' : '처리중'}
{item.status === 'complete' ? '완료' : '처리중'}
</span>
</div>
))}
@ -800,15 +800,15 @@ export function SensorAnalysis() {
{/* Source Images */}
<div className="p-2.5 px-3 flex-1 min-h-0 flex flex-col">
<div className="text-[10px] font-bold text-fg-disabled mb-1.5 uppercase tracking-wider">
<div className="text-caption font-bold text-fg-disabled mb-1.5 uppercase tracking-wider">
📹
</div>
<div className="grid grid-cols-2 gap-1">
{[
{ label: 'D-01 정면', sensor: '광학', color: 'text-color-info' },
{ label: 'D-02 좌현', sensor: 'IR', color: 'text-color-danger' },
{ label: 'D-03 우현', sensor: '광학', color: 'text-color-tertiary' },
{ label: 'D-02 상부', sensor: 'IR', color: 'text-color-danger' },
{ label: 'D-01 정면', sensor: '광학', color: 'text-fg-sub' },
{ label: 'D-02 좌현', sensor: 'IR', color: 'text-fg-sub' },
{ label: 'D-03 우현', sensor: '광학', color: 'text-fg-sub' },
{ label: 'D-02 상부', sensor: 'IR', color: 'text-fg-sub' },
].map((src, i) => (
<div
key={i}
@ -816,13 +816,13 @@ export function SensorAnalysis() {
>
<div
className="absolute inset-0 flex items-center justify-center"
style={{ background: 'linear-gradient(135deg, #0c1624, #1a1a2e)' }}
style={{ background: 'var(--bg-base)' }}
>
<div className="text-fg-disabled/10 text-xs font-mono">
{src.label.split(' ')[0]}
</div>
</div>
<div className="absolute bottom-0 left-0 right-0 bg-black/60 px-1.5 py-1 flex justify-between text-[8px] text-fg-disabled font-korean">
<div className="absolute bottom-0 left-0 right-0 bg-black/60 px-1.5 py-1 flex justify-between text-caption text-fg-disabled font-korean">
<span>{src.label}</span>
<span className={src.color}>{src.sensor}</span>
</div>
@ -863,7 +863,7 @@ export function SensorAnalysis() {
className="absolute bottom-16 left-4"
style={{ fontSize: '9px', fontFamily: 'var(--font-mono)' }}
>
<div style={{ color: '#ef4444' }}>X </div>
<div style={{ color: 'var(--color-danger)' }}>X </div>
<div className="text-green-500">Y </div>
<div className="text-blue-500">Z </div>
</div>
@ -871,13 +871,13 @@ export function SensorAnalysis() {
{/* Title */}
<div className="absolute top-3 left-3 z-[2]">
<div className="text-[10px] font-bold text-fg-disabled uppercase tracking-wider">
<div className="text-caption font-bold text-fg-disabled uppercase tracking-wider">
3D Vessel Analysis
</div>
<div className="text-[13px] font-bold text-color-accent my-1 font-korean">
<div className="text-title-4 font-bold text-color-accent my-1 font-korean">
{selectedItem.name}
</div>
<div className="text-[9px] text-fg-disabled font-mono">
<div className="text-caption text-fg-disabled font-mono">
34.58°N, 129.30°E · {selectedItem.status === 'complete' ? '재구성 완료' : '처리중'}
</div>
</div>
@ -892,10 +892,10 @@ export function SensorAnalysis() {
<button
key={m.id}
onClick={() => setViewMode(m.id)}
className={`px-2.5 py-1.5 text-[10px] font-semibold rounded-sm cursor-pointer border font-korean transition-colors ${
className={`px-2.5 py-1.5 text-caption font-semibold rounded-sm cursor-pointer border font-korean transition-colors ${
viewMode === m.id
? 'bg-[rgba(6,182,212,0.2)] border-color-accent/50 text-color-accent'
: 'bg-black/40 border-color-accent/20 text-fg-disabled hover:bg-black/60 hover:border-color-accent/40'
? 'bg-[rgba(6,182,212,0.2)] border-[rgba(6,182,212,0.5)] text-color-accent'
: 'bg-black/40 border-[rgba(6,182,212,0.2)] text-fg-disabled hover:bg-black/60 hover:border-[rgba(6,182,212,0.4)]'
}`}
>
{m.label}
@ -906,7 +906,7 @@ export function SensorAnalysis() {
{/* Bottom Stats */}
<div
className="absolute bottom-3 left-1/2 -translate-x-1/2 flex gap-3 bg-black/50 backdrop-blur-lg px-4 py-2 rounded-md border z-[2]"
style={{ borderColor: 'rgba(6,182,212,0.15)' }}
style={{ borderColor: 'var(--stroke-default)' }}
>
{[
{ value: selectedItem.points, label: '포인트' },
@ -917,7 +917,7 @@ export function SensorAnalysis() {
].map((s, i) => (
<div key={i} className="text-center">
<div className="font-mono font-bold text-sm text-color-accent">{s.value}</div>
<div className="text-[8px] text-fg-disabled mt-0.5 font-korean">{s.label}</div>
<div className="text-caption text-fg-disabled mt-0.5 font-korean">{s.label}</div>
</div>
))}
</div>
@ -927,10 +927,10 @@ export function SensorAnalysis() {
<div className="w-[270px] bg-bg-surface border-l border-stroke flex flex-col overflow-auto">
{/* Ship/Pollution Info */}
<div className="p-2.5 px-3 border-b border-stroke">
<div className="text-[10px] font-bold text-fg-disabled mb-2 uppercase tracking-wider">
<div className="text-caption font-bold text-fg-disabled mb-2 uppercase tracking-wider">
📊
</div>
<div className="flex flex-col gap-1.5 text-[10px]">
<div className="flex flex-col gap-1.5 text-caption">
{(selectedItem.type === 'vessel'
? [
['대상', selectedItem.name],
@ -962,26 +962,26 @@ export function SensorAnalysis() {
{/* AI Detection Results */}
<div className="p-2.5 px-3 border-b border-stroke">
<div className="text-[10px] font-bold text-fg-disabled mb-2 uppercase tracking-wider">
<div className="text-caption font-bold text-fg-disabled mb-2 uppercase tracking-wider">
🤖 AI
</div>
<div className="flex flex-col gap-1">
{(selectedItem.type === 'vessel'
? [
{ label: '선박 식별', confidence: 94, color: 'bg-color-success' },
{ label: '선종 분류', confidence: 78, color: 'bg-color-caution' },
{ label: '손상 감지', confidence: 45, color: 'bg-color-warning' },
{ label: '화물 분석', confidence: 62, color: 'bg-color-caution' },
{ label: '선종 분류', confidence: 78, color: 'bg-color-success' },
{ label: '손상 감지', confidence: 45, color: 'bg-color-success' },
{ label: '화물 분석', confidence: 62, color: 'bg-color-success' },
]
: [
{ label: '유막 탐지', confidence: 97, color: 'bg-color-success' },
{ label: '유종 분류', confidence: 85, color: 'bg-color-success' },
{ label: '두께 추정', confidence: 72, color: 'bg-color-caution' },
{ label: '확산 예측', confidence: 68, color: 'bg-color-warning' },
{ label: '두께 추정', confidence: 72, color: 'bg-color-success' },
{ label: '확산 예측', confidence: 68, color: 'bg-color-success' },
]
).map((r, i) => (
<div key={i}>
<div className="flex justify-between text-[9px] mb-0.5">
<div className="flex justify-between text-caption mb-0.5">
<span className="text-fg-disabled font-korean">{r.label}</span>
<span className="font-mono font-semibold text-fg">{r.confidence}%</span>
</div>
@ -998,10 +998,10 @@ export function SensorAnalysis() {
{/* Comparison / Measurements */}
<div className="p-2.5 px-3 border-b border-stroke">
<div className="text-[10px] font-bold text-fg-disabled mb-2 uppercase tracking-wider">
<div className="text-caption font-bold text-fg-disabled mb-2 uppercase tracking-wider">
📐 3D
</div>
<div className="flex flex-col gap-1 text-[10px]">
<div className="flex flex-col gap-1 text-caption">
{(selectedItem.type === 'vessel'
? [
['전장 (LOA)', '84.7 m'],
@ -1020,7 +1020,7 @@ export function SensorAnalysis() {
).map(([k, v], i) => (
<div key={i} className="flex justify-between px-2 py-1 bg-bg-base rounded">
<span className="text-fg-disabled font-korean">{k}</span>
<span className="font-mono font-semibold text-color-accent">{v}</span>
<span className="font-mono font-semibold text-fg">{v}</span>
</div>
))}
</div>
@ -1029,14 +1029,15 @@ export function SensorAnalysis() {
{/* Action Buttons */}
<div className="p-2.5 px-3">
<button
className="w-full py-2.5 rounded-sm text-xs font-bold font-korean text-white border-none cursor-pointer mb-2"
className="w-full py-2.5 rounded-sm text-label-2 font-semibold font-korean text-color-accent border cursor-pointer mb-2 transition-colors"
style={{
background: 'linear-gradient(135deg, var(--color-accent), var(--color-info))',
border: '1px solid rgba(6,182,212,.3)',
background: 'rgba(6,182,212,.08)',
}}
>
📊
</button>
<button className="w-full py-2 border border-stroke bg-bg-card text-fg-sub rounded-sm text-[11px] font-semibold font-korean cursor-pointer hover:bg-bg-surface-hover transition-colors">
<button className="w-full py-2 border border-stroke bg-bg-card text-fg-sub rounded-sm text-label-2 font-semibold font-korean cursor-pointer hover:bg-bg-surface-hover transition-colors">
📥 3D
</button>
</div>

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다. Load Diff

파일 보기

@ -1,5 +1,4 @@
import { useState, useEffect } from 'react';
import { typeTagCls } from './assetTypes';
import { fetchOrganizations, fetchOrganizationDetail } from '../services/assetsApi';
import type { AssetOrgCompat } from '../services/assetsApi';
import AssetMap from './AssetMap';
@ -96,20 +95,20 @@ function AssetManagement() {
<div className="flex gap-1">
<button
onClick={() => setViewMode('list')}
className={`px-3 py-1.5 text-[11px] font-semibold rounded-sm font-korean transition-colors ${
className={`px-3 py-1.5 text-label-2 font-semibold rounded-sm font-korean transition-colors ${
viewMode === 'list'
? 'bg-[rgba(6,182,212,0.15)] text-color-accent border border-color-accent/30'
: 'bg-bg-card border border-stroke text-fg-sub hover:bg-bg-surface-hover'
? 'bg-[color-mix(in_srgb,var(--color-accent)_15%,transparent)] text-color-accent border border-[color-mix(in_srgb,var(--color-accent)_30%,transparent)]'
: 'border border-stroke text-fg-sub hover:bg-bg-surface-hover'
}`}
>
📋
</button>
<button
onClick={() => setViewMode('map')}
className={`px-3 py-1.5 text-[11px] font-semibold rounded-sm font-korean transition-colors ${
className={`px-3 py-1.5 text-label-2 font-semibold rounded-sm font-korean transition-colors ${
viewMode === 'map'
? 'bg-[rgba(6,182,212,0.15)] text-color-accent border border-color-accent/30'
: 'bg-bg-card border border-stroke text-fg-sub hover:bg-bg-surface-hover'
? 'bg-[color-mix(in_srgb,var(--color-accent)_15%,transparent)] text-color-accent border border-[color-mix(in_srgb,var(--color-accent)_30%,transparent)]'
: 'border border-stroke text-fg-sub hover:bg-bg-surface-hover'
}`}
>
🗺
@ -170,7 +169,7 @@ function AssetManagement() {
{viewMode === 'list' ? (
/* ── LIST VIEW ── */
<div className="flex-1 bg-bg-card border border-stroke rounded-md overflow-hidden flex flex-col">
<div className="flex-1 border border-stroke rounded-md overflow-hidden flex flex-col">
<div className="flex-1">
<table className="w-full text-left" style={{ tableLayout: 'fixed' }}>
<colgroup>
@ -212,7 +211,7 @@ function AssetManagement() {
return (
<th
key={i}
className={`px-2.5 py-2.5 text-[10px] font-bold font-korean border-b border-stroke ${[0, 5, 6, 7, 8, 9, 10].includes(i) ? 'text-center' : ''} ${isHighlight ? 'text-color-accent bg-color-accent/5' : 'text-fg-sub'}`}
className={`px-2.5 py-2.5 text-caption font-bold font-korean border-b border-stroke ${[0, 5, 6, 7, 8, 9, 10].includes(i) ? 'text-center' : ''} ${isHighlight ? 'text-color-accent bg-[color-mix(in_srgb,var(--color-accent)_5%,transparent)]' : 'text-fg-sub'}`}
>
{h}
</th>
@ -224,59 +223,53 @@ function AssetManagement() {
{paged.map((org, idx) => (
<tr
key={org.id}
className={`border-b border-stroke/50 hover:bg-[rgba(255,255,255,0.02)] cursor-pointer transition-colors ${
selectedOrg?.id === org.id ? 'bg-[rgba(6,182,212,0.03)]' : ''
}`}
className={`border-b border-stroke hover:bg-white/[0.02] cursor-pointer transition-colors`}
onClick={() => {
handleSelectOrg(org);
setViewMode('map');
}}
>
<td className="px-2.5 py-2 text-center font-mono text-[10px]">
<td className="px-2.5 py-2 text-center font-mono text-caption">
{(safePage - 1) * pageSize + idx + 1}
</td>
<td className="px-2.5 py-2">
<span
className={`text-[9px] px-1.5 py-0.5 rounded font-bold font-korean ${typeTagCls(org.type)}`}
>
{org.type}
</span>
<span className="text-caption text-color-accent font-korean">{org.type}</span>
</td>
<td className="px-2.5 py-2 text-[10px] font-semibold font-korean">
<td className="px-2.5 py-2 text-caption font-korean">
{regionShort(org.jurisdiction)}
</td>
<td className="px-2.5 py-2 text-[10px] font-semibold text-color-accent font-korean cursor-pointer truncate">
<td className="px-2.5 py-2 text-caption text-fg-sub font-korean cursor-pointer truncate">
{org.name}
</td>
<td className="px-2.5 py-2 text-[10px] text-fg-disabled font-korean truncate">
<td className="px-2.5 py-2 text-caption text-fg-disabled font-korean truncate">
{org.address}
</td>
<td
className={`px-2.5 py-2 text-center font-mono text-[10px] font-semibold ${equipFilter === 'vessel' ? 'text-color-accent bg-color-accent/5' : ''}`}
className={`px-2.5 py-2 text-center font-mono text-caption ${equipFilter === 'vessel' ? 'text-color-accent bg-[color-mix(in_srgb,var(--color-accent)_5%,transparent)]' : ''}`}
>
{org.vessel}
</td>
<td
className={`px-2.5 py-2 text-center font-mono text-[10px] ${equipFilter === 'skimmer' ? 'text-color-accent font-semibold bg-color-accent/5' : ''}`}
className={`px-2.5 py-2 text-center font-mono text-caption ${equipFilter === 'skimmer' ? 'text-color-accent bg-[color-mix(in_srgb,var(--color-accent)_5%,transparent)]' : ''}`}
>
{org.skimmer}
</td>
<td
className={`px-2.5 py-2 text-center font-mono text-[10px] ${equipFilter === 'pump' ? 'text-color-accent font-semibold bg-color-accent/5' : ''}`}
className={`px-2.5 py-2 text-center font-mono text-caption ${equipFilter === 'pump' ? 'text-color-accent bg-[color-mix(in_srgb,var(--color-accent)_5%,transparent)]' : ''}`}
>
{org.pump}
</td>
<td
className={`px-2.5 py-2 text-center font-mono text-[10px] ${equipFilter === 'vehicle' ? 'text-color-accent font-semibold bg-color-accent/5' : ''}`}
className={`px-2.5 py-2 text-center font-mono text-caption ${equipFilter === 'vehicle' ? 'text-color-accent bg-[color-mix(in_srgb,var(--color-accent)_5%,transparent)]' : ''}`}
>
{org.vehicle}
</td>
<td
className={`px-2.5 py-2 text-center font-mono text-[10px] ${equipFilter === 'sprayer' ? 'text-color-accent font-semibold bg-color-accent/5' : ''}`}
className={`px-2.5 py-2 text-center font-mono text-caption ${equipFilter === 'sprayer' ? 'text-color-accent bg-[color-mix(in_srgb,var(--color-accent)_5%,transparent)]' : ''}`}
>
{org.sprayer}
</td>
<td className="px-2.5 py-2 text-center font-bold text-color-accent font-mono text-[10px]">
<td className="px-2.5 py-2 text-center text-fg-sub font-mono text-caption">
{org.totalAssets}
</td>
</tr>
@ -287,7 +280,7 @@ function AssetManagement() {
{/* Totals Summary */}
<div className="flex items-center justify-end gap-4 px-4 py-2 border-t border-stroke bg-bg-base/80">
<span className="text-[10px] text-fg-disabled font-korean font-semibold mr-auto">
<span className="text-caption text-fg-disabled font-korean font-semibold mr-auto">
({filtered.length} )
</span>
{[
@ -332,15 +325,15 @@ function AssetManagement() {
return (
<div
key={t.key}
className={`flex items-center gap-1 px-1.5 py-0.5 rounded ${equipFilter === t.key ? 'bg-color-accent/10' : ''}`}
className={`flex items-center gap-1 px-1.5 py-0.5 rounded ${equipFilter === t.key ? 'bg-[color-mix(in_srgb,var(--color-accent)_10%,transparent)]' : ''}`}
>
<span
className={`text-[9px] font-korean ${isActive ? 'text-color-accent' : 'text-fg-disabled'}`}
className={`text-caption font-korean ${isActive ? 'text-color-accent' : 'text-fg-disabled'}`}
>
{t.label}
</span>
<span
className={`text-[10px] font-mono font-bold ${isActive ? 'text-color-accent' : 'text-fg'}`}
className={`text-caption font-mono font-bold ${isActive ? 'text-color-accent' : 'text-fg'}`}
>
{t.value.toLocaleString()}
{t.unit}
@ -352,7 +345,7 @@ function AssetManagement() {
{/* Pagination */}
<div className="flex items-center justify-center gap-4 px-4 py-2.5 border-t border-stroke bg-bg-base">
<span className="text-[10px] text-fg-disabled font-korean">
<span className="text-caption text-fg-disabled font-korean">
<span className="font-semibold text-fg-sub">{filtered.length}</span> {' '}
<span className="font-semibold text-fg-sub">
{(safePage - 1) * pageSize + 1}-{Math.min(safePage * pageSize, filtered.length)}
@ -362,14 +355,14 @@ function AssetManagement() {
<button
onClick={() => setCurrentPage(1)}
disabled={safePage <= 1}
className="px-1.5 py-1 text-[10px] rounded border border-stroke bg-bg-card text-fg-sub disabled:opacity-30 hover:bg-bg-surface-hover transition-colors cursor-pointer disabled:cursor-default"
className="px-1.5 py-1 text-caption rounded border border-stroke bg-bg-card text-fg-sub disabled:opacity-30 hover:bg-bg-surface-hover transition-colors cursor-pointer disabled:cursor-default"
>
«
</button>
<button
onClick={() => setCurrentPage((p) => Math.max(1, p - 1))}
disabled={safePage <= 1}
className="px-1.5 py-1 text-[10px] rounded border border-stroke bg-bg-card text-fg-sub disabled:opacity-30 hover:bg-bg-surface-hover transition-colors cursor-pointer disabled:cursor-default"
className="px-1.5 py-1 text-caption rounded border border-stroke bg-bg-card text-fg-sub disabled:opacity-30 hover:bg-bg-surface-hover transition-colors cursor-pointer disabled:cursor-default"
>
</button>
@ -377,9 +370,9 @@ function AssetManagement() {
<button
key={p}
onClick={() => setCurrentPage(p)}
className={`w-6 h-6 text-[10px] font-bold rounded transition-colors cursor-pointer ${
className={`w-6 h-6 text-caption font-bold rounded transition-colors cursor-pointer ${
p === safePage
? 'bg-color-accent/20 text-color-accent border border-color-accent/40'
? 'bg-[color-mix(in_srgb,var(--color-accent)_20%,transparent)] text-color-accent border border-[color-mix(in_srgb,var(--color-accent)_40%,transparent)]'
: 'border border-stroke bg-bg-card text-fg-disabled hover:bg-bg-surface-hover'
}`}
>
@ -389,14 +382,14 @@ function AssetManagement() {
<button
onClick={() => setCurrentPage((p) => Math.min(totalPages, p + 1))}
disabled={safePage >= totalPages}
className="px-1.5 py-1 text-[10px] rounded border border-stroke bg-bg-card text-fg-sub disabled:opacity-30 hover:bg-bg-surface-hover transition-colors cursor-pointer disabled:cursor-default"
className="px-1.5 py-1 text-caption rounded border border-stroke bg-bg-card text-fg-sub disabled:opacity-30 hover:bg-bg-surface-hover transition-colors cursor-pointer disabled:cursor-default"
>
</button>
<button
onClick={() => setCurrentPage(totalPages)}
disabled={safePage >= totalPages}
className="px-1.5 py-1 text-[10px] rounded border border-stroke bg-bg-card text-fg-sub disabled:opacity-30 hover:bg-bg-surface-hover transition-colors cursor-pointer disabled:cursor-default"
className="px-1.5 py-1 text-caption rounded border border-stroke bg-bg-card text-fg-sub disabled:opacity-30 hover:bg-bg-surface-hover transition-colors cursor-pointer disabled:cursor-default"
>
»
</button>
@ -423,10 +416,10 @@ function AssetManagement() {
{/* Header */}
<div className="p-4 border-b border-stroke">
<div className="text-sm font-bold mb-1 font-korean">{selectedOrg.name}</div>
<div className="text-[11px] text-fg-sub font-semibold font-korean mb-1">
<div className="text-label-2 text-fg-sub font-semibold font-korean mb-1">
{selectedOrg.type} · {regionShort(selectedOrg.jurisdiction)} · {selectedOrg.area}
</div>
<div className="text-[11px] text-fg-disabled font-korean">
<div className="text-label-2 text-fg-disabled font-korean">
{selectedOrg.address}
</div>
</div>
@ -437,7 +430,7 @@ function AssetManagement() {
<button
key={t}
onClick={() => setDetailTab(t)}
className={`flex-1 py-2.5 text-center text-[11px] font-semibold font-korean border-b-2 transition-colors cursor-pointer ${
className={`flex-1 py-2.5 text-center text-label-2 font-semibold font-korean border-b-2 transition-colors cursor-pointer ${
detailTab === t
? 'text-color-accent border-color-accent'
: 'text-fg-disabled border-transparent hover:text-fg-sub'
@ -449,7 +442,10 @@ function AssetManagement() {
</div>
{/* Content */}
<div className="flex-1 overflow-y-auto p-3.5 scrollbar-thin">
<div
className="flex-1 overflow-y-auto p-3.5 scrollbar-thin"
style={{ scrollbarGutter: 'stable' }}
>
{/* Summary */}
<div className="grid grid-cols-3 gap-1.5 mb-3">
{[
@ -462,7 +458,7 @@ function AssetManagement() {
className="bg-bg-card border border-stroke rounded-sm p-2.5 text-center"
>
<div className="text-lg font-bold text-color-accent font-mono">{s.value}</div>
<div className="text-[9px] text-fg-disabled mt-0.5 font-korean">
<div className="text-caption text-fg-disabled mt-0.5 font-korean">
{s.label}
</div>
</div>
@ -498,10 +494,10 @@ function AssetManagement() {
key={ci}
className="flex items-center justify-between px-2.5 py-2 bg-bg-card border border-stroke rounded-sm hover:bg-bg-surface-hover transition-colors"
>
<span className="text-[11px] font-semibold flex items-center gap-1.5 font-korean">
<span className="text-label-2 font-semibold flex items-center gap-1.5 font-korean">
{cat.icon} {cat.category}
</span>
<span className="text-[11px] font-bold font-mono">
<span className="text-label-2 font-bold font-mono">
<span className="text-color-accent">{cat.count}</span>
<span className="text-fg-disabled font-normal ml-0.5">{unit}</span>
</span>
@ -528,7 +524,7 @@ function AssetManagement() {
].map(([k, v], i) => (
<div
key={i}
className="flex justify-between px-2.5 py-2 bg-bg-base rounded text-[11px]"
className="flex justify-between px-2.5 py-2 bg-bg-base rounded text-label-2"
>
<span className="text-fg-disabled font-korean">{k}</span>
<span className="font-mono font-semibold text-fg">{v}</span>
@ -541,7 +537,7 @@ function AssetManagement() {
<div className="flex flex-col gap-2">
{/* 기관 기본 정보 */}
<div className="bg-bg-card border border-stroke rounded-sm p-3">
<div className="text-[10px] font-bold text-fg-disabled mb-2 font-korean">
<div className="text-caption font-bold text-fg-disabled mb-2 font-korean">
</div>
{[
@ -553,7 +549,7 @@ function AssetManagement() {
].map(([k, v], j) => (
<div
key={j}
className="flex justify-between py-1.5 text-[11px] border-b border-stroke/30 last:border-b-0"
className="flex justify-between py-1.5 text-label-2 border-b border-stroke last:border-b-0"
>
<span className="text-fg-disabled font-korean shrink-0 mr-2">{k}</span>
<span
@ -568,7 +564,7 @@ function AssetManagement() {
{/* 담당자 목록 */}
{selectedOrg.contacts.length > 0 && (
<div className="bg-bg-card border border-stroke rounded-sm p-3">
<div className="text-[10px] font-bold text-fg-disabled mb-2 font-korean">
<div className="text-caption font-bold text-fg-disabled mb-2 font-korean">
</div>
{selectedOrg.contacts.map((c, i) => (
@ -582,7 +578,7 @@ function AssetManagement() {
.map(([k, v], j) => (
<div
key={j}
className="flex justify-between py-1.5 text-[11px] border-b border-stroke/30 last:border-b-0"
className="flex justify-between py-1.5 text-label-2 border-b border-stroke last:border-b-0"
>
<span className="text-fg-disabled font-korean">{k}</span>
<span
@ -604,19 +600,14 @@ function AssetManagement() {
</div>
{/* Bottom Actions */}
<div className="p-3.5 border-t border-stroke flex gap-2">
<button
className="flex-1 py-2.5 rounded-sm text-xs font-semibold font-korean text-white border-none cursor-pointer"
style={{
background: 'linear-gradient(135deg, var(--color-accent), var(--color-info))',
}}
>
{/* <div className="p-3.5 border-t border-stroke flex gap-2">
<button className="flex-1 py-2.5 rounded-sm text-xs font-semibold font-korean text-white bg-color-accent border-none cursor-pointer hover:opacity-90 transition-opacity">
📥
</button>
<button className="flex-1 py-2.5 rounded-sm text-xs font-semibold font-korean bg-bg-card border border-stroke text-fg-sub cursor-pointer hover:bg-bg-surface-hover transition-colors">
</button>
</div>
</div> */}
</aside>
)}
</div>

파일 보기

@ -1,45 +1,45 @@
import { useMemo, useCallback, useEffect, useRef } from 'react'
import { Map, useControl, useMap } from '@vis.gl/react-maplibre'
import { MapboxOverlay } from '@deck.gl/mapbox'
import { ScatterplotLayer } from '@deck.gl/layers'
import 'maplibre-gl/dist/maplibre-gl.css'
import { useBaseMapStyle } from '@common/hooks/useBaseMapStyle'
import { S57EncOverlay } from '@common/components/map/S57EncOverlay'
import { useMapStore } from '@common/store/mapStore'
import type { AssetOrgCompat } from '../services/assetsApi'
import { typeColor } from './assetTypes'
import { hexToRgba } from '@common/components/map/mapUtils'
import { useMemo, useCallback, useEffect, useRef } from 'react';
import { Map, useControl, useMap } from '@vis.gl/react-maplibre';
import { MapboxOverlay } from '@deck.gl/mapbox';
import { ScatterplotLayer } from '@deck.gl/layers';
import 'maplibre-gl/dist/maplibre-gl.css';
import { useBaseMapStyle } from '@common/hooks/useBaseMapStyle';
import { S57EncOverlay } from '@common/components/map/S57EncOverlay';
import { useMapStore } from '@common/store/mapStore';
import type { AssetOrgCompat } from '../services/assetsApi';
import { typeColor } from './assetTypes';
import { hexToRgba } from '@common/components/map/mapUtils';
// ── DeckGLOverlay ──────────────────────────────────────
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function DeckGLOverlay({ layers }: { layers: any[] }) {
const overlay = useControl<MapboxOverlay>(() => new MapboxOverlay({ interleaved: true }))
overlay.setProps({ layers })
return null
const overlay = useControl<MapboxOverlay>(() => new MapboxOverlay({ interleaved: true }));
overlay.setProps({ layers });
return null;
}
// ── FlyTo Controller ────────────────────────────────────
function FlyToController({ selectedOrg }: { selectedOrg: AssetOrgCompat }) {
const { current: map } = useMap()
const prevIdRef = useRef<number | undefined>(undefined)
const { current: map } = useMap();
const prevIdRef = useRef<number | undefined>(undefined);
useEffect(() => {
if (!map) return
if (!map) return;
if (prevIdRef.current !== undefined && prevIdRef.current !== selectedOrg.id) {
map.flyTo({ center: [selectedOrg.lng, selectedOrg.lat], zoom: 10, duration: 800 })
map.flyTo({ center: [selectedOrg.lng, selectedOrg.lat], zoom: 10, duration: 800 });
}
prevIdRef.current = selectedOrg.id
}, [map, selectedOrg])
prevIdRef.current = selectedOrg.id;
}, [map, selectedOrg]);
return null
return null;
}
interface AssetMapProps {
organizations: AssetOrgCompat[]
selectedOrg: AssetOrgCompat
onSelectOrg: (o: AssetOrgCompat) => void
regionFilter: string
onRegionFilterChange: (v: string) => void
organizations: AssetOrgCompat[];
selectedOrg: AssetOrgCompat;
onSelectOrg: (o: AssetOrgCompat) => void;
regionFilter: string;
onRegionFilterChange: (v: string) => void;
}
function AssetMap({
@ -49,15 +49,15 @@ function AssetMap({
regionFilter,
onRegionFilterChange,
}: AssetMapProps) {
const currentMapStyle = useBaseMapStyle()
const mapToggles = useMapStore((s) => s.mapToggles)
const currentMapStyle = useBaseMapStyle();
const mapToggles = useMapStore((s) => s.mapToggles);
const handleClick = useCallback(
(org: AssetOrgCompat) => {
onSelectOrg(org)
onSelectOrg(org);
},
[onSelectOrg],
)
);
const markerLayer = useMemo(() => {
return new ScatterplotLayer({
@ -65,19 +65,19 @@ function AssetMap({
data: orgs,
getPosition: (d: AssetOrgCompat) => [d.lng, d.lat],
getRadius: (d: AssetOrgCompat) => {
const baseRadius = d.pinSize === 'hq' ? 14 : d.pinSize === 'lg' ? 10 : 7
const isSelected = selectedOrg.id === d.id
return isSelected ? baseRadius + 4 : baseRadius
const baseRadius = d.pinSize === 'hq' ? 14 : d.pinSize === 'lg' ? 10 : 7;
const isSelected = selectedOrg.id === d.id;
return isSelected ? baseRadius + 4 : baseRadius;
},
getFillColor: (d: AssetOrgCompat) => {
const tc = typeColor(d.type)
const isSelected = selectedOrg.id === d.id
return hexToRgba(isSelected ? tc.selected : tc.border, isSelected ? 229 : 178)
const tc = typeColor(d.type);
const isSelected = selectedOrg.id === d.id;
return hexToRgba(isSelected ? tc.selected : tc.border, isSelected ? 229 : 178);
},
getLineColor: (d: AssetOrgCompat) => {
const tc = typeColor(d.type)
const isSelected = selectedOrg.id === d.id
return hexToRgba(isSelected ? tc.selected : tc.border, isSelected ? 255 : 200)
const tc = typeColor(d.type);
const isSelected = selectedOrg.id === d.id;
return hexToRgba(isSelected ? tc.selected : tc.border, isSelected ? 255 : 200);
},
getLineWidth: (d: AssetOrgCompat) => (selectedOrg.id === d.id ? 3 : 2),
stroked: true,
@ -86,7 +86,7 @@ function AssetMap({
radiusUnits: 'pixels',
pickable: true,
onClick: (info: { object?: AssetOrgCompat }) => {
if (info.object) handleClick(info.object)
if (info.object) handleClick(info.object);
},
updateTriggers: {
getRadius: [selectedOrg.id],
@ -94,8 +94,8 @@ function AssetMap({
getLineColor: [selectedOrg.id],
getLineWidth: [selectedOrg.id],
},
})
}, [orgs, selectedOrg, handleClick])
});
}, [orgs, selectedOrg, handleClick]);
return (
<div className="w-full h-full relative">
@ -119,13 +119,13 @@ function AssetMap({
{ value: '중부', label: '중부청' },
{ value: '동해', label: '동해청' },
{ value: '제주', label: '제주청' },
].map(r => (
].map((r) => (
<button
key={r.value}
onClick={() => onRegionFilterChange(r.value)}
className={`px-2.5 py-1.5 text-[10px] font-bold rounded font-korean transition-colors ${
className={`px-2.5 py-1.5 text-caption font-bold rounded font-korean transition-colors ${
regionFilter === r.value
? 'bg-color-accent/20 text-color-accent border border-color-accent/40'
? 'bg-[color-mix(in_srgb,var(--color-accent)_20%,transparent)] text-color-accent border border-[color-mix(in_srgb,var(--color-accent)_40%,transparent)]'
: 'bg-bg-base/80 text-fg-sub border border-stroke hover:bg-bg-surface-hover/80'
}`}
>
@ -136,28 +136,31 @@ function AssetMap({
{/* Legend overlay */}
<div className="absolute bottom-3 left-3 z-[1000] bg-bg-base/90 border border-stroke rounded-sm p-2.5 backdrop-blur-sm">
<div className="text-[9px] text-fg-disabled font-bold mb-1.5 font-korean"></div>
<div className="text-caption text-fg-disabled font-bold mb-1.5 font-korean"></div>
{[
{ color: '#06b6d4', label: '해경관할' },
{ color: '#3b82f6', label: '해경경찰서' },
{ color: '#22c55e', label: '파출소' },
{ color: '#a855f7', label: '관련기관' },
{ color: '#14b8a6', label: '해양환경공단' },
{ color: '#f59e0b', label: '업체' },
{ color: '#ec4899', label: '지자체' },
{ color: '#8b5cf6', label: '기름저장시설' },
{ color: '#0d9488', label: '정유사' },
{ color: '#64748b', label: '해군' },
{ color: '#6b7280', label: '기타' },
].map((item, i) => (
'해경관할',
'해경경찰서',
'파출소',
'관련기관',
'해양환경공단',
'업체',
'지자체',
'기름저장시설',
'정유사',
'해군',
'기타',
].map((label, i) => (
<div key={i} className="flex items-center gap-1.5 mb-0.5 last:mb-0">
<span className="w-2.5 h-2.5 rounded-full inline-block flex-shrink-0" style={{ background: item.color }} />
<span className="text-[10px] text-fg-sub font-korean">{item.label}</span>
<span
className="w-2.5 h-2.5 rounded-full inline-block flex-shrink-0"
style={{ background: typeColor(label).border }}
/>
<span className="text-caption text-fg-sub font-korean">{label}</span>
</div>
))}
</div>
</div>
)
);
}
export default AssetMap
export default AssetMap;

파일 보기

@ -2,15 +2,11 @@ interface TheoryItem {
title: string;
source: string;
desc: string;
tags?: { label: string; color: string }[];
highlight?: boolean;
tags?: string[];
}
interface TheorySection {
icon: string;
title: string;
color: string;
bgTint: string;
items: TheoryItem[];
dividerAfter?: number;
dividerLabel?: string;
@ -18,10 +14,7 @@ interface TheorySection {
const THEORY_SECTIONS: TheorySection[] = [
{
icon: '🚢',
title: '방제선 성능 기준',
color: 'var(--color-info)',
bgTint: 'rgba(59,130,246,.08)',
items: [
{
title: '해양경찰청 방제선 성능기준 고시',
@ -43,10 +36,7 @@ const THEORY_SECTIONS: TheorySection[] = [
],
},
{
icon: '🪢',
title: '오일펜스·흡착재 규격',
color: 'var(--boom, #f59e0b)',
bgTint: 'rgba(245,158,11,.08)',
items: [
{
title: 'ASTM F625 — Standard Guide for Selecting Mechanical Oil Spill Equipment',
@ -61,12 +51,9 @@ const THEORY_SECTIONS: TheorySection[] = [
],
},
{
icon: '⚙️',
title: '방제자원 배치·동원 이론',
color: 'var(--color-tertiary)',
bgTint: 'rgba(168,85,247,.08)',
dividerAfter: 2,
dividerLabel: '📐 최적화 수리모델 참고문헌',
dividerLabel: '최적화 수리모델 참고문헌',
items: [
{
title:
@ -74,7 +61,6 @@ const THEORY_SECTIONS: TheorySection[] = [
source:
'Xu, Y. et al. | Ningbo Univ. | Systems 2025, 13, 716 · DOI: 10.3390/systems13080716',
desc: 'IMOGWO 다목적 최적화 · 스케줄링 시간+경제·생태손실 동시 최소화 · 동적 오일필름 기반 방제정 라우팅',
highlight: true,
},
{
title: 'Dynamic Resource Allocation to Support Oil Spill Response Planning',
@ -92,54 +78,32 @@ const THEORY_SECTIONS: TheorySection[] = [
source:
'Das, T., Goerlandt, F. & Pelot, R. | Multimodal Transportation Vol.3 No.1, 100110, 2023',
desc: '혼합정수계획법으로 응급 방제자원 거점 위치 선택 + 자원 할당 동시 최적화. 비용·응답시간 트레이드오프 파레토 분석.',
highlight: true,
tags: [
{ label: 'MIP 수리모델', color: 'var(--color-tertiary)' },
{ label: '자원 위치 선택', color: 'var(--color-info)' },
{ label: '북극해 적용', color: 'var(--color-accent)' },
],
tags: ['MIP 수리모델', '자원 위치 선택', '북극해 적용'],
},
{
title: '유전알고리즘을 이용하여 최적화된 방제자원 배치안의 분포도 분석',
source: '김혜진, 김용혁 | 한국융합학회논문지 Vol.11 No.4, pp.1116, 2020',
desc: 'GA(유전알고리즘)로 방제자원 배치 최적화 및 시뮬레이션 분포도 분석. 국내 해역 실정에 맞는 자원 배치 패턴 도출.',
highlight: true,
tags: [
{ label: 'GA 메타휴리스틱', color: 'var(--color-tertiary)' },
{ label: '국내 연구', color: 'var(--green, #22c55e)' },
{ label: '배치 분포도 분석', color: 'var(--boom, #f59e0b)' },
],
tags: ['GA 메타휴리스틱', '국내 연구', '배치 분포도 분석'],
},
{
title:
'A Two-Stage Stochastic Optimization Framework for Environmentally Sensitive Oil Spill Response Resource Allocation',
source: 'Rahman, M.A., Kuhel, M.T. & Novoa, C. | arXiv preprint arXiv:2511.22218, 2025',
desc: '확률적 MILP 2단계 프레임워크로 불확실성 포함 최적 자원 배치. 환경민감구역 가중치 반영.',
highlight: true,
tags: [
{ label: '확률적 MILP', color: 'var(--color-tertiary)' },
{ label: '2단계 최적화', color: 'var(--color-info)' },
{ label: '환경민감구역', color: 'var(--green, #22c55e)' },
],
tags: ['확률적 MILP', '2단계 최적화', '환경민감구역'],
},
{
title:
'Mixed-Integer Dynamic Optimization for Oil-Spill Response Planning with Integration of Dynamic Oil Weathering Model',
source: 'You, F. & Leyffer, S. | Argonne National Laboratory Technical Note, 2008',
desc: '동적 최적화(MINLP/MILP) 프레임워크로 오일스필 대응 스케줄링 + 오일 풍화·거동 물리모델 통합.',
highlight: true,
tags: [
{ label: 'MINLP 동적 최적화', color: 'var(--color-tertiary)' },
{ label: '오일 풍화 모델 통합', color: 'var(--boom, #f59e0b)' },
],
tags: ['MINLP 동적 최적화', '오일 풍화 모델 통합'],
},
],
},
{
icon: '🗄',
title: '자산 현행화·데이터 관리',
color: 'var(--green, #22c55e)',
bgTint: 'rgba(34,197,94,.08)',
items: [
{
title: '해양오염방제자원 현황관리 지침',
@ -155,103 +119,31 @@ const THEORY_SECTIONS: TheorySection[] = [
},
];
const TAG_COLORS: Record<string, { bg: string; bd: string; fg: string }> = {
'var(--color-tertiary)': {
bg: 'rgba(168,85,247,0.08)',
bd: 'rgba(168,85,247,0.2)',
fg: '#a855f7',
},
'var(--color-info)': { bg: 'rgba(59,130,246,0.08)', bd: 'rgba(59,130,246,0.2)', fg: '#3b82f6' },
'var(--color-accent)': { bg: 'rgba(6,182,212,0.08)', bd: 'rgba(6,182,212,0.2)', fg: '#06b6d4' },
'var(--green, #22c55e)': { bg: 'rgba(34,197,94,0.08)', bd: 'rgba(34,197,94,0.2)', fg: '#22c55e' },
'var(--boom, #f59e0b)': {
bg: 'rgba(245,158,11,0.08)',
bd: 'rgba(245,158,11,0.2)',
fg: '#f59e0b',
},
};
function TheoryCard({ section }: { section: TheorySection }) {
const badgeBg = section.bgTint.replace(/[\d.]+\)$/, '0.15)');
return (
<div className="bg-bg-card border border-stroke rounded-md overflow-hidden">
{/* Section Header */}
<div
className="px-4 py-3 border-b border-stroke flex items-center gap-2"
style={{ background: section.bgTint }}
>
<span className="text-sm">{section.icon}</span>
<span className="text-xs font-bold" style={{ color: section.color }}>
{section.title}
</span>
<div className="px-4 py-3 border-b border-stroke">
<span className="text-label-1 font-semibold text-fg font-korean">{section.title}</span>
</div>
{/* Items */}
<div className="px-4 py-3.5 flex flex-col gap-2 text-[9px]">
<div className="px-4 py-3.5 flex flex-col gap-2 text-caption">
{section.items.map((item, i) => (
<div key={i}>
{/* Divider */}
{section.dividerAfter !== undefined && i === section.dividerAfter + 1 && (
<div
className="mt-1 mb-3 pt-2"
style={{ borderTop: '1px dashed var(--stroke-default)' }}
>
<div
className="text-[8px] font-bold mb-1.5 opacity-70"
style={{ color: section.color }}
>
<div className="mt-1 mb-3 pt-2 border-t border-dashed border-stroke">
<div className="text-caption font-semibold text-fg mb-1.5">
{section.dividerLabel}
</div>
</div>
)}
<div
className="grid gap-2 px-2.5 py-2 bg-bg-base rounded-md"
style={{
gridTemplateColumns: '24px 1fr',
borderLeft: item.highlight ? `2px solid ${section.color}` : undefined,
}}
>
{/* Number badge */}
<div
className="w-5 h-5 rounded flex items-center justify-center text-[9px] shrink-0"
style={{
background: badgeBg,
fontWeight: item.highlight ? 700 : 400,
color: item.highlight ? section.color : undefined,
}}
>
{['①', '②', '③', '④', '⑤', '⑥', '⑦', '⑧', '⑨', '⑩'][i]}
</div>
<div>
<div className="font-bold mb-0.5">{item.title}</div>
<div className="text-fg-disabled leading-[1.6]">{item.source}</div>
{/* Tags */}
{item.tags && (
<div className="mt-0.5 flex flex-wrap gap-0.5">
{item.tags.map((tag, ti) => {
const tc = TAG_COLORS[tag.color] || {
bg: 'rgba(107,114,128,0.08)',
bd: 'rgba(107,114,128,0.2)',
fg: '#6b7280',
};
return (
<span
key={ti}
className="px-1 py-px rounded text-[8px]"
style={{
color: tc.fg,
background: tc.bg,
border: `1px solid ${tc.bd}`,
}}
>
{tag.label}
</span>
);
})}
</div>
)}
<div className="mt-0.5 text-fg-sub">{item.desc}</div>
</div>
<div className="px-2.5 py-2 bg-bg-base rounded-md">
<div className="font-semibold mb-0.5">{item.title}</div>
<div className="text-fg-disabled leading-[1.6]">{item.source}</div>
{item.tags && <div className="mt-0.5 text-fg-disabled">{item.tags.join(' | ')}</div>}
<div className="mt-0.5 text-fg-sub">{item.desc}</div>
</div>
</div>
))}
@ -263,8 +155,8 @@ function TheoryCard({ section }: { section: TheorySection }) {
function AssetTheory() {
return (
<div className="flex flex-col gap-0">
<div className="text-[18px] font-bold mb-1">📚 </div>
<div className="text-xs text-fg-disabled mb-6">
<div className="text-title-1 font-bold mb-1">📚 </div>
<div className="text-caption text-fg-disabled mb-6">
· ·
</div>

파일 보기

@ -22,19 +22,19 @@ function AssetUpload() {
<div className="flex gap-8 h-full overflow-auto">
{/* Left - Upload */}
<div className="flex-1 max-w-[580px]">
<div className="text-[13px] font-bold mb-3.5 font-korean">📤 </div>
<div className="text-title-4 font-bold mb-3.5 font-korean">📤 </div>
{/* Drop Zone */}
<div className="border-2 border-dashed border-stroke-light rounded-md py-10 px-5 text-center mb-5 cursor-pointer hover:border-color-accent/40 transition-colors">
<div className="border-2 border-dashed border-stroke-light rounded-md py-10 px-5 text-center mb-5 cursor-pointer hover:border-[rgba(6,182,212,0.4)] transition-colors">
<div className="text-4xl mb-2.5 opacity-50">📁</div>
<div className="text-sm font-semibold mb-1.5 font-korean">
</div>
<div className="text-[11px] text-fg-disabled mb-4 font-korean">
<div className="text-label-2 text-fg-disabled mb-4 font-korean">
(.xlsx), CSV · 10MB
</div>
<button
className="px-7 py-2.5 text-[13px] font-semibold rounded-sm text-white border-none cursor-pointer font-korean"
className="px-7 py-2.5 text-title-4 font-semibold rounded-sm text-white border-none cursor-pointer font-korean"
style={{ background: 'linear-gradient(135deg, var(--color-info), #2563eb)' }}
>
@ -120,7 +120,7 @@ function AssetUpload() {
{/* Right - Permission & History */}
<div className="flex-1 max-w-[480px]">
{/* Permission System */}
<div className="text-[13px] font-bold mb-3.5 font-korean">🔐 </div>
<div className="text-title-4 font-bold mb-3.5 font-korean">🔐 </div>
<div className="flex flex-col gap-2 mb-7">
{[
{
@ -164,14 +164,14 @@ function AssetUpload() {
</div>
<div>
<div className={`text-xs font-bold font-korean ${p.color}`}>{p.role}</div>
<div className="text-[10px] text-fg-disabled font-korean">{p.desc}</div>
<div className="text-caption text-fg-disabled font-korean">{p.desc}</div>
</div>
</div>
))}
</div>
{/* Upload History */}
<div className="text-[13px] font-bold mb-3.5 font-korean">📋 </div>
<div className="text-title-4 font-bold mb-3.5 font-korean">📋 </div>
<div className="flex flex-col gap-2">
{uploadHistory.map((h) => (
<div
@ -180,11 +180,11 @@ function AssetUpload() {
>
<div>
<div className="text-xs font-semibold font-korean">{h.fileNm}</div>
<div className="text-[10px] text-fg-disabled mt-0.5 font-korean">
<div className="text-caption text-fg-disabled mt-0.5 font-korean">
{new Date(h.regDtm).toLocaleString('ko-KR')} · {h.uploaderNm} · {h.uploadCnt}
</div>
</div>
<span className="px-2 py-0.5 rounded-full text-[10px] font-semibold bg-[rgba(34,197,94,0.15)] text-color-success">
<span className="px-2 py-0.5 rounded-full text-caption font-semibold bg-[rgba(34,197,94,0.15)] text-color-success">
</span>
</div>

파일 보기

@ -19,10 +19,10 @@ export function AssetsView() {
<div className="flex items-center justify-between border-b border-stroke bg-bg-surface shrink-0">
<div className="flex">
{[
{ id: 'management' as const, icon: '🗂', label: '자산 관리' },
// { id: 'upload' as const, icon: '📤', label: '자산 현행화 (업로드)' },
{ id: 'theory' as const, icon: '📚', label: '방제자원 이론' },
{ id: 'insurance' as const, icon: '🛡', label: '선박 보험정보' },
{ id: 'management' as const, label: '자산 관리' },
// { id: 'upload' as const, label: '자산 현행화 (업로드)' },
{ id: 'theory' as const, label: '방제자원 이론' },
{ id: 'insurance' as const, label: '선박 보험정보' },
].map((tab) => (
<button
key={tab.id}
@ -33,16 +33,16 @@ export function AssetsView() {
: 'text-fg-disabled border-transparent hover:text-fg-sub'
}`}
>
{tab.icon} {tab.label}
{tab.label}
</button>
))}
</div>
<div
className="flex items-center gap-1.5 px-3.5 py-1.5 border rounded-full text-[11px] text-color-info font-korean mr-4"
style={{ borderColor: 'rgba(59,130,246,0.3)' }}
{/* <div
className="flex items-center gap-1.5 px-3.5 py-1.5 border rounded-full text-label-2 text-color-info font-korean mr-4"
style={{ borderColor: 'color-mix(in srgb, var(--color-info) 30%, transparent)' }}
>
👤 _방제과 ( )
</div>
</div> */}
</div>
{/* Content */}

파일 보기

@ -60,10 +60,12 @@ function ShipInsurance() {
const isY = yn === 'Y';
return (
<span
className="px-1.5 py-0.5 rounded text-[9px] font-bold"
className="px-1.5 py-0.5 rounded text-caption font-bold"
style={{
background: isY ? 'rgba(34,197,94,.15)' : 'rgba(100,116,139,.1)',
color: isY ? 'var(--color-success)' : 'var(--text-3)',
background: isY
? 'color-mix(in srgb, var(--color-success) 15%, transparent)'
: 'color-mix(in srgb, var(--stroke-default) 60%, transparent)',
color: isY ? 'var(--color-success)' : 'var(--text-fg-disabled)',
}}
>
{isY ? 'Y' : 'N'}
@ -131,17 +133,31 @@ function ShipInsurance() {
return (
<div className="flex flex-col flex-1 overflow-auto">
{/* 푸터 */}
<div className="mt-auto px-4 py-3 border border-stroke rounded-sm mb-6">
<div className="text-caption text-fg-disabled leading-[1.7]">
<span className="text-fg-sub"> :</span> ·
<br />
<span className="text-fg-sub">:</span> , , ,
, , , ,
</div>
</div>
{/* 헤더 */}
<div className="flex items-start justify-between mb-5">
<div>
<div className="flex items-center gap-2.5 mb-1">
<div className="text-[18px] font-bold"> </div>
<div className="text-title-1 font-bold"> </div>
<div
className="flex items-center gap-1.5 px-2.5 py-0.5 rounded-full text-[10px] font-bold"
className="flex items-center gap-1.5 px-2.5 py-0.5 rounded-full text-caption font-bold"
style={{
background: total > 0 ? 'rgba(34,197,94,.12)' : 'rgba(239,68,68,.12)',
background:
total > 0
? 'color-mix(in srgb, var(--color-success) 12%, transparent)'
: 'color-mix(in srgb, var(--color-danger) 12%, transparent)',
color: total > 0 ? 'var(--color-success)' : 'var(--color-danger)',
border: `1px solid ${total > 0 ? 'rgba(34,197,94,.25)' : 'rgba(239,68,68,.25)'}`,
border: `1px solid ${total > 0 ? 'color-mix(in srgb, var(--color-success) 25%, transparent)' : 'color-mix(in srgb, var(--color-danger) 25%, transparent)'}`,
}}
>
<span
@ -158,23 +174,13 @@ function ShipInsurance() {
<div className="flex gap-2">
<button
onClick={() => window.open('https://www.haewoon.or.kr', '_blank', 'noopener')}
className="px-4 py-2 text-[11px] font-bold cursor-pointer rounded-sm"
style={{
background: 'rgba(59,130,246,.12)',
color: 'var(--color-info)',
border: '1px solid rgba(59,130,246,.3)',
}}
className="px-4 py-2 text-label-2 font-bold cursor-pointer rounded-sm bg-[color-mix(in_srgb,var(--color-accent)_10%,transparent)] text-color-accent border border-[color-mix(in_srgb,var(--color-accent)_30%,transparent)]"
>
API
</button>
<button
onClick={() => window.open('https://new.portmis.go.kr', '_blank', 'noopener')}
className="px-4 py-2 text-[11px] font-bold cursor-pointer rounded-sm"
style={{
background: 'rgba(168,85,247,.12)',
color: 'var(--color-tertiary)',
border: '1px solid rgba(168,85,247,.3)',
}}
className="px-4 py-2 text-label-2 font-bold cursor-pointer rounded-sm bg-[color-mix(in_srgb,var(--color-accent)_10%,transparent)] text-color-accent border border-[color-mix(in_srgb,var(--color-accent)_30%,transparent)]"
>
PortMIS
</button>
@ -182,10 +188,10 @@ function ShipInsurance() {
</div>
{/* 필터 */}
<div className="bg-bg-card border border-stroke rounded-md px-5 py-4 mb-4">
<div className="border border-stroke rounded-md px-5 py-4 mb-4">
<div className="flex gap-2.5 items-end flex-wrap">
<div className="flex-1 min-w-[200px]">
<label className="block text-[10px] font-semibold text-fg-disabled mb-1">
<label className="block text-caption font-semibold text-fg-disabled mb-1">
(//IMO/)
</label>
<input
@ -198,7 +204,7 @@ function ShipInsurance() {
/>
</div>
<div>
<label className="block text-[10px] font-semibold text-fg-disabled mb-1">
<label className="block text-caption font-semibold text-fg-disabled mb-1">
</label>
<select
@ -212,7 +218,7 @@ function ShipInsurance() {
</select>
</div>
<div>
<label className="block text-[10px] font-semibold text-fg-disabled mb-1">
<label className="block text-caption font-semibold text-fg-disabled mb-1">
</label>
<select
@ -237,28 +243,20 @@ function ShipInsurance() {
</div>
<button
onClick={handleSearch}
className="px-5 py-2 text-white border-none rounded-sm text-xs font-bold cursor-pointer"
style={{
background: 'linear-gradient(135deg, var(--color-accent), var(--color-info))',
}}
className="px-4 py-2 bg-bg-base text-fg border border-stroke rounded-sm text-xs cursor-pointer"
>
</button>
<button
onClick={handleReset}
className="px-4 py-2 bg-bg-base text-fg-sub border border-stroke rounded-sm text-xs cursor-pointer"
className="px-4 py-2 bg-bg-base text-fg border border-stroke rounded-sm text-xs cursor-pointer"
>
</button>
<button
onClick={handleDownload}
disabled={total === 0}
className="px-4 py-2 text-xs font-bold cursor-pointer rounded-sm disabled:opacity-30 disabled:cursor-default"
style={{
background: 'rgba(34,197,94,.12)',
color: 'var(--color-success)',
border: '1px solid rgba(34,197,94,.3)',
}}
className="px-4 py-2 text-xs cursor-pointer rounded-sm disabled:opacity-30 disabled:cursor-default bg-bg-base border border-stroke"
>
</button>
@ -276,7 +274,7 @@ function ShipInsurance() {
animation: 'spin 0.8s linear infinite',
}}
/>
<div className="text-[13px] text-fg-sub"> ...</div>
<div className="text-title-4 text-fg-sub"> ...</div>
</div>
)}
@ -291,7 +289,7 @@ function ShipInsurance() {
{/* 테이블 */}
{!isLoading && !error && (
<>
<div className="bg-bg-card border border-stroke rounded-md overflow-hidden mb-3">
<div className="border border-stroke rounded-md overflow-hidden mb-3">
<div className="flex items-center justify-between px-4 py-3 border-b border-stroke">
<div className="text-xs font-bold">
<span className="text-color-accent">{total.toLocaleString()}</span>
@ -303,7 +301,7 @@ function ShipInsurance() {
</div>
</div>
<div className="overflow-x-auto">
<table className="w-full text-[11px] border-collapse whitespace-nowrap">
<table className="w-full text-label-2 border-collapse whitespace-nowrap">
<thead>
<tr className="bg-bg-base">
{[
@ -342,18 +340,22 @@ function ShipInsurance() {
<tr
key={r.insSn}
className="border-b border-stroke"
style={{ background: isExp ? 'rgba(239,68,68,.03)' : undefined }}
style={{
background: isExp
? 'color-mix(in srgb, var(--color-danger) 3%, transparent)'
: undefined,
}}
>
<td className="px-3 py-2 text-center text-fg-disabled font-mono">
{rowNum}
</td>
<td className="px-3 py-2 font-semibold">{r.shipNm}</td>
<td className="px-3 py-2">{r.shipNm}</td>
<td className="px-3 py-2 text-center font-mono">{r.callSign || '—'}</td>
<td className="px-3 py-2 text-center font-mono">{r.imoNo || '—'}</td>
<td className="px-3 py-2 text-center">
<span className="text-[10px]">{r.shipTp}</span>
<span className="text-caption">{r.shipTp}</span>
{r.shipTpDetail && (
<span className="text-fg-disabled text-[9px] ml-1">
<span className="text-fg-disabled text-caption ml-1">
({r.shipTpDetail})
</span>
)}
@ -368,28 +370,27 @@ function ShipInsurance() {
<td className="px-3 py-2 text-center">{ynBadge(r.fuelOilYn)}</td>
<td className="px-3 py-2 text-center">{ynBadge(r.wreckRemovalYn)}</td>
<td
className="px-3 py-2 text-center font-mono text-[10px]"
className="px-3 py-2 text-center font-mono text-caption"
style={{
color: isExp
? 'var(--color-danger)'
: isSoon
? 'var(--color-caution)'
: undefined,
fontWeight: isExp || isSoon ? 700 : undefined,
}}
>
{r.validStart} ~ {r.validEnd}
</td>
<td className="px-3 py-2 text-center text-[10px]">{r.issueOrg}</td>
<td className="px-3 py-2 text-center text-caption">{r.issueOrg}</td>
<td className="px-3 py-2 text-center">
<span
className="px-2 py-0.5 rounded-full text-[9px] font-semibold"
className="px-2 py-0.5 rounded-full text-caption font-semibold"
style={{
background: isExp
? 'rgba(239,68,68,.15)'
? 'color-mix(in srgb, var(--color-danger) 15%, transparent)'
: isSoon
? 'rgba(234,179,8,.15)'
: 'rgba(34,197,94,.15)',
? 'color-mix(in srgb, var(--color-caution) 15%, transparent)'
: 'color-mix(in srgb, var(--color-success) 15%, transparent)',
color: isExp
? 'var(--color-danger)'
: isSoon
@ -414,7 +415,7 @@ function ShipInsurance() {
<button
onClick={() => loadData(page - 1)}
disabled={page <= 1}
className="px-3 py-1.5 text-[11px] rounded-sm border border-stroke bg-bg-base cursor-pointer disabled:opacity-30 disabled:cursor-default"
className="px-3 py-1.5 text-label-2 rounded-sm border border-stroke bg-bg-base cursor-pointer disabled:opacity-30 disabled:cursor-default"
>
</button>
@ -426,13 +427,7 @@ function ShipInsurance() {
<button
key={p}
onClick={() => loadData(p)}
className="w-8 h-8 text-[11px] rounded-sm border cursor-pointer font-mono"
style={{
background: p === page ? 'var(--color-accent)' : 'var(--bg-0)',
color: p === page ? '#fff' : 'var(--text-2)',
borderColor: p === page ? 'var(--color-accent)' : 'var(--stroke-default)',
fontWeight: p === page ? 700 : 400,
}}
className={`w-8 h-8 text-label-2 rounded-sm border cursor-pointer font-mono ${p === page ? 'bg-color-accent text-white border-color-accent font-bold' : 'bg-bg-base text-fg-sub border-stroke'}`}
>
{p}
</button>
@ -441,7 +436,7 @@ function ShipInsurance() {
<button
onClick={() => loadData(page + 1)}
disabled={page >= totalPages}
className="px-3 py-1.5 text-[11px] rounded-sm border border-stroke bg-bg-base cursor-pointer disabled:opacity-30 disabled:cursor-default"
className="px-3 py-1.5 text-label-2 rounded-sm border border-stroke bg-bg-base cursor-pointer disabled:opacity-30 disabled:cursor-default"
>
</button>
@ -449,17 +444,6 @@ function ShipInsurance() {
)}
</>
)}
{/* 푸터 */}
<div className="mt-auto px-4 py-3 bg-bg-card border border-stroke rounded-sm">
<div className="text-[10px] text-fg-disabled leading-[1.7]">
<span className="text-fg-sub font-bold"> :</span> ·
<br />
<span className="text-fg-sub font-bold">:</span> , , ,
, , , ,
</div>
</div>
</div>
);
}

파일 보기

@ -34,18 +34,19 @@ export interface InsuranceRow {
}
export const typeTagCls = (type: string) => {
if (type === '해경관할') return 'bg-[rgba(239,68,68,0.1)] text-color-danger';
if (type === '해경경찰서') return 'bg-[rgba(59,130,246,0.1)] text-color-info';
if (type === '파출소') return 'bg-[rgba(34,197,94,0.1)] text-color-success';
if (type === '관련기관') return 'bg-[rgba(168,85,247,0.1)] text-color-tertiary';
if (type === '해양환경공단') return 'bg-[rgba(6,182,212,0.1)] text-color-accent';
if (type === '업체') return 'bg-[rgba(245,158,11,0.1)] text-color-warning';
if (type === '지자체') return 'bg-[rgba(236,72,153,0.1)] text-[#ec4899]';
if (type === '기름저장시설') return 'bg-[rgba(139,92,246,0.1)] text-[#8b5cf6]';
if (type === '정유사') return 'bg-[rgba(20,184,166,0.1)] text-[#14b8a6]';
if (type === '해군') return 'bg-[rgba(100,116,139,0.1)] text-[#64748b]';
if (type === '기타') return 'bg-[rgba(107,114,128,0.1)] text-[#6b7280]';
return 'bg-[rgba(156,163,175,0.1)] text-[#9ca3af]';
if (type === '해경관할')
return 'bg-[color-mix(in_srgb,var(--color-danger)_10%,transparent)] text-color-danger';
if (type === '해경경찰서')
return 'bg-[color-mix(in_srgb,var(--color-info)_10%,transparent)] text-color-info';
if (type === '파출소')
return 'bg-[color-mix(in_srgb,var(--color-success)_10%,transparent)] text-color-success';
if (type === '관련기관')
return 'bg-[color-mix(in_srgb,var(--color-tertiary)_10%,transparent)] text-color-tertiary';
if (type === '해양환경공단')
return 'bg-[color-mix(in_srgb,var(--color-accent)_10%,transparent)] text-color-accent';
if (type === '업체')
return 'bg-[color-mix(in_srgb,var(--color-warning)_10%,transparent)] text-color-warning';
return 'bg-[color-mix(in_srgb,var(--color-info)_10%,transparent)] text-color-info';
};
export const typeColor = (type: string) => {

파일 보기

@ -10,12 +10,12 @@ const CATEGORY_LABELS: Record<string, string> = {
MANUAL: '해경매뉴얼',
};
// 카테고리별 배지 색상
// 카테고리별 배지 색상 (NOTICE는 danger, 나머지는 중립)
const CATEGORY_COLORS: Record<string, string> = {
NOTICE: 'bg-red-500/20 text-red-400',
DATA: 'bg-green-500/20 text-green-400',
QNA: 'bg-purple-500/20 text-purple-400',
MANUAL: 'bg-blue-500/20 text-blue-400',
NOTICE: 'bg-[color-mix(in_srgb,var(--color-danger)_15%,transparent)] text-color-danger',
DATA: 'bg-bg-elevated text-fg-sub',
QNA: 'bg-bg-elevated text-fg-sub',
MANUAL: 'bg-bg-elevated text-fg-sub',
};
interface BoardDetailViewProps {
@ -55,7 +55,7 @@ export function BoardDetailView({ postSn, onBack, onEdit, onDelete }: BoardDetai
if (isLoading || !post) {
return (
<div className="flex flex-col h-full bg-bg-base items-center justify-center">
<p className="text-fg-disabled text-sm"> ...</p>
<p className="text-fg-disabled text-label-1"> ...</p>
</div>
);
}
@ -69,7 +69,7 @@ export function BoardDetailView({ postSn, onBack, onEdit, onDelete }: BoardDetai
<div className="flex items-center justify-between px-8 py-4 border-b border-stroke bg-bg-surface">
<button
onClick={onBack}
className="flex items-center gap-2 text-sm font-semibold text-fg-sub hover:text-fg transition-colors"
className="flex items-center gap-2 text-label-1 font-semibold text-fg-sub hover:text-fg transition-colors"
>
<span></span>
<span></span>
@ -78,13 +78,13 @@ export function BoardDetailView({ postSn, onBack, onEdit, onDelete }: BoardDetai
<div className="flex items-center gap-2">
<button
onClick={onEdit}
className="px-4 py-2 text-sm font-semibold rounded bg-bg-elevated text-fg border border-stroke hover:bg-bg-card transition-colors"
className="px-4 py-2 text-label-1 font-semibold rounded text-fg bg-[color-mix(in_srgb,var(--color-accent)_30%,transparent)] border border-[color-mix(in_srgb,var(--color-accent)_30%,transparent)] cursor-pointer"
>
</button>
<button
onClick={onDelete}
className="px-4 py-2 text-sm font-semibold rounded bg-red-500/20 text-red-400 border border-red-500/30 hover:bg-red-500/30 transition-colors"
className="px-4 py-2 text-label-1 font-semibold rounded text-fg bg-[color-mix(in_srgb,var(--color-danger)_30%,transparent)] border border-[color-mix(in_srgb,var(--color-danger)_30%,transparent)] cursor-pointer"
>
</button>
@ -99,20 +99,20 @@ export function BoardDetailView({ postSn, onBack, onEdit, onDelete }: BoardDetai
<div className="pb-6 border-b border-stroke">
<div className="flex items-center gap-3 mb-3">
<span
className={`inline-flex items-center px-2.5 py-0.5 rounded text-xs font-semibold ${CATEGORY_COLORS[post.categoryCd] || 'bg-blue-500/20 text-blue-400'}`}
className={`inline-flex items-center px-2.5 py-0.5 rounded text-caption font-semibold ${CATEGORY_COLORS[post.categoryCd] || 'bg-bg-elevated text-fg-sub'}`}
>
{CATEGORY_LABELS[post.categoryCd] || post.categoryCd}
</span>
{post.pinnedYn === 'Y' && (
<span className="inline-flex items-center px-2.5 py-0.5 rounded text-xs font-semibold bg-yellow-500/20 text-yellow-400">
<span className="inline-flex items-center px-2.5 py-0.5 rounded text-caption font-semibold bg-[color-mix(in_srgb,var(--color-accent)_15%,transparent)] text-color-accent">
📌
</span>
)}
</div>
<h1 className="text-2xl font-bold text-fg mb-4">{post.title}</h1>
<div className="flex items-center gap-4 text-sm text-fg-disabled">
<h1 className="text-title-1 font-bold text-fg mb-4">{post.title}</h1>
<div className="flex items-center gap-4 text-label-1 text-fg-disabled">
<span>
: <span className="text-fg-sub font-semibold">{post.authorName}</span>
: <span className="text-fg-sub">{post.authorName}</span>
</span>
<span>|</span>
<span>: {new Date(post.regDtm).toLocaleDateString('ko-KR')}</span>
@ -130,7 +130,7 @@ export function BoardDetailView({ postSn, onBack, onEdit, onDelete }: BoardDetai
{/* 본문 */}
<div className="py-8">
<div className="prose prose-invert max-w-none">
<div className="text-fg text-[15px] leading-relaxed whitespace-pre-wrap">
<div className="text-fg text-subtitle leading-relaxed whitespace-pre-wrap">
{post.content || '(내용 없음)'}
</div>
</div>
@ -139,7 +139,7 @@ export function BoardDetailView({ postSn, onBack, onEdit, onDelete }: BoardDetai
{/* 댓글 섹션 (향후 구현 예정) */}
<div className="py-6 border-t border-stroke">
<div className="text-center py-8">
<p className="text-fg-disabled text-sm"> .</p>
<p className="text-fg-disabled text-label-1"> .</p>
</div>
</div>
</div>

파일 보기

@ -18,10 +18,10 @@ const CATEGORY_FILTER: { label: string; code: string | null }[] = [
];
const CATEGORY_STYLE: Record<string, string> = {
NOTICE: 'bg-red-500/20 text-red-400',
DATA: 'bg-blue-500/20 text-blue-400',
QNA: 'bg-green-500/20 text-green-400',
MANUAL: 'bg-yellow-500/20 text-yellow-400',
NOTICE: 'bg-[color-mix(in_srgb,var(--color-accent)_15%,transparent)] text-color-accent',
DATA: 'bg-bg-elevated text-fg-sub',
QNA: 'bg-bg-elevated text-fg-sub',
MANUAL: 'bg-bg-elevated text-fg-sub',
};
const PAGE_SIZE = 20;
@ -104,9 +104,9 @@ export function BoardListTable({ onPostClick, onWriteClick }: BoardListTableProp
<button
key={cat.label}
onClick={() => handleCategoryChange(cat.code)}
className={`px-4 py-2 text-sm font-semibold rounded transition-all ${
className={`px-4 py-2 text-label-1 font-semibold rounded transition-all ${
selectedCategory === cat.code
? 'bg-color-accent text-bg-0'
? 'bg-color-accent text-white'
: 'bg-bg-elevated text-fg-disabled hover:bg-bg-card hover:text-fg'
}`}
>
@ -123,13 +123,13 @@ export function BoardListTable({ onPostClick, onWriteClick }: BoardListTableProp
value={searchInput}
onChange={(e) => setSearchInput(e.target.value)}
onKeyDown={handleSearchKeyDown}
className="px-4 py-2 text-sm bg-bg-elevated border border-stroke rounded text-fg placeholder-fg-disabled focus:border-color-accent focus:outline-none w-64"
className="px-4 py-2 text-label-1 bg-bg-elevated border border-stroke rounded text-fg placeholder-fg-disabled focus:border-color-accent focus:outline-none w-64"
/>
{canWrite && (
<button
onClick={onWriteClick}
className="px-6 py-2 text-sm font-semibold rounded bg-color-accent text-bg-0 hover:opacity-90 transition-opacity flex items-center gap-2"
className="rounded-sm text-label-1 font-semibold cursor-pointer text-color-accent px-3.5 py-1.5 bg-[color-mix(in_srgb,var(--color-accent)_8%,transparent)] border border-[color-mix(in_srgb,var(--color-accent)_30%,transparent)] flex items-center gap-2"
>
<span>+</span>
<span></span>
@ -142,27 +142,29 @@ export function BoardListTable({ onPostClick, onWriteClick }: BoardListTableProp
<div className="flex-1 overflow-auto px-8 py-6">
{loading ? (
<div className="text-center py-20">
<p className="text-fg-disabled text-sm"> ...</p>
<p className="text-fg-disabled text-label-1"> ...</p>
</div>
) : (
<>
<table className="w-full border-collapse">
<thead>
<tr className="border-b-2 border-stroke">
<th className="px-4 py-3 text-left text-sm font-semibold text-fg-sub w-20">
<th className="px-4 py-3 text-left text-label-1 font-semibold text-fg-sub w-20">
</th>
<th className="px-4 py-3 text-left text-sm font-semibold text-fg-sub w-32">
<th className="px-4 py-3 text-left text-label-1 font-semibold text-fg-sub w-32">
</th>
<th className="px-4 py-3 text-left text-sm font-semibold text-fg-sub"></th>
<th className="px-4 py-3 text-left text-sm font-semibold text-fg-sub w-32">
<th className="px-4 py-3 text-left text-label-1 font-semibold text-fg-sub">
</th>
<th className="px-4 py-3 text-left text-label-1 font-semibold text-fg-sub w-32">
</th>
<th className="px-4 py-3 text-left text-sm font-semibold text-fg-sub w-32">
<th className="px-4 py-3 text-left text-label-1 font-semibold text-fg-sub w-32">
</th>
<th className="px-4 py-3 text-left text-sm font-semibold text-fg-sub w-24">
<th className="px-4 py-3 text-left text-label-1 font-semibold text-fg-sub w-24">
</th>
</tr>
@ -174,9 +176,9 @@ export function BoardListTable({ onPostClick, onWriteClick }: BoardListTableProp
onClick={() => onPostClick(post.sn)}
className="border-b border-stroke hover:bg-bg-elevated cursor-pointer transition-colors"
>
<td className="px-4 py-4 text-sm text-fg">
<td className="px-4 py-4 text-label-1 text-fg">
{post.pinnedYn === 'Y' ? (
<span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-semibold bg-red-500/20 text-red-400">
<span className="inline-flex items-center px-2 py-0.5 rounded text-caption font-semibold bg-[color-mix(in_srgb,var(--color-accent)_15%,transparent)] text-color-accent">
</span>
) : (
@ -185,27 +187,23 @@ export function BoardListTable({ onPostClick, onWriteClick }: BoardListTableProp
</td>
<td className="px-4 py-4">
<span
className={`inline-flex items-center px-2.5 py-0.5 rounded text-xs font-semibold ${
CATEGORY_STYLE[post.categoryCd] || 'bg-gray-500/20 text-gray-400'
className={`inline-flex items-center px-2.5 py-0.5 rounded text-caption font-semibold ${
CATEGORY_STYLE[post.categoryCd] || 'bg-bg-elevated text-fg-sub'
}`}
>
{CATEGORY_MAP[post.categoryCd] || post.categoryCd}
</span>
</td>
<td className="px-4 py-4">
<span
className={`text-sm ${
post.pinnedYn === 'Y' ? 'font-semibold text-fg' : 'text-fg'
} hover:text-color-accent transition-colors`}
>
<span className="text-label-1 text-fg hover:text-color-accent transition-colors">
{post.title}
</span>
</td>
<td className="px-4 py-4 text-sm text-fg-sub">{post.authorName}</td>
<td className="px-4 py-4 text-sm text-fg-disabled">
<td className="px-4 py-4 text-label-1 text-fg-sub">{post.authorName}</td>
<td className="px-4 py-4 text-label-1 text-fg-disabled">
{formatDate(post.regDtm)}
</td>
<td className="px-4 py-4 text-sm text-fg-disabled">{post.viewCnt}</td>
<td className="px-4 py-4 text-label-1 text-fg-disabled">{post.viewCnt}</td>
</tr>
))}
</tbody>
@ -213,7 +211,7 @@ export function BoardListTable({ onPostClick, onWriteClick }: BoardListTableProp
{posts.length === 0 && (
<div className="text-center py-20">
<p className="text-fg-disabled text-sm"> .</p>
<p className="text-fg-disabled text-label-1"> .</p>
</div>
)}
</>
@ -226,7 +224,7 @@ export function BoardListTable({ onPostClick, onWriteClick }: BoardListTableProp
<button
onClick={() => setPage((p) => Math.max(1, p - 1))}
disabled={page <= 1}
className="px-3 py-1.5 text-sm rounded bg-bg-elevated text-fg-disabled hover:bg-bg-card hover:text-fg transition-colors disabled:opacity-40"
className="px-3 py-1.5 text-label-1 rounded bg-bg-elevated text-fg-disabled hover:bg-bg-card hover:text-fg transition-colors disabled:opacity-40"
>
</button>
@ -234,9 +232,9 @@ export function BoardListTable({ onPostClick, onWriteClick }: BoardListTableProp
<button
key={p}
onClick={() => setPage(p)}
className={`px-3 py-1.5 text-sm rounded ${
className={`px-3 py-1.5 text-label-1 rounded ${
page === p
? 'bg-color-accent text-bg-0 font-semibold'
? 'bg-color-accent text-white font-semibold'
: 'bg-bg-elevated text-fg-disabled hover:bg-bg-card hover:text-fg transition-colors'
}`}
>
@ -246,7 +244,7 @@ export function BoardListTable({ onPostClick, onWriteClick }: BoardListTableProp
<button
onClick={() => setPage((p) => Math.min(totalPages, p + 1))}
disabled={page >= totalPages}
className="px-3 py-1.5 text-sm rounded bg-bg-elevated text-fg-disabled hover:bg-bg-card hover:text-fg transition-colors disabled:opacity-40"
className="px-3 py-1.5 text-label-1 rounded bg-bg-elevated text-fg-disabled hover:bg-bg-card hover:text-fg transition-colors disabled:opacity-40"
>
</button>

파일 보기

@ -33,12 +33,12 @@ const CATEGORY_LABELS: Record<string, string> = {
MANUAL: '해경매뉴얼',
};
// 카테고리별 배지 색상
// 카테고리별 배지 색상 (NOTICE는 danger, 나머지 중립)
const CATEGORY_COLORS: Record<string, string> = {
NOTICE: 'bg-red-500/20 text-red-400',
DATA: 'bg-green-500/20 text-green-400',
QNA: 'bg-purple-500/20 text-purple-400',
MANUAL: 'bg-blue-500/20 text-blue-400',
NOTICE: 'bg-[color-mix(in_srgb,var(--color-danger)_15%,transparent)] text-color-danger',
DATA: 'bg-bg-elevated text-fg-sub',
QNA: 'bg-bg-elevated text-fg-sub',
MANUAL: 'bg-bg-elevated text-fg-sub',
};
const PAGE_SIZE = 20;
@ -91,6 +91,13 @@ export function BoardView() {
setPage(1);
}, [activeSubTab]);
// 서브탭 변경 시 목록 화면으로 복귀
useEffect(() => {
setViewMode('list');
setSelectedPostSn(null);
setEditingPostSn(null);
}, [activeSubTab]);
// 상세 보기
const handlePostClick = (sn: number) => {
setSelectedPostSn(sn);
@ -196,19 +203,18 @@ export function BoardView() {
const filteredManuals = manualList;
// 카테고리별 색상 (방제매뉴얼만 accent, 나머지 중립)
const catColor = (cat: string) => {
switch (cat) {
case '방제매뉴얼':
return { bg: 'rgba(6,182,212,.15)', text: '#22d3ee' };
case '대응매뉴얼':
return { bg: 'rgba(249,115,22,.15)', text: '#f97316' };
case '교육자료':
return { bg: 'rgba(34,197,94,.15)', text: '#22c55e' };
case '법령·규정':
return { bg: 'rgba(168,85,247,.15)', text: '#a855f7' };
default:
return { bg: 'rgba(100,100,100,.15)', text: '#999' };
if (cat === '방제매뉴얼') {
return {
bg: 'color-mix(in srgb, var(--color-accent) 15%, transparent)',
text: 'var(--color-accent)',
};
}
return {
bg: 'var(--bg-elevated)',
text: 'var(--fg-sub)',
};
};
if (activeSubTab === 'manual') {
@ -217,38 +223,31 @@ export function BoardView() {
<div className="flex-1 relative overflow-hidden">
<div className="flex flex-col h-full bg-bg-base">
{/* 헤더 */}
<div
className="flex items-center justify-between px-8 py-4 border-b"
style={{ borderColor: 'var(--stroke-default)', background: 'var(--bg-surface)' }}
>
<div className="flex items-center justify-between px-8 py-4 border-b border-stroke bg-bg-surface">
<div className="flex items-center gap-3">
<div className="flex items-center gap-2">
<span className="text-lg">📘</span>
<span className="text-[15px] font-bold"></span>
<span className="text-[10px] ml-1 text-fg-disabled">
<span className="text-subtitle font-bold"></span>
<span className="text-caption ml-1 text-fg-disabled">
{filteredManuals.length}
</span>
</div>
<div className="flex gap-1 ml-4">
{manualCategories.map((cat) => (
<button
key={cat}
onClick={() => setManualCategory(cat)}
className="px-3 py-1.5 text-[11px] font-semibold rounded-md transition-all"
style={{
background:
manualCategory === cat ? 'rgba(6,182,212,.15)' : 'var(--bg-card)',
border:
manualCategory === cat
? '1px solid rgba(6,182,212,.3)'
: '1px solid var(--stroke-default)',
color:
manualCategory === cat ? 'var(--color-accent)' : 'var(--fg-disabled)',
}}
>
{cat}
</button>
))}
{manualCategories.map((cat) => {
const isActive = manualCategory === cat;
return (
<button
key={cat}
onClick={() => setManualCategory(cat)}
className={`px-3 py-1.5 text-label-2 font-semibold rounded-md transition-all border ${
isActive
? 'bg-[color-mix(in_srgb,var(--color-accent)_15%,transparent)] border-[color-mix(in_srgb,var(--color-accent)_30%,transparent)] text-color-accent'
: 'bg-bg-card border-stroke text-fg-disabled'
}`}
>
{cat}
</button>
);
})}
</div>
</div>
<div className="flex items-center gap-3">
@ -257,24 +256,13 @@ export function BoardView() {
placeholder="매뉴얼 검색..."
value={manualSearch}
onChange={(e) => setManualSearch(e.target.value)}
className="px-4 py-2 text-sm rounded w-64"
style={{
background: 'var(--bg-elevated)',
border: '1px solid var(--stroke-default)',
outline: 'none',
}}
className="px-4 py-2 text-label-1 rounded w-64 bg-bg-elevated border border-stroke outline-none"
/>
<button
onClick={() => setShowUploadModal(true)}
className="px-5 py-2 text-[12px] font-semibold rounded-md transition-all flex items-center gap-1.5"
style={{
background: 'rgba(6,182,212,.15)',
border: '1px solid rgba(6,182,212,.3)',
color: '#22d3ee',
cursor: 'pointer',
}}
className="rounded-sm text-label-1 font-semibold cursor-pointer text-color-accent px-3.5 py-1.5 bg-[color-mix(in_srgb,var(--color-accent)_8%,transparent)] border border-[color-mix(in_srgb,var(--color-accent)_30%,transparent)]"
>
📤
</button>
</div>
</div>
@ -283,7 +271,7 @@ export function BoardView() {
<div className="flex-1 overflow-auto px-8 py-6">
{manualLoading ? (
<div className="text-center py-20">
<p className="text-sm text-fg-disabled"> ...</p>
<p className="text-label-1 text-fg-disabled"> ...</p>
</div>
) : (
<div
@ -295,52 +283,29 @@ export function BoardView() {
return (
<div
key={file.manualSn}
className="rounded-xl p-4 transition-all"
style={{
background: 'var(--bg-card)',
border: '1px solid var(--stroke-default)',
cursor: 'pointer',
}}
onMouseEnter={(e) => {
(e.currentTarget as HTMLElement).style.borderColor = 'rgba(6,182,212,.4)';
}}
onMouseLeave={(e) => {
(e.currentTarget as HTMLElement).style.borderColor =
'var(--stroke-default)';
}}
className="rounded-xl p-4 transition-all bg-bg-card border border-stroke cursor-pointer hover:border-[color-mix(in_srgb,var(--color-accent)_40%,transparent)]"
>
<div className="flex items-center justify-between mb-3">
<span
className="px-2 py-0.5 rounded text-[10px] font-semibold"
className="px-2 py-0.5 rounded text-caption font-semibold"
style={{ background: cc.bg, color: cc.text }}
>
{file.catgNm}
</span>
<span
className="text-[10px] font-semibold px-2 py-0.5 rounded"
style={{ background: 'rgba(59,130,246,.1)', color: '#3b82f6' }}
>
<span className="text-caption font-semibold px-2 py-0.5 rounded bg-bg-elevated text-fg-sub">
{file.version}
</span>
</div>
<div className="text-[12px] font-bold mb-3 leading-[1.5]">{file.title}</div>
<div className="text-label-1 font-bold mb-3 leading-[1.5]">
{file.title}
</div>
<div className="flex items-center gap-2 mb-3">
<div
className="flex items-center gap-1.5 px-2 py-1 rounded"
style={{ background: 'rgba(239,68,68,.08)' }}
>
<span style={{ fontSize: 12 }}>📄</span>
<span
className="text-[10px] font-semibold"
style={{ color: '#ef4444' }}
>
<div className="flex items-center gap-1.5 px-2 py-1 rounded bg-bg-elevated">
<span className="text-caption font-semibold text-fg-sub">
{file.fileTp || 'PDF'}
</span>
</div>
<span
className="text-[10px]"
style={{ color: 'var(--fg-disabled)', fontFamily: 'var(--font-mono)' }}
>
<span className="text-caption text-fg-disabled font-mono">
{file.fileSz}
</span>
</div>
@ -358,16 +323,10 @@ export function BoardView() {
});
setShowUploadModal(true);
}}
className="px-2 py-0.5 rounded text-[10px] font-semibold transition-all"
style={{
background: 'rgba(59,130,246,.1)',
border: '1px solid rgba(59,130,246,.2)',
color: '#3b82f6',
cursor: 'pointer',
}}
className="px-2 py-0.5 rounded text-caption font-semibold transition-all bg-bg-elevated border border-stroke text-fg-sub cursor-pointer hover:bg-bg-card"
title="수정"
>
</button>
<button
onClick={async (e) => {
@ -384,34 +343,19 @@ export function BoardView() {
}
}
}}
className="px-2 py-0.5 rounded text-[10px] font-semibold transition-all"
style={{
background: 'rgba(239,68,68,.1)',
border: '1px solid rgba(239,68,68,.2)',
color: '#ef4444',
cursor: 'pointer',
}}
className="px-2 py-0.5 rounded text-caption font-semibold transition-all bg-bg-elevated border border-stroke text-fg-sub cursor-pointer hover:bg-bg-card"
title="삭제"
>
🗑
</button>
</div>
<div
className="flex items-center justify-between pt-3"
style={{ borderTop: '1px solid var(--stroke-default)' }}
>
<div className="flex items-center gap-3 text-[10px] text-fg-disabled">
<div className="flex items-center justify-between pt-3 border-t border-stroke">
<div className="flex items-center gap-3 text-caption text-fg-disabled">
<span>{file.authorNm}</span>
<span>{new Date(file.regDtm).toLocaleDateString('ko-KR')}</span>
</div>
<div className="flex items-center gap-3">
<span
className="text-[10px]"
style={{
color: 'var(--fg-disabled)',
fontFamily: 'var(--font-mono)',
}}
>
<span className="text-caption text-fg-disabled font-mono">
{file.dwnldCnt}
</span>
<button
@ -457,15 +401,9 @@ export function BoardView() {
document.body.removeChild(a);
URL.revokeObjectURL(url);
}}
className="px-3 py-1 rounded text-[10px] font-semibold transition-all"
style={{
background: 'rgba(6,182,212,.1)',
border: '1px solid rgba(6,182,212,.25)',
color: '#22d3ee',
cursor: 'pointer',
}}
className="px-3 py-1 rounded text-caption font-semibold transition-all cursor-pointer bg-[color-mix(in_srgb,var(--color-accent)_10%,transparent)] border border-[color-mix(in_srgb,var(--color-accent)_25%,transparent)] text-color-accent"
>
📥
</button>
</div>
</div>
@ -477,8 +415,7 @@ export function BoardView() {
{!manualLoading && filteredManuals.length === 0 && (
<div className="text-center py-20">
<div style={{ fontSize: 32, opacity: 0.3, marginBottom: 8 }}>📘</div>
<p className="text-sm text-fg-disabled"> .</p>
<p className="text-label-1 text-fg-disabled"> .</p>
</div>
)}
</div>
@ -487,8 +424,7 @@ export function BoardView() {
{/* 업로드 모달 */}
{showUploadModal && (
<div
className="fixed inset-0 z-[9999] flex items-center justify-center"
style={{ background: 'rgba(0,0,0,.55)' }}
className="fixed inset-0 z-[9999] flex items-center justify-center bg-[rgba(0,0,0,.55)]"
onClick={() => {
setShowUploadModal(false);
setEditingManualId(null);
@ -500,8 +436,7 @@ export function BoardView() {
>
<div className="px-5 py-4 border-b border-stroke flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="text-base">{editingManualId ? '✏️' : '📤'}</span>
<span className="text-sm font-bold">
<span className="text-label-1 font-bold">
{editingManualId ? '매뉴얼 수정' : '매뉴얼 업로드'}
</span>
</div>
@ -510,32 +445,28 @@ export function BoardView() {
setShowUploadModal(false);
setEditingManualId(null);
}}
className="cursor-pointer text-fg-disabled text-base leading-none"
className="cursor-pointer text-fg-disabled text-label-1 leading-none"
>
</span>
</div>
<div className="p-5 flex flex-col gap-4">
<div>
<label className="block text-[11px] font-semibold text-fg-sub mb-1.5">
<label className="block text-label-2 font-semibold text-fg-sub mb-1.5">
</label>
<div className="flex gap-1.5">
{['방제매뉴얼', '대응매뉴얼', '교육자료', '법령·규정'].map((cat) => {
const cc = catColor(cat);
const isActive = uploadForm.category === cat;
return (
<button
key={cat}
onClick={() => setUploadForm((prev) => ({ ...prev, category: cat }))}
className="flex-1 py-2 px-1 rounded-md text-[11px] font-semibold cursor-pointer"
style={{
background: isActive ? cc.bg : 'var(--bg-card)',
border: isActive
? `1px solid ${cc.text}40`
: '1px solid var(--stroke-default)',
color: isActive ? cc.text : 'var(--fg-disabled)',
}}
className={`flex-1 py-2 px-1 rounded-md text-label-2 font-semibold cursor-pointer border ${
isActive
? 'bg-[color-mix(in_srgb,var(--color-accent)_15%,transparent)] border-[color-mix(in_srgb,var(--color-accent)_30%,transparent)] text-color-accent'
: 'bg-bg-card border-stroke text-fg-disabled'
}`}
>
{cat}
</button>
@ -544,7 +475,7 @@ export function BoardView() {
</div>
</div>
<div>
<label className="block text-[11px] font-semibold text-fg-sub mb-1.5">
<label className="block text-label-2 font-semibold text-fg-sub mb-1.5">
</label>
<input
@ -554,12 +485,11 @@ export function BoardView() {
onChange={(e) =>
setUploadForm((prev) => ({ ...prev, title: e.target.value }))
}
className="w-full px-3 py-2.5 rounded-md text-xs bg-bg-elevated border border-stroke outline-none"
style={{ boxSizing: 'border-box' }}
className="w-full px-3 py-2.5 rounded-md text-label-1 bg-bg-elevated border border-stroke outline-none box-border"
/>
</div>
<div>
<label className="block text-[11px] font-semibold text-fg-sub mb-1.5">
<label className="block text-label-2 font-semibold text-fg-sub mb-1.5">
</label>
<input
@ -569,12 +499,11 @@ export function BoardView() {
onChange={(e) =>
setUploadForm((prev) => ({ ...prev, version: e.target.value }))
}
className="w-full px-3 py-2.5 rounded-md text-xs bg-bg-elevated border border-stroke outline-none"
style={{ boxSizing: 'border-box' }}
className="w-full px-3 py-2.5 rounded-md text-label-1 bg-bg-elevated border border-stroke outline-none box-border"
/>
</div>
<div>
<label className="block text-[11px] font-semibold text-fg-sub mb-1.5">
<label className="block text-label-2 font-semibold text-fg-sub mb-1.5">
</label>
<div
@ -599,10 +528,9 @@ export function BoardView() {
>
{uploadForm.fileName ? (
<div className="flex items-center justify-center gap-2">
<span className="text-xl">📄</span>
<div className="text-left">
<div className="text-xs font-semibold">{uploadForm.fileName}</div>
<div className="text-[10px] text-fg-disabled font-mono">
<div className="text-label-1 font-semibold">{uploadForm.fileName}</div>
<div className="text-caption text-fg-disabled font-mono">
{uploadForm.fileSize}
</div>
</div>
@ -611,18 +539,17 @@ export function BoardView() {
e.stopPropagation();
setUploadForm((prev) => ({ ...prev, fileName: '', fileSize: '' }));
}}
className="text-xs text-fg-disabled cursor-pointer ml-2"
className="text-label-1 text-fg-disabled cursor-pointer ml-2"
>
</span>
</div>
) : (
<>
<div className="text-[28px] opacity-30 mb-1.5">📁</div>
<div className="text-[11px] text-fg-disabled">
<div className="text-label-2 text-fg-disabled">
</div>
<div className="text-[9px] text-fg-disabled font-mono mt-1">
<div className="text-caption text-fg-disabled font-mono mt-1">
PDF, DOC, HWP, XLSX ( 100MB)
</div>
</>
@ -636,7 +563,7 @@ export function BoardView() {
setShowUploadModal(false);
setEditingManualId(null);
}}
className="px-5 py-2 rounded-md text-xs font-semibold bg-bg-card border border-stroke text-fg-disabled cursor-pointer"
className="px-5 py-2 rounded-md text-label-1 font-semibold bg-bg-card border border-stroke text-fg-disabled cursor-pointer"
>
</button>
@ -685,14 +612,9 @@ export function BoardView() {
alert((err as { message?: string })?.message || '저장에 실패했습니다.');
}
}}
className="px-6 py-2 rounded-md text-xs font-semibold cursor-pointer"
style={{
background: 'rgba(6,182,212,.2)',
border: '1px solid rgba(6,182,212,.35)',
color: '#22d3ee',
}}
className="rounded-sm text-label-1 font-semibold cursor-pointer text-color-accent px-3.5 py-1.5 bg-[color-mix(in_srgb,var(--color-accent)_8%,transparent)] border border-[color-mix(in_srgb,var(--color-accent)_30%,transparent)]"
>
{editingManualId ? '✏️ 수정' : '📤 업로드'}
{editingManualId ? '수정' : '업로드'}
</button>
</div>
</div>
@ -748,7 +670,7 @@ export function BoardView() {
<div className="flex flex-col h-full bg-bg-base">
{/* 헤더 */}
<div className="flex items-center justify-between px-8 py-4 border-b border-stroke bg-bg-surface">
<div className="text-sm text-fg-disabled">
<div className="text-label-1 text-fg-disabled">
<span className="text-fg font-semibold">{totalCount}</span>
</div>
<div className="flex items-center gap-3">
@ -758,14 +680,14 @@ export function BoardView() {
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
onKeyDown={handleSearchKeyDown}
className="px-4 py-2 text-sm bg-bg-elevated border border-stroke rounded text-fg placeholder-fg-disabled focus:border-color-accent focus:outline-none w-64"
className="px-4 py-2 text-label-1 bg-bg-elevated border border-stroke rounded text-fg placeholder-fg-disabled focus:border-color-accent focus:outline-none w-64"
/>
{hasPermission(getWriteResource(), 'CREATE') && (
<button
onClick={handleWriteClick}
className="px-6 py-2 text-sm font-semibold rounded bg-color-accent text-bg-0 hover:opacity-90 transition-opacity"
className="rounded-sm text-label-1 font-semibold cursor-pointer text-color-accent px-4 py-2 bg-[color-mix(in_srgb,var(--color-accent)_8%,transparent)] border border-[color-mix(in_srgb,var(--color-accent)_30%,transparent)]"
>
</button>
)}
</div>
@ -775,29 +697,29 @@ export function BoardView() {
<div className="flex-1 overflow-auto px-8 py-6">
{isLoading ? (
<div className="text-center py-20">
<p className="text-fg-disabled text-sm"> ...</p>
<p className="text-fg-disabled text-label-1"> ...</p>
</div>
) : (
<>
<table className="w-full border-collapse">
<thead>
<tr className="border-b-2 border-stroke">
<th className="px-4 py-3 text-center text-sm font-semibold text-fg-sub w-16">
<th className="px-4 py-3 text-center text-label-1 font-semibold text-fg-sub w-16">
</th>
<th className="px-4 py-3 text-center text-sm font-semibold text-fg-sub w-24">
<th className="px-4 py-3 text-center text-label-1 font-semibold text-fg-sub w-24">
</th>
<th className="px-4 py-3 text-left text-sm font-semibold text-fg-sub">
<th className="px-4 py-3 text-left text-label-1 font-semibold text-fg-sub">
</th>
<th className="px-4 py-3 text-center text-sm font-semibold text-fg-sub w-24">
<th className="px-4 py-3 text-center text-label-1 font-semibold text-fg-sub w-24">
</th>
<th className="px-4 py-3 text-center text-sm font-semibold text-fg-sub w-28">
<th className="px-4 py-3 text-center text-label-1 font-semibold text-fg-sub w-28">
</th>
<th className="px-4 py-3 text-center text-sm font-semibold text-fg-sub w-16">
<th className="px-4 py-3 text-center text-label-1 font-semibold text-fg-sub w-16">
</th>
</tr>
@ -808,10 +730,10 @@ export function BoardView() {
key={post.sn}
className="border-b border-stroke hover:bg-bg-elevated transition-colors"
>
<td className="px-4 py-4 text-sm text-fg text-center">{post.sn}</td>
<td className="px-4 py-4 text-label-1 text-fg text-center">{post.sn}</td>
<td className="px-4 py-4 text-center">
<span
className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-semibold ${CATEGORY_COLORS[post.categoryCd] || 'bg-blue-500/20 text-blue-400'}`}
className={`inline-flex items-center px-2 py-0.5 rounded text-caption font-semibold ${CATEGORY_COLORS[post.categoryCd] || 'bg-bg-elevated text-fg-sub'}`}
>
{CATEGORY_LABELS[post.categoryCd] || post.categoryCd}
</span>
@ -820,20 +742,18 @@ export function BoardView() {
className="px-4 py-4 cursor-pointer"
onClick={() => handlePostClick(post.sn)}
>
<span
className={`text-sm ${post.pinnedYn === 'Y' ? 'font-semibold text-fg' : 'text-fg'} hover:text-color-accent transition-colors`}
>
<span className="text-label-1 text-fg hover:text-color-accent transition-colors">
{post.pinnedYn === 'Y' && '📌 '}
{post.title}
</span>
</td>
<td className="px-4 py-4 text-sm text-fg-sub text-center">
<td className="px-4 py-4 text-label-1 text-fg-sub text-center">
{post.authorName}
</td>
<td className="px-4 py-4 text-sm text-fg-disabled text-center">
<td className="px-4 py-4 text-label-1 text-fg-disabled text-center">
{new Date(post.regDtm).toLocaleDateString('ko-KR')}
</td>
<td className="px-4 py-4 text-sm text-fg-disabled text-center">
<td className="px-4 py-4 text-label-1 text-fg-disabled text-center">
{post.viewCnt}
</td>
</tr>
@ -843,7 +763,7 @@ export function BoardView() {
{posts.length === 0 && (
<div className="text-center py-20">
<p className="text-fg-disabled text-sm"> .</p>
<p className="text-fg-disabled text-label-1"> .</p>
</div>
)}
</>
@ -857,9 +777,9 @@ export function BoardView() {
<button
key={p}
onClick={() => setPage(p)}
className={`px-3 py-1.5 text-sm rounded transition-colors ${
className={`px-3 py-1.5 text-label-1 rounded transition-colors ${
p === page
? 'bg-color-accent/20 text-color-accent font-semibold'
? 'bg-[color-mix(in_srgb,var(--color-accent)_20%,transparent)] text-color-accent font-semibold'
: 'bg-bg-elevated text-fg-disabled hover:bg-bg-card hover:text-fg'
}`}
>

파일 보기

@ -126,34 +126,50 @@ export function BoardWriteForm({
if (isFetching) {
return (
<div className="flex flex-col h-full bg-bg-base items-center justify-center">
<p className="text-fg-disabled text-sm"> ...</p>
<p className="text-fg-disabled text-label-1"> ...</p>
</div>
);
}
return (
<div className="flex flex-col h-full bg-bg-base">
<form onSubmit={handleSubmit} className="flex flex-col h-full bg-bg-base">
{/* 헤더 */}
<div className="flex items-center justify-between px-8 py-4 border-b border-stroke bg-bg-surface">
<h2 className="text-lg font-semibold text-fg">
<h2 className="text-title-3 font-bold text-fg">
{isEditMode ? '게시글 수정' : '게시글 작성'}
</h2>
<div className="flex items-center gap-2">
<button
type="button"
onClick={onCancel}
className="rounded-sm text-label-1 font-semibold cursor-pointer text-fg-sub px-4 py-2 bg-bg-elevated border border-stroke"
>
</button>
<button
type="submit"
disabled={isLoading}
className="rounded-sm text-label-1 font-semibold cursor-pointer text-color-accent px-4 py-2 bg-[color-mix(in_srgb,var(--color-accent)_8%,transparent)] border border-[color-mix(in_srgb,var(--color-accent)_30%,transparent)] disabled:opacity-50"
>
{isLoading ? '저장 중...' : isEditMode ? '수정하기' : '등록하기'}
</button>
</div>
</div>
{/* 폼 */}
<form onSubmit={handleSubmit} className="flex-1 flex flex-col overflow-hidden">
<div className="flex-1 flex flex-col overflow-hidden">
<div className="flex-1 overflow-auto px-8 py-6">
<div className="max-w-4xl mx-auto space-y-6">
{/* 분류 선택 */}
<div>
<label className="block text-sm font-semibold text-fg-sub mb-2">
<span className="text-red-500">*</span>
<label className="block text-label-1 font-semibold text-fg-sub mb-2">
<span className="text-color-danger">*</span>
</label>
<select
value={categoryCd}
onChange={(e) => setCategoryCd(e.target.value)}
disabled={isEditMode}
className="w-full px-4 py-2.5 text-sm bg-bg-elevated border border-stroke rounded text-fg focus:border-color-accent focus:outline-none disabled:opacity-50"
className="w-full px-4 py-2.5 text-label-1 bg-bg-elevated border border-stroke rounded text-fg focus:border-color-accent focus:outline-none disabled:opacity-50"
>
{CATEGORY_OPTIONS.map((opt) => (
<option key={opt.code} value={opt.code}>
@ -165,8 +181,8 @@ export function BoardWriteForm({
{/* 제목 */}
<div>
<label className="block text-sm font-semibold text-fg-sub mb-2">
<span className="text-red-500">*</span>
<label className="block text-label-1 font-semibold text-fg-sub mb-2">
<span className="text-color-danger">*</span>
</label>
<input
type="text"
@ -174,14 +190,14 @@ export function BoardWriteForm({
onChange={(e) => setTitle(e.target.value)}
maxLength={200}
placeholder="제목을 입력하세요"
className="w-full px-4 py-2.5 text-sm bg-bg-elevated border border-stroke rounded text-fg placeholder-fg-disabled focus:border-color-accent focus:outline-none"
className="w-full px-4 py-2.5 text-label-1 bg-bg-elevated border border-stroke rounded text-fg placeholder-fg-disabled focus:border-color-accent focus:outline-none"
/>
</div>
{/* 내용 */}
<div>
<label className="block text-sm font-semibold text-fg-sub mb-2">
<span className="text-red-500">*</span>
<label className="block text-label-1 font-semibold text-fg-sub mb-2">
<span className="text-color-danger">*</span>
</label>
<textarea
value={content}
@ -189,13 +205,13 @@ export function BoardWriteForm({
maxLength={10000}
placeholder="내용을 입력하세요"
rows={15}
className="w-full px-4 py-3 text-sm bg-bg-elevated border border-stroke rounded text-fg placeholder-fg-disabled focus:border-color-accent focus:outline-none resize-none"
className="w-full px-4 py-3 text-label-1 bg-bg-elevated border border-stroke rounded text-fg placeholder-fg-disabled focus:border-color-accent focus:outline-none resize-none"
/>
</div>
{/* 파일 첨부 (향후 API 연동 예정) */}
<div>
<label className="block text-sm font-semibold text-fg-sub mb-2"></label>
<label className="block text-label-1 font-semibold text-fg-sub mb-2"></label>
<div className="flex items-center gap-3">
<input
type="file"
@ -207,34 +223,16 @@ export function BoardWriteForm({
/>
<label
htmlFor="file-upload"
className="px-4 py-2 text-sm font-semibold rounded bg-bg-elevated text-fg border border-stroke hover:bg-bg-card cursor-pointer transition-colors"
className="px-4 py-2 text-label-1 font-semibold rounded bg-bg-elevated text-fg border border-stroke hover:bg-bg-card cursor-pointer transition-colors"
>
</label>
<span className="text-sm text-fg-disabled"> </span>
<span className="text-label-1 text-fg-disabled"> </span>
</div>
</div>
</div>
</div>
{/* 하단 버튼 */}
<div className="flex items-center justify-end gap-3 px-8 py-4 border-t border-stroke bg-bg-surface">
<button
type="button"
onClick={onCancel}
className="px-6 py-2.5 text-sm font-semibold rounded bg-bg-elevated text-fg border border-stroke hover:bg-bg-card transition-colors"
>
</button>
<button
type="submit"
disabled={isLoading}
className="px-6 py-2.5 text-sm font-semibold rounded bg-color-accent text-bg-0 hover:opacity-90 transition-opacity disabled:opacity-50"
>
{isLoading ? '저장 중...' : isEditMode ? '수정하기' : '등록하기'}
</button>
</div>
</form>
</div>
</div>
</form>
);
}

파일 보기

@ -548,7 +548,7 @@ ${styles}
SEBC
</span>
<span
className="text-[8px] font-semibold text-color-accent py-[2px] px-2 rounded-md"
className="text-caption font-semibold text-color-accent py-[2px] px-2 rounded-md"
style={{ background: 'rgba(6,182,212,.1)' }}
>
Standard European Behaviour Classification
@ -585,11 +585,11 @@ ${styles}
</div>
<div className="text-title-4 font-mono font-extrabold text-color-accent">G</div>
<div className="text-label-2 font-bold my-1">Gas</div>
<div className="text-[8px] text-fg-sub leading-normal">
<div className="text-caption text-fg-sub leading-normal">
.
</div>
<div
className="mt-1.5 text-[7px] font-semibold text-color-accent p-[3px]"
className="mt-1.5 text-caption font-semibold text-color-accent p-[3px]"
style={{ background: 'rgba(6,182,212,.08)', borderRadius: 3 }}
>
@ -616,11 +616,11 @@ ${styles}
</div>
<div className="text-title-4 font-mono font-extrabold text-color-accent">E</div>
<div className="text-label-2 font-bold my-1">Evaporator</div>
<div className="text-[8px] text-fg-sub leading-normal">
<div className="text-caption text-fg-sub leading-normal">
.
</div>
<div
className="mt-1.5 text-[7px] font-semibold text-color-accent p-[3px]"
className="mt-1.5 text-caption font-semibold text-color-accent p-[3px]"
style={{ background: 'rgba(6,182,212,.08)', borderRadius: 3 }}
>
+
@ -647,11 +647,11 @@ ${styles}
</div>
<div className="text-title-4 font-mono font-extrabold text-color-accent">F</div>
<div className="text-label-2 font-bold my-1">Floater</div>
<div className="text-[8px] text-fg-sub leading-normal">
<div className="text-caption text-fg-sub leading-normal">
{'해수면 위에 부유. 비중 < 1.0, 불용성 물질'}
</div>
<div
className="mt-1.5 text-[7px] font-semibold text-color-accent p-[3px]"
className="mt-1.5 text-caption font-semibold text-color-accent p-[3px]"
style={{ background: 'rgba(6,182,212,.08)', borderRadius: 3 }}
>
@ -678,11 +678,11 @@ ${styles}
</div>
<div className="text-title-4 font-mono font-extrabold text-color-accent">D</div>
<div className="text-label-2 font-bold my-1">Dissolver</div>
<div className="text-[8px] text-fg-sub leading-normal">
<div className="text-caption text-fg-sub leading-normal">
.
</div>
<div
className="mt-1.5 text-[7px] font-semibold text-color-accent p-[3px]"
className="mt-1.5 text-caption font-semibold text-color-accent p-[3px]"
style={{ background: 'rgba(6,182,212,.08)', borderRadius: 3 }}
>
@ -709,11 +709,11 @@ ${styles}
</div>
<div className="text-title-4 font-mono font-extrabold text-color-accent">S</div>
<div className="text-label-2 font-bold my-1">Sinker</div>
<div className="text-[8px] text-fg-sub leading-normal">
<div className="text-caption text-fg-sub leading-normal">
{'해저로 침강. 비중 > 1.0, 저층 오염 축적'}
</div>
<div
className="mt-1.5 text-[7px] font-semibold text-color-accent p-[3px]"
className="mt-1.5 text-caption font-semibold text-color-accent p-[3px]"
style={{ background: 'rgba(6,182,212,.08)', borderRadius: 3 }}
>
3D
@ -724,7 +724,7 @@ ${styles}
<div className="rounded-md p-3 border border-stroke bg-bg-card">
<div className="text-label-2 font-bold mb-2">🔀 </div>
<div
className="grid text-center text-[8px]"
className="grid text-center text-caption"
style={{ gridTemplateColumns: 'repeat(5,1fr)', gap: 6 }}
>
<div className="rounded p-1.5 bg-bg-base">
@ -799,20 +799,20 @@ ${styles}
</div>
<div className="flex gap-1">
<span
className="text-[8px] font-semibold text-color-accent px-1.5 py-0.5"
className="text-caption font-semibold text-color-accent px-1.5 py-0.5"
style={{ borderRadius: 3, background: 'rgba(6,182,212,.1)' }}
>
G/GD
</span>
<span
className="text-[8px] font-semibold text-color-accent px-1.5 py-0.5"
className="text-caption font-semibold text-color-accent px-1.5 py-0.5"
style={{ borderRadius: 3, background: 'rgba(6,182,212,.1)' }}
>
</span>
</div>
</div>
<div className="grid grid-cols-2 gap-1 text-[8px] mb-2">
<div className="grid grid-cols-2 gap-1 text-caption mb-2">
<div className="rounded bg-bg-base px-1.5 py-1">
<span className="text-fg-disabled">CAS:</span>{' '}
<span className="font-mono">7664-41-7</span>
@ -839,7 +839,7 @@ ${styles}
</div>
</div>
<div
className="grid text-center text-[7px]"
className="grid text-center text-caption"
style={{ gridTemplateColumns: '1fr 1fr 1fr', gap: 4 }}
>
<div
@ -879,7 +879,7 @@ ${styles}
<b>300 ppm</b>
</div>
</div>
<div className="mt-1.5 text-[8px] text-fg-disabled leading-normal">
<div className="mt-1.5 text-caption text-fg-disabled leading-normal">
. .
.
</div>
@ -898,20 +898,20 @@ ${styles}
</div>
<div className="flex gap-1">
<span
className="text-[8px] font-semibold text-color-accent px-1.5 py-0.5"
className="text-caption font-semibold text-color-accent px-1.5 py-0.5"
style={{ borderRadius: 3, background: 'rgba(6,182,212,.1)' }}
>
ED
</span>
<span
className="text-[8px] font-semibold text-color-accent px-1.5 py-0.5"
className="text-caption font-semibold text-color-accent px-1.5 py-0.5"
style={{ borderRadius: 3, background: 'rgba(6,182,212,.1)' }}
>
</span>
</div>
</div>
<div className="grid grid-cols-2 gap-1 text-[8px] mb-2">
<div className="grid grid-cols-2 gap-1 text-caption mb-2">
<div className="rounded bg-bg-base px-1.5 py-1">
<span className="text-fg-disabled">CAS:</span>{' '}
<span className="font-mono">67-56-1</span>
@ -938,7 +938,7 @@ ${styles}
</div>
</div>
<div
className="grid text-center text-[7px]"
className="grid text-center text-caption"
style={{ gridTemplateColumns: '1fr 1fr 1fr', gap: 4 }}
>
<div
@ -978,7 +978,7 @@ ${styles}
<b>6,000 ppm</b>
</div>
</div>
<div className="mt-1.5 text-[8px] text-fg-disabled leading-normal">
<div className="mt-1.5 text-caption text-fg-disabled leading-normal">
. .
. 2007 FODDANGER호 95L .
</div>
@ -995,20 +995,20 @@ ${styles}
</div>
<div className="flex gap-1">
<span
className="text-[8px] font-semibold text-color-accent px-1.5 py-0.5"
className="text-caption font-semibold text-color-accent px-1.5 py-0.5"
style={{ borderRadius: 3, background: 'rgba(6,182,212,.1)' }}
>
G
</span>
<span
className="text-[8px] font-semibold text-color-accent px-1.5 py-0.5"
className="text-caption font-semibold text-color-accent px-1.5 py-0.5"
style={{ borderRadius: 3, background: 'rgba(6,182,212,.1)' }}
>
</span>
</div>
</div>
<div className="grid grid-cols-2 gap-1 text-[8px] mb-2">
<div className="grid grid-cols-2 gap-1 text-caption mb-2">
<div className="rounded bg-bg-base px-1.5 py-1">
<span className="text-fg-disabled">CAS:</span>{' '}
<span className="font-mono">1333-74-0</span>
@ -1034,7 +1034,7 @@ ${styles}
<span className="font-mono text-color-accent">75.0%</span>
</div>
</div>
<div className="mt-1.5 text-[8px] text-fg-disabled leading-normal">
<div className="mt-1.5 text-caption text-fg-disabled leading-normal">
(4~75%). · . BLEVE
. .
</div>
@ -1055,20 +1055,20 @@ ${styles}
</div>
<div className="flex gap-1">
<span
className="text-[8px] font-semibold text-color-accent px-1.5 py-0.5"
className="text-caption font-semibold text-color-accent px-1.5 py-0.5"
style={{ borderRadius: 3, background: 'rgba(6,182,212,.1)' }}
>
G
</span>
<span
className="text-[8px] font-semibold text-color-accent px-1.5 py-0.5"
className="text-caption font-semibold text-color-accent px-1.5 py-0.5"
style={{ borderRadius: 3, background: 'rgba(6,182,212,.1)' }}
>
/
</span>
</div>
</div>
<div className="grid grid-cols-2 gap-1 text-[8px] mb-2">
<div className="grid grid-cols-2 gap-1 text-caption mb-2">
<div className="rounded bg-bg-base px-1.5 py-1">
<span className="text-fg-disabled">CAS:</span>{' '}
<span className="font-mono">74-82-8</span>
@ -1094,7 +1094,7 @@ ${styles}
<span className="font-mono text-color-accent">15.0%</span>
</div>
</div>
<div className="mt-1.5 text-[8px] text-fg-disabled leading-normal">
<div className="mt-1.5 text-caption text-fg-disabled leading-normal">
(-162°C) RPT(), Pool Fire . Flash
. · LNG .
</div>
@ -1113,20 +1113,20 @@ ${styles}
</div>
<div className="flex gap-1">
<span
className="text-[8px] font-semibold text-color-accent px-1.5 py-0.5"
className="text-caption font-semibold text-color-accent px-1.5 py-0.5"
style={{ borderRadius: 3, background: 'rgba(6,182,212,.1)' }}
>
S/SD
</span>
<span
className="text-[8px] font-semibold text-color-accent px-1.5 py-0.5"
className="text-caption font-semibold text-color-accent px-1.5 py-0.5"
style={{ borderRadius: 3, background: 'rgba(6,182,212,.1)' }}
>
</span>
</div>
</div>
<div className="grid grid-cols-2 gap-1 text-[8px] mb-2">
<div className="grid grid-cols-2 gap-1 text-caption mb-2">
<div className="rounded bg-bg-base px-1.5 py-1">
<span className="text-fg-disabled">CAS:</span>{' '}
<span className="font-mono">108-95-2</span>
@ -1152,7 +1152,7 @@ ${styles}
<span className="font-mono text-color-accent">84 g/L</span>
</div>
</div>
<div className="mt-1.5 text-[8px] text-fg-disabled leading-normal">
<div className="mt-1.5 text-caption text-fg-disabled leading-normal">
1.07 <b className="text-color-accent">Sinker </b> . ROMS
3.5. HNS (31.8kg/).
</div>
@ -1171,20 +1171,20 @@ ${styles}
</div>
<div className="flex gap-1">
<span
className="text-[8px] font-semibold text-color-accent px-1.5 py-0.5"
className="text-caption font-semibold text-color-accent px-1.5 py-0.5"
style={{ borderRadius: 3, background: 'rgba(6,182,212,.1)' }}
>
FE
</span>
<span
className="text-[8px] font-semibold text-color-accent px-1.5 py-0.5"
className="text-caption font-semibold text-color-accent px-1.5 py-0.5"
style={{ borderRadius: 3, background: 'rgba(6,182,212,.1)' }}
>
</span>
</div>
</div>
<div className="grid grid-cols-2 gap-1 text-[8px] mb-2">
<div className="grid grid-cols-2 gap-1 text-caption mb-2">
<div className="rounded bg-bg-base px-1.5 py-1">
<span className="text-fg-disabled">CAS:</span>{' '}
<span className="font-mono">108-88-3</span>
@ -1210,7 +1210,7 @@ ${styles}
<span className="font-mono">0.52 g/L</span>
</div>
</div>
<div className="mt-1.5 text-[8px] text-fg-disabled leading-normal">
<div className="mt-1.5 text-caption text-fg-disabled leading-normal">
. (4°C) .
HNS. .
</div>
@ -1293,7 +1293,7 @@ ${styles}
</div>
<div className="flex flex-col gap-1">
<div
className="text-[8px] rounded px-2 py-1.5"
className="text-caption rounded px-2 py-1.5"
style={{
background: 'rgba(6,182,212,.03)',
border: '1px solid rgba(6,182,212,.1)',
@ -1302,7 +1302,7 @@ ${styles}
<b className="text-color-accent">ERPG-1</b> ,
</div>
<div
className="text-[8px] rounded px-2 py-1.5"
className="text-caption rounded px-2 py-1.5"
style={{
background: 'rgba(6,182,212,.03)',
border: '1px solid rgba(6,182,212,.1)',
@ -1311,7 +1311,7 @@ ${styles}
<b className="text-color-accent">ERPG-2</b> ,
</div>
<div
className="text-[8px] rounded px-2 py-1.5"
className="text-caption rounded px-2 py-1.5"
style={{
background: 'rgba(6,182,212,.03)',
border: '1px solid rgba(6,182,212,.1)',
@ -1465,7 +1465,7 @@ ${styles}
</tr>
</tbody>
</table>
<div className="mt-2 text-[7px] text-fg-disabled">
<div className="mt-2 text-caption text-fg-disabled">
AEGL: 60분 / ERPG: 1시간 / IDLH: 30분 / LFL: 폭발하한
</div>
</div>
@ -1556,7 +1556,7 @@ ${styles}
🔎
</button>
</div>
<div className="text-[8px] text-fg-disabled leading-[1.6]">
<div className="text-caption text-fg-disabled leading-[1.6]">
· <b className="text-color-accent"> </b>{' '}
&nbsp;|&nbsp; / {' '}
<b className="text-color-accent">, </b> &nbsp;|&nbsp; {' '}
@ -2080,11 +2080,11 @@ function HmsDetailPanel({
{nfpa.reactivity}
</text>
</svg>
<div className="text-center text-[7px] font-semibold text-fg-disabled mt-0.5">
<div className="text-center text-caption font-semibold text-fg-disabled mt-0.5">
NFPA 704
</div>
</div>
<div className="flex-1 flex flex-col gap-1 text-[8px]">
<div className="flex-1 flex flex-col gap-1 text-caption">
<div
style={{
padding: '4px 8px',
@ -2295,7 +2295,7 @@ function HmsDetailPanel({
>
<div className="text-base mb-[3px]">🧑🚒</div>
<div className="font-bold text-color-accent"></div>
<div className="text-[8px] text-fg-disabled mt-0.5">{s.ppeClose}</div>
<div className="text-caption text-fg-disabled mt-0.5">{s.ppeClose}</div>
</div>
<div
className="text-center rounded"
@ -2307,7 +2307,7 @@ function HmsDetailPanel({
>
<div className="text-base mb-[3px]">🦺</div>
<div className="font-bold text-color-accent"></div>
<div className="text-[8px] text-fg-disabled mt-0.5">{s.ppeFar}</div>
<div className="text-caption text-fg-disabled mt-0.5">{s.ppeFar}</div>
</div>
</div>
</div>
@ -2328,7 +2328,7 @@ function HmsDetailPanel({
📄 MSDS
</div>
<button
className="text-[8px] font-semibold cursor-pointer rounded"
className="text-caption font-semibold cursor-pointer rounded"
style={{
padding: '3px 10px',
background: 'rgba(6,182,212,.1)',
@ -2339,7 +2339,7 @@ function HmsDetailPanel({
📥
</button>
</div>
<div className="text-[8px] text-fg-sub leading-[1.7] p-2.5">
<div className="text-caption text-fg-sub leading-[1.7] p-2.5">
<b>§2 ·:</b> {s.msds.hazard}
<br />
<b>§4 :</b> {s.msds.firstAid}
@ -2607,7 +2607,7 @@ function HmsDetailPanel({
<div className="text-label-1 font-bold text-color-accent">
📋
</div>
<div className="text-[8px] text-fg-disabled"> </div>
<div className="text-caption text-fg-disabled"> </div>
</div>
<div className="p-3">
<table className="w-full border-collapse text-caption">
@ -2714,7 +2714,9 @@ function HmsDetailPanel({
}}
>
<div className="text-label-1 font-bold text-color-accent">🏗 </div>
<div className="text-[8px] text-fg-disabled">Port-MIS </div>
<div className="text-caption text-fg-disabled">
Port-MIS
</div>
</div>
<div className="p-3">
<table className="w-full border-collapse text-caption">

파일 보기

@ -3849,15 +3849,16 @@ function RealtimeComparePanel() {
<button
className="px-4 py-1.5 rounded-md text-caption font-bold cursor-pointer text-white"
style={{
background: 'var(--color-accent)',
border: 'none',
// background: 'var(--color-accent)',
// border: '1px solid var(--color-accent)',
color: 'var(--color-accent)',
}}
>
</button>
<button
className="px-3 py-1.5 rounded-md text-caption font-semibold cursor-pointer text-fg-sub bg-bg-card"
style={{ border: '1px solid var(--stroke-default)' }}
style={{ color: 'var(--color-accent)' }}
>
</button>

파일 보기

@ -171,7 +171,7 @@ function HNSManualViewer() {
<div className="text-label-2 font-bold" style={{ color: `${s.color},1)` }}>
{s.label}
</div>
<div className="text-fg-disabled whitespace-pre-line text-[8px] mt-[3px] leading-[1.3]">
<div className="text-fg-disabled whitespace-pre-line text-caption mt-[3px] leading-[1.3]">
{s.desc}
</div>
</div>
@ -180,7 +180,7 @@ function HNSManualViewer() {
</div>
{/* 출처 */}
<div className="text-fg-disabled rounded-sm bg-bg-card p-[10px] text-[8px] leading-[1.5]">
<div className="text-fg-disabled rounded-sm bg-bg-card p-[10px] text-caption leading-[1.5]">
<b>:</b> Marine HNS Response Manual Bonn Agreement / HELCOM / REMPEC (WestMOPoCo
Project, 2024 )
<br />

파일 보기

@ -12,42 +12,101 @@ import { useState } from 'react';
type Status = 'forbidden' | 'allowed' | 'conditional';
interface DischargeRule {
category: string
item: string
zones: [Status, Status, Status, Status, Status] // [~3NM, 3~12NM, 12~25NM, 25~50NM, 50NM+]
condition?: string
category: string;
item: string;
zones: [Status, Status, Status, Status, Status]; // [~3NM, 3~12NM, 12~25NM, 25~50NM, 50NM+]
condition?: string;
}
const RULES: DischargeRule[] = [
// 분뇨
{ category: '분뇨', item: '분뇨마쇄소독장치', zones: ['forbidden', 'conditional', 'conditional', 'conditional', 'conditional'], condition: '항속 4노트 이상시 서서히 배출 / 400톤 미만 국내항해 선박은 3해리 이내 가능' },
{ category: '분뇨', item: '분뇨저장탱크', zones: ['forbidden', 'forbidden', 'conditional', 'conditional', 'conditional'], condition: '항속 4노트 이상시 서서히 배출' },
{ category: '분뇨', item: '분뇨처리장치', zones: ['allowed', 'allowed', 'allowed', 'allowed', 'allowed'], condition: '수산자원보호구역, 보호수면 및 육성수면은 불가' },
{
category: '분뇨',
item: '분뇨마쇄소독장치',
zones: ['forbidden', 'conditional', 'conditional', 'conditional', 'conditional'],
condition: '항속 4노트 이상시 서서히 배출 / 400톤 미만 국내항해 선박은 3해리 이내 가능',
},
{
category: '분뇨',
item: '분뇨저장탱크',
zones: ['forbidden', 'forbidden', 'conditional', 'conditional', 'conditional'],
condition: '항속 4노트 이상시 서서히 배출',
},
{
category: '분뇨',
item: '분뇨처리장치',
zones: ['allowed', 'allowed', 'allowed', 'allowed', 'allowed'],
condition: '수산자원보호구역, 보호수면 및 육성수면은 불가',
},
// 음식물찌꺼기
{ category: '음식물찌꺼기', item: '미분쇄 음식물', zones: ['forbidden', 'forbidden', 'allowed', 'allowed', 'allowed'] },
{ category: '음식물찌꺼기', item: '분쇄·연마 음식물 (25mm 이하)', zones: ['forbidden', 'conditional', 'allowed', 'allowed', 'allowed'], condition: '25mm 이하 개구 스크린 통과 가능시' },
{
category: '음식물찌꺼기',
item: '미분쇄 음식물',
zones: ['forbidden', 'forbidden', 'allowed', 'allowed', 'allowed'],
},
{
category: '음식물찌꺼기',
item: '분쇄·연마 음식물 (25mm 이하)',
zones: ['forbidden', 'conditional', 'allowed', 'allowed', 'allowed'],
condition: '25mm 이하 개구 스크린 통과 가능시',
},
// 화물잔류물
{ category: '화물잔류물', item: '부유성 화물잔류물', zones: ['forbidden', 'forbidden', 'forbidden', 'allowed', 'allowed'] },
{ category: '화물잔류물', item: '침강성 화물잔류물', zones: ['forbidden', 'forbidden', 'allowed', 'allowed', 'allowed'] },
{ category: '화물잔류물', item: '화물창 세정수', zones: ['forbidden', 'forbidden', 'conditional', 'conditional', 'conditional'], condition: '해양환경에 해롭지 않은 일반세제 사용시' },
{
category: '화물잔류물',
item: '부유성 화물잔류물',
zones: ['forbidden', 'forbidden', 'forbidden', 'allowed', 'allowed'],
},
{
category: '화물잔류물',
item: '침강성 화물잔류물',
zones: ['forbidden', 'forbidden', 'allowed', 'allowed', 'allowed'],
},
{
category: '화물잔류물',
item: '화물창 세정수',
zones: ['forbidden', 'forbidden', 'conditional', 'conditional', 'conditional'],
condition: '해양환경에 해롭지 않은 일반세제 사용시',
},
// 화물유
{ category: '화물유', item: '화물유 섞인 평형수·세정수·선저폐수', zones: ['forbidden', 'forbidden', 'forbidden', 'forbidden', 'conditional'], condition: '항해 중, 순간배출률 1해리당 30L 이하, 기름오염방지설비 작동 중' },
{
category: '화물유',
item: '화물유 섞인 평형수·세정수·선저폐수',
zones: ['forbidden', 'forbidden', 'forbidden', 'forbidden', 'conditional'],
condition: '항해 중, 순간배출률 1해리당 30L 이하, 기름오염방지설비 작동 중',
},
// 유해액체물질
{ category: '유해액체물질', item: '유해액체물질 섞인 세정수', zones: ['forbidden', 'forbidden', 'conditional', 'conditional', 'conditional'], condition: '자항선 7노트/비자항선 4노트 이상, 수심 25m 이상, 수면하 배출구 사용' },
{
category: '유해액체물질',
item: '유해액체물질 섞인 세정수',
zones: ['forbidden', 'forbidden', 'conditional', 'conditional', 'conditional'],
condition: '자항선 7노트/비자항선 4노트 이상, 수심 25m 이상, 수면하 배출구 사용',
},
// 폐기물
{ category: '폐기물', item: '플라스틱 제품', zones: ['forbidden', 'forbidden', 'forbidden', 'forbidden', 'forbidden'] },
{ category: '폐기물', item: '포장유해물질·용기', zones: ['forbidden', 'forbidden', 'forbidden', 'forbidden', 'forbidden'] },
{ category: '폐기물', item: '중금속 포함 쓰레기', zones: ['forbidden', 'forbidden', 'forbidden', 'forbidden', 'forbidden'] },
]
{
category: '폐기물',
item: '플라스틱 제품',
zones: ['forbidden', 'forbidden', 'forbidden', 'forbidden', 'forbidden'],
},
{
category: '폐기물',
item: '포장유해물질·용기',
zones: ['forbidden', 'forbidden', 'forbidden', 'forbidden', 'forbidden'],
},
{
category: '폐기물',
item: '중금속 포함 쓰레기',
zones: ['forbidden', 'forbidden', 'forbidden', 'forbidden', 'forbidden'],
},
];
const ZONE_LABELS = ['~3해리', '3~12해리', '12~25해리', '25~50해리', '50해리+']
const ZONE_COLORS = ['#ef4444', '#f97316', '#eab308', '#22c55e', '#64748b']
const ZONE_LABELS = ['~3해리', '3~12해리', '12~25해리', '25~50해리', '50해리+'];
const ZONE_COLORS = ['#ef4444', '#f97316', '#eab308', '#22c55e', '#64748b'];
function StatusBadge({ status }: { status: Status }) {
if (status === 'forbidden')
return (
<span
className="text-[8px] font-bold px-1.5 py-0.5 rounded"
className="text-caption font-bold px-1.5 py-0.5 rounded"
style={{ background: 'rgba(239,68,68,0.15)', color: 'var(--color-danger)' }}
>
@ -56,7 +115,7 @@ function StatusBadge({ status }: { status: Status }) {
if (status === 'allowed')
return (
<span
className="text-[8px] font-bold px-1.5 py-0.5 rounded"
className="text-caption font-bold px-1.5 py-0.5 rounded"
style={{ background: 'rgba(34,197,94,0.15)', color: 'var(--color-success)' }}
>
@ -64,7 +123,7 @@ function StatusBadge({ status }: { status: Status }) {
);
return (
<span
className="text-[8px] font-bold px-1.5 py-0.5 rounded"
className="text-caption font-bold px-1.5 py-0.5 rounded"
style={{ background: 'rgba(234,179,8,0.15)', color: 'var(--color-caution)' }}
>
@ -73,16 +132,22 @@ function StatusBadge({ status }: { status: Status }) {
}
interface DischargeZonePanelProps {
lat: number
lon: number
distanceNm: number
zoneIndex: number
onClose: () => void
lat: number;
lon: number;
distanceNm: number;
zoneIndex: number;
onClose: () => void;
}
export function DischargeZonePanel({ lat, lon, distanceNm, zoneIndex, onClose }: DischargeZonePanelProps) {
const zoneIdx = zoneIndex
const [expandedCat, setExpandedCat] = useState<string | null>(null)
export function DischargeZonePanel({
lat,
lon,
distanceNm,
zoneIndex,
onClose,
}: DischargeZonePanelProps) {
const zoneIdx = zoneIndex;
const [expandedCat, setExpandedCat] = useState<string | null>(null);
const categories = [...new Set(RULES.map((r) => r.category))];
@ -108,10 +173,10 @@ export function DischargeZonePanel({ lat, lon, distanceNm, zoneIndex, onClose }:
}}
>
<div>
<div className="text-[11px] font-bold text-fg font-korean">🚢 </div>
<div className="text-[8px] text-fg-sub font-korean"> 22</div>
<div className="text-label-2 font-bold text-fg font-korean">🚢 </div>
<div className="text-caption text-fg-sub font-korean"> 22</div>
</div>
<span onClick={onClose} className="text-[14px] cursor-pointer text-fg-sub hover:text-fg">
<span onClick={onClose} className="text-title-3 cursor-pointer text-fg-sub hover:text-fg">
</span>
</div>
@ -122,14 +187,17 @@ export function DischargeZonePanel({ lat, lon, distanceNm, zoneIndex, onClose }:
style={{ padding: '8px 14px', borderBottom: '1px solid var(--stroke-light)' }}
>
<div className="flex items-center justify-between mb-1.5">
<span className="text-[9px] text-fg-sub font-korean"> </span>
<span className="text-[9px] text-fg font-mono">
<span className="text-caption text-fg-sub font-korean"> </span>
<span className="text-caption text-fg font-mono">
{lat.toFixed(4)}°N, {lon.toFixed(4)}°E
</span>
</div>
<div className="flex items-center justify-between mb-2">
<span className="text-[9px] text-fg-sub font-korean"> </span>
<span className="text-[11px] font-bold font-mono" style={{ color: ZONE_COLORS[zoneIdx] }}>
<span className="text-caption text-fg-sub font-korean"> </span>
<span
className="text-label-2 font-bold font-mono"
style={{ color: ZONE_COLORS[zoneIdx] }}
>
{distanceNm.toFixed(1)} NM
</span>
</div>
@ -177,13 +245,13 @@ export function DischargeZonePanel({ lat, lon, distanceNm, zoneIndex, onClose }:
<div
style={{ width: 6, height: 6, borderRadius: '50%', background: summaryColor }}
/>
<span className="text-[10px] font-bold text-fg font-korean">{cat}</span>
<span className="text-caption font-bold text-fg font-korean">{cat}</span>
</div>
<div className="flex items-center gap-2">
<span className="text-[8px] font-semibold" style={{ color: summaryColor }}>
<span className="text-caption font-semibold" style={{ color: summaryColor }}>
{allForbidden ? '전체 불가' : allAllowed ? '전체 가능' : '항목별 상이'}
</span>
<span className="text-[9px] text-fg-sub">{isExpanded ? '▾' : '▸'}</span>
<span className="text-caption text-fg-sub">{isExpanded ? '▾' : '▸'}</span>
</div>
</div>
@ -200,7 +268,7 @@ export function DischargeZonePanel({ lat, lon, distanceNm, zoneIndex, onClose }:
borderRadius: 4,
}}
>
<span className="text-[9px] text-fg font-korean">{rule.item}</span>
<span className="text-caption text-fg font-korean">{rule.item}</span>
<StatusBadge status={rule.zones[zoneIdx]} />
</div>
))}
@ -211,7 +279,7 @@ export function DischargeZonePanel({ lat, lon, distanceNm, zoneIndex, onClose }:
.map((r, i) => (
<div
key={i}
className="text-[7px] text-fg-sub font-korean leading-relaxed"
className="text-caption text-fg-sub font-korean leading-relaxed"
>
💡 {r.item}: {r.condition}
</div>
@ -230,7 +298,7 @@ export function DischargeZonePanel({ lat, lon, distanceNm, zoneIndex, onClose }:
className="shrink-0"
style={{ padding: '6px 14px', borderTop: '1px solid var(--stroke-light)' }}
>
<div className="text-[7px] text-fg-sub font-korean leading-relaxed">
<div className="text-caption text-fg-sub font-korean leading-relaxed">
. .
</div>
</div>

파일 보기

@ -233,10 +233,10 @@ export function IncidentsLeftPanel({
setSelectedPeriod('');
resetPage();
}}
className="bg-bg-base border border-stroke font-mono text-[11px] outline-none flex-1"
className="bg-bg-base border border-stroke font-mono text-label-2 outline-none flex-1"
style={{ padding: '5px 8px', borderRadius: 'var(--radius-sm)' }}
/>
<span className="text-fg-disabled text-[11px]">~</span>
<span className="text-fg-disabled text-label-2">~</span>
<input
type="date"
value={dateTo}
@ -245,12 +245,12 @@ export function IncidentsLeftPanel({
setSelectedPeriod('');
resetPage();
}}
className="bg-bg-base border border-stroke font-mono text-[11px] outline-none flex-1"
className="bg-bg-base border border-stroke font-mono text-label-2 outline-none flex-1"
style={{ padding: '5px 8px', borderRadius: 'var(--radius-sm)' }}
/>
<button
onClick={resetPage}
className="text-[11px] font-semibold cursor-pointer whitespace-nowrap text-white border-none"
className="text-label-2 font-semibold cursor-pointer whitespace-nowrap text-white border-none"
style={{
padding: '5px 12px',
background: 'linear-gradient(135deg,var(--color-accent),var(--color-info))',
@ -267,7 +267,7 @@ export function IncidentsLeftPanel({
<button
key={p}
onClick={() => handlePeriodClick(p)}
className="text-[10px] font-semibold cursor-pointer"
className="text-caption font-semibold cursor-pointer"
style={{
padding: '3px 8px',
borderRadius: '14px',
@ -290,7 +290,7 @@ export function IncidentsLeftPanel({
style={{ background: 'rgba(6,182,212,0.03)' }}
>
<div
className="text-[10px] font-bold text-fg-disabled mb-2"
className="text-caption font-bold text-fg-disabled mb-2"
style={{ letterSpacing: '0.8px' }}
>
📅 ({todayLabel})
@ -306,7 +306,7 @@ export function IncidentsLeftPanel({
setSelectedRegion(r);
resetPage();
}}
className="text-[11px] cursor-pointer"
className="text-label-2 cursor-pointer"
style={{
padding: '4px 10px',
borderRadius: 'var(--radius-sm)',
@ -349,7 +349,7 @@ export function IncidentsLeftPanel({
setSelectedStatus(s.id);
resetPage();
}}
className="flex items-center gap-1 text-[10px] font-semibold cursor-pointer"
className="flex items-center gap-1 text-caption font-semibold cursor-pointer"
style={{
padding: '4px 10px',
borderRadius: '12px',
@ -372,7 +372,7 @@ export function IncidentsLeftPanel({
</div>
{/* Count */}
<div className="px-4 py-1.5 text-[11px] text-fg-disabled shrink-0 border-b border-stroke">
<div className="px-4 py-1.5 text-label-2 text-fg-disabled shrink-0 border-b border-stroke">
{filteredIncidents.length}
</div>
@ -385,7 +385,7 @@ export function IncidentsLeftPanel({
}}
>
{pagedIncidents.length === 0 ? (
<div className="px-4 py-10 text-center text-fg-disabled text-[11px]">
<div className="px-4 py-10 text-center text-fg-disabled text-label-2">
.
</div>
) : (
@ -449,7 +449,7 @@ export function IncidentsLeftPanel({
{inc.name}
</div>
<span
className="shrink-0 text-[10px] font-semibold"
className="shrink-0 text-caption font-semibold"
style={{
padding: '2px 10px',
borderRadius: '10px',
@ -461,7 +461,7 @@ export function IncidentsLeftPanel({
</span>
</div>
{/* Row 2: meta */}
<div className="flex items-center gap-2 text-[10px] text-fg-disabled mb-[5px]">
<div className="flex items-center gap-2 text-caption text-fg-disabled mb-[5px]">
<span>
📅 {inc.date} {inc.time}
</span>
@ -472,7 +472,7 @@ export function IncidentsLeftPanel({
<div className="flex flex-wrap gap-1">
{inc.causeType && (
<span
className="text-[10px] font-medium text-fg-sub"
className="text-caption font-medium text-fg-sub"
style={{
padding: '2px 8px',
borderRadius: '3px',
@ -485,7 +485,7 @@ export function IncidentsLeftPanel({
)}
{inc.oilType && (
<span
className="text-[10px] font-medium text-color-warning"
className="text-caption font-medium text-color-warning"
style={{
padding: '2px 8px',
borderRadius: '3px',
@ -498,7 +498,7 @@ export function IncidentsLeftPanel({
)}
{inc.prediction && (
<span
className="text-[10px] font-medium text-color-success"
className="text-caption font-medium text-color-success"
style={{
padding: '2px 8px',
borderRadius: '3px',
@ -512,7 +512,7 @@ export function IncidentsLeftPanel({
</div>
<div className="flex gap-1">
<button
className="inc-wx-btn cursor-pointer text-[11px]"
className="inc-wx-btn cursor-pointer text-label-2"
onClick={(e) => handleWeatherClick(e, inc.id)}
title="사고 위치 기상정보"
style={{
@ -537,7 +537,7 @@ export function IncidentsLeftPanel({
setMediaModalIncident(inc);
}}
title="현장정보 조회"
className="cursor-pointer text-[11px]"
className="cursor-pointer text-label-2"
style={{
padding: '3px 7px',
borderRadius: '4px',
@ -548,7 +548,7 @@ export function IncidentsLeftPanel({
transition: '0.15s',
}}
>
📹 <span className="text-[8px]">{inc.mediaCount}</span>
📹 <span className="text-caption">{inc.mediaCount}</span>
</button>
)}
</div>
@ -576,7 +576,7 @@ export function IncidentsLeftPanel({
{/* Pagination */}
<div className="flex items-center justify-between bg-bg-surface shrink-0 border-t border-stroke px-3 py-2">
<div className="text-[9px] text-fg-disabled">
<div className="text-caption text-fg-disabled">
<b>{filteredIncidents.length}</b> {(safePage - 1) * pageSize + 1}-
{Math.min(safePage * pageSize, filteredIncidents.length)}
</div>
@ -612,7 +612,7 @@ export function IncidentsLeftPanel({
onChange={(e) => {
/* page size change placeholder */ void e;
}}
className="bg-bg-base border border-stroke text-fg-sub text-[9px] outline-none rounded px-1.5 py-[3px]"
className="bg-bg-base border border-stroke text-fg-sub text-caption outline-none rounded px-1.5 py-[3px]"
>
<option>6</option>
<option>10</option>
@ -638,7 +638,7 @@ function PgBtn({
<button
onClick={onClick}
disabled={disabled}
className="flex items-center justify-center font-mono text-[9px]"
className="flex items-center justify-center font-mono text-caption"
style={{
minWidth: '24px',
height: '24px',
@ -694,8 +694,8 @@ const WeatherPopup = forwardRef<
<div className="flex items-center gap-1.5">
<span className="text-sm">🌤</span>
<div>
<div className="text-[11px] font-bold">{data?.locNm || '기상정보 없음'}</div>
<div className="text-fg-disabled font-mono text-[8px]">{data?.obsDtm || '-'}</div>
<div className="text-label-2 font-bold">{data?.locNm || '기상정보 없음'}</div>
<div className="text-fg-disabled font-mono text-caption">{data?.obsDtm || '-'}</div>
</div>
</div>
<span onClick={onClose} className="cursor-pointer text-fg-disabled text-sm p-0.5">
@ -710,12 +710,12 @@ const WeatherPopup = forwardRef<
<div className="text-[28px]">{data?.icon || '❓'}</div>
<div>
<div className="font-bold font-mono text-[20px]">{data?.temp || '-'}</div>
<div className="text-fg-disabled text-[9px]">{data?.weatherDc || '-'}</div>
<div className="text-fg-disabled text-caption">{data?.weatherDc || '-'}</div>
</div>
</div>
{/* Detail grid */}
<div className="grid grid-cols-2 gap-1.5 text-[9px]">
<div className="grid grid-cols-2 gap-1.5 text-caption">
<WxCell icon="💨" label="풍향/풍속" value={data?.wind} />
<WxCell icon="🌊" label="파고" value={data?.wave} />
<WxCell icon="💧" label="습도" value={data?.humid} />
@ -735,8 +735,8 @@ const WeatherPopup = forwardRef<
>
<span className="text-xs"></span>
<div>
<div className="text-fg-disabled text-[7px]"> ()</div>
<div className="font-bold font-mono text-[10px] text-color-info">
<div className="text-fg-disabled text-caption"> ()</div>
<div className="font-bold font-mono text-caption text-color-info">
{data?.highTide || '-'}
</div>
</div>
@ -747,8 +747,8 @@ const WeatherPopup = forwardRef<
>
<span className="text-xs"></span>
<div>
<div className="text-fg-disabled text-[7px]"> ()</div>
<div className="text-color-accent font-bold font-mono text-[10px]">
<div className="text-fg-disabled text-caption"> ()</div>
<div className="text-color-accent font-bold font-mono text-caption">
{data?.lowTide || '-'}
</div>
</div>
@ -757,9 +757,9 @@ const WeatherPopup = forwardRef<
{/* 24h Forecast */}
<div className="bg-bg-base mt-2.5 px-2.5 py-2 rounded-md">
<div className="font-bold text-fg-disabled text-[8px] mb-1.5">24h </div>
<div className="font-bold text-fg-disabled text-caption mb-1.5">24h </div>
{forecast.length > 0 ? (
<div className="flex justify-between font-mono text-fg-sub text-[8px]">
<div className="flex justify-between font-mono text-fg-sub text-caption">
{forecast.map((f, i) => (
<div key={i} className="text-center">
<div>{f.hour}</div>
@ -769,7 +769,7 @@ const WeatherPopup = forwardRef<
))}
</div>
) : (
<div className="text-fg-disabled text-center text-[8px] py-1"> </div>
<div className="text-fg-disabled text-center text-caption py-1"> </div>
)}
</div>
@ -782,8 +782,8 @@ const WeatherPopup = forwardRef<
border: '1px solid rgba(249,115,22,0.12)',
}}
>
<div className="font-bold text-color-warning text-[8px] mb-[3px]"> </div>
<div className="text-fg-sub text-[8px] leading-[1.5]">{data?.impactDc || '-'}</div>
<div className="font-bold text-color-warning text-caption mb-[3px]"> </div>
<div className="text-fg-sub text-caption leading-[1.5]">{data?.impactDc || '-'}</div>
</div>
</div>
</div>
@ -794,9 +794,9 @@ WeatherPopup.displayName = 'WeatherPopup';
function WxCell({ icon, label, value }: { icon: string; label: string; value?: string | null }) {
return (
<div className="flex items-center bg-bg-base rounded gap-[6px] py-1.5 px-2">
<span className="text-[12px]">{icon}</span>
<span className="text-label-1">{icon}</span>
<div>
<div className="text-fg-disabled text-[7px]">{label}</div>
<div className="text-fg-disabled text-caption">{label}</div>
<div className="font-semibold font-mono">{value || '-'}</div>
</div>
</div>

파일 보기

@ -311,7 +311,7 @@ export function IncidentsRightPanel({
if (!incident) {
return (
<div className="flex flex-col items-center justify-center bg-bg-surface border-l border-stroke w-[280px] min-w-[280px]">
<div className="text-center text-fg-disabled text-[11px]">
<div className="text-center text-fg-disabled text-label-2">
<div className="text-[32px] mb-2 opacity-30">📊</div>
<br />
@ -326,7 +326,7 @@ export function IncidentsRightPanel({
{/* Header */}
<div className="px-[14px] py-2.5 border-b border-stroke shrink-0">
<div className="text-xs font-bold mb-0.5">🔬 </div>
<div className="text-[9px] text-fg-disabled">
<div className="text-caption text-fg-disabled">
: <b className="text-color-accent">{incident.name}</b>
</div>
</div>
@ -350,7 +350,7 @@ export function IncidentsRightPanel({
</span>
</div>
<button
className="text-[10px] font-semibold cursor-pointer"
className="text-caption font-semibold cursor-pointer"
style={{
padding: '3px 10px',
borderRadius: '4px',
@ -364,7 +364,7 @@ export function IncidentsRightPanel({
</div>
<div className="flex flex-col gap-1">
{sec.items.length === 0 ? (
<div className="text-[9px] text-fg-disabled text-center py-1.5">
<div className="text-caption text-fg-disabled text-center py-1.5">
</div>
) : (
@ -387,15 +387,15 @@ export function IncidentsRightPanel({
style={{ accentColor: sec.color }}
/>
<div className="flex-1 min-w-0">
<div className="text-[10px] font-semibold whitespace-nowrap overflow-hidden text-ellipsis">
<div className="text-caption font-semibold whitespace-nowrap overflow-hidden text-ellipsis">
{item.name}
</div>
<div className="text-fg-disabled font-mono text-[8px]">{item.sub}</div>
<div className="text-fg-disabled font-mono text-caption">{item.sub}</div>
</div>
<span
onClick={() => removePredItem(item.id)}
title="제거"
className="text-[10px] cursor-pointer text-fg-disabled shrink-0"
className="text-caption cursor-pointer text-fg-disabled shrink-0"
>
</span>
@ -403,7 +403,7 @@ export function IncidentsRightPanel({
))
)}
</div>
<div className="flex items-center gap-1.5 mt-1.5 text-[9px] text-fg-disabled">
<div className="flex items-center gap-1.5 mt-1.5 text-caption text-fg-disabled">
: <b style={{ color: sec.color }}>{checkedCount}</b> · {sec.totalLabel}
</div>
</div>
@ -421,7 +421,7 @@ export function IncidentsRightPanel({
</span>
</div>
<button
className="text-[10px] font-semibold cursor-pointer"
className="text-caption font-semibold cursor-pointer"
style={{
padding: '3px 10px',
borderRadius: '4px',
@ -433,8 +433,8 @@ export function IncidentsRightPanel({
📋
</button>
</div>
<div className="text-[9px] text-fg-disabled text-center py-1.5"> </div>
<div className="flex items-center gap-1.5 mt-1.5 text-[9px] text-fg-disabled">
<div className="text-caption text-fg-disabled text-center py-1.5"> </div>
<div className="flex items-center gap-1.5 mt-1.5 text-caption text-fg-disabled">
: <b style={{ color: sec.color }}>0</b> · 0
</div>
</div>
@ -448,7 +448,7 @@ export function IncidentsRightPanel({
</div>
<div className="flex flex-col gap-[3px]">
{sensCategories.length === 0 ? (
<div className="text-[9px] text-fg-disabled text-center py-1.5">
<div className="text-caption text-fg-disabled text-center py-1.5">
</div>
) : (
@ -463,7 +463,7 @@ export function IncidentsRightPanel({
return (
<label
key={cat.category}
className="flex items-center cursor-pointer text-[9px] gap-[5px] rounded-[3px]"
className="flex items-center cursor-pointer text-caption gap-[5px] rounded-[3px]"
style={{ padding: '4px 6px', background: `rgba(${r},${g},${b},0.06)` }}
>
<input
@ -499,23 +499,23 @@ export function IncidentsRightPanel({
<span className="text-sm">🛡</span>
<span className="text-xs font-bold text-color-boom"> </span>
{nearbyOrgs.length > 0 && (
<span className="ml-auto text-[9px] font-mono text-color-boom">
<span className="ml-auto text-caption font-mono text-color-boom">
{nearbyOrgs.length}
</span>
)}
</div>
{!selectedVessel ? (
<div className="py-2.5 text-center text-fg-disabled text-[10px] leading-[1.7]">
<div className="py-2.5 text-center text-fg-disabled text-caption leading-[1.7]">
<div className="text-xl mb-1 opacity-40">🚢</div>
<br />
</div>
) : nearbyLoading ? (
<div className="py-2.5 text-center text-fg-disabled text-[10px]"> ...</div>
<div className="py-2.5 text-center text-fg-disabled text-caption"> ...</div>
) : nearbyOrgs.length === 0 ? (
<div className="py-2.5 text-center text-fg-disabled text-[10px]">
<div className="py-2.5 text-center text-fg-disabled text-caption">
</div>
) : (
@ -532,19 +532,19 @@ export function IncidentsRightPanel({
<div className="flex-1 min-w-0">
<div className="flex items-center gap-1 mb-[2px]">
<span
className="text-[8px] px-[4px] py-[1px] rounded-[2px] font-bold shrink-0"
className="text-caption px-[4px] py-[1px] rounded-[2px] font-bold shrink-0"
style={{ background: 'rgba(245,158,11,0.15)', color: '#f59e0b' }}
>
{org.orgTp}
</span>
<span className="text-[10px] font-bold text-fg truncate">{org.orgNm}</span>
<span className="text-caption font-bold text-fg truncate">{org.orgNm}</span>
</div>
<div className="text-[9px] text-fg-disabled">
<div className="text-caption text-fg-disabled">
{org.areaNm}
{org.totalAssets > 0 ? ` · 장비 ${org.totalAssets}` : ''}
</div>
</div>
<span className="text-[9px] font-mono text-color-boom shrink-0">
<span className="text-caption font-mono text-color-boom shrink-0">
{org.distanceNm.toFixed(1)} nm
</span>
</div>
@ -555,8 +555,8 @@ export function IncidentsRightPanel({
{/* Radius slider */}
<div className="mt-2 pt-2" style={{ borderTop: '1px solid rgba(245,158,11,0.1)' }}>
<div className="flex items-center justify-between mb-[5px]">
<span className="text-[9px] text-fg-disabled"> </span>
<span className="text-[10px] font-bold font-mono text-color-boom">
<span className="text-caption text-fg-disabled"> </span>
<span className="text-caption font-bold font-mono text-color-boom">
{nearbyRadius} nm
</span>
</div>
@ -593,7 +593,7 @@ export function IncidentsRightPanel({
<button
key={v.mode}
onClick={() => onViewModeChange(v.mode)}
className="flex-1 text-[10px] cursor-pointer"
className="flex-1 text-caption cursor-pointer"
style={{
padding: '6px',
borderRadius: 'var(--radius-sm)',
@ -624,7 +624,7 @@ export function IncidentsRightPanel({
const sensChecked = checkedSensCategories.size;
onRunAnalysis(checkedSections, sensChecked);
}}
className="w-full text-[11px] font-bold cursor-pointer"
className="w-full text-label-2 font-bold cursor-pointer"
style={{
padding: '8px',
background: analysisActive

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다. Load Diff

파일 보기

@ -60,7 +60,7 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose:
style={{ background: 'rgba(0,0,0,0.7)', backdropFilter: 'blur(6px)' }}
>
<div
className="text-center text-[12px] text-fg-disabled"
className="text-center text-label-1 text-fg-disabled"
style={{
width: 300,
padding: 40,
@ -114,8 +114,8 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose:
<div className="flex items-center gap-[10px]">
<span className="text-lg">📋</span>
<div>
<div className="text-[14px] font-[800] text-fg"> {incident.name}</div>
<div className="text-[10px] text-fg-disabled font-mono">
<div className="text-title-3 font-[800] text-fg"> {incident.name}</div>
<div className="text-caption text-fg-disabled font-mono">
{incident.name} · {incident.date} · {media.photoCnt} / {media.videoCnt} /
{media.satCnt} / CCTV {media.cctvCnt}
</div>
@ -161,7 +161,7 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose:
{/* Close */}
<span
onClick={onClose}
className="text-[18px] cursor-pointer text-fg-disabled rounded"
className="text-title-1 cursor-pointer text-fg-disabled rounded"
style={{ padding: '2px 6px' }}
>
@ -174,7 +174,7 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose:
className="shrink-0 flex items-center gap-[10px]"
style={{ padding: '6px 20px', borderBottom: '1px solid var(--stroke-light)' }}
>
<span className="text-[9px] text-fg-disabled whitespace-nowrap">TIMELINE</span>
<span className="text-caption text-fg-disabled whitespace-nowrap">TIMELINE</span>
<div className="flex-1 relative" style={{ height: 16 }}>
<div
style={{
@ -204,7 +204,7 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose:
/>
))}
</div>
<div className="flex gap-2 text-[8px] font-mono text-fg-disabled whitespace-nowrap">
<div className="flex gap-2 text-caption font-mono text-fg-disabled whitespace-nowrap">
<span style={{ color: '#ef4444' }}> </span>
<span style={{ color: '#f59e0b' }}> </span>
<span className="text-fg-disabled"> </span>
@ -231,8 +231,8 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose:
style={{ padding: '8px 16px', borderBottom: '1px solid var(--stroke-light)' }}
>
<div className="flex items-center gap-[6px]">
<span className="text-[12px]">📷</span>
<span className="text-[12px] font-bold text-fg">
<span className="text-label-1">📷</span>
<span className="text-label-1 font-bold text-fg">
{str(media.photoMeta, 'title', '현장 사진')}
</span>
</div>
@ -245,10 +245,10 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose:
<div className="text-[48px]" style={{ color: 'var(--stroke-default)' }}>
📷
</div>
<div className="text-[12px] text-fg-sub font-semibold">
<div className="text-label-1 text-fg-sub font-semibold">
{incident.name.replace('유류유출', '유출 현장').replace('오염', '현장')}
</div>
<div className="text-[9px] text-fg-disabled font-mono">
<div className="text-caption text-fg-disabled font-mono">
{str(media.photoMeta, 'date')} · {str(media.photoMeta, 'by')}
</div>
</div>
@ -262,7 +262,7 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose:
(_, i) => (
<div
key={i}
className="flex items-center justify-center text-[14px] cursor-pointer"
className="flex items-center justify-center text-title-3 cursor-pointer"
style={{
width: 40,
height: 36,
@ -281,10 +281,12 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose:
)}
</div>
<div className="flex justify-between items-center">
<span className="text-[8px] text-fg-disabled">
<span className="text-caption text-fg-disabled">
📷 {num(media.photoMeta, 'thumbCount')} · {str(media.photoMeta, 'stage')}
</span>
<span className="text-[8px] text-color-tertiary cursor-pointer">🔗 R&D </span>
<span className="text-caption text-color-tertiary cursor-pointer">
🔗 R&D
</span>
</div>
</div>
</div>
@ -298,13 +300,13 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose:
style={{ padding: '8px 16px', borderBottom: '1px solid var(--stroke-light)' }}
>
<div className="flex items-center gap-[6px]">
<span className="text-[12px]">🎬</span>
<span className="text-[12px] font-bold text-fg">
<span className="text-label-1">🎬</span>
<span className="text-label-1 font-bold text-fg">
{str(media.droneMeta, 'title', '드론 영상')}
</span>
</div>
<span
className="text-[9px] font-bold text-color-danger rounded"
className="text-caption font-bold text-color-danger rounded"
style={{
padding: '2px 8px',
background: 'rgba(239,68,68,0.15)',
@ -317,8 +319,8 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose:
<div className="text-[48px]" style={{ color: 'var(--stroke-default)' }}>
🎬
</div>
<div className="text-[12px] text-fg-sub font-semibold"> </div>
<div className="text-[9px] text-fg-disabled font-mono">
<div className="text-label-1 text-fg-sub font-semibold"> </div>
<div className="text-caption text-fg-disabled font-mono">
{str(media.droneMeta, 'device')} · {str(media.droneMeta, 'alt')}
</div>
</div>
@ -328,9 +330,9 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose:
style={{ padding: '10px 16px', borderTop: '1px solid var(--stroke-light)' }}
>
<div className="flex items-center justify-center gap-3">
<span className="text-[12px] text-fg-disabled cursor-pointer"></span>
<span className="text-label-1 text-fg-disabled cursor-pointer"></span>
<div
className="flex items-center justify-center text-[12px] text-color-tertiary cursor-pointer"
className="flex items-center justify-center text-label-1 text-color-tertiary cursor-pointer"
style={{
width: 28,
height: 28,
@ -341,18 +343,18 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose:
>
</div>
<span className="text-[12px] text-fg-disabled cursor-pointer"></span>
<span className="text-[10px] text-fg-disabled font-mono">
<span className="text-label-1 text-fg-disabled cursor-pointer"></span>
<span className="text-caption text-fg-disabled font-mono">
02:34 / {str(media.droneMeta, 'duration')}
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-[8px] text-fg-disabled">
<span className="text-caption text-fg-disabled">
🎬 {num(media.droneMeta, 'videoCount')} · {str(media.droneMeta, 'stage')}
</span>
<div className="flex gap-[8px]">
<span className="text-[8px] text-color-info cursor-pointer">📂 </span>
<span className="text-[8px] text-color-tertiary cursor-pointer">
<span className="text-caption text-color-info cursor-pointer">📂 </span>
<span className="text-caption text-color-tertiary cursor-pointer">
🔗 R&D
</span>
</div>
@ -369,8 +371,8 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose:
style={{ padding: '8px 16px', borderBottom: '1px solid #21262d' }}
>
<div className="flex items-center gap-[6px]">
<span className="text-[12px]">🛰</span>
<span className="text-[12px] font-bold text-fg">
<span className="text-label-1">🛰</span>
<span className="text-label-1 font-bold text-fg">
{str(media.satMeta, 'title', '위성영상')}
</span>
</div>
@ -397,16 +399,16 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose:
}}
>
<div
className="absolute text-[9px] font-bold text-color-danger font-mono bg-bg-base"
className="absolute text-caption font-bold text-color-danger font-mono bg-bg-base"
style={{ top: -10, left: 8, padding: '0 4px' }}
>
{str(media.satMeta, 'detection')}
</div>
<div className="text-[40px] text-fg-disabled">🛰</div>
<div className="text-[11px] text-fg-sub font-semibold">
<div className="text-label-2 text-fg-sub font-semibold">
{str(media.satMeta, 'title', '위성영상')}
</div>
<div className="text-[8px] text-fg-disabled font-mono">
<div className="text-caption text-fg-disabled font-mono">
{str(media.satMeta, 'date')} · {str(media.satMeta, 'resolution')}
</div>
</div>
@ -414,7 +416,7 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose:
{str(media.satMeta, 'detection') === '—' && (
<div className="text-center">
<div className="text-[40px] text-fg-disabled">🛰</div>
<div className="text-[11px] text-fg-disabled" style={{ marginTop: 8 }}>
<div className="text-label-2 text-fg-disabled" style={{ marginTop: 8 }}>
</div>
</div>
@ -429,7 +431,7 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose:
{Array.from({ length: num(media.satMeta, 'thumbCount') }).map((_, i) => (
<div
key={i}
className="flex items-center justify-center text-[14px] text-fg-disabled cursor-pointer"
className="flex items-center justify-center text-title-3 text-fg-disabled cursor-pointer"
style={{
width: 40,
height: 36,
@ -444,10 +446,12 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose:
</div>
)}
<div className="flex justify-between items-center">
<span className="text-[8px] text-fg-disabled">
<span className="text-caption text-fg-disabled">
🛰 {num(media.satMeta, 'thumbCount')} · {str(media.satMeta, 'sensor')}
</span>
<span className="text-[8px] text-color-info cursor-pointer">🔍 / </span>
<span className="text-caption text-color-info cursor-pointer">
🔍 /
</span>
</div>
</div>
</div>
@ -461,15 +465,15 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose:
style={{ padding: '8px 16px', borderBottom: '1px solid #21262d' }}
>
<div className="flex items-center gap-[6px]">
<span className="text-[12px]">📹</span>
<span className="text-[12px] font-bold text-fg">
<span className="text-label-1">📹</span>
<span className="text-label-1 font-bold text-fg">
CCTV {str(media.cctvMeta, 'title', 'CCTV')}
</span>
</div>
<div className="flex items-center gap-[6px]">
{bool(media.cctvMeta, 'live') && (
<span
className="text-[9px] font-bold text-color-success rounded"
className="text-caption font-bold text-color-success rounded"
style={{
padding: '2px 8px',
background: 'rgba(34,197,94,0.15)',
@ -484,17 +488,17 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose:
<div className="flex-1 flex items-center justify-center flex-col gap-2 relative">
{bool(media.cctvMeta, 'live') && (
<div
className="absolute text-[9px] font-bold text-color-danger font-mono"
className="absolute text-caption font-bold text-color-danger font-mono"
style={{ top: 10, left: 16 }}
>
LIVE {new Date().toLocaleTimeString('ko-KR', { hour12: false })}
</div>
)}
<div className="text-[48px] text-fg-disabled">📹</div>
<div className="text-[12px] text-fg-sub font-semibold">
<div className="text-label-1 text-fg-sub font-semibold">
{str(media.cctvMeta, 'title', 'CCTV').replace('#', 'CCTV #')}
</div>
<div className="text-[9px] text-fg-disabled font-mono">
<div className="text-caption text-fg-disabled font-mono">
{str(media.cctvMeta, 'ptz')} · {str(media.cctvMeta, 'angle')} ·{' '}
{bool(media.cctvMeta, 'live') ? '실시간 스트리밍' : '녹화 영상'}
</div>
@ -529,13 +533,15 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose:
))}
</div>
<div className="flex justify-between items-center">
<span className="text-[8px] text-fg-disabled">
<span className="text-caption text-fg-disabled">
📹 CCTV {num(media.cctvMeta, 'camCount')} ·{' '}
{str(media.cctvMeta, 'location')}
</span>
<div className="flex gap-[8px]">
<span className="text-[8px] text-color-danger cursor-pointer">🔴 </span>
<span className="text-[8px] text-color-info cursor-pointer">🎥 PTZ</span>
<span className="text-caption text-color-danger cursor-pointer">
🔴
</span>
<span className="text-caption text-color-info cursor-pointer">🎥 PTZ</span>
</div>
</div>
</div>
@ -552,7 +558,7 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose:
borderTop: '1px solid #30363d',
}}
>
<div className="flex gap-4 text-[10px] font-mono text-fg-disabled">
<div className="flex gap-4 text-caption font-mono text-fg-disabled">
<span>
📷 <b className="text-fg">{media.photoCnt}</b>
</span>
@ -601,7 +607,7 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose:
function NavBtn({ label }: { label: string }) {
return (
<button
className="flex items-center justify-center text-[10px] text-fg-disabled cursor-pointer rounded bg-bg-elevated"
className="flex items-center justify-center text-caption text-fg-disabled cursor-pointer rounded bg-bg-elevated"
style={{
width: 22,
height: 22,
@ -628,7 +634,7 @@ function BottomBtn({
}) {
return (
<button
className="flex items-center gap-1 text-[10px] font-bold cursor-pointer rounded-sm"
className="flex items-center gap-1 text-caption font-bold cursor-pointer rounded-sm"
style={{
padding: '6px 14px',
background: bg,

파일 보기

@ -135,7 +135,7 @@ function pointToSegmentNm(
closeLon = aLon;
closeLat = aLat;
} else {
const t = Math.max(0, Math.min(1, ((-ax) * dx + (-ay) * dy) / lenSq));
const t = Math.max(0, Math.min(1, (-ax * dx + -ay * dy) / lenSq));
closeLon = aLon + (bLon - aLon) * t;
closeLat = aLat + (bLat - aLat) * t;
}
@ -153,7 +153,14 @@ export function estimateDistanceFromCoast(lat: number, lon: number): number {
let minDist = Infinity;
for (const ring of cachedBaselineRings) {
for (let i = 0; i < ring.length - 1; i++) {
const dist = pointToSegmentNm(lat, lon, ring[i][0], ring[i][1], ring[i + 1][0], ring[i + 1][1]);
const dist = pointToSegmentNm(
lat,
lon,
ring[i][0],
ring[i][1],
ring[i + 1][0],
ring[i + 1][1],
);
if (dist < minDist) minDist = dist;
}
}
@ -171,7 +178,7 @@ function pointInRing(lon: number, lat: number, ring: [number, number][]): boolea
const xj = ring[j][0];
const yj = ring[j][1];
if ((yi > lat) !== (yj > lat) && lon < ((xj - xi) * (lat - yi)) / (yj - yi) + xi) {
if (yi > lat !== yj > lat && lon < ((xj - xi) * (lat - yi)) / (yj - yi) + xi) {
inside = !inside;
}
}

파일 보기

@ -116,7 +116,7 @@ export function BacktrackModal({
</div>
<div className="flex-1">
<h2 className="text-base font-bold m-0"> </h2>
<div className="text-[11px] text-fg-disabled mt-[2px]">
<div className="text-label-2 text-fg-disabled mt-[2px]">
AIS
</div>
</div>
@ -144,7 +144,7 @@ export function BacktrackModal({
>
{/* Analysis Conditions */}
<div>
<h3 className="text-[12px] font-bold text-fg-sub mb-[10px]"> </h3>
<h3 className="text-label-1 font-bold text-fg-sub mb-[10px]"> </h3>
<div
style={{
display: 'grid',
@ -161,7 +161,7 @@ export function BacktrackModal({
}}
className="border border-stroke"
>
<div className="text-[9px] text-fg-disabled mb-1"> </div>
<div className="text-caption text-fg-disabled mb-1"> </div>
<input
type="datetime-local"
value={inputTime}
@ -180,7 +180,7 @@ export function BacktrackModal({
}}
className="border border-stroke"
>
<div className="text-[9px] text-fg-disabled mb-1"> </div>
<div className="text-caption text-fg-disabled mb-1"> </div>
<select
value={inputRange}
onChange={(e) => setInputRange(e.target.value)}
@ -202,7 +202,7 @@ export function BacktrackModal({
}}
className="border border-stroke"
>
<div className="text-[9px] text-fg-disabled mb-1"> </div>
<div className="text-caption text-fg-disabled mb-1"> </div>
<div className="flex items-center gap-1">
<input
type="number"
@ -214,7 +214,7 @@ export function BacktrackModal({
step={0.5}
style={{ ...inputStyle, flex: 1 }}
/>
<span className="text-[10px] text-fg-disabled shrink-0">NM</span>
<span className="text-caption text-fg-disabled shrink-0">NM</span>
</div>
</div>
@ -227,8 +227,8 @@ export function BacktrackModal({
}}
className="border border-stroke"
>
<div className="text-[9px] text-fg-disabled mb-1"> </div>
<div className="text-[12px] font-semibold font-mono">
<div className="text-caption text-fg-disabled mb-1"> </div>
<div className="text-label-1 font-semibold font-mono">
{conditions.spillLocation.lat.toFixed(4)}°N,{' '}
{conditions.spillLocation.lon.toFixed(4)}°E
</div>
@ -244,10 +244,10 @@ export function BacktrackModal({
gridColumn: '1 / -1',
}}
>
<div className="text-[9px] text-fg-disabled mb-1"> </div>
<div className="text-caption text-fg-disabled mb-1"> </div>
<div className="text-sm font-bold text-color-tertiary font-mono">
{conditions.totalVessels}{' '}
<span className="text-[10px] font-medium text-fg-disabled">(AIS )</span>
<span className="text-caption font-medium text-fg-disabled">(AIS )</span>
</div>
</div>
</div>
@ -257,7 +257,7 @@ export function BacktrackModal({
{phase === 'results' && vessels.length > 0 && (
<div>
<div className="flex items-center justify-between mb-3">
<h3 className="text-[12px] font-bold text-fg-sub m-0"> </h3>
<h3 className="text-label-1 font-bold text-fg-sub m-0"> </h3>
<div
style={{
padding: '4px 10px',
@ -265,7 +265,7 @@ export function BacktrackModal({
background: 'rgba(239,68,68,0.1)',
border: '1px solid rgba(239,68,68,0.3)',
}}
className="text-[10px] font-bold text-color-danger"
className="text-caption font-bold text-color-danger"
>
{conditions.totalVessels} {vessels.length}
</div>
@ -303,7 +303,7 @@ export function BacktrackModal({
border: 'none',
color: '#fff',
}}
className="flex-1 text-[13px] font-bold cursor-pointer"
className="flex-1 text-title-4 font-bold cursor-pointer"
>
🔍
</button>
@ -318,7 +318,7 @@ export function BacktrackModal({
color: 'var(--color-tertiary)',
cursor: 'wait',
}}
className="flex-1 text-[13px] font-bold border border-stroke"
className="flex-1 text-title-4 font-bold border border-stroke"
>
AIS ...
</button>
@ -333,7 +333,7 @@ export function BacktrackModal({
border: 'none',
color: '#fff',
}}
className="flex-1 text-[13px] font-bold cursor-pointer"
className="flex-1 text-title-4 font-bold cursor-pointer"
>
🗺
</button>
@ -380,8 +380,8 @@ function VesselCard({ vessel }: { vessel: BacktrackVessel }) {
{vessel.rank}
</div>
<div className="flex-1">
<div className="text-[13px] font-bold font-mono">{vessel.name}</div>
<div className="text-[9px] text-fg-disabled font-mono mt-[2px]">
<div className="text-title-4 font-bold font-mono">{vessel.name}</div>
<div className="text-caption text-fg-disabled font-mono mt-[2px]">
IMO: {vessel.imo} · {vessel.type} · {vessel.flag}
</div>
</div>
@ -392,7 +392,7 @@ function VesselCard({ vessel }: { vessel: BacktrackVessel }) {
>
{vessel.probability}%
</div>
<div className="text-[8px] text-fg-disabled"> </div>
<div className="text-caption text-fg-disabled"> </div>
</div>
</div>
@ -430,12 +430,12 @@ function VesselCard({ vessel }: { vessel: BacktrackVessel }) {
: '1px solid var(--stroke-default)',
}}
>
<div className="text-[8px] text-fg-disabled mb-[2px]">{s.label}</div>
<div className="text-caption text-fg-disabled mb-[2px]">{s.label}</div>
<div
style={{
color: s.highlight ? 'var(--color-danger)' : 'var(--fg-default)',
}}
className="text-[10px] font-semibold font-mono"
className="text-caption font-semibold font-mono"
>
{s.value}
</div>
@ -453,7 +453,7 @@ function VesselCard({ vessel }: { vessel: BacktrackVessel }) {
borderRadius: '6px',
lineHeight: '1.5',
}}
className="text-[9px] text-fg-sub"
className="text-caption text-fg-sub"
>
{vessel.description}
</div>

파일 보기

@ -67,7 +67,11 @@ ${styles}
return (
<div className="flex flex-col h-full overflow-hidden bg-bg-base">
<div className="flex-1 overflow-y-auto scrollbar-thin p-5" ref={contentRef}>
<div
className="flex-1 overflow-y-auto scrollbar-thin p-5"
ref={contentRef}
style={{ scrollbarGutter: 'stable' }}
>
{/* 헤더 */}
<div className="flex items-center justify-between mb-5">
<div className="flex items-center gap-3">

파일 보기

@ -166,8 +166,8 @@ export function RecalcModal({
🔄
</div>
<div className="flex-1">
<h2 className="text-[15px] font-bold m-0"> </h2>
<div className="text-[10px] text-fg-disabled mt-[2px]">
<h2 className="text-subtitle font-bold m-0"> </h2>
<div className="text-caption text-fg-disabled mt-[2px]">
·
</div>
</div>
@ -202,10 +202,10 @@ export function RecalcModal({
borderRadius: '8px',
}}
>
<div className="text-[9px] font-bold text-color-accent mb-1.5"> </div>
<div className="text-caption font-bold text-color-accent mb-1.5"> </div>
<div
style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '4px' }}
className="text-[9px]"
className="text-caption"
>
<InfoItem label="사고명" value={incidentName} />
<InfoItem label="유종" value={initOilType} />
@ -281,7 +281,7 @@ export function RecalcModal({
<FieldGroup label="유출 위치 (좌표)">
<div className="flex gap-1.5">
<div className="flex-1">
<div className="text-[8px] text-fg-disabled mb-[3px]"> (N)</div>
<div className="text-caption text-fg-disabled mb-[3px]"> (N)</div>
<input
type="number"
className="prd-i font-mono"
@ -291,7 +291,7 @@ export function RecalcModal({
/>
</div>
<div className="flex-1">
<div className="text-[8px] text-fg-disabled mb-[3px]"> (E)</div>
<div className="text-caption text-fg-disabled mb-[3px]"> (E)</div>
<input
type="number"
className="prd-i font-mono"
@ -347,7 +347,7 @@ export function RecalcModal({
background: 'var(--bg-card)',
opacity: phase !== 'editing' ? 0.5 : 1,
}}
className="flex-1 text-[12px] font-semibold border border-stroke text-fg-sub cursor-pointer"
className="flex-1 text-label-1 font-semibold border border-stroke text-fg-sub cursor-pointer"
>
</button>
@ -378,7 +378,7 @@ export function RecalcModal({
: '#fff',
opacity: models.size === 0 && phase === 'editing' ? 0.5 : 1,
}}
className="flex-[2] text-[12px] font-bold"
className="flex-[2] text-label-1 font-bold"
>
{phase === 'done'
? '✅ 재계산 완료!'
@ -395,7 +395,7 @@ export function RecalcModal({
function FieldGroup({ label, children }: { label: string; children: React.ReactNode }) {
return (
<div>
<div className="text-[10px] font-bold text-fg-sub mb-1.5">{label}</div>
<div className="text-caption font-bold text-fg-sub mb-1.5">{label}</div>
{children}
</div>
);

파일 보기

@ -442,7 +442,7 @@ export function RightPanel({
}}
>
<div
className="w-[30px] h-[30px] rounded-md flex items-center justify-center text-[15px]"
className="w-[30px] h-[30px] rounded-md flex items-center justify-center text-subtitle"
style={{
background: 'rgba(6,182,212,0.1)',
border: '1px solid rgba(6,182,212,0.2)',

파일 보기

@ -410,8 +410,8 @@ const S = {
marginBottom: '24px',
borderRadius: '6px',
border: '1px solid var(--stroke-default)',
fontFamily: "'Pretendard', 'Noto Sans KR', sans-serif",
fontSize: '12px',
fontFamily: 'var(--font-korean)',
fontSize: 'var(--font-size-label-1)',
lineHeight: '1.6',
position: 'relative' as const,
width: '100%',
@ -421,14 +421,14 @@ const S = {
background: 'rgba(6,182,212,0.12)',
color: 'var(--color-accent)',
padding: '8px 16px',
fontSize: '13px',
fontSize: 'var(--font-size-title-4)',
fontWeight: 700,
marginBottom: '12px',
borderRadius: '4px',
border: '1px solid rgba(6,182,212,0.2)',
},
subHeader: {
fontSize: '14px',
fontSize: 'var(--font-size-title-3)',
fontWeight: 700,
color: 'var(--color-accent)',
marginBottom: '12px',
@ -439,7 +439,7 @@ const S = {
width: '100%',
tableLayout: 'fixed' as const,
borderCollapse: 'collapse' as const,
fontSize: '11px',
fontSize: 'var(--font-size-caption)',
marginBottom: '16px',
},
th: {
@ -449,20 +449,20 @@ const S = {
fontWeight: 600,
color: 'var(--fg-sub)',
textAlign: 'center' as const,
fontSize: '10px',
fontSize: 'var(--font-size-caption)',
},
td: {
border: '1px solid var(--stroke-default)',
padding: '5px 10px',
textAlign: 'center' as const,
fontSize: '11px',
fontSize: 'var(--font-size-caption)',
color: 'var(--fg-sub)',
},
tdLeft: {
border: '1px solid var(--stroke-default)',
padding: '5px 10px',
textAlign: 'left' as const,
fontSize: '11px',
fontSize: 'var(--font-size-caption)',
color: 'var(--fg-sub)',
},
thLabel: {
@ -472,7 +472,7 @@ const S = {
fontWeight: 600,
color: 'var(--fg-sub)',
textAlign: 'left' as const,
fontSize: '11px',
fontSize: 'var(--font-size-caption)',
width: '120px',
},
mapPlaceholder: {
@ -485,7 +485,7 @@ const S = {
alignItems: 'center',
justifyContent: 'center',
color: 'var(--fg-disabled)',
fontSize: '13px',
fontSize: 'var(--font-size-title-4)',
fontWeight: 600,
marginBottom: '16px',
},
@ -498,7 +498,7 @@ const inputStyle: React.CSSProperties = {
border: '1px solid var(--stroke-light)',
borderRadius: '3px',
padding: '4px 8px',
fontSize: '11px',
fontSize: 'var(--font-size-caption)',
outline: 'none',
textAlign: 'center',
};
@ -546,7 +546,7 @@ function AddRowBtn({ onClick, label }: { onClick: () => void; label?: string })
return (
<button
onClick={onClick}
className="px-3 py-1 text-[10px] font-semibold text-color-accent bg-[rgba(6,182,212,0.08)] border border-dashed border-color-accent rounded-sm cursor-pointer mb-3"
className="px-3 py-1 text-label-2 font-semibold text-color-accent bg-[rgba(6,182,212,0.08)] border border-dashed border-color-accent rounded-sm cursor-pointer mb-3"
>
+ {label || '행 추가'}
</button>
@ -569,11 +569,11 @@ function Page1({
return (
<div style={S.page}>
<div className="absolute top-2.5 right-4 text-[9px] text-fg-disabled font-semibold">
<div className="absolute top-2.5 right-4 text-caption text-fg-disabled font-semibold">
</div>
<div
className="text-color-accent text-[18px] font-bold mb-5 rounded px-5 py-3 text-center tracking-wide border"
className="text-color-accent text-title-1 font-bold mb-5 rounded px-5 py-3 text-center tracking-wide border"
style={{ background: 'rgba(6,182,212,0.1)', border: '1px solid rgba(6,182,212,0.2)' }}
>
@ -713,7 +713,7 @@ function Page2({
return (
<div style={S.page}>
<div className="absolute top-2.5 right-4 text-[9px] text-fg-disabled font-semibold">
<div className="absolute top-2.5 right-4 text-caption text-fg-disabled font-semibold">
</div>
<div style={S.sectionTitle}>2. </div>
@ -969,7 +969,7 @@ function Page3({
}) {
return (
<div style={S.page}>
<div className="absolute top-2.5 right-4 text-[9px] text-fg-disabled font-semibold">
<div className="absolute top-2.5 right-4 text-caption text-fg-disabled font-semibold">
</div>
<div style={S.sectionTitle}></div>
@ -985,7 +985,7 @@ function Page3({
border: '1px solid var(--stroke-light)',
borderRadius: '4px',
padding: '16px',
fontSize: '13px',
fontSize: 'var(--font-size-title-4)',
outline: 'none',
resize: 'vertical',
lineHeight: '1.8',
@ -1002,7 +1002,7 @@ function Page3({
padding: '16px',
color: data.analysis ? 'var(--fg-default)' : 'var(--fg-disabled)',
fontStyle: data.analysis ? 'normal' : 'italic',
fontSize: '13px',
fontSize: 'var(--font-size-title-4)',
whiteSpace: 'pre-wrap',
lineHeight: '1.8',
}}
@ -1684,7 +1684,7 @@ function Page4({
return (
<div style={S.page}>
<div className="absolute top-2.5 right-4 text-[9px] text-fg-disabled font-semibold">
<div className="absolute top-2.5 right-4 text-caption text-fg-disabled font-semibold">
</div>
<div style={S.sectionTitle}>4. </div>
@ -1820,7 +1820,9 @@ function Page4({
<tbody>
{data.esi.map((e, i) => (
<tr key={i}>
<td style={{ ...S.td, fontWeight: 600, fontSize: '10px' }}>{e.code}</td>
<td style={{ ...S.td, fontWeight: 600, fontSize: 'var(--font-size-caption)' }}>
{e.code}
</td>
<td style={S.tdLeft}>{e.type}</td>
<ECell
value={e.length}
@ -2229,7 +2231,7 @@ function Page5({
};
return (
<div style={S.page}>
<div className="absolute top-2.5 right-4 text-[9px] text-fg-disabled font-semibold">
<div className="absolute top-2.5 right-4 text-caption text-fg-disabled font-semibold">
</div>
<div style={S.sectionTitle}> ( )</div>
@ -2283,7 +2285,7 @@ function Page6({
};
return (
<div style={S.page}>
<div className="absolute top-2.5 right-4 text-[9px] text-fg-disabled font-semibold">
<div className="absolute top-2.5 right-4 text-caption text-fg-disabled font-semibold">
</div>
<div style={S.sectionTitle}>5. ·</div>
@ -2414,7 +2416,7 @@ function Page6({
border: '1px solid var(--stroke-light)',
borderRadius: '4px',
padding: '12px',
fontSize: '12px',
fontSize: 'var(--font-size-label-1)',
outline: 'none',
resize: 'vertical',
}}
@ -2430,7 +2432,7 @@ function Page6({
padding: '12px',
color: data.etcEquipment ? 'var(--fg-default)' : 'var(--fg-disabled)',
fontStyle: data.etcEquipment ? 'normal' : 'italic',
fontSize: '12px',
fontSize: 'var(--font-size-label-1)',
}}
>
{data.etcEquipment || '-'}
@ -2458,7 +2460,7 @@ function Page7({
onChange({ ...data, result: { ...data.result, [k]: v } });
return (
<div style={S.page}>
<div className="absolute top-2.5 right-4 text-[9px] text-fg-disabled font-semibold">
<div className="absolute top-2.5 right-4 text-caption text-fg-disabled font-semibold">
</div>
<div style={S.sectionTitle}>/ </div>
@ -2591,25 +2593,25 @@ export function OilSpillReportTemplate({ mode, initialData, onSave, onBack }: Pr
{onBack && (
<button
onClick={onBack}
className="px-3 py-1.5 text-[12px] font-semibold text-fg-sub bg-transparent border-none cursor-pointer"
className="px-3 py-1.5 text-label-1 font-semibold text-fg-sub bg-transparent border-none cursor-pointer"
>
</button>
)}
<h2 className="text-[18px] font-bold">
<h2 className="text-title-1 font-bold">
{editing ? (
<input
value={data.title}
onChange={(e) => setData({ ...data, title: e.target.value })}
placeholder="보고서 제목 입력"
className="text-[18px] font-bold bg-bg-base border border-[var(--stroke-light)] rounded px-2.5 py-1 outline-none w-full max-w-[600px]"
className="text-title-1 font-bold bg-bg-base border border-[var(--stroke-light)] rounded px-2.5 py-1 outline-none w-full max-w-[600px]"
/>
) : (
data.title || '유류오염사고 대응지원 상황도'
)}
</h2>
<span
className="px-2.5 py-[3px] text-[10px] font-semibold rounded border"
className="px-2.5 py-[3px] text-label-2 font-semibold rounded border"
style={
editing
? {
@ -2630,7 +2632,7 @@ export function OilSpillReportTemplate({ mode, initialData, onSave, onBack }: Pr
<div className="flex items-center gap-2">
<button
onClick={() => setViewMode('all')}
className="px-3.5 py-1.5 text-[11px] font-semibold rounded cursor-pointer"
className="px-3.5 py-1.5 text-label-2 font-semibold rounded cursor-pointer"
style={{
border:
viewMode === 'all'
@ -2644,7 +2646,7 @@ export function OilSpillReportTemplate({ mode, initialData, onSave, onBack }: Pr
</button>
<button
onClick={() => setViewMode('page')}
className="px-3.5 py-1.5 text-[11px] font-semibold rounded cursor-pointer"
className="px-3.5 py-1.5 text-label-2 font-semibold rounded cursor-pointer"
style={{
border:
viewMode === 'page'
@ -2659,14 +2661,14 @@ export function OilSpillReportTemplate({ mode, initialData, onSave, onBack }: Pr
{editing && (
<button
onClick={handleSave}
className="px-4 py-1.5 text-[11px] font-bold rounded cursor-pointer border border-[#22c55e] bg-[rgba(34,197,94,0.15)] text-color-success"
className="px-4 py-1.5 text-label-2 font-bold rounded cursor-pointer border border-[#22c55e] bg-[rgba(34,197,94,0.15)] text-color-success"
>
</button>
)}
<button
onClick={() => window.print()}
className="px-3.5 py-1.5 text-[11px] font-semibold rounded cursor-pointer border border-[var(--color-danger)] bg-[rgba(239,68,68,0.1)] text-color-danger"
className="px-3.5 py-1.5 text-label-2 font-semibold rounded cursor-pointer border border-[var(--color-danger)] bg-[rgba(239,68,68,0.1)] text-color-danger"
>
/ PDF
</button>
@ -2680,7 +2682,7 @@ export function OilSpillReportTemplate({ mode, initialData, onSave, onBack }: Pr
<button
key={i}
onClick={() => setCurrentPage(i)}
className="px-3 py-1.5 text-[11px] font-semibold rounded cursor-pointer"
className="px-3 py-1.5 text-label-2 font-semibold rounded cursor-pointer"
style={{
border:
currentPage === i
@ -2707,7 +2709,7 @@ export function OilSpillReportTemplate({ mode, initialData, onSave, onBack }: Pr
<button
onClick={() => setCurrentPage((p) => Math.max(0, p - 1))}
disabled={currentPage === 0}
className="px-5 py-2 text-[12px] font-semibold rounded border border-stroke bg-bg-elevated cursor-pointer"
className="px-5 py-2 text-label-1 font-semibold rounded border border-stroke bg-bg-elevated cursor-pointer"
style={{
color: currentPage === 0 ? 'var(--fg-disabled)' : 'var(--fg-default)',
opacity: currentPage === 0 ? 0.4 : 1,
@ -2715,13 +2717,13 @@ export function OilSpillReportTemplate({ mode, initialData, onSave, onBack }: Pr
>
</button>
<span className="px-4 py-2 text-[12px] text-fg-sub">
<span className="px-4 py-2 text-label-1 text-fg-sub">
{currentPage + 1} / {pages.length}
</span>
<button
onClick={() => setCurrentPage((p) => Math.min(pages.length - 1, p + 1))}
disabled={currentPage === pages.length - 1}
className="px-5 py-2 text-[12px] font-semibold rounded cursor-pointer"
className="px-5 py-2 text-label-1 font-semibold rounded cursor-pointer"
style={{
border: '1px solid var(--color-accent)',
background: 'rgba(6,182,212,0.1)',

파일 보기

@ -38,15 +38,22 @@ const MapSlot = ({ label, step, mapData, captured, onCapture, onReset }: MapSlot
{/* 라벨 */}
<div className="flex items-center gap-1.5 mb-1.5">
<span
className="text-[11px] font-bold font-korean px-2 py-0.5 rounded"
style={{ background: 'rgba(6,182,212,0.12)', color: '#06b6d4', border: '1px solid rgba(6,182,212,0.25)' }}
className="text-label-2 font-bold font-korean px-2 py-0.5 rounded"
style={{
background: 'color-mix(in srgb, var(--color-accent) 12%, transparent)',
color: 'var(--color-accent)',
border: '1px solid color-mix(in srgb, var(--color-accent) 25%, transparent)',
}}
>
{label}
</span>
</div>
{/* 지도 + 캡처 오버레이 */}
<div className="relative rounded-lg border border-stroke overflow-hidden" style={{ aspectRatio: '16/9' }}>
<div
className="relative rounded-lg border border-stroke overflow-hidden"
style={{ aspectRatio: '16/9' }}
>
<MapView
center={mapData.center}
zoom={mapData.zoom}
@ -65,19 +72,28 @@ const MapSlot = ({ label, step, mapData, captured, onCapture, onReset }: MapSlot
<div className="absolute top-2 right-2 z-10" style={{ width: '180px' }}>
<div
className="rounded-lg overflow-hidden"
style={{ border: '1px solid rgba(6,182,212,0.5)', boxShadow: '0 4px 16px rgba(0,0,0,0.5)' }}
style={{
border: '1px solid color-mix(in srgb, var(--color-accent) 50%, transparent)',
boxShadow: '0 4px 16px rgba(0,0,0,0.5)',
}}
>
<img src={captured} alt={`${label} 캡처`} className="w-full block" />
<div
className="flex items-center justify-between px-2 py-1"
style={{ background: 'rgba(15,23,42,0.85)', borderTop: '1px solid rgba(6,182,212,0.3)' }}
style={{
background: 'rgba(15,23,42,0.85)',
borderTop: '1px solid color-mix(in srgb, var(--color-accent) 30%, transparent)',
}}
>
<span className="text-[9px] font-korean font-semibold" style={{ color: '#06b6d4' }}>
<span
className="text-caption font-korean font-semibold"
style={{ color: 'var(--color-accent)' }}
>
📷
</span>
<button
onClick={onReset}
className="text-[9px] font-korean hover:text-fg transition-colors"
className="text-caption font-korean hover:text-fg transition-colors"
style={{ color: 'rgba(148,163,184,0.8)' }}
>
@ -90,17 +106,21 @@ const MapSlot = ({ label, step, mapData, captured, onCapture, onReset }: MapSlot
{/* 캡처 버튼 */}
<div className="flex items-center justify-between mt-1.5">
<p className="text-[9px] text-fg-disabled font-korean">
<p className="text-caption text-fg-disabled font-korean">
{captured ? 'PDF 출력 시 포함됩니다.' : '원하는 범위를 선택 후 캡처하세요.'}
</p>
<button
onClick={handleCapture}
disabled={isCapturing || !!captured}
className="px-2.5 py-1 text-[10px] font-semibold rounded transition-all font-korean flex items-center gap-1"
className="px-2.5 py-1 text-label-2 font-semibold rounded transition-all font-korean flex items-center gap-1"
style={{
background: captured ? 'rgba(6,182,212,0.06)' : 'rgba(6,182,212,0.12)',
border: '1px solid rgba(6,182,212,0.4)',
color: captured ? 'rgba(6,182,212,0.5)' : '#06b6d4',
background: captured
? 'color-mix(in srgb, var(--color-accent) 6%, transparent)'
: 'color-mix(in srgb, var(--color-accent) 12%, transparent)',
border: '1px solid color-mix(in srgb, var(--color-accent) 40%, transparent)',
color: captured
? 'color-mix(in srgb, var(--color-accent) 50%, transparent)'
: 'var(--color-accent)',
opacity: isCapturing ? 0.6 : 1,
cursor: captured ? 'default' : 'pointer',
}}
@ -123,7 +143,7 @@ const OilSpreadMapPanel = ({
}: OilSpreadMapPanelProps) => {
if (!mapData) {
return (
<div className="w-full h-[200px] bg-bg-card border border-stroke rounded-lg flex items-center justify-center text-fg-disabled text-[12px] font-korean mb-4">
<div className="w-full h-[200px] bg-bg-card border border-stroke rounded-lg flex items-center justify-center text-fg-disabled text-label-1 font-korean mb-4">
. .
</div>
);

파일 보기

@ -305,8 +305,8 @@ function ReportGenerator({ onSave }: ReportGeneratorProps) {
<div className="flex flex-col h-full w-full">
{/* Header */}
<div className="px-6 py-4 border-b border-stroke bg-bg-surface">
<h2 className="text-[16px] font-bold text-fg font-korean"> </h2>
<p className="text-[11px] text-fg-disabled font-korean mt-1">
<h2 className="text-title-2 font-bold text-fg font-korean"> </h2>
<p className="text-label-2 text-fg-disabled font-korean mt-1">
.
</p>
@ -329,12 +329,12 @@ function ReportGenerator({ onSave }: ReportGeneratorProps) {
>
<div className="text-[22px] mb-1">{c.icon}</div>
<div
className="text-[12px] font-bold"
className="text-label-1 font-bold"
style={{ color: isActive ? c.color : 'var(--fg-disabled)' }}
>
{c.label}
</div>
<div className="text-[9px] text-fg-disabled mt-0.5">{c.desc}</div>
<div className="text-caption text-fg-disabled mt-0.5">{c.desc}</div>
</button>
);
})}
@ -346,7 +346,7 @@ function ReportGenerator({ onSave }: ReportGeneratorProps) {
<div className="w-[250px] min-w-[250px] border-r border-stroke bg-bg-surface flex flex-col overflow-y-auto shrink-0">
{/* 템플릿 선택 */}
<div className="px-4 py-3 border-b border-stroke">
<h3 className="text-[11px] font-bold text-fg-sub font-korean flex items-center gap-2">
<h3 className="text-label-2 font-bold text-fg-sub font-korean flex items-center gap-2">
📄 릿
</h3>
</div>
@ -361,9 +361,9 @@ function ReportGenerator({ onSave }: ReportGeneratorProps) {
background: selectedTemplate === i ? cat.bgActive : 'var(--bg-elevated)',
}}
>
<span className="text-[14px]">{tmpl.icon}</span>
<span className="text-title-3">{tmpl.icon}</span>
<span
className="text-[11px] font-semibold"
className="text-label-2 font-semibold"
style={{ color: selectedTemplate === i ? cat.color : 'var(--fg-sub)' }}
>
{tmpl.label}
@ -374,7 +374,7 @@ function ReportGenerator({ onSave }: ReportGeneratorProps) {
{/* 섹션 체크 */}
<div className="px-4 py-3 border-b border-stroke">
<h3 className="text-[11px] font-bold text-fg-sub font-korean flex items-center gap-2">
<h3 className="text-label-2 font-bold text-fg-sub font-korean flex items-center gap-2">
📋
</h3>
</div>
@ -391,7 +391,7 @@ function ReportGenerator({ onSave }: ReportGeneratorProps) {
}}
>
<div
className="w-[18px] h-[18px] rounded shrink-0 mt-[1px] flex items-center justify-center text-[10px]"
className="w-[18px] h-[18px] rounded shrink-0 mt-[1px] flex items-center justify-center text-label-2"
style={{
background: sec.checked ? cat.color : 'var(--bg-card)',
color: sec.checked ? '#fff' : 'transparent',
@ -402,15 +402,15 @@ function ReportGenerator({ onSave }: ReportGeneratorProps) {
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-1">
<span className="text-[12px]">{sec.icon}</span>
<span className="text-label-1">{sec.icon}</span>
<span
className="text-[11px] font-bold"
className="text-label-2 font-bold"
style={{ color: sec.checked ? 'var(--fg-default)' : 'var(--fg-disabled)' }}
>
{sec.title}
</span>
</div>
<p className="text-[9px] text-fg-disabled mt-0.5">{sec.desc}</p>
<p className="text-caption text-fg-disabled mt-0.5">{sec.desc}</p>
</div>
</button>
))}
@ -421,10 +421,10 @@ function ReportGenerator({ onSave }: ReportGeneratorProps) {
<div className="flex-1 flex flex-col overflow-hidden">
{/* Preview Header */}
<div className="flex items-center justify-between px-6 py-3 border-b border-stroke bg-bg-surface">
<h3 className="text-[13px] font-bold text-fg font-korean flex items-center gap-2">
<h3 className="text-title-4 font-bold text-fg font-korean flex items-center gap-2">
📄
<span
className="text-[10px] font-semibold px-2 py-0.5 rounded"
className="text-label-2 font-semibold px-2 py-0.5 rounded"
style={{ background: cat.bgActive, color: cat.color }}
>
{cat.templates[selectedTemplate].label}
@ -433,7 +433,7 @@ function ReportGenerator({ onSave }: ReportGeneratorProps) {
<div className="flex items-center gap-2">
<button
onClick={handleDownload}
className="px-3 py-1.5 text-[11px] font-semibold rounded transition-all font-korean flex items-center gap-1.5"
className="px-3 py-1.5 text-label-2 font-semibold rounded transition-all font-korean flex items-center gap-1.5"
style={{
background: cat.bgActive,
border: `1px solid ${cat.borderColor}`,
@ -444,7 +444,7 @@ function ReportGenerator({ onSave }: ReportGeneratorProps) {
</button>
<button
onClick={handleSave}
className="px-3 py-1.5 text-[11px] font-semibold rounded border border-stroke bg-bg-elevated text-fg hover:bg-bg-surface-hover transition-all font-korean flex items-center gap-1.5"
className="px-3 py-1.5 text-label-2 font-semibold rounded border border-stroke bg-bg-elevated text-fg hover:bg-bg-surface-hover transition-all font-korean flex items-center gap-1.5"
>
💾
</button>
@ -456,11 +456,11 @@ function ReportGenerator({ onSave }: ReportGeneratorProps) {
{/* Report Header */}
<div className="rounded-lg border border-stroke p-8 mb-6 bg-bg-elevated">
<div className="text-center">
<p className="text-[10px] text-fg-disabled font-korean mb-2">
<p className="text-label-2 text-fg-disabled font-korean mb-2">
</p>
<h2 className="text-[20px] font-bold text-fg font-korean mb-2">{cat.reportName}</h2>
<p className="text-[12px] font-korean" style={{ color: cat.color }}>
<p className="text-label-1 font-korean" style={{ color: cat.color }}>
{cat.templates[selectedTemplate].label}
</p>
</div>
@ -473,7 +473,7 @@ function ReportGenerator({ onSave }: ReportGeneratorProps) {
className="rounded-lg border border-stroke mb-4 overflow-hidden bg-bg-elevated"
>
<div className="px-5 py-3 border-b border-stroke">
<h4 className="text-[13px] font-bold text-fg font-korean flex items-center gap-2">
<h4 className="text-title-4 font-bold text-fg font-korean flex items-center gap-2">
{sec.icon} {sec.title}
</h4>
</div>
@ -492,7 +492,7 @@ function ReportGenerator({ onSave }: ReportGeneratorProps) {
/>
{oilPayload?.spreadSteps && oilPayload.spreadSteps.length > 0 && (
<div className="mb-4 overflow-x-auto">
<table className="w-full border-collapse text-[11px]">
<table className="w-full border-collapse text-label-2">
<thead>
<tr className="border-b border-stroke bg-bg-card">
<th className="px-3 py-2 text-center font-semibold text-fg-disabled font-korean">
@ -558,7 +558,7 @@ function ReportGenerator({ onSave }: ReportGeneratorProps) {
key={i}
className="bg-bg-surface border border-stroke rounded-lg p-4 text-center"
>
<p className="text-[10px] text-fg-disabled font-korean mb-1">
<p className="text-label-2 text-fg-disabled font-korean mb-1">
{m.label}
</p>
<p
@ -576,7 +576,7 @@ function ReportGenerator({ onSave }: ReportGeneratorProps) {
<>
{oilPayload && !oilPayload.hasSimulation && (
<div
className="mb-3 px-3 py-2 rounded text-[10px] font-korean"
className="mb-3 px-3 py-2 rounded text-label-2 font-korean"
style={{
background: 'rgba(249,115,22,0.08)',
border: '1px solid rgba(249,115,22,0.3)',
@ -615,16 +615,16 @@ function ReportGenerator({ onSave }: ReportGeneratorProps) {
],
].map((row, i) => (
<tr key={i} className="border-b border-stroke">
<td className="px-4 py-3 text-[11px] text-fg-disabled font-korean bg-[rgba(255,255,255,0.02)]">
<td className="px-4 py-3 text-label-2 text-fg-disabled font-korean bg-[rgba(255,255,255,0.02)]">
{row[0]}
</td>
<td className="px-4 py-3 text-[12px] text-fg font-mono font-semibold text-right">
<td className="px-4 py-3 text-label-1 text-fg font-mono font-semibold text-right">
{row[1]}
</td>
<td className="px-4 py-3 text-[11px] text-fg-disabled font-korean bg-[rgba(255,255,255,0.02)]">
<td className="px-4 py-3 text-label-2 text-fg-disabled font-korean bg-[rgba(255,255,255,0.02)]">
{row[2]}
</td>
<td className="px-4 py-3 text-[12px] text-fg font-mono font-semibold text-right">
<td className="px-4 py-3 text-label-1 text-fg font-mono font-semibold text-right">
{row[3]}
</td>
</tr>
@ -638,7 +638,7 @@ function ReportGenerator({ onSave }: ReportGeneratorProps) {
const resources = oilPayload?.sensitiveResources;
if (!resources || resources.length === 0) {
return (
<p className="text-[12px] text-fg-disabled font-korean italic">
<p className="text-label-1 text-fg-disabled font-korean italic">
.
</p>
);
@ -652,13 +652,13 @@ function ReportGenerator({ onSave }: ReportGeneratorProps) {
</colgroup>
<thead>
<tr className="border-b border-stroke">
<th className="px-4 py-2 text-[11px] text-fg-disabled font-korean text-left bg-[rgba(255,255,255,0.02)]">
<th className="px-4 py-2 text-label-2 text-fg-disabled font-korean text-left bg-[rgba(255,255,255,0.02)]">
</th>
<th className="px-4 py-2 text-[11px] text-fg-disabled font-korean text-right bg-[rgba(255,255,255,0.02)]">
<th className="px-4 py-2 text-label-2 text-fg-disabled font-korean text-right bg-[rgba(255,255,255,0.02)]">
</th>
<th className="px-4 py-2 text-[11px] text-fg-disabled font-korean text-right bg-[rgba(255,255,255,0.02)]">
<th className="px-4 py-2 text-label-2 text-fg-disabled font-korean text-right bg-[rgba(255,255,255,0.02)]">
</th>
</tr>
@ -666,14 +666,14 @@ function ReportGenerator({ onSave }: ReportGeneratorProps) {
<tbody>
{resources.map((r, i) => (
<tr key={i} className="border-b border-stroke">
<td className="px-4 py-3 text-[12px] text-fg font-korean">
<td className="px-4 py-3 text-label-1 text-fg font-korean">
{r.category}
</td>
<td className="px-4 py-3 text-[12px] text-fg text-right">
<td className="px-4 py-3 text-label-1 text-fg text-right">
<span className="font-mono">{r.count}</span>
<span className="font-korean"></span>
</td>
<td className="px-4 py-3 text-[12px] text-fg font-mono text-right">
<td className="px-4 py-3 text-label-1 text-fg font-mono text-right">
{r.totalArea != null ? `${r.totalArea.toFixed(2)} ha` : '—'}
</td>
</tr>
@ -686,7 +686,7 @@ function ReportGenerator({ onSave }: ReportGeneratorProps) {
(() => {
if (!oilPayload) {
return (
<p className="text-[12px] text-fg-disabled font-korean italic">
<p className="text-label-1 text-fg-disabled font-korean italic">
.
</p>
);
@ -696,14 +696,14 @@ function ReportGenerator({ onSave }: ReportGeneratorProps) {
!coastLength || coastLength === '—' || coastLength.startsWith('0.00');
if (hasNoCoastal) {
return (
<p className="text-[12px] text-fg-sub font-korean">
<p className="text-label-1 text-fg-sub font-korean">
{' '}
<span className="font-semibold text-fg"> </span>.
</p>
);
}
return (
<p className="text-[12px] text-fg-sub font-korean">
<p className="text-label-1 text-fg-sub font-korean">
:{' '}
<span className="font-semibold text-fg">
{oilPayload.coastal?.firstTime ?? '—'}
@ -715,9 +715,9 @@ function ReportGenerator({ onSave }: ReportGeneratorProps) {
);
})()}
{sec.id === 'oil-defense' && (
<div className="text-[12px] text-fg-disabled font-korean">
<div className="text-label-1 text-fg-disabled font-korean">
<p className="mb-2"> .</p>
<div className="w-full h-[100px] bg-bg-card border border-stroke rounded-lg flex items-center justify-center text-fg-disabled text-[12px] font-korean">
<div className="w-full h-[100px] bg-bg-card border border-stroke rounded-lg flex items-center justify-center text-fg-disabled text-label-1 font-korean">
[ ]
</div>
</div>
@ -727,7 +727,7 @@ function ReportGenerator({ onSave }: ReportGeneratorProps) {
const wx = oilPayload?.weather;
if (!wx) {
return (
<p className="text-[12px] text-fg-disabled font-korean italic">
<p className="text-label-1 text-fg-disabled font-korean italic">
· .
</p>
);
@ -767,11 +767,11 @@ function ReportGenerator({ onSave }: ReportGeneratorProps) {
return (
<div className="space-y-2">
<div className="flex items-center gap-2 mb-3">
<span className="text-[11px] font-semibold text-accent-1 font-korean">
<span className="text-label-2 font-semibold text-accent-1 font-korean">
{stationLabel}
</span>
{capturedAt && (
<span className="text-[10px] text-fg-disabled font-korean">
<span className="text-label-2 text-fg-disabled font-korean">
: {capturedAt}
</span>
)}
@ -779,10 +779,10 @@ function ReportGenerator({ onSave }: ReportGeneratorProps) {
<div className="grid grid-cols-2 gap-x-6 gap-y-1.5">
{rows.map((row) => (
<div key={row.label} className="flex items-center gap-2">
<span className="text-[10px] text-fg-disabled font-korean w-[64px] shrink-0">
<span className="text-label-2 text-fg-disabled font-korean w-[64px] shrink-0">
{row.label}
</span>
<span className="text-[12px] font-semibold text-fg font-mono">
<span className="text-label-1 font-semibold text-fg font-mono">
{row.value}
</span>
</div>
@ -803,7 +803,7 @@ function ReportGenerator({ onSave }: ReportGeneratorProps) {
style={{ maxHeight: '300px', objectFit: 'contain' }}
/>
) : (
<div className="w-full h-[140px] bg-bg-card border border-stroke rounded-lg flex items-center justify-center text-fg-disabled text-[12px] font-korean mb-4">
<div className="w-full h-[140px] bg-bg-card border border-stroke rounded-lg flex items-center justify-center text-fg-disabled text-label-1 font-korean mb-4">
[ ]
</div>
)}
@ -832,7 +832,7 @@ function ReportGenerator({ onSave }: ReportGeneratorProps) {
key={i}
className="bg-bg-surface border border-stroke rounded-lg p-4 text-center"
>
<p className="text-[10px] text-fg-disabled font-korean mb-1">
<p className="text-label-2 text-fg-disabled font-korean mb-1">
{m.label}
</p>
<p
@ -841,7 +841,9 @@ function ReportGenerator({ onSave }: ReportGeneratorProps) {
>
{m.value}
</p>
<p className="text-[8px] text-fg-disabled font-korean mt-1">{m.desc}</p>
<p className="text-caption text-fg-disabled font-korean mt-1">
{m.desc}
</p>
</div>
))}
</div>
@ -877,26 +879,29 @@ function ReportGenerator({ onSave }: ReportGeneratorProps) {
className="bg-bg-surface border border-stroke rounded-lg p-4 text-center"
>
<p
className="text-[9px] font-bold font-korean mb-1"
className="text-caption font-bold font-korean mb-1"
style={{ color: h.color }}
>
{h.label}
</p>
<p className="text-[18px] font-bold font-mono" style={{ color: h.color }}>
<p
className="text-title-1 font-bold font-mono"
style={{ color: h.color }}
>
{h.value}
</p>
{h.area && (
<p className="text-[10px] text-fg-disabled font-mono mt-0.5">
<p className="text-label-2 text-fg-disabled font-mono mt-0.5">
{h.area}
</p>
)}
<p className="text-[8px] text-fg-disabled font-korean mt-1">{h.desc}</p>
<p className="text-caption text-fg-disabled font-korean mt-1">{h.desc}</p>
</div>
))}
</div>
)}
{sec.id === 'hns-substance' && (
<div className="grid grid-cols-2 gap-2 text-[11px]">
<div className="grid grid-cols-2 gap-2 text-label-2">
{[
{ k: '물질명', v: hnsPayload?.substance.name || '—' },
{ k: 'UN번호', v: hnsPayload?.substance.un || '—' },
@ -913,7 +918,7 @@ function ReportGenerator({ onSave }: ReportGeneratorProps) {
))}
<div className="col-span-2 flex justify-between px-3 py-2 bg-bg-surface rounded border border-[rgba(239,68,68,0.3)]">
<span className="text-fg-disabled font-korean"></span>
<span className="text-[var(--color-danger)] font-semibold font-mono text-[10px]">
<span className="text-[var(--color-danger)] font-semibold font-mono text-label-2">
{hnsPayload?.substance.toxicity || '—'}
</span>
</div>
@ -921,7 +926,7 @@ function ReportGenerator({ onSave }: ReportGeneratorProps) {
)}
{sec.id === 'hns-ppe' && (
<div className="flex flex-wrap gap-2">
<span className="text-fg-disabled font-korean text-[11px]"></span>
<span className="text-fg-disabled font-korean text-label-2"></span>
</div>
)}
{sec.id === 'hns-facility' && (
@ -935,15 +940,17 @@ function ReportGenerator({ onSave }: ReportGeneratorProps) {
key={i}
className="bg-bg-surface border border-stroke rounded-lg p-4 text-center"
>
<div className="text-[18px] mb-1">{f.icon}</div>
<p className="text-[14px] font-bold text-fg font-mono">{f.value}</p>
<p className="text-[9px] text-fg-disabled font-korean mt-1">{f.label}</p>
<div className="text-title-1 mb-1">{f.icon}</div>
<p className="text-title-3 font-bold text-fg font-mono">{f.value}</p>
<p className="text-caption text-fg-disabled font-korean mt-1">
{f.label}
</p>
</div>
))}
</div>
)}
{sec.id === 'hns-3d' && (
<div className="w-full h-[160px] bg-bg-card border border-stroke rounded-lg flex items-center justify-center text-fg-disabled text-[12px] font-korean">
<div className="w-full h-[160px] bg-bg-card border border-stroke rounded-lg flex items-center justify-center text-fg-disabled text-label-1 font-korean">
[3D ]
</div>
)}
@ -967,9 +974,11 @@ function ReportGenerator({ onSave }: ReportGeneratorProps) {
key={i}
className="bg-bg-surface border border-stroke rounded-lg p-3 text-center"
>
<div className="text-[16px] mb-0.5">{w.icon}</div>
<p className="text-[13px] font-bold text-fg font-mono">{w.value}</p>
<p className="text-[8px] text-fg-disabled font-korean mt-1">{w.label}</p>
<div className="text-title-2 mb-0.5">{w.icon}</div>
<p className="text-title-4 font-bold text-fg font-mono">{w.value}</p>
<p className="text-caption text-fg-disabled font-korean mt-1">
{w.label}
</p>
</div>
))}
</div>
@ -988,8 +997,13 @@ function ReportGenerator({ onSave }: ReportGeneratorProps) {
key={i}
className="bg-bg-surface border border-stroke rounded-lg p-3 text-center"
>
<p className="text-[9px] text-fg-disabled font-korean mb-1">{s.label}</p>
<p className="text-[16px] font-bold font-mono" style={{ color: s.color }}>
<p className="text-caption text-fg-disabled font-korean mb-1">
{s.label}
</p>
<p
className="text-title-2 font-bold font-mono"
style={{ color: s.color }}
>
{s.value}
</p>
</div>
@ -999,7 +1013,7 @@ function ReportGenerator({ onSave }: ReportGeneratorProps) {
{sec.id === 'rescue-timeline' && (
<div className="flex flex-col gap-2">
<div className="flex items-center gap-3 px-3 py-2 bg-bg-surface rounded border border-stroke">
<span className="text-[11px] text-fg-disabled font-korean"></span>
<span className="text-label-2 text-fg-disabled font-korean"></span>
</div>
</div>
)}
@ -1015,17 +1029,19 @@ function ReportGenerator({ onSave }: ReportGeneratorProps) {
key={i}
className="bg-bg-surface border border-stroke rounded-lg p-4 text-center"
>
<p className="text-[9px] text-fg-disabled font-korean mb-1">{c.label}</p>
<p className="text-caption text-fg-disabled font-korean mb-1">
{c.label}
</p>
<p className="text-[24px] font-bold font-mono" style={{ color: c.color }}>
{c.value}
</p>
<p className="text-[8px] text-fg-disabled font-korean mt-0.5"></p>
<p className="text-caption text-fg-disabled font-korean mt-0.5"></p>
</div>
))}
</div>
)}
{sec.id === 'rescue-resource' && (
<table className="w-full text-[11px] border-collapse">
<table className="w-full text-label-2 border-collapse">
<thead>
<tr className="border-b border-stroke">
<th className="px-3 py-2 text-left text-fg-disabled font-korean"></th>
@ -1044,7 +1060,7 @@ function ReportGenerator({ onSave }: ReportGeneratorProps) {
<tr className="border-b border-stroke">
<td
colSpan={4}
className="px-3 py-3 text-center text-fg-disabled font-korean text-[11px]"
className="px-3 py-3 text-center text-fg-disabled font-korean text-label-2"
>
</td>
@ -1063,8 +1079,13 @@ function ReportGenerator({ onSave }: ReportGeneratorProps) {
key={i}
className="bg-bg-surface border border-stroke rounded-lg p-4 text-center"
>
<p className="text-[9px] text-fg-disabled font-korean mb-1">{g.label}</p>
<p className="text-[14px] font-bold font-mono" style={{ color: g.color }}>
<p className="text-caption text-fg-disabled font-korean mb-1">
{g.label}
</p>
<p
className="text-title-3 font-bold font-mono"
style={{ color: g.color }}
>
{g.value}
</p>
</div>
@ -1083,9 +1104,11 @@ function ReportGenerator({ onSave }: ReportGeneratorProps) {
key={i}
className="bg-bg-surface border border-stroke rounded-lg p-3 text-center"
>
<div className="text-[16px] mb-0.5">{w.icon}</div>
<p className="text-[13px] font-bold text-fg font-mono">{w.value}</p>
<p className="text-[8px] text-fg-disabled font-korean mt-1">{w.label}</p>
<div className="text-title-2 mb-0.5">{w.icon}</div>
<p className="text-title-4 font-bold text-fg font-mono">{w.value}</p>
<p className="text-caption text-fg-disabled font-korean mt-1">
{w.label}
</p>
</div>
))}
</div>

파일 보기

@ -121,11 +121,11 @@ export function ReportsView() {
<div className="flex flex-col h-full">
<div className="flex items-center justify-between px-5 py-3 border-b border-stroke bg-bg-surface">
<div className="flex items-center gap-3">
<span className="text-[11px] text-fg-disabled font-korean"></span>
<span className="text-label-2 text-fg-disabled font-korean"></span>
<select
value={filterJurisdiction}
onChange={(e) => setFilterJurisdiction(e.target.value)}
className="px-3 py-1.5 text-[11px] bg-bg-elevated border border-stroke rounded text-fg font-korean outline-none focus:border-color-accent"
className="px-3 py-1.5 text-label-2 bg-bg-elevated border border-stroke rounded text-fg font-korean outline-none focus:border-color-accent"
>
<option value="전체"></option>
{jurisdictions.map((j) => (
@ -141,9 +141,15 @@ export function ReportsView() {
placeholder="보고서명 검색..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-52 px-3 py-1.5 text-[11px] bg-bg-elevated border border-stroke rounded text-fg placeholder-fg-disabled font-korean outline-none focus:border-color-accent"
className="w-52 px-3 py-1.5 text-label-2 bg-bg-elevated border border-stroke rounded text-fg placeholder-fg-disabled font-korean outline-none focus:border-color-accent"
/>
<button className="px-3 py-1.5 text-[11px] font-semibold rounded bg-bg-card border border-stroke text-fg-sub hover:bg-bg-surface-hover hover:text-fg transition-all font-korean">
<button
className="rounded-sm text-label-2 font-semibold cursor-pointer text-fg-sub px-3.5 py-1.5 font-korean"
style={{
border: '1px solid var(--stroke-default)',
background: 'var(--bg-card)',
}}
>
</button>
<button
@ -151,7 +157,11 @@ export function ReportsView() {
setView({ screen: 'templates' });
setActiveSubTab('template');
}}
className="px-3 py-1.5 text-[11px] font-semibold rounded bg-color-accent text-bg-0 hover:shadow-[0_0_8px_rgba(6,182,212,0.3)] transition-all font-korean"
className="rounded-sm text-label-2 font-semibold cursor-pointer text-color-accent px-3.5 py-1.5 font-korean"
style={{
border: '1px solid rgba(6,182,212,.3)',
background: 'rgba(6,182,212,.08)',
}}
>
+
</button>
@ -167,7 +177,7 @@ export function ReportsView() {
setView({ screen: 'templates' });
setActiveSubTab('template');
}}
className="px-4 py-2 text-xs font-semibold rounded-md border border-color-accent text-color-accent hover:bg-[rgba(6,182,212,0.1)] transition-all font-korean mt-4"
className="px-4 py-2 text-xs font-semibold rounded-md border border-color-accent text-color-accent hover:bg-[color-mix(in_srgb,var(--color-accent)_10%,transparent)] transition-all font-korean mt-4"
>
릿
</button>
@ -200,43 +210,43 @@ export function ReportsView() {
filteredReports.length > 0
}
onChange={toggleAll}
className="accent-[#06b6d4] w-3.5 h-3.5"
className="accent-[var(--color-accent)] w-3.5 h-3.5"
/>
</th>
<th className="px-3 py-3 text-[11px] font-semibold text-fg-disabled text-center font-korean">
<th className="px-3 py-3 text-label-2 font-semibold text-fg-disabled text-center font-korean">
</th>
<th className="px-4 py-3 text-[11px] font-semibold text-fg-disabled text-left font-korean">
<th className="px-4 py-3 text-label-2 font-semibold text-fg-disabled text-left font-korean">
</th>
<th className="px-3 py-3 text-[11px] font-semibold text-fg-disabled text-center font-korean">
<th className="px-3 py-3 text-label-2 font-semibold text-fg-disabled text-center font-korean">
</th>
<th className="px-3 py-3 text-[11px] font-semibold text-fg-disabled text-center font-korean">
<th className="px-3 py-3 text-label-2 font-semibold text-fg-disabled text-center font-korean">
</th>
<th className="px-3 py-3 text-[11px] font-semibold text-fg-disabled text-center font-korean">
<th className="px-3 py-3 text-label-2 font-semibold text-fg-disabled text-center font-korean">
</th>
<th className="px-3 py-3 text-[11px] font-semibold text-fg-disabled text-center font-korean">
<th className="px-3 py-3 text-label-2 font-semibold text-fg-disabled text-center font-korean">
</th>
<th className="px-3 py-3 text-[11px] font-semibold text-fg-disabled text-center font-korean">
<th className="px-3 py-3 text-label-2 font-semibold text-fg-disabled text-center font-korean">
</th>
<th className="px-3 py-3 text-[11px] font-semibold text-fg-disabled text-center font-korean">
<th className="px-3 py-3 text-label-2 font-semibold text-fg-disabled text-center font-korean">
</th>
<th className="px-3 py-3 text-[11px] font-semibold text-fg-disabled text-center font-korean">
<th className="px-3 py-3 text-label-2 font-semibold text-fg-disabled text-center font-korean">
</th>
<th className="px-3 py-3 text-[11px] font-semibold text-fg-disabled text-center font-korean">
<th className="px-3 py-3 text-label-2 font-semibold text-fg-disabled text-center font-korean">
</th>
<th className="px-3 py-3 text-[11px] font-semibold text-fg-disabled text-center font-korean">
<th className="px-3 py-3 text-label-2 font-semibold text-fg-disabled text-center font-korean">
</th>
<th className="px-3 py-3 text-[11px] font-semibold text-fg-disabled text-center font-korean">
<th className="px-3 py-3 text-label-2 font-semibold text-fg-disabled text-center font-korean">
</th>
</tr>
@ -252,10 +262,10 @@ export function ReportsView() {
type="checkbox"
checked={selectedIds.has(report.id)}
onChange={() => toggleSelect(report.id)}
className="accent-[#06b6d4] w-3.5 h-3.5"
className="accent-[var(--color-accent)] w-3.5 h-3.5"
/>
</td>
<td className="px-3 py-3 text-[12px] text-fg-disabled text-center font-mono">
<td className="px-3 py-3 text-label-1 text-fg-disabled text-center font-mono">
{idx + 1}
</td>
<td className="px-4 py-3 truncate">
@ -268,18 +278,18 @@ export function ReportsView() {
setPreviewReport(report);
}
}}
className="text-[12px] font-semibold text-color-accent hover:underline text-left font-korean truncate max-w-full block"
className="text-label-1 font-semibold text-color-accent hover:underline text-left font-korean truncate max-w-full block"
>
{report.title || '제목 없음'}
</button>
</td>
<td className="px-3 py-3 text-center">
<span
className="inline-block px-2.5 py-1 text-[10px] font-semibold rounded font-korean"
className="inline-block px-2.5 py-1 text-caption font-semibold rounded font-korean"
style={{
background:
typeColors[report.reportType]?.bg || 'rgba(138,150,168,0.15)',
color: typeColors[report.reportType]?.text || '#8a96a8',
color: typeColors[report.reportType]?.text || 'var(--fg-disabled)',
}}
>
{report.reportType}
@ -293,7 +303,7 @@ export function ReportsView() {
{style ? (
<div className="inline-flex items-center gap-1.5">
<div
className="flex items-center justify-center w-5 h-5 rounded-full text-[11px]"
className="flex items-center justify-center w-5 h-5 rounded-full text-label-2"
style={{
background: style.bg,
boxShadow: `0 0 6px ${style.text}25`,
@ -302,30 +312,30 @@ export function ReportsView() {
{style.icon}
</div>
<span
className="text-[9px] font-semibold font-korean"
className="text-caption font-semibold font-korean"
style={{ color: style.text }}
>
{cat}
</span>
</div>
) : (
<span className="text-[10px] text-fg-disabled font-korean">-</span>
<span className="text-label-2 text-fg-disabled font-korean">-</span>
)}
</td>
);
})()}
<td className="px-3 py-3 text-[11px] text-fg-sub text-center font-mono">
<td className="px-3 py-3 text-label-2 text-fg-sub text-center font-mono">
{formatDate(report.createdAt)}
</td>
<td className="px-3 py-3 text-[11px] text-fg-sub text-center font-korean">
<td className="px-3 py-3 text-label-2 text-fg-sub text-center font-korean">
{report.author || '-'}
</td>
<td className="px-3 py-3 text-[11px] text-fg-sub text-center font-korean">
<td className="px-3 py-3 text-label-2 text-fg-sub text-center font-korean">
{report.jurisdiction}
</td>
<td className="px-3 py-3 text-center">
<span
className="inline-block px-2.5 py-1 text-[10px] font-semibold rounded font-korean"
className="inline-block px-2.5 py-1 text-caption font-semibold rounded font-korean"
style={{
background: statusColors[report.status]?.bg,
color: statusColors[report.status]?.text,
@ -344,29 +354,29 @@ export function ReportsView() {
setView({ screen: 'edit', data: { ...report } });
}
}}
className="text-[11px] text-color-accent hover:underline font-korean"
className="text-label-2 text-color-accent hover:underline font-korean"
>
</button>
</td>
<td className="px-3 py-3 text-center">
{report.hasMapCapture || report.capturedMapImage ? (
<span title="확산예측 지도 캡처 있음" className="text-[14px]">
<span title="확산예측 지도 캡처 있음" className="text-title-3">
📷
</span>
) : (
<span className="text-[11px] text-fg-disabled"></span>
<span className="text-label-2 text-fg-disabled"></span>
)}
</td>
<td className="px-3 py-3 text-center">
<button className="inline-flex items-center gap-1 px-2 py-1 text-[10px] font-semibold rounded bg-[rgba(239,68,68,0.12)] text-[#ef4444] border border-[rgba(239,68,68,0.25)] hover:bg-[rgba(239,68,68,0.2)] transition-all">
<button className="inline-flex items-center gap-1 px-2 py-1 text-caption font-semibold rounded bg-[color-mix(in_srgb,var(--color-danger)_12%,transparent)] text-color-danger border border-[color-mix(in_srgb,var(--color-danger)_25%,transparent)] hover:bg-[color-mix(in_srgb,var(--color-danger)_20%,transparent)] transition-all">
PDF
</button>
</td>
<td className="px-3 py-3 text-center">
<button
onClick={() => handleDelete(report.id)}
className="w-7 h-7 rounded flex items-center justify-center text-color-danger hover:bg-[rgba(239,68,68,0.1)] transition-all text-sm"
className="w-7 h-7 rounded flex items-center justify-center text-color-danger hover:bg-[color-mix(in_srgb,var(--color-danger)_10%,transparent)] transition-all text-sm"
>
🗑
</button>
@ -473,18 +483,18 @@ export function ReportsView() {
)[previewReport.reportType] || '📄'}
</div>
<div
className="text-center font-korean text-[13px] font-bold leading-snug"
className="text-center font-korean text-title-4 font-bold leading-snug"
style={{ wordBreak: 'keep-all' }}
>
{previewReport.title || '제목 없음'}
</div>
<div className="text-center mt-2">
<span
className="inline-block px-2.5 py-1 text-[10px] font-semibold rounded font-korean"
className="inline-block px-2.5 py-1 text-caption font-semibold rounded font-korean"
style={{
background:
typeColors[previewReport.reportType]?.bg || 'rgba(138,150,168,0.15)',
color: typeColors[previewReport.reportType]?.text || '#8a96a8',
color: typeColors[previewReport.reportType]?.text || 'var(--fg-disabled)',
}}
>
{previewReport.reportType}
@ -493,19 +503,21 @@ export function ReportsView() {
</div>
{/* 메타 정보 */}
<div className="flex flex-col gap-2.5 font-korean text-[11px] px-[18px] py-3.5 border-b border-stroke">
<div className="flex flex-col gap-2.5 font-korean text-label-2 px-[18px] py-3.5 border-b border-stroke">
<div className="flex flex-col gap-0.5">
<span className="text-fg-disabled text-[9px] uppercase tracking-wide">
<span className="text-fg-disabled text-caption uppercase tracking-wide">
</span>
<span className="font-semibold">{previewReport.author || '—'}</span>
</div>
<div className="flex flex-col gap-0.5">
<span className="text-fg-disabled text-[9px] uppercase tracking-wide"></span>
<span className="text-fg-disabled text-caption uppercase tracking-wide">
</span>
<span className="font-semibold">{previewReport.jurisdiction}</span>
</div>
<div className="flex flex-col gap-0.5">
<span className="text-fg-disabled text-[9px] uppercase tracking-wide">
<span className="text-fg-disabled text-caption uppercase tracking-wide">
</span>
<span className="font-mono font-semibold">
@ -513,7 +525,9 @@ export function ReportsView() {
</span>
</div>
<div className="flex flex-col gap-0.5">
<span className="text-fg-disabled text-[9px] uppercase tracking-wide"></span>
<span className="text-fg-disabled text-caption uppercase tracking-wide">
</span>
<b
style={{
color: statusColors[previewReport.status]?.text || 'var(--fg-default)',
@ -531,7 +545,7 @@ export function ReportsView() {
setPreviewReport(null);
setView({ screen: 'edit', data: { ...previewReport } });
}}
className="w-full font-korean text-[11px] font-semibold cursor-pointer rounded-md border border-[rgba(6,182,212,0.3)] bg-[rgba(6,182,212,0.08)] text-color-accent py-2"
className="w-full font-korean text-label-2 font-semibold cursor-pointer rounded-md border border-[color-mix(in_srgb,var(--color-accent)_30%,transparent)] bg-[color-mix(in_srgb,var(--color-accent)_8%,transparent)] text-color-accent py-2"
>
</button>
@ -542,7 +556,7 @@ export function ReportsView() {
{/* 하단 다운로드 버튼 */}
<div className="flex flex-col gap-2 px-4 py-3.5 border-t border-stroke">
<div className="text-center font-korean mb-0.5 text-[9px] text-fg-disabled">
<div className="text-center font-korean mb-0.5 text-caption text-fg-disabled">
</div>
<button
@ -567,10 +581,10 @@ export function ReportsView() {
exportAsPDF(html, previewReport.title || tpl.label);
}
}}
className="w-full flex items-center justify-center gap-1.5 font-korean text-[12px] font-bold cursor-pointer rounded-md py-[11px]"
className="w-full flex items-center justify-center gap-1.5 font-korean text-label-1 font-bold cursor-pointer rounded-md py-[11px]"
style={{
border: '1px solid rgba(239,68,68,0.4)',
background: 'rgba(239,68,68,0.1)',
border: '1px solid color-mix(in srgb, var(--color-danger) 40%, transparent)',
background: 'color-mix(in srgb, var(--color-danger) 10%, transparent)',
color: 'var(--color-danger)',
}}
>
@ -606,7 +620,7 @@ export function ReportsView() {
}
}
}}
className="w-full flex items-center justify-center gap-1.5 font-korean text-[12px] font-bold cursor-pointer rounded-md py-[11px]"
className="w-full flex items-center justify-center gap-1.5 font-korean text-label-1 font-bold cursor-pointer rounded-md py-[11px]"
style={{
border: '1px solid rgba(59,130,246,0.4)',
background: 'rgba(59,130,246,0.1)',
@ -623,14 +637,14 @@ export function ReportsView() {
{/* 헤더 */}
<div className="flex items-center justify-between shrink-0 px-5 py-3.5 border-b border-stroke">
<div className="flex items-center gap-2">
<span className="font-korean text-[9px] px-2 py-[3px] rounded bg-[rgba(6,182,212,0.1)] text-color-accent font-semibold">
<span className="font-korean text-caption px-2 py-[3px] rounded bg-[rgba(6,182,212,0.1)] text-color-accent font-semibold">
</span>
<span className="font-korean text-[12px] text-fg-disabled"> </span>
<span className="font-korean text-label-1 text-fg-disabled"> </span>
</div>
<span
onClick={() => setPreviewReport(null)}
className="text-[18px] cursor-pointer text-fg-disabled leading-none hover:text-fg transition-colors"
className="text-title-1 cursor-pointer text-fg-disabled leading-none hover:text-fg transition-colors"
>
</span>
@ -642,12 +656,12 @@ export function ReportsView() {
{/* 1. 사고개요 */}
<div>
<div
className="font-korean text-[12px] font-bold text-color-accent border-b pb-1 mb-2"
className="font-korean text-label-1 font-bold text-color-accent border-b pb-1 mb-2"
style={{ borderColor: 'rgba(6,182,212,0.15)' }}
>
1.
</div>
<div className="font-korean text-[12px] leading-[1.7] whitespace-pre-wrap mt-2">
<div className="font-korean text-label-1 leading-[1.7] whitespace-pre-wrap mt-2">
{[
previewReport.incident.name && `사고명: ${previewReport.incident.name}`,
previewReport.incident.occurTime &&
@ -667,12 +681,12 @@ export function ReportsView() {
{/* 2. 유출현황 */}
<div>
<div
className="font-korean text-[12px] font-bold text-color-accent border-b pb-1 mb-2"
className="font-korean text-label-1 font-bold text-color-accent border-b pb-1 mb-2"
style={{ borderColor: 'rgba(6,182,212,0.15)' }}
>
2.
</div>
<div className="font-korean text-[12px] leading-[1.7] whitespace-pre-wrap mt-2">
<div className="font-korean text-label-1 leading-[1.7] whitespace-pre-wrap mt-2">
{[
previewReport.incident.pollutant &&
`유출유종: ${previewReport.incident.pollutant}`,
@ -698,7 +712,7 @@ export function ReportsView() {
<div className="grid grid-cols-2 gap-3">
{previewReport.step3MapImage && (
<div className="relative">
<span className="absolute top-1.5 left-1.5 z-10 text-[10px] font-semibold px-1.5 py-0.5 rounded bg-cyan-500/70 text-white">
<span className="absolute top-1.5 left-1.5 z-10 text-caption font-semibold px-1.5 py-0.5 rounded bg-cyan-500/70 text-white">
3
</span>
<img
@ -711,7 +725,7 @@ export function ReportsView() {
)}
{previewReport.step6MapImage && (
<div className="relative">
<span className="absolute top-1.5 left-1.5 z-10 text-[10px] font-semibold px-1.5 py-0.5 rounded bg-cyan-500/70 text-white">
<span className="absolute top-1.5 left-1.5 z-10 text-caption font-semibold px-1.5 py-0.5 rounded bg-cyan-500/70 text-white">
6
</span>
<img

파일 보기

@ -91,7 +91,7 @@ function SectionBlock({ section, getVal, setVal }: SectionBlockProps) {
position: 'absolute',
top: 10,
right: 16,
fontSize: '9px',
fontSize: 'var(--font-size-caption)',
color: 'var(--fg-disabled)',
fontWeight: 600,
}}
@ -102,14 +102,14 @@ function SectionBlock({ section, getVal, setVal }: SectionBlockProps) {
{/* Section header */}
<div
style={{
background: 'rgba(6,182,212,0.12)',
background: 'color-mix(in srgb, var(--color-accent) 12%, transparent)',
color: 'var(--color-accent)',
padding: '8px 16px',
fontSize: '13px',
fontSize: 'var(--font-size-title-4)',
fontWeight: 700,
marginBottom: '16px',
borderRadius: '4px',
border: '1px solid rgba(6,182,212,0.2)',
border: '1px solid color-mix(in srgb, var(--color-accent) 20%, transparent)',
}}
>
{section.title}
@ -121,7 +121,7 @@ function SectionBlock({ section, getVal, setVal }: SectionBlockProps) {
width: '100%',
tableLayout: 'fixed',
borderCollapse: 'collapse',
fontSize: '11px',
fontSize: 'var(--font-size-caption)',
}}
>
<colgroup>
@ -139,7 +139,11 @@ function SectionBlock({ section, getVal, setVal }: SectionBlockProps) {
style={{ border: '1px solid var(--stroke-default)', padding: '12px 14px' }}
>
<span
style={{ color: 'var(--fg-disabled)', fontStyle: 'italic', fontSize: '11px' }}
style={{
color: 'var(--fg-disabled)',
fontStyle: 'italic',
fontSize: 'var(--font-size-caption)',
}}
>
{section.title} ( )
</span>
@ -161,7 +165,7 @@ function SectionBlock({ section, getVal, setVal }: SectionBlockProps) {
fontWeight: 600,
color: 'var(--fg-sub)',
verticalAlign: 'top',
fontSize: '11px',
fontSize: 'var(--font-size-caption)',
}}
>
{field.label}
@ -182,11 +186,10 @@ function SectionBlock({ section, getVal, setVal }: SectionBlockProps) {
border: '1px solid var(--stroke-light)',
borderRadius: '3px',
padding: '8px 12px',
fontSize: '12px',
fontSize: 'var(--font-size-label-1)',
outline: 'none',
resize: 'vertical',
color: 'var(--fg-default)',
fontFamily: 'inherit',
}}
/>
</td>
@ -206,7 +209,7 @@ function SectionBlock({ section, getVal, setVal }: SectionBlockProps) {
fontWeight: 600,
color: 'var(--fg-sub)',
verticalAlign: 'middle',
fontSize: '11px',
fontSize: 'var(--font-size-caption)',
}}
>
{field.label}
@ -220,12 +223,12 @@ function SectionBlock({ section, getVal, setVal }: SectionBlockProps) {
display: 'flex',
alignItems: 'center',
gap: '6px',
fontSize: '11px',
fontSize: 'var(--font-size-caption)',
color: 'var(--fg-sub)',
cursor: 'pointer',
}}
>
<input type="checkbox" style={{ accentColor: '#06b6d4' }} />
<input type="checkbox" style={{ accentColor: 'var(--color-accent)' }} />
{opt}
</label>
))}
@ -246,7 +249,7 @@ function SectionBlock({ section, getVal, setVal }: SectionBlockProps) {
fontWeight: 600,
color: 'var(--fg-sub)',
verticalAlign: 'middle',
fontSize: '11px',
fontSize: 'var(--font-size-caption)',
}}
>
{field.label}
@ -260,7 +263,7 @@ function SectionBlock({ section, getVal, setVal }: SectionBlockProps) {
width: '100%',
background: 'transparent',
border: 'none',
fontSize: '11px',
fontSize: 'var(--font-size-caption)',
outline: 'none',
color: 'var(--fg-default)',
padding: '2px 0',
@ -353,7 +356,7 @@ export default function TemplateEditPage({
<div className="flex items-center gap-3">
<button
onClick={onBack}
className="text-[12px] font-semibold text-fg-sub hover:text-fg transition-colors whitespace-nowrap"
className="text-label-1 font-semibold text-fg-sub hover:text-fg transition-colors whitespace-nowrap"
>
</button>
@ -361,14 +364,14 @@ export default function TemplateEditPage({
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="보고서 제목 입력"
className="text-[17px] font-bold bg-bg-base border border-[var(--stroke-light)] rounded px-2.5 py-1 outline-none w-[380px] max-w-[480px]"
className="text-title-1 font-bold bg-bg-base border border-[var(--stroke-light)] rounded px-2.5 py-1 outline-none w-[380px] max-w-[480px]"
/>
<span
className="px-2.5 py-[3px] text-[10px] font-semibold rounded border whitespace-nowrap"
className="px-2.5 py-[3px] text-label-2 font-semibold rounded border whitespace-nowrap"
style={{
background: 'rgba(251,191,36,0.15)',
color: '#f59e0b',
borderColor: 'rgba(251,191,36,0.3)',
background: 'color-mix(in srgb, var(--color-warning) 15%, transparent)',
color: 'var(--color-warning)',
borderColor: 'color-mix(in srgb, var(--color-warning) 30%, transparent)',
}}
>
@ -377,13 +380,16 @@ export default function TemplateEditPage({
<div className="flex items-center gap-2">
<button
onClick={() => setViewMode('all')}
className="px-3.5 py-1.5 text-[11px] font-semibold rounded cursor-pointer"
className="px-3.5 py-1.5 text-label-2 font-semibold rounded cursor-pointer"
style={{
border:
viewMode === 'all'
? '1px solid var(--color-accent)'
: '1px solid var(--stroke-default)',
background: viewMode === 'all' ? 'rgba(6,182,212,0.1)' : 'var(--bg-elevated)',
background:
viewMode === 'all'
? 'color-mix(in srgb, var(--color-accent) 10%, transparent)'
: 'var(--bg-elevated)',
color: viewMode === 'all' ? 'var(--color-accent)' : 'var(--fg-disabled)',
}}
>
@ -391,13 +397,16 @@ export default function TemplateEditPage({
</button>
<button
onClick={() => setViewMode('page')}
className="px-3.5 py-1.5 text-[11px] font-semibold rounded cursor-pointer"
className="px-3.5 py-1.5 text-label-2 font-semibold rounded cursor-pointer"
style={{
border:
viewMode === 'page'
? '1px solid var(--color-accent)'
: '1px solid var(--stroke-default)',
background: viewMode === 'page' ? 'rgba(6,182,212,0.1)' : 'var(--bg-elevated)',
background:
viewMode === 'page'
? 'color-mix(in srgb, var(--color-accent) 10%, transparent)'
: 'var(--bg-elevated)',
color: viewMode === 'page' ? 'var(--color-accent)' : 'var(--fg-disabled)',
}}
>
@ -405,13 +414,13 @@ export default function TemplateEditPage({
</button>
<button
onClick={handleSave}
className="px-4 py-1.5 text-[11px] font-bold rounded cursor-pointer border border-[#22c55e] bg-[rgba(34,197,94,0.15)] text-color-success"
className="px-4 py-1.5 text-label-2 font-bold rounded cursor-pointer border border-color-success bg-[color-mix(in_srgb,var(--color-success)_15%,transparent)] text-color-success"
>
</button>
<button
onClick={() => window.print()}
className="px-3.5 py-1.5 text-[11px] font-semibold rounded cursor-pointer border border-[var(--color-danger)] bg-[rgba(239,68,68,0.1)] text-color-danger"
className="px-3.5 py-1.5 text-label-2 font-semibold rounded cursor-pointer border border-[var(--color-danger)] bg-[color-mix(in_srgb,var(--color-danger)_10%,transparent)] text-color-danger"
>
/ PDF
</button>
@ -425,13 +434,16 @@ export default function TemplateEditPage({
<button
key={i}
onClick={() => setCurrentPage(i)}
className="px-3 py-1.5 text-[11px] font-semibold rounded cursor-pointer"
className="px-3 py-1.5 text-label-2 font-semibold rounded cursor-pointer"
style={{
border:
currentPage === i
? '1px solid var(--color-accent)'
: '1px solid var(--stroke-default)',
background: currentPage === i ? 'rgba(6,182,212,0.15)' : 'transparent',
background:
currentPage === i
? 'color-mix(in srgb, var(--color-accent) 15%, transparent)'
: 'transparent',
color: currentPage === i ? 'var(--color-accent)' : 'var(--fg-disabled)',
}}
>
@ -457,7 +469,7 @@ export default function TemplateEditPage({
<button
onClick={() => setCurrentPage((p) => Math.max(0, p - 1))}
disabled={currentPage === 0}
className="px-5 py-2 text-[12px] font-semibold rounded border border-stroke bg-bg-elevated cursor-pointer"
className="px-5 py-2 text-label-1 font-semibold rounded border border-stroke bg-bg-elevated cursor-pointer"
style={{
color: currentPage === 0 ? 'var(--fg-disabled)' : 'var(--fg-default)',
opacity: currentPage === 0 ? 0.4 : 1,
@ -465,16 +477,16 @@ export default function TemplateEditPage({
>
</button>
<span className="px-4 py-2 text-[12px] text-fg-sub">
<span className="px-4 py-2 text-label-1 text-fg-sub">
{currentPage + 1} / {sections.length}
</span>
<button
onClick={() => setCurrentPage((p) => Math.min(sections.length - 1, p + 1))}
disabled={currentPage === sections.length - 1}
className="px-5 py-2 text-[12px] font-semibold rounded cursor-pointer"
className="px-5 py-2 text-label-1 font-semibold rounded cursor-pointer"
style={{
border: '1px solid var(--color-accent)',
background: 'rgba(6,182,212,0.1)',
background: 'color-mix(in srgb, var(--color-accent) 10%, transparent)',
color:
currentPage === sections.length - 1
? 'var(--fg-disabled)'

파일 보기

@ -108,8 +108,8 @@ function TemplateFormEditor({ onSave, onBack }: TemplateFormEditorProps) {
{/* Left Sidebar - Template Selection */}
<div className="w-60 min-w-[240px] border-r border-stroke bg-bg-surface flex flex-col py-4 px-3 gap-2 overflow-y-auto shrink-0">
<div className="px-1 mb-2">
<h3 className="text-[13px] font-bold text-fg font-korean"> 릿 </h3>
<p className="text-[9px] text-fg-disabled font-korean mt-1">
<h3 className="text-title-4 font-bold text-fg font-korean"> 릿 </h3>
<p className="text-caption text-fg-disabled font-korean mt-1">
릿 .
</p>
</div>
@ -125,11 +125,11 @@ function TemplateFormEditor({ onSave, onBack }: TemplateFormEditorProps) {
>
<span className="text-lg mb-1">{t.icon}</span>
<span
className={`text-[12px] font-bold font-korean ${selectedType === t.id ? 'text-color-accent' : 'text-fg'}`}
className={`text-label-1 font-bold font-korean ${selectedType === t.id ? 'text-color-accent' : 'text-fg'}`}
>
{t.label}
</span>
<span className="text-[9px] text-fg-disabled font-korean mt-0.5">{t.desc}</span>
<span className="text-caption text-fg-disabled font-korean mt-0.5">{t.desc}</span>
</button>
))}
</div>
@ -140,16 +140,19 @@ function TemplateFormEditor({ onSave, onBack }: TemplateFormEditorProps) {
<div className="flex items-center justify-between px-6 py-3 border-b border-stroke bg-bg-surface">
<div className="flex items-center gap-3">
<span className="text-lg">{template.icon}</span>
<span className="text-[15px] font-bold text-fg font-korean">{template.label}</span>
<span className="text-subtitle font-bold text-fg font-korean">{template.label}</span>
<span
className="px-2 py-0.5 text-[9px] font-semibold rounded font-korean"
style={{ background: 'rgba(6,182,212,0.15)', color: '#06b6d4' }}
className="px-2 py-0.5 text-caption font-semibold rounded font-korean"
style={{
background: 'color-mix(in srgb, var(--color-accent) 15%, transparent)',
color: 'var(--color-accent)',
}}
>
</span>
</div>
<div className="flex items-center gap-3">
<span className="text-[10px] text-fg-disabled font-korean">:</span>
<span className="text-label-2 text-fg-disabled font-korean">:</span>
<button
onClick={() => setAutoSave(!autoSave)}
className={`relative w-9 h-[18px] rounded-full transition-all ${autoSave ? 'bg-color-accent' : 'bg-bg-card border border-stroke'}`}
@ -159,8 +162,8 @@ function TemplateFormEditor({ onSave, onBack }: TemplateFormEditorProps) {
/>
</button>
<span
className="text-[10px] font-semibold font-korean"
style={{ color: autoSave ? '#06b6d4' : 'var(--fg-disabled)' }}
className="text-label-2 font-semibold font-korean"
style={{ color: autoSave ? 'var(--color-accent)' : 'var(--fg-disabled)' }}
>
{autoSave ? 'ON' : 'OFF'}
</span>
@ -171,7 +174,7 @@ function TemplateFormEditor({ onSave, onBack }: TemplateFormEditorProps) {
<div className="flex-1 overflow-y-auto px-6 py-5">
{template.sections.map((section, sIdx) => (
<div key={sIdx} className="mb-6 w-full">
<h4 className="text-[13px] font-bold font-korean mb-3 text-cyan-500">
<h4 className="text-title-4 font-bold font-korean mb-3 text-color-accent">
{section.title}
</h4>
<table className="w-full table-fixed border-collapse">
@ -184,7 +187,7 @@ function TemplateFormEditor({ onSave, onBack }: TemplateFormEditorProps) {
<tr key={fIdx} className="border-b border-stroke">
{field.label ? (
<>
<td className="px-4 py-3 text-[11px] font-semibold text-fg-disabled font-korean bg-[rgba(255,255,255,0.03)] align-middle">
<td className="px-4 py-3 text-label-2 font-semibold text-fg-disabled font-korean bg-[rgba(255,255,255,0.03)] align-middle">
{field.label}
</td>
<td className="px-4 py-2 align-middle">
@ -193,7 +196,7 @@ function TemplateFormEditor({ onSave, onBack }: TemplateFormEditorProps) {
value={getVal(field.key)}
onChange={(e) => setVal(field.key, e.target.value)}
placeholder={`${field.label} 입력`}
className="w-full bg-transparent text-[12px] text-fg font-korean outline-none placeholder-fg-disabled"
className="w-full bg-transparent text-label-1 text-fg font-korean outline-none placeholder-fg-disabled"
/>
)}
{field.type === 'checkbox-group' && field.options && (
@ -201,11 +204,11 @@ function TemplateFormEditor({ onSave, onBack }: TemplateFormEditorProps) {
{field.options.map((opt) => (
<label
key={opt}
className="flex items-center gap-1.5 text-[11px] text-fg-sub font-korean cursor-pointer"
className="flex items-center gap-1.5 text-label-2 text-fg-sub font-korean cursor-pointer"
>
<input
type="checkbox"
className="accent-[#06b6d4] w-3.5 h-3.5"
className="accent-[var(--color-accent)] w-3.5 h-3.5"
/>
{opt}
</label>
@ -220,7 +223,7 @@ function TemplateFormEditor({ onSave, onBack }: TemplateFormEditorProps) {
value={getVal(field.key)}
onChange={(e) => setVal(field.key, e.target.value)}
placeholder="내용을 입력하세요..."
className="w-full min-h-[120px] bg-bg-elevated border border-stroke rounded-md px-3 py-2 text-[12px] text-fg font-korean outline-none placeholder-fg-disabled resize-y focus:border-color-accent"
className="w-full min-h-[120px] bg-bg-elevated border border-stroke rounded-md px-3 py-2 text-label-1 text-fg font-korean outline-none placeholder-fg-disabled resize-y focus:border-color-accent"
/>
</td>
)}
@ -237,13 +240,13 @@ function TemplateFormEditor({ onSave, onBack }: TemplateFormEditorProps) {
<div className="flex items-center gap-2">
<button
onClick={() => doExport('pdf')}
className="px-3 py-2 text-[11px] font-semibold rounded bg-color-danger text-white hover:opacity-90 transition-all"
className="px-3 py-2 text-label-2 font-semibold rounded bg-color-danger text-white hover:opacity-90 transition-all"
>
PDF
</button>
<button
onClick={() => doExport('hwp')}
className="px-3 py-2 text-[11px] font-semibold rounded bg-[#2563eb] text-white hover:opacity-90 transition-all"
className="px-3 py-2 text-label-2 font-semibold rounded bg-color-info text-white hover:opacity-90 transition-all"
>
HWPX
</button>
@ -251,19 +254,19 @@ function TemplateFormEditor({ onSave, onBack }: TemplateFormEditorProps) {
<div className="flex items-center gap-3">
<button
onClick={onBack}
className="px-4 py-2 text-[11px] font-semibold rounded text-fg-disabled hover:text-fg transition-all font-korean"
className="px-4 py-2 text-label-2 font-semibold rounded text-fg-disabled hover:text-fg transition-all font-korean"
>
</button>
<button
onClick={() => setShowPreview(true)}
className="px-4 py-2 text-[11px] font-semibold rounded border border-color-accent text-color-accent hover:bg-[rgba(6,182,212,0.1)] transition-all font-korean"
className="px-4 py-2 text-label-2 font-semibold rounded border border-color-accent text-color-accent hover:bg-[rgba(6,182,212,0.1)] transition-all font-korean"
>
</button>
<button
onClick={handleSave}
className="px-5 py-2 text-[11px] font-semibold rounded bg-color-accent text-bg-0 hover:shadow-[0_0_12px_rgba(6,182,212,0.3)] transition-all font-korean flex items-center gap-1"
className="px-5 py-2 text-label-2 font-semibold rounded bg-color-accent text-bg-0 hover:shadow-[0_0_12px_rgba(6,182,212,0.3)] transition-all font-korean flex items-center gap-1"
>
</button>
@ -285,10 +288,15 @@ function TemplateFormEditor({ onSave, onBack }: TemplateFormEditorProps) {
<div className="flex items-center justify-between px-6 py-4 border-b border-stroke">
<div className="flex items-center gap-3">
<span className="text-lg">{template.icon}</span>
<span className="text-[15px] font-bold text-fg font-korean">{template.label}</span>
<span className="text-subtitle font-bold text-fg font-korean">
{template.label}
</span>
<span
className="px-2 py-0.5 text-[9px] font-semibold rounded font-korean"
style={{ background: 'rgba(6,182,212,0.15)', color: '#06b6d4' }}
className="px-2 py-0.5 text-caption font-semibold rounded font-korean"
style={{
background: 'color-mix(in srgb, var(--color-accent) 15%, transparent)',
color: 'var(--color-accent)',
}}
>
</span>
@ -306,13 +314,13 @@ function TemplateFormEditor({ onSave, onBack }: TemplateFormEditorProps) {
<div className="w-full">
{/* Report Title */}
<div className="text-center mb-8">
<h2 className="text-[18px] font-bold text-fg font-korean mb-1">
<h2 className="text-title-1 font-bold text-fg font-korean mb-1">
</h2>
<h3 className="text-[15px] font-semibold font-korean text-cyan-500">
<h3 className="text-subtitle font-semibold font-korean text-color-accent">
{formData['incident.name'] || template.label}
</h3>
<p className="text-[11px] text-fg-disabled font-korean mt-2">
<p className="text-label-2 text-fg-disabled font-korean mt-2">
: {reportMeta.writeTime} | : {reportMeta.author || '-'} | :{' '}
{reportMeta.jurisdiction}
</p>
@ -322,8 +330,11 @@ function TemplateFormEditor({ onSave, onBack }: TemplateFormEditorProps) {
{template.sections.map((section, sIdx) => (
<div key={sIdx} className="mb-5">
<h4
className="text-[13px] font-bold font-korean mb-2 px-2 py-1.5 rounded"
style={{ color: '#06b6d4', background: 'rgba(6,182,212,0.06)' }}
className="text-title-4 font-bold font-korean mb-2 px-2 py-1.5 rounded"
style={{
color: 'var(--color-accent)',
background: 'color-mix(in srgb, var(--color-accent) 6%, transparent)',
}}
>
{section.title}
</h4>
@ -337,10 +348,10 @@ function TemplateFormEditor({ onSave, onBack }: TemplateFormEditorProps) {
const val = getVal(field.key);
return field.label ? (
<tr key={fIdx} className="border-b border-stroke">
<td className="px-4 py-2.5 text-[11px] font-semibold text-fg-disabled font-korean bg-[rgba(255,255,255,0.03)] border-r border-stroke align-middle">
<td className="px-4 py-2.5 text-label-2 font-semibold text-fg-disabled font-korean bg-[rgba(255,255,255,0.03)] border-r border-stroke align-middle">
{field.label}
</td>
<td className="px-4 py-2.5 text-[12px] text-fg font-korean align-middle">
<td className="px-4 py-2.5 text-label-1 text-fg font-korean align-middle">
{val || <span className="text-fg-disabled">-</span>}
</td>
</tr>
@ -348,7 +359,7 @@ function TemplateFormEditor({ onSave, onBack }: TemplateFormEditorProps) {
<tr key={fIdx} className="border-b border-stroke">
<td
colSpan={2}
className="px-4 py-3 text-[12px] text-fg font-korean whitespace-pre-wrap"
className="px-4 py-3 text-label-1 text-fg font-korean whitespace-pre-wrap"
>
{val || <span className="text-fg-disabled"> </span>}
</td>
@ -366,7 +377,7 @@ function TemplateFormEditor({ onSave, onBack }: TemplateFormEditorProps) {
<div className="flex items-center justify-end gap-3 px-6 py-3 border-t border-stroke">
<button
onClick={() => setShowPreview(false)}
className="px-4 py-2 text-[11px] font-semibold rounded text-fg-disabled hover:text-fg transition-all font-korean"
className="px-4 py-2 text-label-2 font-semibold rounded text-fg-disabled hover:text-fg transition-all font-korean"
>
</button>
@ -375,7 +386,7 @@ function TemplateFormEditor({ onSave, onBack }: TemplateFormEditorProps) {
setShowPreview(false);
handleSave();
}}
className="px-5 py-2 text-[11px] font-semibold rounded bg-color-accent text-bg-0 hover:shadow-[0_0_12px_rgba(6,182,212,0.3)] transition-all font-korean"
className="px-5 py-2 text-label-2 font-semibold rounded bg-color-accent text-bg-0 hover:shadow-[0_0_12px_rgba(6,182,212,0.3)] transition-all font-korean"
>
</button>

파일 보기

@ -390,9 +390,9 @@ export const CATEGORIES: CategoryDef[] = [
icon: '🧪',
label: 'HNS 대기확산',
desc: 'ALOHA · WRF-Chem',
color: 'var(--color-warning)',
borderColor: 'rgba(249,115,22,0.4)',
bgActive: 'rgba(249,115,22,0.08)',
color: 'var(--color-accent)',
borderColor: 'rgba(6,182,212,0.4)',
bgActive: 'rgba(6,182,212,0.08)',
reportName: 'HNS 대기확산 예측보고서',
templates: [
{ icon: '🧪', label: 'HNS 예측보고서' },
@ -456,9 +456,9 @@ export const CATEGORIES: CategoryDef[] = [
icon: '🚨',
label: '긴급구난',
desc: '복원성 · 좌초위험 분석',
color: 'var(--color-danger)',
borderColor: 'rgba(239,68,68,0.4)',
bgActive: 'rgba(239,68,68,0.08)',
color: 'var(--color-accent)',
borderColor: 'rgba(6,182,212,0.4)',
bgActive: 'rgba(6,182,212,0.08)',
reportName: '긴급구난 상황보고서',
templates: [
{ icon: '🚨', label: '긴급구난 상황보고' },

Some files were not shown because too many files have changed in this diff Show More