2025-07-28 19:59:35 +09:00
#nullable enable
2025-07-22 19:58:14 +09:00
using Best.HTTP ;
using Cysharp.Threading.Tasks ;
using System ;
using System.Collections.Generic ;
using System.IO ;
2025-07-28 19:59:35 +09:00
using System.Threading.Tasks ;
2025-07-22 19:58:14 +09:00
using UnityEngine ;
using UVC.Data.Core ;
using UVC.Data.Http ;
2025-08-13 18:35:11 +09:00
using UVC.Factory.Component ;
2025-07-28 19:59:35 +09:00
using UVC.Factory.Playback.UI ;
2025-07-22 19:58:14 +09:00
using UVC.Util ;
namespace UVC.Factory.Playback
{
2025-07-30 20:16:21 +09:00
/// <summary>
/// Playback 관련 서비스 클래스입니다.
/// - 싱글턴 패턴으로 사용합니다.
/// - 재생 데이터 요청, 다운로드, 시간 스케일 조정 등 주요 기능을 제공합니다.
///
/// <example>
/// <code>
/// // 재생 시작 예시
/// var itemData = new UIPlaybackListItemData { date = "2024-07-29", time = "13", sqlFileName = "2024-07-29_13.sqlite" };
/// await PlaybackService.Instance.StartAsync(itemData);
///
/// // 재생 목록 데이터 요청 예시
/// var data = await PlaybackService.Instance.RequestDataAsync();
///
/// // 재생 종료 예시
/// PlaybackService.Instance.Exit();
/// </code>
/// </example>
/// </summary>
2025-07-22 19:58:14 +09:00
public class PlaybackService
{
#region Singleton
2025-07-30 20:16:21 +09:00
// 싱글턴 인스턴스. PlaybackService.Instance로 접근합니다.
2025-07-22 19:58:14 +09:00
private static readonly PlaybackService instance = new PlaybackService ( new PlaybackRepository ( ) ) ;
public static PlaybackService Instance = > instance ;
static PlaybackService ( ) { }
#endregion
2025-07-30 20:16:21 +09:00
// 재생 데이터가 저장되는 폴더 경로입니다.
2025-07-28 19:59:35 +09:00
public static readonly string PlaybackFolderPath = Path . Combine ( Application . persistentDataPath , "playback" ) ; //streamingAssetsPath, "playback"); appData 폴더로 변경
2025-07-22 19:58:14 +09:00
private readonly PlaybackRepository repository ;
2025-08-13 18:35:11 +09:00
private string date = string . Empty ;
private string time = string . Empty ;
private string fileName = string . Empty ;
2025-07-22 19:58:14 +09:00
2025-08-12 20:02:42 +09:00
public Action ? OnStartPlayback ;
public Action ? OnExitPlayback ;
2025-08-13 18:35:11 +09:00
public Action ? OnChangedTime ;
2025-07-22 19:58:14 +09:00
2025-07-28 19:59:35 +09:00
private float timeScale = 1.0f ;
2025-07-30 20:16:21 +09:00
/// <summary>
/// 재생 시간 스케일(배속)입니다. 1.0f가 기본입니다.
/// </summary>
/// <example>
/// <code>
/// // 재생 속도를 2배로 변경
/// PlaybackService.Instance.TimeScale = 2.0f;
/// </code>
/// </example>
2025-07-28 19:59:35 +09:00
public float TimeScale
{
get = > timeScale ;
internal set
{
if ( value < 1f ) value = 1f ;
if ( timeScale ! = value )
{
timeScale = value ;
//Time.timeScale = timeScale;
OnChangeTimeScale ? . Invoke ( timeScale ) ;
}
}
}
2025-08-13 18:35:11 +09:00
public Action < float > ? OnChangeTimeScale ;
2025-07-22 19:58:14 +09:00
2025-07-30 20:16:21 +09:00
/// <summary>
/// 생성자. 일반적으로 직접 호출하지 않고 싱글턴 인스턴스를 사용합니다.
/// </summary>
2025-08-13 18:35:11 +09:00
private PlaybackService ( PlaybackRepository repository )
2025-07-22 19:58:14 +09:00
{
this . repository = repository ;
}
2025-07-30 20:16:21 +09:00
/// <summary>
/// 서버에서 재생 가능한 날짜별 데이터 목록을 비동기로 요청합니다.
/// </summary>
/// <returns>성공 시 날짜별 재생 목록 딕셔너리, 실패 시 null</returns>
/// <example>
/// <code>
/// var data = await PlaybackService.Instance.RequestDataAsync();
/// if (data != null)
/// {
/// foreach (var date in data.Keys)
/// {
/// Debug.Log($"날짜: {date}");
/// }
/// }
/// </code>
/// </example>
2025-07-22 19:58:14 +09:00
public async UniTask < Dictionary < string , Dictionary < string , string > > ? > RequestDataAsync ( )
{
Dictionary < string , Dictionary < string , string > > ? data = await repository . RequestPlaybackDateList ( ) ;
return data ;
}
2025-07-30 20:16:21 +09:00
/// <summary>
/// 재생을 위한 기본 정보 데이터를 비동기로 처리합니다.
/// </summary>
/// <param name="date">날짜(예: "2024-07-29")</param>
/// <param name="time">시간(예: "13")</param>
/// <param name="fileName">파일명(예: "2024-07-29_13.sqlite")</param>
/// <param name="minute">분(기본값: "00")</param>
/// <param name="second">초(기본값: "00")</param>
/// <example>
/// <code>
/// await PlaybackService.Instance.DispatchBaseInfoData("2024-07-29", "13", "2024-07-29_13.sqlite");
/// </code>
/// </example>
2025-07-22 19:58:14 +09:00
public async UniTask DispatchBaseInfoData ( string date , string time , string fileName , string minute = "00" , string second = "00" )
{
2025-07-28 19:59:35 +09:00
await UniTask . RunOnThreadPool ( async ( ) = >
2025-07-22 19:58:14 +09:00
{
2025-07-28 19:59:35 +09:00
//헝가리 시간임
this . date = date ;
this . time = time ;
this . fileName = fileName ;
DateTime dateTime = DateTimeUtil . UtcParse ( $"{date}T{int.Parse(time).ToString(" 00 ")}:{minute}:{second}.000Z" ) ;
string formatTime = DateTimeUtil . FormatTime ( dateTime ) ;
//baseInfo 가져오기
List < PlaybackSQLiteDataEntity > list = await repository . SelectBySecondBaseInfo ( date , fileName , formatTime ) ;
if ( list . Count > 0 )
2025-07-22 19:58:14 +09:00
{
2025-08-13 18:35:11 +09:00
if ( OnChangedTime ! = null ) OnChangedTime . Invoke ( ) ;
2025-07-28 19:59:35 +09:00
HttpRequestConfig httpRequestConfig = new HttpRequestConfig ( "" ) ;
httpRequestConfig . SetUpdatedDataOnly ( true ) ;
httpRequestConfig . SetSplitResponseByKey ( true ) ;
httpRequestConfig . AddSplitConfig ( "AGV" , DataMapperValidator . Get ( "AGV" ) ) ;
httpRequestConfig . AddSplitConfig ( "ALARM" , DataMapperValidator . Get ( "ALARM" ) ) ;
foreach ( var item in list )
{
2025-08-18 13:11:39 +09:00
HttpDataProcessor . ProcessSplitResponse ( httpRequestConfig , item . data , null , true ) ;
2025-07-28 19:59:35 +09:00
}
2025-07-22 19:58:14 +09:00
}
2025-07-28 19:59:35 +09:00
} ) ;
2025-07-22 19:58:14 +09:00
}
/// <summary>
2025-07-30 20:16:21 +09:00
/// 실시간 재생 데이터(특정 초 단위)를 비동기로 처리합니다.
2025-07-22 19:58:14 +09:00
/// </summary>
2025-07-30 20:16:21 +09:00
/// <param name="second">0 ~ 3600 (초 단위)</param>
/// <param name="speed">재생 속도</param>
/// <example>
/// <code>
/// await PlaybackService.Instance.DispatchRealTimeData(120, 1); // 120초(2분) 위치 데이터 처리
/// </code>
/// </example>
2025-07-22 19:58:14 +09:00
public async UniTask DispatchRealTimeData ( int second , int speed )
{
2025-07-28 19:59:35 +09:00
await UniTask . RunOnThreadPool ( async ( ) = >
2025-07-22 19:58:14 +09:00
{
2025-07-28 19:59:35 +09:00
int newSecond = second ;
if ( newSecond > 36000 ) newSecond = 36000 ;
//utc 시간으로 변환
2025-10-30 18:36:26 +09:00
DateTime dateTime = DateTimeUtil . UtcParse ( $"{date}T{int.Parse(time).ToString(" 00 ")}:00:00.000Z" ) . AddSeconds ( newSecond ) ; //.AddChild(-DateTimeUtil.UtcKoreaGap);
2025-07-28 19:59:35 +09:00
string formatTime = DateTimeUtil . FormatTime ( dateTime ) ;
List < PlaybackSQLiteDataEntity > list = await repository . SelectBySecondAsync ( date , fileName , formatTime , 1 ) ;
//Debug.Log($"DispatchRealTimeData {date} {time} {formatTime} {newSecond} {list.Count}");
if ( list . Count > 0 )
2025-07-22 19:58:14 +09:00
{
2025-07-28 19:59:35 +09:00
HttpRequestConfig httpRequestConfig = new HttpRequestConfig ( "" ) ;
httpRequestConfig . SetUpdatedDataOnly ( true ) ;
httpRequestConfig . SetSplitResponseByKey ( true ) ;
httpRequestConfig . AddSplitConfig ( "AGV" , DataMapperValidator . Get ( "AGV" ) ) ;
httpRequestConfig . AddSplitConfig ( "ALARM" , DataMapperValidator . Get ( "ALARM" ) ) ;
foreach ( var item in list )
{
HttpDataProcessor . ProcessSplitResponse ( httpRequestConfig , item . data ) ;
}
2025-07-22 19:58:14 +09:00
}
2025-07-28 19:59:35 +09:00
} ) ;
2025-07-22 19:58:14 +09:00
}
2025-07-30 20:16:21 +09:00
/// <summary>
/// 재생을 시작합니다. UI를 표시하고 데이터를 세팅합니다.
/// </summary>
/// <param name="data">재생 목록 아이템 데이터</param>
/// <example>
/// <code>
/// var itemData = new UIPlaybackListItemData { date = "2024-07-29", time = "13", sqlFileName = "2024-07-29_13.sqlite" };
/// await PlaybackService.Instance.StartAsync(itemData);
/// </code>
/// </example>
2025-08-01 13:30:58 +09:00
public async UniTask StartAsync ( UIPlaybackListItemData data )
2025-07-28 19:59:35 +09:00
{
timeScale = 1.0f ; //기본 시간 스케일 설정
UIPlayback . Instance . Show ( ) ;
await UIPlayback . Instance . SetData ( data . date , data . time , data . sqlFileName ) ;
2025-08-12 20:02:42 +09:00
OnStartPlayback ? . Invoke ( ) ;
2025-07-28 19:59:35 +09:00
}
2025-07-22 19:58:14 +09:00
2025-07-30 20:16:21 +09:00
/// <summary>
/// 재생을 종료합니다. (이벤트 발생)
/// </summary>
/// <example>
/// <code>
/// PlaybackService.Instance.Exit();
/// </code>
/// </example>
2025-07-28 19:59:35 +09:00
public void Exit ( )
2025-07-22 19:58:14 +09:00
{
2025-07-28 19:59:35 +09:00
OnExitPlayback ? . Invoke ( ) ;
2025-07-22 19:58:14 +09:00
}
2025-07-28 19:59:35 +09:00
2025-07-30 20:16:21 +09:00
/// <summary>
/// 재생 데이터 파일을 준비(다운로드 및 압축 해제)합니다.
/// 이미 파일이 있으면 바로 콜백을 호출합니다.
/// </summary>
/// <param name="date">날짜(예: "2024-12-05")</param>
/// <param name="time">시간(예: "13")</param>
/// <param name="fileName">파일명(예: "2024-12-05_0.sqlite.7z")</param>
/// <param name="OnProgress">진행 상황 콜백 (진행 바이트, 전체 바이트, 퍼센트)</param>
/// <param name="OnComplete">완료 콜백 (에러 메시지, 성공 시 null)</param>
/// <returns>다운로드 요청 객체(필요시 Abort 등 제어 가능), 이미 파일이 있으면 null</returns>
/// <example>
/// <code>
/// // 파일 준비 및 다운로드 예시
/// PlaybackService.Instance.ReadyData(
/// "2024-12-05", "13", "2024-12-05_0.sqlite.7z",
/// (progress, total, percent) => Debug.Log($"{progress}/{total} ({percent * 100:F1}%)"),
/// (error) => {
/// if (string.IsNullOrEmpty(error))
/// Debug.Log("파일 준비 완료");
/// else
/// Debug.LogError($"오류: {error}");
/// }
/// );
/// </code>
/// </example>
2025-07-22 19:58:14 +09:00
public HTTPRequest ? ReadyData ( string date , string time , string fileName , Action < long , long , float > OnProgress , Action < string > OnComplete )
{
//date : "2024-12-05"
//fileName : "2024-12-05_0.sqlite.7z"
2025-07-28 19:59:35 +09:00
string playbackPath = PlaybackService . PlaybackFolderPath ;
2025-07-22 19:58:14 +09:00
string tempPath = Path . Combine ( playbackPath , "temp" ) ; //한국 시간으로 변경하기 때문에 임시 폴더 만들어서 압축 해제 후 이동
string datePath = Path . Combine ( playbackPath , date ) ;
var fileNameArr = fileName . Split ( "." ) ;
string zipFilePath = Path . Combine ( datePath , fileName ) ;
string sqlFilePath = Path . Combine ( datePath , fileNameArr [ 0 ] + ".sqlite" ) ;
2025-10-30 18:36:26 +09:00
DateTime utcDateTime = DateTimeUtil . Parse ( fileNameArr [ 0 ] , "yyyy-MM-dd_H" ) ; //.AddChild(-DateTimeUtil.UtcKoreaGap);
2025-07-22 19:58:14 +09:00
string utcDatePath = Path . Combine ( playbackPath , utcDateTime . ToString ( "yyyy-MM-dd" ) ) ;
string utcFileName = utcDateTime . ToString ( "yyyy-MM-dd_H" ) + "." + fileNameArr [ 1 ] + "." + fileNameArr [ 2 ] ;
var utcFileNameArr = utcFileName . Split ( "." ) ;
string utcZipFilePath = Path . Combine ( tempPath , utcFileName ) ;
string utcSqlFilePath = Path . Combine ( tempPath , utcFileNameArr [ 0 ] + ".sqlite" ) ;
if ( ! Directory . Exists ( playbackPath ) ) Directory . CreateDirectory ( playbackPath ) ;
if ( ! Directory . Exists ( datePath ) ) Directory . CreateDirectory ( datePath ) ;
if ( ! Directory . Exists ( utcDatePath ) ) Directory . CreateDirectory ( utcDatePath ) ;
if ( ! Directory . Exists ( tempPath ) ) Directory . CreateDirectory ( tempPath ) ;
if ( File . Exists ( sqlFilePath ) )
{
Debug . Log ( $"ONREADY SQP FILE" ) ;
if ( OnProgress ! = null ) OnProgress . Invoke ( 100 , 100 , 1.0f ) ;
if ( OnComplete ! = null ) OnComplete . Invoke ( null ) ;
return null ;
}
else
{
long downloadTotal = 0 ;
return repository . DownloadPlaybackData ( utcFileName , utcZipFilePath ,
( long progress , long length ) = >
{
float percent = ( float ) progress / ( float ) length ;
Debug . Log ( $"DownloadPlaybackData OnProgress:{percent}" ) ;
if ( OnProgress ! = null ) OnProgress . Invoke ( progress , length , percent / 2 ) ;
downloadTotal = length ;
} , async ( ) = >
{
await UniTask . Delay ( 500 ) ;
Debug . Log ( $"DownloadPlaybackData OnComplete" ) ;
if ( File . Exists ( utcZipFilePath ) )
{
if ( OnProgress ! = null ) OnProgress . Invoke ( 50 , 100 , 0.5f ) ;
2025-07-28 19:59:35 +09:00
//압축해제 후
2025-07-22 19:58:14 +09:00
var zipper = new Zipper ( ) ;
string errorMessage = await zipper . Decompress ( utcZipFilePath , tempPath , ( long read , long total , float percent ) = >
{
if ( OnProgress ! = null )
{
2025-07-28 19:59:35 +09:00
bool isComplte = false ;
float percentRate = 0.5f + percent / 2 ;
if ( percentRate > 0.99 )
{
percentRate = 0.99f ;
isComplte = true ;
}
2025-07-22 19:58:14 +09:00
OnProgress . Invoke ( downloadTotal + read , downloadTotal + total , 0.5f + percent / 2 ) ;
2025-07-28 19:59:35 +09:00
if ( isComplte )
2025-07-22 19:58:14 +09:00
{
Debug . Log ( $" DownloadReadData :{downloadTotal + read} , DownloadTotalData :{downloadTotal + total} ,DownloadPlaybackData OnProgress:{percent}" ) ;
}
}
} ) ;
Debug . Log ( $"zipper1 errorMessage:{errorMessage} utcSqlFilePath:{utcSqlFilePath} sqlFilePath:{sqlFilePath} utcZipFilePath:{utcZipFilePath}" ) ;
////파일 접근 문제면 2회 0.5초 후에 다시 실행.
//if (errorMessage == "Could not open input(7z) file")
//{
// await UniTask.Delay(500);
// errorMessage = await zipper.Decompress(utcZipFilePath, tempPath, (long read, long total, float percent) =>
// {
// if (OnProgress != null) OnProgress.Invoke(downloadTotal + read, downloadTotal + total, 0.5f + percent / 2);
// });
// Debug.Log($"zipper2 errorMessage:{errorMessage} utcSqlFilePath:{utcSqlFilePath} sqlFilePath:{sqlFilePath} utcZipFilePath:{utcZipFilePath}");
//}
//if (errorMessage == "Could not open input(7z) file")
//{
// await UniTask.Delay(500);
// errorMessage = await zipper.Decompress(utcZipFilePath, tempPath, (long read, long total, float percent) =>
// {
// if (OnProgress != null) OnProgress.Invoke(downloadTotal + read, downloadTotal + total, 0.5f + percent / 2);
// });
// Debug.Log($"zipper3 errorMessage:{errorMessage} utcSqlFilePath:{utcSqlFilePath} sqlFilePath:{sqlFilePath} utcZipFilePath:{utcZipFilePath}");
//}
2025-07-28 19:59:35 +09:00
await UniTask . RunOnThreadPool ( ( ) = >
2025-07-22 19:58:14 +09:00
{
2025-07-28 19:59:35 +09:00
//압축해제 한 파일 이동
if ( File . Exists ( utcSqlFilePath ) )
{
//동일한 파일명이 있을경우 제거후 다시
File . Copy ( utcSqlFilePath , sqlFilePath ) ;
File . Delete ( utcSqlFilePath ) ;
}
//zip 파일 삭제
File . Delete ( utcZipFilePath ) ;
} ) ;
2025-07-22 19:58:14 +09:00
if ( OnComplete ! = null ) OnComplete . Invoke ( errorMessage ) ;
}
} ,
2025-08-12 20:02:42 +09:00
( string error ) = >
2025-07-22 19:58:14 +09:00
{
Debug . Log ( $"DownloadPlaybackData OnError:{error}" ) ;
if ( OnComplete ! = null ) OnComplete . Invoke ( error ) ;
} ) ;
}
}
}
}