draggableList/Tab 개발 중

This commit is contained in:
logonkhi
2025-07-30 20:16:21 +09:00
parent 231af33e6f
commit f7befb048c
99 changed files with 8819 additions and 1172 deletions

View File

@@ -0,0 +1,75 @@
#nullable enable
using UnityEngine;
using UVC.UI.List;
namespace UVC.Factory.Modal
{
public class ConfigDataOrderModal : MonoBehaviour
{
[SerializeField]
private DraggableScrollList? draggableList;
protected virtual void Awake()
{
if (draggableList == null)
{
Debug.LogError("draggableList 참조가 설정되지 않았습니다.");
return;
}
// 이벤트 구독
draggableList.OnItemReordered += OnItemReordered;
draggableList.OnItemSelected += OnItemSelected;
}
private void Start()
{
// 1. DraggableItemData 설정
for (int i = 0; i < 10; i++)
{
draggableList?.AddItem(new DraggableItemData($"Item {i + 1}", i));
}
}
/// <summary>
/// 아이템 순서 변경 이벤트 처리
/// </summary>
/// <param name = "sender" > 이벤트 발생자</param>
/// <param name = "e" > 이벤트 인자</param>
private void OnItemReordered(object? sender, DraggableItemReorderEventArgs e)
{
Debug.Log($"아이템 순서 변경됨: ID={e.ItemId}, {e.OldIndex} -> {e.NewIndex}");
// 여기에 순서 변경에 대한 비즈니스 로직 구현
// 예: 서버에 변경사항 전송, 설정 저장 등
}
/// <summary>
/// 아이템 선택 이벤트 처리
/// </summary>
/// <param name="sender">이벤트 발생자</param>
/// <param name="item">선택된 아이템</param>
private void OnItemSelected(object? sender, DraggableListItem item)
{
if (item?.Data != null)
{
Debug.Log($"아이템 선택됨: {item.Data.Id}");
// 선택된 아이템에 대한 처리
// 예: 상세 정보 표시, 편집 모드 진입 등
}
}
/// <summary>
/// 컴포넌트 정리
/// </summary>
private void OnDestroy()
{
if (draggableList != null)
{
draggableList.OnItemReordered -= OnItemReordered;
draggableList.OnItemSelected -= OnItemSelected;
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: dc92cc933c1ce844c955ffc32982d3af

View File

@@ -8,33 +8,75 @@ using UVC.UI.Modal;
namespace UVC.Factory.Playback
{
/// <summary>
/// 재생(Playback) 명령을 실행하는 클래스입니다.
///
/// <para>
/// 이 명령은 팩토리 카메라를 비활성화하고, 재생 목록 모달을 띄운 뒤,
/// 사용자가 재생 항목을 선택하면 재생을 시작합니다.
/// 사용자가 취소하면 재생을 종료합니다.
/// </para>
///
/// <example>
/// <code>
/// // ICommand 인터페이스를 구현하므로 다음과 같이 사용할 수 있습니다.
/// ICommand playbackCommand = new PlaybackCommand();
/// playbackCommand.Execute();
/// </code>
/// </example>
/// </summary>
public class PlaybackCommand : ICommand
{
/// <summary>
/// 재생 명령을 실행합니다.
///
/// <para>
/// 1. 카메라를 비활성화합니다.<br/>
/// 2. 재생 목록 모달을 띄웁니다.<br/>
/// 3. 사용자가 항목을 선택하면 재생을 시작합니다.<br/>
/// 4. 사용자가 취소하면 재생을 종료합니다.<br/>
/// </para>
///
/// <example>
/// <code>
/// // 명령 실행 예시
/// var command = new PlaybackCommand();
/// command.Execute();
/// </code>
/// </example>
/// </summary>
/// <param name="parameter">사용하지 않음</param>
public async void Execute(object? parameter = null)
{
// 1. 카메라 비활성화
FactoryCameraController.Instance.Enable = false;
// 2. 재생 목록 모달 생성 및 표시
var modalContent = new ModalContent(UIPlaybackListModal.PrefabPath)
{
Title = "Playback List",
ConfirmButtonText = "Play",
ShowCancelButton = false
};
// 3. 사용자가 항목을 선택할 때까지 대기
UIPlaybackListItemData? result = await UVC.UI.Modal.Modal.Open<UIPlaybackListItemData>(modalContent);
Debug.Log($"PlaybackCommand result==null:{result==null}");
if (result != null)
{
// 4. 항목 선택 시: 로딩 표시, 재생 시작
UILoading.Show();
UIPlaybackListItemData data = result;
Debug.Log($"PlaybackCommand data:{data}");
DataRepository.Instance.MqttReceiver.Stop();
await PlaybackService.Instance.StartAsync(data);
FactoryCameraController.Instance.Enable = true;
UILoading.Hide();
}
else
{
{
// 5. 취소 시: 재생 종료
UILoading.Show();
PlaybackService.Instance.Exit();
FactoryCameraController.Instance.Enable = true;

View File

@@ -1,7 +1,6 @@
#nullable enable
using Best.HTTP;
using Cysharp.Threading.Tasks;
using SampleProject.Config;
using System;
using System.Collections.Generic;
using UnityEngine;
@@ -15,6 +14,23 @@ namespace UVC.Factory.Playback
private PlaybackSQLiteService? sqliteService = null;
/// <summary>
/// 서버에서 재생 목록 날짜 리스트를 요청합니다.
///
/// 예시:
/// <code>
/// var repo = new PlaybackRepository();
/// var dateList = await repo.RequestPlaybackDateList();
/// if (dateList != null)
/// {
/// foreach (var date in dateList.Keys)
/// {
/// Debug.Log($"날짜: {date}");
/// }
/// }
/// </code>
/// </summary>
/// <returns>성공 시 날짜별 재생 목록 딕셔너리, 실패 시 null</returns>
public async UniTask<Dictionary<string, Dictionary<string, string>>?> RequestPlaybackDateList()
{
@@ -45,6 +61,29 @@ namespace UVC.Factory.Playback
}
}
/// <summary>
/// 서버에서 재생 데이터 파일을 다운로드합니다.
///
/// 예시:
/// <code>
/// var repo = new PlaybackRepository();
/// string fileName = "sample.db";
/// string savePath = Application.persistentDataPath + "/sample.db";
/// repo.DownloadPlaybackData(
/// fileName,
/// savePath,
/// (current, total) => Debug.Log($"{current}/{total} bytes 다운로드 중"),
/// () => Debug.Log("다운로드 완료"),
/// (error) => Debug.LogError($"다운로드 실패: {error}")
/// );
/// </code>
/// </summary>
/// <param name="fileName">다운로드할 파일명</param>
/// <param name="savePath">저장 경로</param>
/// <param name="OnProgress">다운로드 진행 콜백 (현재, 전체 바이트)</param>
/// <param name="OnComplete">다운로드 완료 콜백</param>
/// <param name="OnError">다운로드 실패 콜백 (에러 메시지)</param>
/// <returns>다운로드 요청 객체(필요시 Abort 등 제어 가능), 실패 시 null</returns>
public HTTPRequest? DownloadPlaybackData(string fileName, string savePath, Action<long, long> OnProgress, Action OnComplete, Action<string> OnError)
{
try
@@ -59,15 +98,29 @@ namespace UVC.Factory.Playback
}
/// <summary>
/// selectTime보다 +- second 사이의 데이터 요청. selectTime, second 포함
/// selectTime보다 ±second 사이의 데이터를 조회합니다. selectTime, second 포함.
///
/// 예시:
/// <code>
/// var repo = new PlaybackRepository();
/// string date = "2024-07-29";
/// string sqlFileName = "sample.db";
/// string selectTime = "2024-07-29T12:00:00.000Z";
/// int second = 10;
/// var list = await repo.SelectBySecondAsync(date, sqlFileName, selectTime, second, true, 5);
/// foreach (var entity in list)
/// {
/// Debug.Log($"데이터: {entity.data}, 시간: {entity.timestamp}");
/// }
/// </code>
/// </summary>
/// <param name="date"></param>
/// <param name="sqlFileName"></param>
/// <param name="selectTime">yyyy-MM-ddTHH:mm:ss.fffZ format string</param>
/// <param name="second"></param>
/// <param name="orderAsc">true: 오래된 시간이 먼저, false: 최근 시간이 먼저</param>
/// <param name="limit"></param>
/// <returns></returns>
/// <param name="date">폴더명(날짜 등)</param>
/// <param name="sqlFileName">SQLite 파일명</param>
/// <param name="selectTime">yyyy-MM-ddTHH:mm:ss.fffZ 형식의 기준 시간</param>
/// <param name="second">±초(양수: 미래, 음수: 과거)</param>
/// <param name="orderAsc">true: 오래된 시간부터, false: 최근 시간부터</param>
/// <param name="limit">최대 조회 개수(0이면 제한 없음)</param>
/// <returns>조회된 데이터 리스트</returns>
public async UniTask<List<PlaybackSQLiteDataEntity>> SelectBySecondAsync(string date, string sqlFileName, string selectTime, int second, bool orderAsc = true, int limit = 0)
{
validationSqliteService(date, sqlFileName);
@@ -75,22 +128,46 @@ namespace UVC.Factory.Playback
}
/// <summary>
/// selectTime보다 +- second 사이의 데이터 요청. selectTime, second 포함
/// baseInfo 테이블에서 selectTime보다 ±second 사이의 데이터를 조회합니다. selectTime, second 포함.
///
/// 예시:
/// <code>
/// var repo = new PlaybackRepository();
/// string date = "2024-07-29";
/// string sqlFileName = "sample.db";
/// string selectTime = "2024-07-29T12:00:00.000Z";
/// int second = -5;
/// var list = await repo.SelectBySecondBaseInfo(date, sqlFileName, selectTime, second, false, 1);
/// foreach (var entity in list)
/// {
/// Debug.Log($"데이터: {entity.data}, 시간: {entity.timestamp}");
/// }
/// </code>
/// </summary>
/// <param name="date"></param>
/// <param name="sqlFileName"></param>
/// <param name="selectTime">yyyy-MM-ddTHH:mm:ss.fffZ format string</param>
/// <param name="second"></param>
/// <param name="orderAsc">true: 오래된 시간이 먼저, false: 최근 시간이 먼저</param>
/// <param name="limit"></param>
/// <returns></returns>
/// <param name="date">폴더명(날짜 등)</param>
/// <param name="sqlFileName">SQLite 파일명</param>
/// <param name="selectTime">yyyy-MM-ddTHH:mm:ss.fffZ 형식의 기준 시간</param>
/// <param name="second">±초(양수: 미래, 음수: 과거)</param>
/// <param name="orderAsc">true: 오래된 시간부터, false: 최근 시간부터</param>
/// <param name="limit">최대 조회 개수</param>
/// <returns>조회된 데이터 리스트</returns>
public async UniTask<List<PlaybackSQLiteDataEntity>> SelectBySecondBaseInfo(string date, string sqlFileName, string selectTime, int second = 59, bool orderAsc = true, int limit = 1)
{
validationSqliteService(date, sqlFileName);
return await sqliteService!.SelectBySecondBaseInfo(selectTime, second, orderAsc, limit);
}
/// <summary>
/// 내부적으로 SQLite 서비스가 올바른 파일에 연결되어 있는지 확인하고, 필요시 재연결합니다.
///
/// 예시:
/// <code>
/// // 일반적으로 직접 호출할 필요 없음(내부에서 자동 호출)
/// validationSqliteService("2024-07-29", "sample.db");
/// </code>
/// </summary>
/// <param name="date">폴더명(날짜 등)</param>
/// <param name="sqlFileName">SQLite 파일명</param>
private void validationSqliteService(string date, string sqlFileName)
{
if (sqliteService == null) sqliteService = new PlaybackSQLiteService();

View File

@@ -4,7 +4,6 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using UnityEngine;
using UVC.Factory.Playback;
using UVC.Util;
@@ -12,16 +11,10 @@ namespace UVC.Factory
{
public class PlaybackSQLiteService
{
//#region Singleton
//private static readonly SQLiteService instance = new SQLiteService();
//public static SQLiteService Instance => instance;
//static SQLiteService() { }
//#endregion
// SQLite 데이터베이스 연결 객체
private SQLiteConnection dbConnection;
// 데이터베이스 연결 여부 확인
public bool Connected { get => dbConnection != null; }
private string date;
@@ -29,6 +22,16 @@ namespace UVC.Factory
private string sqliteFileName;
public string SqliteFileName { get => sqliteFileName; }
/// <summary>
/// 데이터베이스 파일에 연결합니다.
/// 예시:
/// <code>
/// var service = new PlaybackSQLiteService();
/// service.Connect("2024-07-29", "sample.db");
/// </code>
/// </summary>
/// <param name="date">폴더명(날짜 등)</param>
/// <param name="sqliteFileName">SQLite 파일명</param>
public void Connect(string date, string sqliteFileName)
{
this.date = date;
@@ -36,6 +39,13 @@ namespace UVC.Factory
dbConnection = new SQLiteConnection(Path.Combine(PlaybackService.PlaybackFolderPath, date, sqliteFileName));
}
/// <summary>
/// 데이터베이스 연결을 닫습니다.
/// 예시:
/// <code>
/// service.CloseDB();
/// </code>
/// </summary>
public void CloseDB()
{
dbConnection.Close();
@@ -43,30 +53,23 @@ namespace UVC.Factory
}
/// <summary>
/// 추가하기
/// realTime 테이블에 데이터를 추가합니다.
/// 예시:
/// <code>
/// int rows = service.Insert("센서값", "2024-07-29T12:00:00.000Z", "온도값");
/// </code>
/// </summary>
/// <param name="data"></param>
/// <param name="timeStamp">yyyy-MM-ddTHH:mm:ss.fffZ format string</param>
/// <param name="temp"></param>
/// <returns>데이터베이스에서 추가된 행 수</returns>
/// <param name="data">저장할 데이터(문자열)</param>
/// <param name="timeStamp">yyyy-MM-ddTHH:mm:ss.fffZ 형식의 시간</param>
/// <param name="temp">임시 데이터(옵션)</param>
/// <returns>추가된 행 수</returns>
public int Insert(string data, string timeStamp, string temp = null)
{
var query = $"INSERT INTO realTime (data, timestamp, temp) VALUES ('{data}', '{timeStamp}', " + (temp == null ? "null" : "'" + temp + "'") + ");";
int changedRowLen = dbConnection.Execute(query);
return changedRowLen;
}
/// <summary>
/// selectTime보다 +- second 사이의 데이터 요청. selectTime, second 포함
/// second > 0 : selectTime <= data < selectTime + second
/// second < 0 : selectTime + second < data <= selectTime
/// </summary>
/// <param name="selectTime">yyyy-MM-ddTHH:mm:ss.fffZ format string</param>
/// <param name="second"></param>
/// <param name="orderAsc">true: 오래된 시간이 먼저, false: 최근 시간이 먼저</param>
/// <param name="limit"></param>
/// <returns></returns>
///
readonly string[] queryParts =
{
"SELECT * FROM realTime WHERE ",
@@ -77,6 +80,20 @@ namespace UVC.Factory
" ORDER BY timestamp ",
" LIMIT ",
};
/// <summary>
/// 특정 시간(selectTime) 기준으로 ±second 범위의 데이터를 조회합니다.
/// 예시:
/// <code>
/// // 10초 뒤까지의 데이터 5개를 조회(오름차순)
/// var list = await service.SelectBySecond("2024-07-29T12:00:00.000Z", 10, true, 5);
/// </code>
/// </summary>
/// <param name="selectTime">기준 시간(yyyy-MM-ddTHH:mm:ss.fffZ)</param>
/// <param name="second">±초(양수: 미래, 음수: 과거)</param>
/// <param name="orderAsc">true: 오래된 시간부터, false: 최근 시간부터</param>
/// <param name="limit">최대 조회 개수(0이면 제한 없음)</param>
/// <returns>조회된 데이터 리스트</returns>
public async UniTask<List<PlaybackSQLiteDataEntity>> SelectBySecond(string selectTime, int second, bool orderAsc = true, int limit = 0)
{
bool isMainThread = PlayerLoopHelper.IsMainThread;
@@ -104,25 +121,26 @@ namespace UVC.Factory
//Debug.Log($"SelectBySecond {query}");
var query = queryBuilder.ToString();
queryBuilder.Clear();
// 쿼리 실행 및 결과 반환
return dbConnection.Query<PlaybackSQLiteDataEntity>(query);
});
if (!isMainThread) await UniTask.SwitchToThreadPool();
return result;
}
/// <summary>
/// selectTime보다 +- second 사이의 데이터 요청. selectTime, second 포함
/// second > 0 : selectTime <= data < selectTime + second
/// second < 0 : selectTime + second < data <= selectTime
/// </summary>
/// <param name="selectTime">yyyy-MM-ddTHH:mm:ss.fffZ format string</param>
/// <param name="second"></param>
/// <param name="orderAsc">true: 오래된 시간이 먼저, false: 최근 시간이 먼저</param>
/// <param name="limit"></param>
/// <returns></returns>
///
StringBuilder queryBuilder = new();
/// baseInfo 테이블에서 특정 시간(selectTime) 기준으로 ±second 범위의 데이터를 조회합니다.
/// 예시:
/// <code>
/// // 5초 전까지의 데이터 1개를 조회(내림차순)
/// var list = await service.SelectBySecondBaseInfo("2024-07-29T12:00:00.000Z", -5);
/// </code>
/// </summary>
/// <param name="selectTime">기준 시간(yyyy-MM-ddTHH:mm:ss.fffZ)</param>
/// <param name="second">±초(양수: 미래, 음수: 과거)</param>
/// <param name="orderAsc">true: 오래된 시간부터, false: 최근 시간부터</param>
/// <param name="limit">최대 조회 개수</param>
/// <returns>조회된 데이터 리스트</returns>
public async UniTask<List<PlaybackSQLiteDataEntity>> SelectBySecondBaseInfo(string selectTime, int second, bool orderAsc = false, int limit = 1)
{
bool isMainThread = PlayerLoopHelper.IsMainThread;
@@ -148,6 +166,7 @@ namespace UVC.Factory
//Debug.Log($"SelectBySecondBaseInfo {query}");
var query = queryBuilder.ToString();
queryBuilder.Clear();
// 쿼리 실행 및 결과 반환
return dbConnection.Query<PlaybackSQLiteDataEntity>(query);
});
if (!isMainThread) await UniTask.SwitchToThreadPool();
@@ -155,14 +174,25 @@ namespace UVC.Factory
}
}
/// <summary>
/// 데이터베이스에서 사용하는 데이터 구조체입니다.
/// 예시:
/// <code>
/// var entity = new PlaybackSQLiteDataEntity {
/// data = "센서값",
/// timestamp = "2024-07-29T12:00:00.000Z",
/// temp = "임시값"
/// };
/// </code>
/// </summary>
[System.Serializable]
public class PlaybackSQLiteDataEntity
{
public string data { get; set; }
[PrimaryKey]
public string timestamp { get; set; }
public DateTime timestampHungary { get => DateTimeUtil.UtcStringToKoreaDateTime(timestamp); }
// timestampHungary는 timestamp를 DateTime으로 변환한 값입니다.
public DateTime timestampHungary { get => DateTimeUtil.UtcStringToHungaryDateTime(timestamp); }
public string temp { get; set; }
}
}

View File

@@ -12,14 +12,34 @@ using UVC.Factory.Playback.UI;
using UVC.Util;
namespace UVC.Factory.Playback
{
/// <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>
public class PlaybackService
{
#region Singleton
// 싱글턴 인스턴스. PlaybackService.Instance로 접근합니다.
private static readonly PlaybackService instance = new PlaybackService(new PlaybackRepository());
public static PlaybackService Instance => instance;
static PlaybackService() { }
#endregion
// 재생 데이터가 저장되는 폴더 경로입니다.
public static readonly string PlaybackFolderPath = Path.Combine(Application.persistentDataPath, "playback");//streamingAssetsPath, "playback"); appData 폴더로 변경
private readonly PlaybackRepository repository;
@@ -31,6 +51,15 @@ namespace UVC.Factory.Playback
public Action OnExitPlayback;
private float timeScale = 1.0f;
/// <summary>
/// 재생 시간 스케일(배속)입니다. 1.0f가 기본입니다.
/// </summary>
/// <example>
/// <code>
/// // 재생 속도를 2배로 변경
/// PlaybackService.Instance.TimeScale = 2.0f;
/// </code>
/// </example>
public float TimeScale
{
get => timeScale;
@@ -48,17 +77,49 @@ namespace UVC.Factory.Playback
public Action<float> OnChangeTimeScale;
/// <summary>
/// 생성자. 일반적으로 직접 호출하지 않고 싱글턴 인스턴스를 사용합니다.
/// </summary>
public PlaybackService(PlaybackRepository repository)
{
this.repository = repository;
}
/// <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>
public async UniTask<Dictionary<string, Dictionary<string, string>>?> RequestDataAsync()
{
Dictionary<string, Dictionary<string, string>>? data = await repository.RequestPlaybackDateList();
return data;
}
/// <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>
public async UniTask DispatchBaseInfoData(string date, string time, string fileName, string minute = "00", string second = "00")
{
await UniTask.RunOnThreadPool(async () =>
@@ -87,9 +148,15 @@ namespace UVC.Factory.Playback
}
/// <summary>
///
/// 실시간 재생 데이터(특정 초 단위)를 비동기로 처리합니다.
/// </summary>
/// <param name="second">0 ~ 3600</param>
/// <param name="second">0 ~ 3600 (초 단위)</param>
/// <param name="speed">재생 속도</param>
/// <example>
/// <code>
/// await PlaybackService.Instance.DispatchRealTimeData(120, 1); // 120초(2분) 위치 데이터 처리
/// </code>
/// </example>
public async UniTask DispatchRealTimeData(int second, int speed)
{
await UniTask.RunOnThreadPool(async () =>
@@ -117,6 +184,16 @@ namespace UVC.Factory.Playback
});
}
/// <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>
public async Task StartAsync(UIPlaybackListItemData data)
{
timeScale = 1.0f; //기본 시간 스케일 설정
@@ -124,11 +201,44 @@ namespace UVC.Factory.Playback
await UIPlayback.Instance.SetData(data.date, data.time, data.sqlFileName);
}
/// <summary>
/// 재생을 종료합니다. (이벤트 발생)
/// </summary>
/// <example>
/// <code>
/// PlaybackService.Instance.Exit();
/// </code>
/// </example>
public void Exit()
{
OnExitPlayback?.Invoke();
}
/// <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>
public HTTPRequest? ReadyData(string date, string time, string fileName, Action<long, long, float> OnProgress, Action<string> OnComplete)
{
//date : "2024-12-05"

View File

@@ -3,15 +3,18 @@ using System;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
using UVC.Extension;
using UVC.UI;
using UVC.UI.Loading;
namespace UVC.Factory.Playback.UI
{
/// <summary>
/// 재생 UI를 관리하는 클래스입니다.
/// - UI 요소의 표시/숨김, 버튼/슬라이더 등 UI 이벤트를 처리합니다.
/// - 실제 재생 로직은 UIPlaybackController에서 처리합니다.
/// </summary>
public class UIPlayback : MonoBehaviour
{
// 싱글톤 패턴: 어디서든 UIPlayback.Instance로 접근할 수 있습니다.
private static UIPlayback instance;
public static UIPlayback Instance
{
@@ -22,98 +25,129 @@ namespace UVC.Factory.Playback.UI
}
}
/// <summary>
/// UIPlayback 프리팹을 동적으로 생성합니다.
/// </summary>
/// <returns>생성된 UIPlayback 인스턴스</returns>
private static UIPlayback CreateUIPlayBack()
{
GameObject prefab = Resources.Load<GameObject>("Prefabs/Factory/Playback/UIPlayback");
// Resources 폴더에서 프리팹을 불러와 인스턴스화합니다.
GameObject prefab = Resources.Load<GameObject>("Prefabs/UI/Playback/UIPlayback");
GameObject go = GameObject.Instantiate(prefab);
return go.GetComponent<UIPlayback>();
}
// Inspector에서 연결할 UI 컴포넌트들입니다.
[SerializeField]
[Tooltip("종료 버튼")]
private Button exitButton;
[SerializeField]
[Tooltip("종료 버튼")]
private TextMeshProUGUI dateTimeTxt0;
[SerializeField]
[Tooltip("종료 버튼")]
private TextMeshProUGUI dateTimeTxt1;
[SerializeField]
[Tooltip("play 버튼")]
private Button playButton;
[SerializeField]
[Tooltip("play 버튼 이미지")]
private Image playButtonImage;
[SerializeField]
[Tooltip("play 버튼 이미지 Sprite")]
private Sprite playButtonImagePlay;
[SerializeField]
[Tooltip("play 버튼 Puase 이미지 Sprite")]
private Sprite playButtonImagePause;
[SerializeField]
[Tooltip("Speed Slider")]
private UISliderWithLabel sliderSpeed;
[SerializeField]
[Tooltip("투명 조절 Slider")]
private SliderWithEvent opacitySlider;
[SerializeField]
[Tooltip("Progress Bar")]
private UIPlaybackProgressBar progressBar;
[SerializeField]
private CanvasGroup canvasGroup;
[SerializeField]
private UIDragger uiDragger;
private bool isPlaying = false;
private bool preparingData = false;
// 컨트롤러: 실제 재생 로직을 담당합니다.
private UIPlaybackController controller;
private string date;
private string time;
private string fileName;
// UI 이벤트를 외부(Controller)로 전달하기 위한 이벤트입니다.
public event Action OnClickExitButton;
public event Action OnClickPlayButton;
public event Action<int> OnChangeProgressValue;
public event Action<int> OnChangeSpeedValue;
public event Action<float> OnChangeOpacityValue;
private bool isTick = false;
private bool IsTick
/// <summary>
/// 오브젝트가 생성될 때 호출됩니다.
/// </summary>
private void Awake()
{
get => isTick;
set
{
if (isTick != value)
{
var temp = isTick;
isTick = value;
if (!temp && value) OnTimer().Forget();
}
}
controller = new UIPlaybackController(this);
Init();
}
/// <summary>
/// UI 이벤트 리스너를 등록합니다.
/// </summary>
private void Init()
{
exitButton.onClick.AddListener(OnClickExit);
playButton.onClick.AddListener(OnClickPlay);
// 버튼 클릭 시 이벤트 발생
exitButton.onClick.AddListener(() => OnClickExitButton?.Invoke());
playButton.onClick.AddListener(() => OnClickPlayButton?.Invoke());
progressBar.OnChangeValue += OnChangeProgress;
sliderSpeed.OnChangeValue += OnChangeSpeed;
opacitySlider.onValueChanged.AddListener(OnValueChangedOpcity);
// 슬라이더/프로그레스바 값 변경 시 이벤트 발생
progressBar.OnChangeValue += (value) => OnChangeProgressValue?.Invoke(value);
sliderSpeed.OnChangeValue += (value) => OnChangeSpeedValue?.Invoke(value);
opacitySlider.onValueChanged.AddListener((value) => OnChangeOpacityValue?.Invoke(value));
}
/// <summary>
/// 오브젝트가 파괴될 때 호출됩니다.
/// </summary>
private void OnDestroy()
{
exitButton.onClick.RemoveListener(OnClickExit);
playButton.onClick.RemoveListener(OnClickPlay);
// 모든 이벤트 리스너 해제
exitButton.onClick.RemoveAllListeners();
playButton.onClick.RemoveAllListeners();
progressBar.OnChangeValue = null;
sliderSpeed.OnChangeValue = null;
opacitySlider.onValueChanged.RemoveListener(OnValueChangedOpcity);
if (isPlaying) IsTick = false;
opacitySlider.onValueChanged.RemoveAllListeners();
controller.Dispose();
}
/// <summary>
/// UI를 화면에 표시합니다.
/// </summary>
/// <example>
/// <code>
/// // UIPlayback을 화면에 띄우는 예시
/// UIPlayback.Instance.Show();
/// </code>
/// </example>
public void Show()
{
if (playButton == null) Init();
gameObject.SetActive(true);
if (transform.parent == null)
{
// ModalCanvas에 붙여서 항상 위에 보이도록 설정
var canvases = GameObject.FindObjectsByType<Canvas>(FindObjectsSortMode.None);
foreach (var canvas in canvases)
{
@@ -127,146 +161,118 @@ namespace UVC.Factory.Playback.UI
}
}
/// <summary>
/// UI를 화면에서 숨깁니다.
/// </summary>
/// <example>
/// <code>
/// // UIPlayback을 숨기는 예시
/// UIPlayback.Instance.Hide();
/// </code>
/// </example>
public void Hide()
{
UpdateTimeScale(1);
IsTick = false;
controller.UpdateTimeScale(1);
controller.IsTick = false;
gameObject.SetActive(false);
}
private void OnClickExit()
/// <summary>
/// 재생에 필요한 데이터를 설정합니다.
/// </summary>
/// <param name="date">날짜(예: "2024-07-29")</param>
/// <param name="time">시간(초 단위 문자열, 예: "3600")</param>
/// <param name="fileName">파일명</param>
/// <returns>비동기 작업(UniTask)</returns>
/// <example>
/// <code>
/// await UIPlayback.Instance.SetData("2024-07-29", "3600", "sample.sqlite");
/// </code>
/// </example>
public UniTask SetData(string date, string time, string fileName)
{
UILoading.Show();
isPlaying = false;
UpdatePlayState();
Hide();
PlaybackService.Instance.Exit();
return controller.SetData(date, time, fileName);
}
private void OnClickPlay()
#region View Update Methods
/// <summary>
/// 날짜/시간 텍스트를 갱신합니다.
/// </summary>
/// <param name="date">표시할 날짜 문자열</param>
public void UpdateDateTime(string date)
{
isPlaying = !isPlaying;
UpdatePlayState();
dateTimeTxt0.text = dateTimeTxt1.text = date;
}
private void OnChangeProgress(int newValue)
/// <summary>
/// 재생 진행 바를 초기화합니다.
/// </summary>
/// <param name="time">전체 재생 시간(초)</param>
public void InitProgressBar(int time) => progressBar.Init(time);
/// <summary>
/// 속도 슬라이더를 초기화합니다.
/// </summary>
public void InitSpeedSlider() => sliderSpeed.Init();
/// <summary>
/// 재생/일시정지 버튼 이미지를 갱신합니다.
/// </summary>
/// <param name="isPlaying">재생 중이면 true, 아니면 false</param>
public void UpdatePlayButtonState(bool isPlaying)
{
ChangePlayTime().Forget();
}
private void OnChangeSpeed(int newValue)
{
if (isPlaying)
{
//if (Time.timeScale != sliderSpeed.Value) UpdateTimeScale(sliderSpeed.Value);
UpdateTimeScale(sliderSpeed.Value);
}
else
{
UpdateTimeScale(1);
}
}
private void OnValueChangedOpcity(float newValue)
{
canvasGroup.alpha = opacitySlider.value;
}
private async UniTaskVoid ChangePlayTime()
{
bool tempIsPlaing = isPlaying;
isPlaying = false;
int newSecond = (int)progressBar.Value;
if (newSecond == progressBar.MaxValue)
{
newSecond -= 60;
progressBar.Value = newSecond;
}
preparingData = true;
progressBar.Interactable = !preparingData;
IsTick = false;
UILoading.Show();
UpdatePlayState();
await UniTask.WaitForSeconds(0.5f);
int minute = (int)newSecond / 60;
int seconds = (int)newSecond % 60;
await PlaybackService.Instance.DispatchBaseInfoData(date, time, fileName, minute.ToString("00"), seconds.ToString("00"));
preparingData = false;
progressBar.Interactable = !preparingData;
if (isPlaying != tempIsPlaing)
{
isPlaying = tempIsPlaing;
UpdatePlayState();
}
UILoading.Hide();
await UniTask.WaitForSeconds(0.5f);
}
public async UniTask SetData(string date, string time, string fileName)
{
Init();
this.date = date;
this.time = time;
this.fileName = fileName;
Debug.Log($"UIPlayback SetData {date} {time}");
int timeInt = int.Parse(time);
dateTimeTxt0.text = dateTimeTxt1.text = date.Substring(2).Replace("-", ".");
progressBar.Init(timeInt);
sliderSpeed.Init();
UpdateTimeScale(1);
canvasGroup.alpha = opacitySlider.value = 1;
preparingData = true;
progressBar.Interactable = !preparingData;
isPlaying = false;
UpdatePlayState();
await PlaybackService.Instance.DispatchBaseInfoData(date, time, fileName);
preparingData = false;
progressBar.Interactable = !preparingData;
}
private void UpdatePlayState()
{
playButton.enabled = false;
progressBar.enabled = false;
playButtonImage.sprite = isPlaying ? playButtonImagePause : playButtonImagePlay;
IsTick = isPlaying;
progressBar.enabled = true;
playButton.enabled = true;
OnChangeSpeed(sliderSpeed.Value);
}
private async UniTaskVoid OnTimer()
/// <summary>
/// 재생 진행 바의 값을 설정합니다.
/// </summary>
/// <param name="value">설정할 값(초)</param>
public void SetProgressValue(int value)
{
if (progressBar.Value == progressBar.MaxValue)
if (progressBar.Value != value)
{
if (isPlaying) OnClickPlay();
return;
}
progressBar.Value += 1;
//PlaybackService.Instance.DispatchingTimelineEvent = false;
PlaybackService.Instance.DispatchRealTimeData(progressBar.Value, sliderSpeed.Value).Forget();
if (isTick)
{
//PlaybackService.Instance.DispatchingTimelineEvent = true;
await UniTask.Delay(TimeSpan.FromMilliseconds(1000 / sliderSpeed.Value));
OnTimer().Forget();
progressBar.Value = value;
}
}
private void UpdateTimeScale(float timeScale)
/// <summary>
/// 현재 재생 위치(초)를 반환합니다.
/// </summary>
public int GetProgressValue() => progressBar.Value;
/// <summary>
/// 전체 재생 시간(초)를 반환합니다.
/// </summary>
public int GetProgressMaxValue() => progressBar.MaxValue;
/// <summary>
/// 현재 선택된 재생 속도를 반환합니다.
/// </summary>
public int GetSpeedValue() => sliderSpeed.Value;
/// <summary>
/// UI의 투명도를 설정합니다.
/// </summary>
/// <param name="value">0(완전 투명) ~ 1(완전 불투명)</param>
public void SetOpacity(float value)
{
PlaybackService.Instance.TimeScale = timeScale;
canvasGroup.alpha = value;
opacitySlider.value = value;
}
/// <summary>
/// UI의 상호작용 가능 여부를 설정합니다.
/// </summary>
/// <param name="isInteractable">true면 버튼/슬라이더 사용 가능</param>
public void SetUIInteractable(bool isInteractable)
{
playButton.enabled = isInteractable;
progressBar.Interactable = isInteractable;
}
#endregion
}
}

View File

@@ -0,0 +1,283 @@
using Cysharp.Threading.Tasks;
using System;
using UVC.UI.Loading;
namespace UVC.Factory.Playback.UI
{
/// <summary>
/// UIPlaybackController는 UIPlayback(View)에서 발생하는 이벤트를 받아
/// 실제 재생, 일시정지, 데이터 준비 등 비즈니스 로직을 처리하는 컨트롤러입니다.
///
/// <b>예시: UIPlayback과의 연결</b>
/// <code>
/// // UIPlayback에서 컨트롤러를 생성할 때 View를 넘겨줍니다.
/// var controller = new UIPlaybackController(this);
/// </code>
/// </summary>
public class UIPlaybackController : IDisposable
{
/// <summary>
/// View 역할을 하는 UIPlayback 참조입니다.
/// </summary>
private readonly UIPlayback view;
// 재생 중 여부
private bool isPlaying = false;
// 데이터 준비 중 여부
private bool preparingData = false;
// 재생에 필요한 정보
private string date;
private string time;
private string fileName;
// 타이머 동작 여부
private bool isTick = false;
/// <summary>
/// 타이머 동작 여부를 외부에서 제어할 수 있습니다.
/// true로 설정하면 내부적으로 OnTimer()가 실행됩니다.
/// </summary>
/// <example>
/// <code>
/// // 타이머를 시작하려면
/// controller.IsTick = true;
/// // 타이머를 멈추려면
/// controller.IsTick = false;
/// </code>
/// </example>
public bool IsTick
{
get => isTick;
set
{
if (isTick != value)
{
isTick = value;
if (isTick) OnTimer().Forget();
}
}
}
/// <summary>
/// 생성자에서 View와 이벤트를 연결합니다.
/// </summary>
/// <param name="view">UIPlayback 인스턴스</param>
public UIPlaybackController(UIPlayback view)
{
this.view = view;
SubscribeToViewEvents();
}
/// <summary>
/// View에서 발생하는 이벤트를 컨트롤러의 메서드와 연결합니다.
/// </summary>
private void SubscribeToViewEvents()
{
view.OnClickExitButton += OnClickExit;
view.OnClickPlayButton += OnClickPlay;
view.OnChangeProgressValue += OnChangeProgress;
view.OnChangeSpeedValue += OnChangeSpeed;
view.OnChangeOpacityValue += OnValueChangedOpacity;
}
/// <summary>
/// 컨트롤러가 더 이상 필요 없을 때 이벤트 연결을 해제합니다.
/// </summary>
public void Dispose()
{
if (isPlaying) IsTick = false;
view.OnClickExitButton -= OnClickExit;
view.OnClickPlayButton -= OnClickPlay;
view.OnChangeProgressValue -= OnChangeProgress;
view.OnChangeSpeedValue -= OnChangeSpeed;
view.OnChangeOpacityValue -= OnValueChangedOpacity;
}
/// <summary>
/// 재생에 필요한 데이터를 설정하고 UI를 초기화합니다.
/// </summary>
/// <param name="date">날짜(예: "2024-07-29")</param>
/// <param name="time">시간(초 단위 문자열, 예: "3600")</param>
/// <param name="fileName">파일명</param>
/// <returns>비동기 작업(UniTask)</returns>
/// <example>
/// <code>
/// // 재생 데이터를 설정하는 예시
/// await controller.SetData("2024-07-29", "3600", "sample.sqlite");
/// </code>
/// </example>
public async UniTask SetData(string date, string time, string fileName)
{
this.date = date;
this.time = time;
this.fileName = fileName;
int timeInt = int.Parse(time);
view.UpdateDateTime(date.Substring(2).Replace("-", "."));
view.InitProgressBar(timeInt);
view.InitSpeedSlider();
view.SetOpacity(1);
UpdateTimeScale(1);
preparingData = true;
view.SetUIInteractable(!preparingData);
isPlaying = false;
UpdatePlayState();
// 실제 데이터 로딩 (비동기)
await PlaybackService.Instance.DispatchBaseInfoData(date, time, fileName);
preparingData = false;
view.SetUIInteractable(!preparingData);
}
/// <summary>
/// 종료 버튼 클릭 시 호출됩니다.
/// </summary>
/// <remarks>
/// UI를 숨기고, 재생 상태를 초기화하며, PlaybackService에 종료를 알립니다.
/// </remarks>
private void OnClickExit()
{
UILoading.Show();
isPlaying = false;
UpdatePlayState();
view.Hide();
PlaybackService.Instance.Exit();
}
/// <summary>
/// 재생/일시정지 버튼 클릭 시 호출됩니다.
/// </summary>
/// <remarks>
/// 데이터 준비 중에는 동작하지 않습니다.
/// </remarks>
private void OnClickPlay()
{
if (preparingData) return;
isPlaying = !isPlaying;
UpdatePlayState();
}
// <summary>
/// 재생 위치(프로그레스바) 변경 시 호출됩니다.
/// </summary>
/// <param name="newValue">변경된 위치(초)</param>
private void OnChangeProgress(int newValue)
{
ChangePlayTime().Forget();
}
/// <summary>
/// 재생 속도 변경 시 호출됩니다.
/// </summary>
/// <param name="newValue">변경된 속도 값</param>
private void OnChangeSpeed(int newValue)
{
UpdateTimeScale(isPlaying ? view.GetSpeedValue() : 1);
}
/// <summary>
/// 투명도 슬라이더 변경 시 호출됩니다.
/// </summary>
/// <param name="newValue">0~1 사이의 투명도 값</param>
private void OnValueChangedOpacity(float newValue)
{
view.SetOpacity(newValue);
}
/// <summary>
/// 재생/일시정지 상태에 따라 UI와 타이머를 갱신합니다.
/// </summary>
private void UpdatePlayState()
{
view.UpdatePlayButtonState(isPlaying);
IsTick = isPlaying;
OnChangeSpeed(view.GetSpeedValue());
}
/// <summary>
/// 재생 위치를 변경할 때 호출됩니다.
/// 데이터 준비, UI 비활성화, 로딩 표시 등 처리 후 재생 위치를 이동합니다.
/// </summary>
private async UniTaskVoid ChangePlayTime()
{
bool tempIsPlaying = isPlaying;
isPlaying = false;
UpdatePlayState();
int newSecond = view.GetProgressValue();
if (newSecond == view.GetProgressMaxValue())
{
newSecond -= 60;
view.SetProgressValue(newSecond);
}
preparingData = true;
view.SetUIInteractable(!preparingData);
IsTick = false;
UILoading.Show();
int minute = newSecond / 60;
int seconds = newSecond % 60;
// 새로운 위치로 데이터 요청
await PlaybackService.Instance.DispatchBaseInfoData(date, time, fileName, minute.ToString("00"), seconds.ToString("00"));
preparingData = false;
view.SetUIInteractable(!preparingData);
if (tempIsPlaying)
{
isPlaying = true;
UpdatePlayState();
}
UILoading.Hide();
}
/// <summary>
/// 재생 중일 때 일정 간격으로 호출되어 진행 바를 업데이트합니다.
/// </summary>
/// <remarks>
/// IsTick이 true일 때만 동작합니다.
/// </remarks>
/// <example>
/// <code>
/// // 타이머를 시작하려면
/// controller.IsTick = true;
/// </code>
/// </example>
private async UniTaskVoid OnTimer()
{
if (view.GetProgressValue() >= view.GetProgressMaxValue())
{
if (isPlaying) OnClickPlay();
return;
}
view.SetProgressValue(view.GetProgressValue() + 1);
// 실시간 데이터 요청
PlaybackService.Instance.DispatchRealTimeData(view.GetProgressValue(), view.GetSpeedValue()).Forget();
if (IsTick)
{
// 재생 속도에 따라 대기 시간 조절
await UniTask.Delay(TimeSpan.FromMilliseconds(1000 / view.GetSpeedValue()));
if (IsTick) OnTimer().Forget();
}
}
/// <summary>
/// 재생 속도를 PlaybackService에 반영합니다.
/// </summary>
/// <param name="timeScale">적용할 재생 속도</param>
/// <example>
/// <code>
/// // 재생 속도를 2배로 변경
/// controller.UpdateTimeScale(2f);
/// </code>
/// </example>
internal void UpdateTimeScale(float timeScale)
{
PlaybackService.Instance.TimeScale = timeScale;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: eedd46434a3229b479a141df871ae2ae

View File

@@ -14,20 +14,55 @@ using UVC.Util;
namespace UVC.Factory.Playback.UI
{
/// <summary>
/// 재생 목록의 각 아이템(1시간 단위 파일)을 나타내는 UI 컴포넌트입니다.
/// 다운로드, 삭제, 선택 등 다양한 상태를 관리합니다.
///
/// <para>샘플 사용법:</para>
/// <code>
/// // 1. 풀에서 아이템 생성 및 초기화
/// var itemData = new UIPlaybackListItemData {
/// date = "2024-07-29",
/// time = "13",
/// zipFileName = "2024-07-29_13.zip",
/// sqlFileName = "2024-07-29_13.sql"
/// };
/// var item = UIPlaybackListItem.CreateFromPool(parentTransform);
/// item.Init(itemData);
///
/// // 2. 선택 이벤트 등록
/// item.OnSelect = (data, selected) => {
/// Debug.Log($"{data.time}시 아이템이 {(selected ? "선택됨" : "선택 해제됨")}");
/// };
///
/// // 3. 상태 변경 이벤트 등록
/// item.OnChangeStatus = (data, status) => {
/// Debug.Log($"{data.time}시 아이템 상태: {status}");
/// };
///
/// // 4. 다운로드 버튼 클릭(직접 호출 예시)
/// // item.SendMessage("onClickDownload");
/// </code>
/// </summary>
public enum UIPlaybackListItemStatus
{
/// <summary>기본 상태(다운로드 전)</summary>
Default,
/// <summary>다운로드 중</summary>
Downloading,
/// <summary>다운로드 완료</summary>
Downloaded
}
/// <summary>
/// 재생 목록 아이템의 데이터 구조체입니다.
/// </summary>
public class UIPlaybackListItemData
{
public string date = "";
public string time = "";
public string zipFileName = "";
public string sqlFileName = "";
public string date = ""; // 날짜 (예: "2024-07-29")
public string time = ""; // 시간 (예: "13")
public string zipFileName = ""; // ZIP 파일명
public string sqlFileName = ""; // SQL 파일명
public UIPlaybackListItemStatus status = UIPlaybackListItemStatus.Default;
public override string ToString()
@@ -36,13 +71,28 @@ namespace UVC.Factory.Playback.UI
}
}
/// <summary>
/// 재생 목록의 각 아이템을 관리하는 UI 컴포넌트입니다.
/// - 풀링, 상태 관리, 다운로드/삭제/중지 버튼, 선택 이벤트 등을 제공합니다.
/// </summary>
public class UIPlaybackListItem : UnityEngine.MonoBehaviour, IPointerClickHandler
{
// 오브젝트 풀: 아이템을 효율적으로 재사용하기 위한 풀입니다.
protected static ItemPool<UIPlaybackListItem> pool;
public static readonly string PrefabPath = "Prefabs/Factory/Playback/UIPlaybackListItem";
/// <summary>
/// 프리팹 경로(Resources 폴더 기준)
/// </summary>
public static readonly string PrefabPath = "Prefabs/UI/Playback/UIPlaybackListItem";
/// <summary>
/// 풀에서 아이템을 생성(또는 재사용)합니다.
/// </summary>
/// <param name="parent">부모 Transform</param>
/// <returns>UIPlaybackListItem 인스턴스</returns>
/// <example>
/// var item = UIPlaybackListItem.CreateFromPool(parentTransform);
/// </example>
public static UIPlaybackListItem CreateFromPool(Transform parent)
{
if (pool == null)
@@ -53,6 +103,9 @@ namespace UVC.Factory.Playback.UI
return pool.GetItem(true, parent);
}
/// <summary>
/// 현재 활성화된(화면에 보이는) 아이템 목록
/// </summary>
public static List<UIPlaybackListItem> ActiveItems
{
get
@@ -62,6 +115,9 @@ namespace UVC.Factory.Playback.UI
}
}
/// <summary>
/// 현재 다운로드 중인 아이템 목록
/// </summary>
public static List<UIPlaybackListItem> DownloadingItems
{
get
@@ -70,6 +126,9 @@ namespace UVC.Factory.Playback.UI
}
}
/// <summary>
/// 모든 아이템을 풀에 반환(비활성화)합니다.
/// </summary>
public static void ReleaseAll()
{
if (pool != null)
@@ -79,6 +138,9 @@ namespace UVC.Factory.Playback.UI
}
}
/// <summary>
/// 모든 아이템을 풀에 반환하고, 재활용 목록도 비웁니다.
/// </summary>
public static void ClearAll()
{
if (pool != null)
@@ -89,6 +151,7 @@ namespace UVC.Factory.Playback.UI
}
}
// --- UI 요소들 (Unity 에디터에서 할당) ---
[SerializeField]
private Image loadingImage;
[SerializeField]
@@ -104,13 +167,26 @@ namespace UVC.Factory.Playback.UI
[SerializeField]
private TextMeshProUGUI text;
/// <summary>
/// 아이템이 선택될 때 호출되는 콜백 (data, 선택여부)
/// </summary>
public Action<UIPlaybackListItemData, bool> OnSelect { get; set; }
/// <summary>
/// 아이템 상태가 변경될 때 호출되는 콜백 (data, 변경된 상태)
/// </summary>
public Action<UIPlaybackListItemData, UIPlaybackListItemStatus> OnChangeStatus { get; set; }
private UIPlaybackListItemData data;
/// <summary>
/// 이 아이템의 데이터
/// </summary>
public UIPlaybackListItemData Data { get => data; }
/// <summary>
/// 아이템의 현재 상태 (Default/Downloading/Downloaded)
/// </summary>
public UIPlaybackListItemStatus Status
{
get => data.status;
@@ -125,8 +201,12 @@ namespace UVC.Factory.Playback.UI
}
}
private bool selected = false;
/// <summary>
/// 아이템이 선택되었는지 여부
/// </summary>
public bool Selected
{
get => selected;
@@ -140,10 +220,17 @@ namespace UVC.Factory.Playback.UI
}
}
// 다운로드 요청 객체 (다운로드 중일 때만 사용)
private HTTPRequest? downloadRequest;
/// <summary>
/// 아이템을 초기화합니다. (데이터 바인딩 및 버튼 이벤트 등록)
/// </summary>
/// <param name="data">초기화할 데이터</param>
/// <example>
/// var item = UIPlaybackListItem.CreateFromPool(parent);
/// item.Init(data);
/// </example>
public void Init(UIPlaybackListItemData data)
{
transform.localScale = Vector3.one;
@@ -158,6 +245,10 @@ namespace UVC.Factory.Playback.UI
ChangeStatus();
}
/// <summary>
/// 다운로드 버튼 클릭 시 호출됩니다.
/// - 용량 체크, 동시 다운로드 제한, 다운로드 시작 등 처리
/// </summary>
private void onClickDownload()
{
if (Status == UIPlaybackListItemStatus.Downloading) return;
@@ -179,6 +270,8 @@ namespace UVC.Factory.Playback.UI
}
Status = UIPlaybackListItemStatus.Downloading;
// 실제 다운로드 요청 (진행률/완료/에러 콜백 처리)
downloadRequest = PlaybackService.Instance.ReadyData(data.date, data.time, data.zipFileName,
(long read, long total, float percent) =>
{
@@ -206,6 +299,9 @@ namespace UVC.Factory.Playback.UI
});
}
/// <summary>
/// 다운로드/삭제 시 파일을 실제로 삭제합니다.
/// </summary>
private void deleteFile()
{
try
@@ -231,6 +327,9 @@ namespace UVC.Factory.Playback.UI
}
}
/// <summary>
/// 다운로드 중지 버튼 클릭 시 호출됩니다.
/// </summary>
private void onClickStop()
{
if (downloadRequest != null && Status == UIPlaybackListItemStatus.Downloading)
@@ -243,23 +342,36 @@ namespace UVC.Factory.Playback.UI
}
}
/// <summary>
/// 삭제 버튼 클릭 시 호출됩니다.
/// </summary>
private void onClickDelete()
{
deleteFile();
Status = UIPlaybackListItemStatus.Default;
}
/// <summary>
/// 아이템을 클릭하면 선택/해제 상태가 토글됩니다.
/// </summary>
/// <param name="eventData">이벤트 데이터</param>
public void OnPointerClick(PointerEventData eventData)
{
Selected = !selected;
if (OnSelect != null) OnSelect.Invoke(data, Selected);
}
/// <summary>
/// 풀에 반환될 때 호출 (이벤트 해제 등)
/// </summary>
public void Release()
{
OnDestroy();
}
/// <summary>
/// 오브젝트가 파괴될 때 호출 (이벤트 해제)
/// </summary>
private void OnDestroy()
{
downloadButton.onClick.RemoveListener(onClickDownload);
@@ -270,6 +382,9 @@ namespace UVC.Factory.Playback.UI
transform.SetParent(null);
}
/// <summary>
/// 상태에 따라 UI를 갱신합니다.
/// </summary>
private void ChangeStatus()
{
downloadText.text = "";

View File

@@ -13,11 +13,43 @@ using UVC.Util;
namespace UVC.Factory.Playback.UI
{
/// <summary>
/// 📅 재생 목록(Playback List)을 날짜별로 보여주고, 원하는 시간대의 파일을 선택할 수 있는 모달 창입니다.
/// - 날짜 드롭다운, 시간별 리스트, 다운로드 상태, 선택/확인 버튼 등을 제공합니다.
///
/// <para>샘플 사용법:</para>
/// <code>
/// // 1. 모달을 띄우는 코드 예시
/// var content = new ModalContent(UIPlaybackListModal.PrefabPath)
/// {
/// Title = "Playback List",
/// ConfirmButtonText = "Play",
/// ShowCancelButton = false
/// };
/// // 모달을 열고, 사용자가 선택한 결과를 기다립니다.
/// var result = await Modal.Open<UIPlaybackListItemData>(content);
/// if (result != null)
/// {
/// Debug.Log($"선택된 파일: {result.date} {result.time}시");
/// }
/// </code>
/// </summary>
[RequireComponent(typeof(CanvasGroup))]
public class UIPlaybackListModal : ModalView
{
public static new readonly string PrefabPath = "Prefabs/Factory/Playback/UIPlaybackListModal";
/// <summary>
/// 이 모달의 프리팹 경로(Resources 폴더 기준)
/// </summary>
public static readonly string PrefabPath = "Prefabs/UI/Playback/UIPlaybackListModal";
/// <summary>
/// 프리팹에서 모달을 생성합니다.
/// </summary>
/// <param name="parent">부모 Transform (생략 가능)</param>
/// <returns>UIPlaybackListModal 인스턴스</returns>
/// <example>
/// var modal = UIPlaybackListModal.CreateFromPrefab(parentTransform);
/// </example>
public static UIPlaybackListModal CreateFromPrefab(Transform parent = null)
{
GameObject prefab = Resources.Load<GameObject>(PrefabPath);
@@ -26,26 +58,40 @@ namespace UVC.Factory.Playback.UI
return modal;
}
// 날짜별로 시간대 파일 목록을 저장하는 딕셔너리
private Dictionary<string, List<UIPlaybackListItemData>>? data;
/// <summary>
/// '확인' 버튼을 누를 수 있는지 여부
/// (다운로드 완료된 항목이 선택되어 있고, 현재 다운로드 중인 항목이 없을 때만 true)
/// </summary>
public bool IsOkable => (selectedItem != null && selectedItem.status == UIPlaybackListItemStatus.Downloaded && UIPlaybackListItem.DownloadingItems.Count == 0);
// 현재 선택된 아이템 데이터
private UIPlaybackListItemData? selectedItem = null;
[SerializeField]
private TMP_Dropdown dropdownDate;
private TMP_Dropdown dropdownDate; // 날짜 선택 드롭다운
[SerializeField]
private ScrollRect scrollRectTime;
private ScrollRect scrollRectTime; // 시간대 리스트 스크롤 영역
// 모달이 닫힐 때 반환할 결과 데이터
private UIPlaybackListItemData? resultData = null;
/// <summary>
/// 모달이 열릴 때 호출됩니다. (비동기)
/// </summary>
/// <param name="content">모달에 표시할 내용/설정</param>
public override async UniTask OnOpen(ModalContent content)
{
await base.OnOpen(content); // 부모의 OnOpen을 먼저 호출해서 기본 UI를 설정해요.
initContent();
}
/// <summary>
/// 모달이 닫힐 때 결과로 반환할 데이터를 돌려줍니다.
/// </summary>
/// <returns>선택된 UIPlaybackListItemData 또는 null</returns>
public override object? GetResult()
{
if (data != null) data.Clear();
@@ -53,29 +99,40 @@ namespace UVC.Factory.Playback.UI
return resultData;
}
/// <summary>
/// 모달이 닫힐 때 호출됩니다.
/// </summary>
public override async UniTask OnClose(ModalContent content)
{
await base.OnClose(content);
}
/// <summary>
/// 모달의 내용을 초기화합니다. (날짜/시간대 데이터 불러오기)
/// </summary>
private async void initContent()
{
confirmButton.interactable = false;
// 서버에서 날짜/시간대 목록을 받아옵니다.
Dictionary<string, Dictionary<string, string>>? data = await PlaybackService.Instance.RequestDataAsync();
dropdownDate.onValueChanged.AddListener(DropDownDateChanged);
LocalSetData();
if (data != null) SetData(data);
LocalSetData(); // 로컬 파일 기준으로 먼저 리스트를 만듭니다.
if (data != null) SetData(data); // 서버 데이터가 있으면 반영합니다.
}
/// <summary>
/// 오브젝트가 파괴될 때 이벤트 해제 및 정리
/// </summary>
protected void OnDestroy()
{
dropdownDate.onValueChanged.RemoveListener(DropDownDateChanged);
UIPlaybackListItem.ClearAll();
}
/// <summary>
/// 로컬 저장소에서 날짜/시간대 파일 목록을 읽어와 리스트를 만듭니다.
/// </summary>
public void LocalSetData()
{
Dictionary<string, List<UIPlaybackListItemData>> newData = new Dictionary<string, List<UIPlaybackListItemData>>();
@@ -131,6 +188,10 @@ namespace UVC.Factory.Playback.UI
dropdownDate.value = -1;
}
/// <summary>
/// 서버에서 받은 날짜/시간대 데이터를 리스트에 반영합니다.
/// </summary>
/// <param name="data">서버에서 받은 데이터</param>
public void SetData(Dictionary<string, Dictionary<string, string>> data)
{
//{
@@ -142,7 +203,7 @@ namespace UVC.Factory.Playback.UI
//}
Dictionary<string, Dictionary<string, string>> dataList = new Dictionary<string, Dictionary<string, string>>();
//헝가리 시간으로 변경
// 날짜/시간대 정규화
foreach (var keyPair in data)
{
if (!dataList.ContainsKey(keyPair.Key)) dataList[keyPair.Key] = new Dictionary<string, string>();
@@ -215,8 +276,7 @@ namespace UVC.Factory.Playback.UI
}
}
// 서버 데이터 기준으로 추가
foreach (var keyPair in dataList)
{
string date = keyPair.Key;
@@ -238,7 +298,8 @@ namespace UVC.Factory.Playback.UI
});
}
}
//내림차순 정리
// 시간 오름차순 정렬
if (newData.ContainsKey(date)) newData[date].Sort((a, b) => int.Parse(a.time) - int.Parse(b.time));
}
@@ -249,14 +310,17 @@ namespace UVC.Factory.Playback.UI
}
/// <summary>
/// 날짜 드롭다운이 변경될 때 호출됩니다.
/// </summary>
/// <param name="value">선택된 인덱스</param>
private void DropDownDateChanged(int value)
{
// 선택된 날짜의 시간대 리스트를 갱신
//dropdownDatePlaceHolder.gameObject.SetActive(value == -1);
string key = dropdownDate.options[dropdownDate.value].text;
Debug.Log($"DropDownDateChanged dropdownDate.value:{dropdownDate.value} value:{value} key:{key}");
if (data.ContainsKey(key))
if (data != null && data.ContainsKey(key))
{
if (UIPlaybackListItem.ActiveItems.Count > 0) UIPlaybackListItem.ReleaseAll();
List<UIPlaybackListItemData> itemList = data[key];
@@ -270,11 +334,16 @@ namespace UVC.Factory.Playback.UI
selectedItem = null;
updateButtonStatus();
}
//scroll move to top
scrollRectTime.normalizedPosition = new Vector2(0, 1);
// 스크롤을 맨 위로 이동
scrollRectTime.normalizedPosition = new Vector2(0, 1);
}
/// <summary>
/// 시간대 아이템이 선택될 때 호출됩니다.
/// </summary>
/// <param name="data">선택된 아이템 데이터</param>
/// <param name="selected">선택 여부</param>
private void OnItemSelect(UIPlaybackListItemData data, bool selected)
{
if (selected)
@@ -296,17 +365,26 @@ namespace UVC.Factory.Playback.UI
updateButtonStatus();
}
/// <summary>
/// 아이템의 다운로드 상태가 변경될 때 호출됩니다.
/// </summary>
private void OnItemChangeStatus(UIPlaybackListItemData data, UIPlaybackListItemStatus status)
{
bool enable = UIPlaybackListItem.DownloadingItems.Count == 0;
dropdownDate.interactable = enable;
}
/// <summary>
/// '확인' 버튼의 활성화 상태를 갱신합니다.
/// </summary>
private void updateButtonStatus()
{
confirmButton.interactable = IsOkable;
}
/// <summary>
/// '확인' 버튼 클릭 시 호출됩니다.
/// </summary>
public override void OnConfirmButtonClicked()
{
resultData = selectedItem;

View File

@@ -1,4 +1,4 @@
using System;
using System;
using TMPro;
using UnityEngine;
using UVC.Extension;
@@ -6,18 +6,59 @@ using UVC.UI;
namespace UVC.Factory.Playback.UI
{
/// <summary>
/// 재생 위치를 표시하고 조작할 수 있는 UI 프로그레스바 컴포넌트입니다.
/// - 현재 재생 시간, 전체 시간, 슬라이더(Seek Bar)로 구성됩니다.
/// - 슬라이더를 드래그하거나 클릭하여 재생 위치를 변경할 수 있습니다.
///
/// <para>샘플 사용법:</para>
/// <code>
/// // UIPlaybackProgressBar를 가진 오브젝트를 찾고 초기화
/// var progressBar = FindObjectOfType&lt;UIPlaybackProgressBar&gt;();
/// progressBar.Init(1); // 1시간짜리 재생바로 초기화
///
/// // 값이 변경될 때마다 호출되는 콜백 등록
/// progressBar.OnChangeValue = (value) =&gt; {
/// Debug.Log($"재생 위치가 {value}초로 변경됨");
/// };
///
/// // 재생 위치를 코드로 변경
/// progressBar.Value = 120; // 2분(120초) 위치로 이동
/// </code>
/// </summary>
public class UIPlaybackProgressBar : MonoBehaviour
{
private TextMeshProUGUI playTimeTxt;
private TextMeshProUGUI totalTimeTxt;
private SliderWithEvent progressBar;
private float progressBarPrevValue = 0;
[Header("UI Playback Progress Bar")]
[Tooltip("UI 활성/비활성 및 상호작용 제어용")]
[SerializeField]
private CanvasGroup canvasGroup;// UI 활성/비활성 및 상호작용 제어용
private CanvasGroup canvasGroup;
[Tooltip("현재 재생 시간 텍스트")]
[SerializeField]
private TextMeshProUGUI playTimeTxt;// 현재 재생 시간 텍스트
[Tooltip("전체 재생 시간 텍스트")]
[SerializeField]
private TextMeshProUGUI totalTimeTxt;// 전체 재생 시간 텍스트
[Tooltip("재생 위치를 조작하는 슬라이더")]
[SerializeField]
private SliderWithEvent progressBar;// 재생 위치를 조작하는 슬라이더
private float progressBarPrevValue = 0;// 이전 슬라이더 값(변경 감지용)
// 전체 시간(시간 단위, 예: 1이면 1시간)
private int time;
/// <summary>
/// 슬라이더 값이 변경될 때 호출되는 이벤트입니다.
/// <para>예시: progressBar.OnChangeValue = (value) =&gt; { ... };</para>
/// </summary>
public Action<int> OnChangeValue { get; set; }
/// <summary>
/// 현재 슬라이더(재생 위치) 값(초 단위)입니다.
/// 값을 설정하면 UI와 내부 상태가 함께 갱신됩니다.
/// </summary>
public int Value
{
get
@@ -36,17 +77,26 @@ namespace UVC.Factory.Playback.UI
}
}
/// <summary>
/// 슬라이더의 최대값(전체 재생 시간, 초 단위)입니다.
/// </summary>
public int MaxValue
{
get => (int)progressBar.maxValue;
}
/// <summary>
/// 프로그레스바의 상호작용 가능 여부를 설정/조회합니다.
/// </summary>
public bool Interactable
{
get => canvasGroup.interactable;
set => canvasGroup.interactable = value;
}
/// <summary>
/// 오브젝트가 파괴될 때 이벤트 리스너를 해제합니다.
/// </summary>
private void OnDestroy()
{
progressBar.onValueChanged.RemoveListener(OnChangeSlider);
@@ -55,27 +105,35 @@ namespace UVC.Factory.Playback.UI
progressBar.OnEndDragAction = null;
}
/// <summary>
/// 프로그레스바를 초기화합니다.
/// </summary>
/// <param name="time">전체 시간(시간 단위, 예: 1이면 1시간)</param>
/// <example>
/// progressBar.Init(1); // 1시간짜리 재생바로 초기화
/// </example>
public void Init(int time)
{
this.time = time;
canvasGroup = GetComponent<CanvasGroup>();
playTimeTxt = transform.FindChildren("PlayTimeTxt").GetComponent<TextMeshProUGUI>();
totalTimeTxt = transform.FindChildren("TotalTimeTxt").GetComponent<TextMeshProUGUI>();
progressBar = GetComponentInChildren<SliderWithEvent>();
// 슬라이더 이벤트 등록
progressBar.onValueChanged.AddListener(OnChangeSlider);
progressBar.OnClickAction += OnClickProgressBar;
progressBar.OnDragAction += OnDragProgressBar;
progressBar.OnEndDragAction += OnEndDragProgressBar;
// 초기 텍스트 및 값 설정
playTimeTxt.text = $"{time.ToString("00")}:00:00";
totalTimeTxt.text = $"{(time + 1).ToString("00")}:00:00";
progressBar.value = 0;
Interactable = true;
}
/// <summary>
/// 슬라이더 값이 변경될 때 호출됩니다.
/// 상호작용 불가 상태면 값을 되돌립니다.
/// </summary>
/// <param name="newValue">변경된 값</param>
private void OnChangeSlider(float newValue)
{
if (!Interactable)
@@ -86,6 +144,10 @@ namespace UVC.Factory.Playback.UI
}
}
/// <summary>
/// 슬라이더를 클릭했을 때 호출됩니다.
/// 값이 변경되면 OnChangeValue 이벤트가 발생합니다.
/// </summary>
private void OnClickProgressBar()
{
float snapedValue = SnapProgressBarValue();
@@ -98,6 +160,9 @@ namespace UVC.Factory.Playback.UI
}
}
/// <summary>
/// 슬라이더를 드래그하는 동안 계속 호출됩니다.
/// </summary>
private void OnDragProgressBar()
{
float snapedValue = SnapProgressBarValue();
@@ -105,6 +170,10 @@ namespace UVC.Factory.Playback.UI
UpdateTimeText();
}
/// <summary>
/// 슬라이더 드래그가 끝났을 때 호출됩니다.
/// 값이 변경되면 OnChangeValue 이벤트가 발생합니다.
/// </summary>
private void OnEndDragProgressBar()
{
float snapedValue = SnapProgressBarValue();
@@ -117,6 +186,10 @@ namespace UVC.Factory.Playback.UI
}
}
/// <summary>
/// 슬라이더 값을 60초(1분) 단위로 스냅합니다.
/// </summary>
/// <returns>스냅된 값</returns>
private float SnapProgressBarValue()
{
float value = progressBar.value;
@@ -125,6 +198,9 @@ namespace UVC.Factory.Playback.UI
return value;
}
/// <summary>
/// 현재 재생 시간 텍스트를 갱신합니다.
/// </summary>
private void UpdateTimeText()
{
int minute = (int)progressBar.value / 60;