Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
Menu
Open sidebar
Administrator
sensor_mvp
Commits
6423f0de
Commit
6423f0de
authored
Sep 02, 2025
by
Sensor MVP Team
Browse files
Merge master into main: Add comprehensive project files and documentation
parents
d32dec3c
fcfb9aec
Changes
87
Show whitespace changes
Inline
Side-by-side
web-dashboard/src/utils/dataTransformers.ts
0 → 100644
View file @
6423f0de
import
{
SensorData
,
Device
}
from
'
../services/api
'
;
import
{
isSensorData
,
isDevice
,
toSafeNumber
,
toSafeString
,
safeGet
}
from
'
./typeGuards
'
;
// 차트 데이터 인터페이스
export
interface
ChartDataPoint
{
device_id
:
string
;
temperature
:
number
;
humidity
:
number
;
pm10
:
number
;
pm25
:
number
;
pressure
:
number
;
illumination
:
number
;
tvoc
:
number
;
co2
:
number
;
o2
:
number
;
co
:
number
;
timestamp
?:
string
;
}
// 센서 데이터를 차트 데이터로 변환
export
const
transformSensorDataToChartData
=
(
sensorData
:
SensorData
[]):
ChartDataPoint
[]
=>
{
if
(
!
Array
.
isArray
(
sensorData
)
||
sensorData
.
length
===
0
)
{
return
createDefaultChartData
();
}
try
{
const
validData
=
sensorData
.
filter
(
data
=>
{
if
(
!
isSensorData
(
data
))
{
console
.
warn
(
'
Invalid sensor data found:
'
,
data
);
return
false
;
}
return
true
;
});
if
(
validData
.
length
===
0
)
{
console
.
warn
(
'
No valid sensor data found, returning default data
'
);
return
createDefaultChartData
();
}
return
validData
.
map
(
data
=>
({
device_id
:
toSafeString
(
data
.
device_id
,
'
Unknown
'
),
temperature
:
toSafeNumber
(
data
.
temperature
,
0
),
humidity
:
toSafeNumber
(
data
.
humidity
,
0
),
pm10
:
toSafeNumber
(
data
.
pm10
,
0
),
pm25
:
toSafeNumber
(
data
.
pm25
,
0
),
pressure
:
toSafeNumber
(
data
.
pressure
,
0
),
illumination
:
toSafeNumber
(
data
.
illumination
,
0
),
tvoc
:
toSafeNumber
(
data
.
tvoc
,
0
),
co2
:
toSafeNumber
(
data
.
co2
,
0
),
o2
:
toSafeNumber
(
data
.
o2
,
0
),
co
:
toSafeNumber
(
data
.
co
,
0
),
timestamp
:
toSafeString
(
data
.
recorded_time
,
''
)
}));
}
catch
(
error
)
{
console
.
error
(
'
Error transforming sensor data to chart data:
'
,
error
);
return
createDefaultChartData
();
}
};
// 기본 차트 데이터 생성
export
const
createDefaultChartData
=
():
ChartDataPoint
[]
=>
{
return
[{
device_id
:
'
No Data
'
,
temperature
:
0
,
humidity
:
0
,
pm10
:
0
,
pm25
:
0
,
pressure
:
0
,
illumination
:
0
,
tvoc
:
0
,
co2
:
0
,
o2
:
0
,
co
:
0
,
timestamp
:
new
Date
().
toISOString
()
}];
};
// 센서 카드 데이터 인터페이스
export
interface
SensorCardData
{
title
:
string
;
value
:
number
;
unit
:
string
;
icon
:
string
;
bgColor
:
string
;
precision
:
number
;
}
// 센서 데이터를 카드 데이터로 변환
export
const
transformSensorDataToCardData
=
(
sensorData
:
SensorData
|
null
):
SensorCardData
[]
=>
{
if
(
!
sensorData
||
!
isSensorData
(
sensorData
))
{
return
createDefaultCardData
();
}
try
{
return
[
{
title
:
'
현재 온도
'
,
value
:
toSafeNumber
(
sensorData
.
temperature
,
0
),
unit
:
'
°C
'
,
icon
:
'
🌡️
'
,
bgColor
:
'
bg-green-100
'
,
precision
:
1
},
{
title
:
'
현재 습도
'
,
value
:
toSafeNumber
(
sensorData
.
humidity
,
0
),
unit
:
'
%
'
,
icon
:
'
💧
'
,
bgColor
:
'
bg-yellow-100
'
,
precision
:
1
},
{
title
:
'
PM10
'
,
value
:
toSafeNumber
(
sensorData
.
pm10
,
0
),
unit
:
'
μg/m³
'
,
icon
:
'
🌫️
'
,
bgColor
:
'
bg-orange-100
'
,
precision
:
1
},
{
title
:
'
PM2.5
'
,
value
:
toSafeNumber
(
sensorData
.
pm25
,
0
),
unit
:
'
μg/m³
'
,
icon
:
'
💨
'
,
bgColor
:
'
bg-red-100
'
,
precision
:
1
},
{
title
:
'
기압
'
,
value
:
toSafeNumber
(
sensorData
.
pressure
,
0
),
unit
:
'
hPa
'
,
icon
:
'
🌪️
'
,
bgColor
:
'
bg-blue-100
'
,
precision
:
1
},
{
title
:
'
조도
'
,
value
:
toSafeNumber
(
sensorData
.
illumination
,
0
),
unit
:
'
lux
'
,
icon
:
'
☀️
'
,
bgColor
:
'
bg-yellow-100
'
,
precision
:
0
},
{
title
:
'
TVOC
'
,
value
:
toSafeNumber
(
sensorData
.
tvoc
,
0
),
unit
:
'
ppb
'
,
icon
:
'
🧪
'
,
bgColor
:
'
bg-purple-100
'
,
precision
:
1
},
{
title
:
'
CO2
'
,
value
:
toSafeNumber
(
sensorData
.
co2
,
0
),
unit
:
'
ppm
'
,
icon
:
'
🌿
'
,
bgColor
:
'
bg-green-100
'
,
precision
:
0
},
{
title
:
'
O2
'
,
value
:
toSafeNumber
(
sensorData
.
o2
,
0
),
unit
:
'
%
'
,
icon
:
'
💨
'
,
bgColor
:
'
bg-cyan-100
'
,
precision
:
1
},
{
title
:
'
CO
'
,
value
:
toSafeNumber
(
sensorData
.
co
,
0
),
unit
:
'
ppm
'
,
icon
:
'
🔥
'
,
bgColor
:
'
bg-red-100
'
,
precision
:
2
}
];
}
catch
(
error
)
{
console
.
error
(
'
Error transforming sensor data to card data:
'
,
error
);
return
createDefaultCardData
();
}
};
// 기본 카드 데이터 생성
export
const
createDefaultCardData
=
():
SensorCardData
[]
=>
{
return
[
{
title
:
'
현재 온도
'
,
value
:
0
,
unit
:
'
°C
'
,
icon
:
'
🌡️
'
,
bgColor
:
'
bg-gray-100
'
,
precision
:
1
},
{
title
:
'
현재 습도
'
,
value
:
0
,
unit
:
'
%
'
,
icon
:
'
💧
'
,
bgColor
:
'
bg-gray-100
'
,
precision
:
1
},
{
title
:
'
PM10
'
,
value
:
0
,
unit
:
'
μg/m³
'
,
icon
:
'
🌫️
'
,
bgColor
:
'
bg-gray-100
'
,
precision
:
1
},
{
title
:
'
PM2.5
'
,
value
:
0
,
unit
:
'
μg/m³
'
,
icon
:
'
💨
'
,
bgColor
:
'
bg-gray-100
'
,
precision
:
1
},
{
title
:
'
기압
'
,
value
:
0
,
unit
:
'
hPa
'
,
icon
:
'
🌪️
'
,
bgColor
:
'
bg-gray-100
'
,
precision
:
1
},
{
title
:
'
조도
'
,
value
:
0
,
unit
:
'
lux
'
,
icon
:
'
☀️
'
,
bgColor
:
'
bg-gray-100
'
,
precision
:
0
},
{
title
:
'
TVOC
'
,
value
:
0
,
unit
:
'
ppb
'
,
icon
:
'
🧪
'
,
bgColor
:
'
bg-gray-100
'
,
precision
:
1
},
{
title
:
'
CO2
'
,
value
:
0
,
unit
:
'
ppm
'
,
icon
:
'
🌿
'
,
bgColor
:
'
bg-gray-100
'
,
precision
:
0
},
{
title
:
'
O2
'
,
value
:
0
,
unit
:
'
%
'
,
icon
:
'
💨
'
,
bgColor
:
'
bg-gray-100
'
,
precision
:
1
},
{
title
:
'
CO
'
,
value
:
0
,
unit
:
'
ppm
'
,
icon
:
'
🔥
'
,
bgColor
:
'
bg-gray-100
'
,
precision
:
2
}
];
};
// 데이터 정규화 함수
export
const
normalizeSensorData
=
(
data
:
any
):
SensorData
|
null
=>
{
if
(
!
data
||
typeof
data
!==
'
object
'
)
{
return
null
;
}
try
{
// 필수 필드 검증
const
requiredFields
=
[
'
id
'
,
'
device_id
'
,
'
node_id
'
,
'
temperature
'
,
'
humidity
'
];
const
hasRequiredFields
=
requiredFields
.
every
(
field
=>
data
.
hasOwnProperty
(
field
)
&&
data
[
field
]
!==
null
&&
data
[
field
]
!==
undefined
);
if
(
!
hasRequiredFields
)
{
console
.
warn
(
'
Missing required fields in sensor data:
'
,
data
);
return
null
;
}
// 데이터 정규화
const
normalized
:
SensorData
=
{
id
:
toSafeNumber
(
data
.
id
,
0
),
device_id
:
toSafeString
(
data
.
device_id
,
''
),
node_id
:
toSafeNumber
(
data
.
node_id
,
0
),
temperature
:
toSafeNumber
(
data
.
temperature
,
0
),
humidity
:
toSafeNumber
(
data
.
humidity
,
0
),
longitude
:
toSafeNumber
(
data
.
longitude
,
0
),
latitude
:
toSafeNumber
(
data
.
latitude
,
0
),
recorded_time
:
toSafeString
(
data
.
recorded_time
,
new
Date
().
toISOString
()),
received_time
:
toSafeString
(
data
.
received_time
,
new
Date
().
toISOString
()),
// 선택적 필드들
float_value
:
safeGet
(
data
,
'
float_value
'
,
undefined
),
signed_int32_value
:
safeGet
(
data
,
'
signed_int32_value
'
,
undefined
),
unsigned_int32_value
:
safeGet
(
data
,
'
unsigned_int32_value
'
,
undefined
),
raw_tem
:
safeGet
(
data
,
'
raw_tem
'
,
undefined
),
raw_hum
:
safeGet
(
data
,
'
raw_hum
'
,
undefined
),
pm10
:
safeGet
(
data
,
'
pm10
'
,
undefined
),
pm25
:
safeGet
(
data
,
'
pm25
'
,
undefined
),
pressure
:
safeGet
(
data
,
'
pressure
'
,
undefined
),
illumination
:
safeGet
(
data
,
'
illumination
'
,
undefined
),
tvoc
:
safeGet
(
data
,
'
tvoc
'
,
undefined
),
co2
:
safeGet
(
data
,
'
co2
'
,
undefined
),
o2
:
safeGet
(
data
,
'
o2
'
,
undefined
),
co
:
safeGet
(
data
,
'
co
'
,
undefined
)
};
return
normalized
;
}
catch
(
error
)
{
console
.
error
(
'
Error normalizing sensor data:
'
,
error
);
return
null
;
}
};
// 데이터 유효성 검증 함수
export
const
validateChartData
=
(
data
:
ChartDataPoint
[]):
boolean
=>
{
if
(
!
Array
.
isArray
(
data
)
||
data
.
length
===
0
)
{
return
false
;
}
return
data
.
every
(
item
=>
{
if
(
!
item
||
typeof
item
!==
'
object
'
)
{
return
false
;
}
// 필수 필드 검증
if
(
!
item
.
device_id
||
typeof
item
.
device_id
!==
'
string
'
)
{
return
false
;
}
// 숫자 필드 검증
const
numberFields
=
[
'
temperature
'
,
'
humidity
'
,
'
pm10
'
,
'
pm25
'
,
'
pressure
'
,
'
illumination
'
,
'
tvoc
'
,
'
co2
'
,
'
o2
'
,
'
co
'
];
return
numberFields
.
every
(
field
=>
{
const
value
=
item
[
field
as
keyof
ChartDataPoint
];
return
typeof
value
===
'
number
'
&&
!
isNaN
(
value
)
&&
isFinite
(
value
);
});
});
};
// 데이터 통계 계산 함수
export
const
calculateDataStats
=
(
data
:
ChartDataPoint
[]):
{
avg
:
ChartDataPoint
;
min
:
ChartDataPoint
;
max
:
ChartDataPoint
;
}
=>
{
if
(
!
Array
.
isArray
(
data
)
||
data
.
length
===
0
)
{
const
defaultPoint
=
createDefaultChartData
()[
0
];
return
{
avg
:
defaultPoint
,
min
:
defaultPoint
,
max
:
defaultPoint
};
}
const
numericFields
=
[
'
temperature
'
,
'
humidity
'
,
'
pm10
'
,
'
pm25
'
,
'
pressure
'
,
'
illumination
'
,
'
tvoc
'
,
'
co2
'
,
'
o2
'
,
'
co
'
]
as
const
;
const
stats
=
{
avg
:
{
...
data
[
0
]
},
min
:
{
...
data
[
0
]
},
max
:
{
...
data
[
0
]
}
};
numericFields
.
forEach
(
field
=>
{
const
values
=
data
.
map
(
item
=>
item
[
field
]).
filter
(
val
=>
typeof
val
===
'
number
'
&&
!
isNaN
(
val
));
if
(
values
.
length
>
0
)
{
stats
.
avg
[
field
]
=
values
.
reduce
((
sum
,
val
)
=>
sum
+
val
,
0
)
/
values
.
length
;
stats
.
min
[
field
]
=
Math
.
min
(...
values
);
stats
.
max
[
field
]
=
Math
.
max
(...
values
);
}
});
return
stats
;
};
\ No newline at end of file
web-dashboard/src/utils/exportData.ts
0 → 100644
View file @
6423f0de
import
{
SensorData
}
from
'
../services/api
'
;
export
interface
ExportOptions
{
format
:
'
csv
'
|
'
json
'
;
includeHeaders
?:
boolean
;
dateRange
?:
{
start
:
string
;
end
:
string
;
};
}
/**
* 센서 데이터를 CSV 형식으로 변환
*/
const
convertToCSV
=
(
data
:
SensorData
[],
includeHeaders
:
boolean
=
true
):
string
=>
{
if
(
data
.
length
===
0
)
return
''
;
const
headers
=
[
'
ID
'
,
'
Device ID
'
,
'
Node ID
'
,
'
Temperature (°C)
'
,
'
Humidity (%)
'
,
'
Longitude
'
,
'
Latitude
'
,
'
PM10 (μg/m³)
'
,
'
PM2.5 (μg/m³)
'
,
'
Pressure (hPa)
'
,
'
Illumination (lux)
'
,
'
TVOC (ppb)
'
,
'
CO2 (ppm)
'
,
'
O2 (%)
'
,
'
CO (ppm)
'
,
'
Recorded Time
'
,
'
Received Time
'
];
const
rows
=
data
.
map
(
item
=>
[
item
.
id
,
item
.
device_id
,
item
.
node_id
,
item
.
temperature
,
item
.
humidity
,
item
.
longitude
,
item
.
latitude
,
item
.
pm10
||
''
,
item
.
pm25
||
''
,
item
.
pressure
||
''
,
item
.
illumination
||
''
,
item
.
tvoc
||
''
,
item
.
co2
||
''
,
item
.
o2
||
''
,
item
.
co
||
''
,
item
.
recorded_time
,
item
.
received_time
]);
let
csv
=
''
;
if
(
includeHeaders
)
{
csv
+=
headers
.
join
(
'
,
'
)
+
'
\n
'
;
}
csv
+=
rows
.
map
(
row
=>
row
.
map
(
cell
=>
typeof
cell
===
'
string
'
&&
cell
.
includes
(
'
,
'
)
?
`"
${
cell
}
"`
:
cell
).
join
(
'
,
'
)
).
join
(
'
\n
'
);
return
csv
;
};
/**
* 센서 데이터를 JSON 형식으로 변환
*/
const
convertToJSON
=
(
data
:
SensorData
[]):
string
=>
{
return
JSON
.
stringify
(
data
,
null
,
2
);
};
/**
* 데이터를 파일로 다운로드
*/
const
downloadFile
=
(
content
:
string
,
filename
:
string
,
mimeType
:
string
)
=>
{
const
blob
=
new
Blob
([
content
],
{
type
:
mimeType
});
const
url
=
URL
.
createObjectURL
(
blob
);
const
link
=
document
.
createElement
(
'
a
'
);
link
.
href
=
url
;
link
.
download
=
filename
;
document
.
body
.
appendChild
(
link
);
link
.
click
();
document
.
body
.
removeChild
(
link
);
URL
.
revokeObjectURL
(
url
);
};
/**
* 센서 데이터 내보내기
*/
export
const
exportSensorData
=
(
data
:
SensorData
[],
options
:
ExportOptions
):
void
=>
{
const
{
format
,
includeHeaders
=
true
,
dateRange
}
=
options
;
// 날짜 범위 필터링
let
filteredData
=
data
;
if
(
dateRange
)
{
filteredData
=
data
.
filter
(
item
=>
{
const
recordedTime
=
new
Date
(
item
.
recorded_time
);
const
start
=
new
Date
(
dateRange
.
start
);
const
end
=
new
Date
(
dateRange
.
end
);
return
recordedTime
>=
start
&&
recordedTime
<=
end
;
});
}
// 파일명 생성
const
timestamp
=
new
Date
().
toISOString
().
slice
(
0
,
19
).
replace
(
/:/g
,
'
-
'
);
const
filename
=
`sensor-data-
${
timestamp
}
.
${
format
}
`
;
let
content
:
string
;
let
mimeType
:
string
;
if
(
format
===
'
csv
'
)
{
content
=
convertToCSV
(
filteredData
,
includeHeaders
);
mimeType
=
'
text/csv;charset=utf-8;
'
;
}
else
{
content
=
convertToJSON
(
filteredData
);
mimeType
=
'
application/json;charset=utf-8;
'
;
}
downloadFile
(
content
,
filename
,
mimeType
);
};
/**
* 현재 페이지의 센서 데이터 내보내기 (Dashboard용)
*/
export
const
exportCurrentData
=
(
data
:
SensorData
[],
format
:
'
csv
'
|
'
json
'
=
'
csv
'
):
void
=>
{
exportSensorData
(
data
,
{
format
});
};
\ No newline at end of file
web-dashboard/src/utils/formatters.ts
0 → 100644
View file @
6423f0de
/**
* 센서 값 포맷팅 함수
* @param value 센서 값
* @param precision 소수점 자릿수 (기본값: 1)
* @returns 포맷팅된 값 또는 'N/A'
*/
export
const
formatSensorValue
=
(
value
:
number
|
undefined
,
precision
:
number
=
1
):
string
=>
{
if
(
value
===
undefined
||
value
===
null
)
{
return
'
N/A
'
;
}
// 매우 작은 값(e-40 등) 처리
if
(
Math
.
abs
(
value
)
<
1
e
-
10
)
{
return
'
0.0
'
;
}
// 무한대 값 처리
if
(
!
isFinite
(
value
))
{
return
'
N/A
'
;
}
return
value
.
toFixed
(
precision
);
};
/**
* 센서 값 검증 함수
* @param value 센서 값
* @param min 최소값
* @param max 최대값
* @returns 유효한 값인지 여부
*/
export
const
validateSensorValue
=
(
value
:
number
|
undefined
,
min
:
number
=
-
1000
,
max
:
number
=
10000
):
boolean
=>
{
if
(
value
===
undefined
||
value
===
null
)
{
return
false
;
}
if
(
!
isFinite
(
value
))
{
return
false
;
}
if
(
Math
.
abs
(
value
)
<
1
e
-
10
)
{
return
true
;
// 0 값은 유효
}
return
value
>=
min
&&
value
<=
max
;
};
/**
* 센서별 기본 정밀도 반환
* @param sensorType 센서 타입
* @returns 기본 정밀도
*/
export
const
getSensorPrecision
=
(
sensorType
:
string
):
number
=>
{
const
precisionMap
:
{
[
key
:
string
]:
number
}
=
{
temperature
:
1
,
humidity
:
1
,
pressure
:
1
,
illumination
:
0
,
pm10
:
1
,
pm25
:
1
,
tvoc
:
1
,
co2
:
0
,
o2
:
1
,
co
:
2
,
};
return
precisionMap
[
sensorType
]
||
1
;
};
\ No newline at end of file
web-dashboard/src/utils/logger.ts
0 → 100644
View file @
6423f0de
export
enum
LogLevel
{
DEBUG
=
'
debug
'
,
INFO
=
'
info
'
,
WARN
=
'
warn
'
,
ERROR
=
'
error
'
}
export
interface
LogEntry
{
timestamp
:
string
;
level
:
LogLevel
;
message
:
string
;
data
?:
any
;
userId
?:
string
;
sessionId
?:
string
;
userAgent
?:
string
;
url
?:
string
;
}
export
interface
UserAction
{
action
:
string
;
component
:
string
;
data
?:
any
;
timestamp
:
string
;
sessionId
:
string
;
}
class
Logger
{
private
logs
:
LogEntry
[]
=
[];
private
userActions
:
UserAction
[]
=
[];
private
maxLogs
=
1000
;
private
sessionId
:
string
;
constructor
()
{
this
.
sessionId
=
this
.
generateSessionId
();
this
.
setupGlobalErrorHandler
();
}
private
generateSessionId
():
string
{
return
`session_
${
Date
.
now
()}
_
${
Math
.
random
().
toString
(
36
).
substr
(
2
,
9
)}
`
;
}
private
setupGlobalErrorHandler
()
{
window
.
addEventListener
(
'
error
'
,
(
event
)
=>
{
this
.
error
(
'
Global error
'
,
{
message
:
event
.
message
,
filename
:
event
.
filename
,
lineno
:
event
.
lineno
,
colno
:
event
.
colno
,
error
:
event
.
error
?.
stack
});
});
window
.
addEventListener
(
'
unhandledrejection
'
,
(
event
)
=>
{
this
.
error
(
'
Unhandled promise rejection
'
,
{
reason
:
event
.
reason
,
promise
:
event
.
promise
});
});
}
private
addLog
(
level
:
LogLevel
,
message
:
string
,
data
?:
any
)
{
const
logEntry
:
LogEntry
=
{
timestamp
:
new
Date
().
toISOString
(),
level
,
message
,
data
,
sessionId
:
this
.
sessionId
,
userAgent
:
navigator
.
userAgent
,
url
:
window
.
location
.
href
};
this
.
logs
.
push
(
logEntry
);
// 로그 개수 제한
if
(
this
.
logs
.
length
>
this
.
maxLogs
)
{
this
.
logs
=
this
.
logs
.
slice
(
-
this
.
maxLogs
);
}
// 콘솔에 출력
console
[
level
](
`[
${
level
.
toUpperCase
()}
]
${
message
}
`
,
data
||
''
);
// 에러 로그는 서버로 전송
if
(
level
===
LogLevel
.
ERROR
)
{
this
.
sendErrorToServer
(
logEntry
);
}
}
private
async
sendErrorToServer
(
logEntry
:
LogEntry
)
{
try
{
await
fetch
(
'
/api/logs/error
'
,
{
method
:
'
POST
'
,
headers
:
{
'
Content-Type
'
:
'
application/json
'
,
},
body
:
JSON
.
stringify
(
logEntry
)
});
}
catch
(
error
)
{
console
.
error
(
'
Failed to send error log to server:
'
,
error
);
}
}
debug
(
message
:
string
,
data
?:
any
)
{
this
.
addLog
(
LogLevel
.
DEBUG
,
message
,
data
);
}
info
(
message
:
string
,
data
?:
any
)
{
this
.
addLog
(
LogLevel
.
INFO
,
message
,
data
);
}
warn
(
message
:
string
,
data
?:
any
)
{
this
.
addLog
(
LogLevel
.
WARN
,
message
,
data
);
}
error
(
message
:
string
,
data
?:
any
)
{
this
.
addLog
(
LogLevel
.
ERROR
,
message
,
data
);
}
// 사용자 행동 추적
trackUserAction
(
action
:
string
,
component
:
string
,
data
?:
any
)
{
const
userAction
:
UserAction
=
{
action
,
component
,
data
,
timestamp
:
new
Date
().
toISOString
(),
sessionId
:
this
.
sessionId
};
this
.
userActions
.
push
(
userAction
);
// 사용자 행동 개수 제한
if
(
this
.
userActions
.
length
>
this
.
maxLogs
)
{
this
.
userActions
=
this
.
userActions
.
slice
(
-
this
.
maxLogs
);
}
this
.
debug
(
`User action:
${
action
}
`
,
{
component
,
data
});
}
// 로그 내보내기
exportLogs
():
LogEntry
[]
{
return
[...
this
.
logs
];
}
// 사용자 행동 내보내기
exportUserActions
():
UserAction
[]
{
return
[...
this
.
userActions
];
}
// 로그 초기화
clearLogs
()
{
this
.
logs
=
[];
this
.
userActions
=
[];
}
// 세션 ID 가져오기
getSessionId
():
string
{
return
this
.
sessionId
;
}
// 로그 통계
getLogStats
()
{
const
stats
=
{
total
:
this
.
logs
.
length
,
byLevel
:
{
debug
:
0
,
info
:
0
,
warn
:
0
,
error
:
0
},
userActions
:
this
.
userActions
.
length
};
this
.
logs
.
forEach
(
log
=>
{
stats
.
byLevel
[
log
.
level
]
++
;
});
return
stats
;
}
}
export
const
logger
=
new
Logger
();
\ No newline at end of file
web-dashboard/src/utils/typeGuards.ts
0 → 100644
View file @
6423f0de
import
{
SensorData
,
Device
,
HistoryResponse
}
from
'
../services/api
'
;
// 센서 데이터 타입 가드
export
const
isSensorData
=
(
data
:
any
):
data
is
SensorData
=>
{
return
data
&&
typeof
data
===
'
object
'
&&
typeof
data
.
id
===
'
number
'
&&
typeof
data
.
device_id
===
'
string
'
&&
typeof
data
.
node_id
===
'
number
'
&&
typeof
data
.
temperature
===
'
number
'
&&
typeof
data
.
humidity
===
'
number
'
&&
typeof
data
.
longitude
===
'
number
'
&&
typeof
data
.
latitude
===
'
number
'
&&
typeof
data
.
recorded_time
===
'
string
'
&&
typeof
data
.
received_time
===
'
string
'
;
};
// 디바이스 타입 가드
export
const
isDevice
=
(
data
:
any
):
data
is
Device
=>
{
return
data
&&
typeof
data
===
'
object
'
&&
typeof
data
.
id
===
'
number
'
&&
typeof
data
.
device_id
===
'
string
'
&&
typeof
data
.
name
===
'
string
'
&&
typeof
data
.
description
===
'
string
'
&&
// description은 빈 문자열도 허용
typeof
data
.
status
===
'
string
'
&&
typeof
data
.
last_seen
===
'
string
'
&&
typeof
data
.
created_at
===
'
string
'
&&
typeof
data
.
updated_at
===
'
string
'
;
};
// 히스토리 응답 타입 가드
export
const
isHistoryResponse
=
(
data
:
any
):
data
is
HistoryResponse
=>
{
return
data
&&
typeof
data
===
'
object
'
&&
typeof
data
.
success
===
'
boolean
'
&&
Array
.
isArray
(
data
.
data
)
&&
typeof
data
.
total
===
'
number
'
;
};
// 센서 데이터 배열 타입 가드
export
const
isSensorDataArray
=
(
data
:
any
):
data
is
SensorData
[]
=>
{
return
Array
.
isArray
(
data
)
&&
data
.
every
(
item
=>
isSensorData
(
item
));
};
// 디바이스 배열 타입 가드
export
const
isDeviceArray
=
(
data
:
any
):
data
is
Device
[]
=>
{
return
Array
.
isArray
(
data
)
&&
data
.
every
(
item
=>
isDevice
(
item
));
};
// 숫자 타입 가드
export
const
isNumber
=
(
value
:
any
):
value
is
number
=>
{
return
typeof
value
===
'
number
'
&&
!
isNaN
(
value
)
&&
isFinite
(
value
);
};
// 문자열 타입 가드
export
const
isString
=
(
value
:
any
):
value
is
string
=>
{
return
typeof
value
===
'
string
'
&&
value
.
length
>
0
;
};
// 객체 타입 가드
export
const
isObject
=
(
value
:
any
):
value
is
Record
<
string
,
any
>
=>
{
return
value
!==
null
&&
typeof
value
===
'
object
'
&&
!
Array
.
isArray
(
value
);
};
// 배열 타입 가드
export
const
isArray
=
(
value
:
any
):
value
is
any
[]
=>
{
return
Array
.
isArray
(
value
);
};
// API 응답 타입 가드
export
const
isApiResponse
=
(
data
:
any
):
boolean
=>
{
return
data
&&
typeof
data
===
'
object
'
;
};
// 차트 데이터 타입 가드
export
const
isChartData
=
(
data
:
any
):
boolean
=>
{
return
Array
.
isArray
(
data
)
&&
data
.
length
>
0
&&
data
.
every
(
item
=>
item
&&
typeof
item
===
'
object
'
&&
typeof
item
.
device_id
===
'
string
'
);
};
// 안전한 숫자 변환 함수
export
const
toSafeNumber
=
(
value
:
any
,
defaultValue
:
number
=
0
):
number
=>
{
if
(
value
===
null
||
value
===
undefined
)
{
return
defaultValue
;
}
const
num
=
Number
(
value
);
if
(
isNumber
(
num
))
{
return
num
;
}
return
defaultValue
;
};
// 안전한 문자열 변환 함수
export
const
toSafeString
=
(
value
:
any
,
defaultValue
:
string
=
''
):
string
=>
{
if
(
value
===
null
||
value
===
undefined
)
{
return
defaultValue
;
}
const
str
=
String
(
value
);
if
(
isString
(
str
))
{
return
str
;
}
return
defaultValue
;
};
// 객체에서 안전하게 값 추출하는 함수
export
const
safeGet
=
<
T
>
(
obj
:
any
,
key
:
string
,
defaultValue
:
T
):
T
=>
{
if
(
!
isObject
(
obj
)
||
!
(
key
in
obj
))
{
return
defaultValue
;
}
return
obj
[
key
]
??
defaultValue
;
};
// 배열에서 안전하게 인덱스 접근하는 함수
export
const
safeGetAt
=
<
T
>
(
arr
:
any
[],
index
:
number
,
defaultValue
:
T
):
T
=>
{
if
(
!
isArray
(
arr
)
||
index
<
0
||
index
>=
arr
.
length
)
{
return
defaultValue
;
}
return
arr
[
index
]
??
defaultValue
;
};
\ No newline at end of file
web-dashboard/tailwind.config.js
0 → 100644
View file @
6423f0de
/** @type {import('tailwindcss').Config} */
module
.
exports
=
{
content
:
[
"
./src/**/*.{js,jsx,ts,tsx}
"
,
],
theme
:
{
extend
:
{},
},
plugins
:
[
require
(
'
@tailwindcss/forms
'
),
],
}
\ No newline at end of file
web-dashboard/tsconfig.json
0 → 100644
View file @
6423f0de
{
"compilerOptions"
:
{
"target"
:
"es5"
,
"lib"
:
[
"dom"
,
"dom.iterable"
,
"es6"
],
"allowJs"
:
true
,
"skipLibCheck"
:
true
,
"esModuleInterop"
:
true
,
"allowSyntheticDefaultImports"
:
true
,
"strict"
:
true
,
"forceConsistentCasingInFileNames"
:
true
,
"noFallthroughCasesInSwitch"
:
true
,
"module"
:
"esnext"
,
"moduleResolution"
:
"node"
,
"resolveJsonModule"
:
true
,
"isolatedModules"
:
true
,
"noEmit"
:
true
,
"jsx"
:
"react-jsx"
},
"include"
:
[
"src"
]
}
\ No newline at end of file
Prev
1
2
3
4
5
Next
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment