seawolf2357 commited on
Commit
5a1dde5
ยท
verified ยท
1 Parent(s): 8900e3d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +888 -893
app.py CHANGED
@@ -1,9 +1,12 @@
1
  """
2
  Simple Video Editor - ํ—ˆ๊น…ํŽ˜์ด์Šค ์ŠคํŽ˜์ด์Šค์šฉ
3
  CapCut/VEED ์Šคํƒ€์ผ ๊ฐ„๋‹จ ์˜์ƒ ํŽธ์ง‘๊ธฐ (ํฐ์ƒ‰ ํ…Œ๋งˆ)
 
4
  """
5
 
6
  import gradio as gr
 
 
7
 
8
  EDITOR_HTML = """
9
  <!DOCTYPE html>
@@ -20,11 +23,10 @@ EDITOR_HTML = """
20
  box-sizing: border-box;
21
  }
22
 
23
- body {
24
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Noto Sans KR', sans-serif;
25
  background: #f5f5f7;
26
  color: #1d1d1f;
27
- overflow: hidden;
28
  font-size: 13px;
29
  }
30
 
@@ -32,8 +34,11 @@ EDITOR_HTML = """
32
  .editor-container {
33
  display: flex;
34
  flex-direction: column;
35
- height: 100vh;
36
- max-height: 850px;
 
 
 
37
  }
38
 
39
  /* ========== ์ƒ๋‹จ ํˆด๋ฐ” ========== */
@@ -162,34 +167,12 @@ EDITOR_HTML = """
162
  padding: 8px;
163
  }
164
 
165
- /* ========== ์—…๋กœ๋“œ ์˜์—ญ ========== */
166
- .upload-zone {
167
- border: 2px dashed #d1d5db;
168
- border-radius: 8px;
169
- padding: 16px 8px;
170
  text-align: center;
171
- cursor: pointer;
172
- transition: all 0.2s;
173
- margin-bottom: 8px;
174
- background: #fafafa;
175
- }
176
-
177
- .upload-zone:hover {
178
- border-color: #6366f1;
179
- background: #f0f0ff;
180
- }
181
-
182
- .upload-zone svg {
183
- width: 24px;
184
- height: 24px;
185
  color: #9ca3af;
186
- margin-bottom: 6px;
187
- }
188
-
189
- .upload-zone p {
190
  font-size: 11px;
191
- color: #6b7280;
192
- line-height: 1.4;
193
  }
194
 
195
  /* ========== ๋ฏธ๋””์–ด ๊ทธ๋ฆฌ๋“œ ========== */
@@ -280,6 +263,17 @@ EDITOR_HTML = """
280
  color: white;
281
  }
282
 
 
 
 
 
 
 
 
 
 
 
 
283
  /* ========== ํ”„๋ฆฌ๋ทฐ ์˜์—ญ ========== */
284
  .preview-area {
285
  flex: 1;
@@ -790,27 +784,21 @@ EDITOR_HTML = """
790
  margin: 4px 0;
791
  }
792
 
793
- /* ========== ํŒŒ์ผ ์ž…๋ ฅ ์ˆจ๊ธฐ๊ธฐ ========== */
794
- #fileInput {
795
- display: none !important;
796
- }
797
-
798
- /* ========== ๋””๋ฒ„๊ทธ ๋ฉ”์‹œ์ง€ ========== */
799
- .debug-msg {
800
- position: fixed;
801
- bottom: 10px;
802
- left: 10px;
803
- background: #000;
804
- color: #0f0;
805
- padding: 5px 10px;
806
  font-size: 11px;
807
- border-radius: 4px;
808
- z-index: 9999;
809
- font-family: monospace;
810
  }
811
  </style>
812
  </head>
813
  <body>
 
814
  <div class="editor-container">
815
  <!-- ========== ์ƒ๋‹จ ํˆด๋ฐ” ========== -->
816
  <div class="toolbar">
@@ -824,20 +812,13 @@ EDITOR_HTML = """
824
  Simple Video Editor
825
  </div>
826
  <div class="toolbar-actions">
827
- <button class="btn btn-secondary" onclick="undoAction()" title="Ctrl+Z">
828
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
829
  <path d="M3 7v6h6M3 13a9 9 0 1 0 2.5-6.5"/>
830
  </svg>
831
  ์‹คํ–‰์ทจ์†Œ
832
  </button>
833
- <button class="btn btn-primary" onclick="document.getElementById('fileInput').click()" title="ํŒŒ์ผ ์ถ”๊ฐ€">
834
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
835
- <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M17 8l-5-5-5 5M12 3v12"/>
836
- </svg>
837
- ํŒŒ์ผ ์ถ”๊ฐ€
838
- </button>
839
- <input type="file" id="fileInput" multiple accept="video/*,image/*,audio/*" onchange="handleFileUpload(this)">
840
- <button class="btn btn-success" onclick="exportVideo()" id="exportBtn">
841
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
842
  <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3"/>
843
  </svg>
@@ -852,11 +833,8 @@ EDITOR_HTML = """
852
  <div class="media-library">
853
  <div class="library-header">๐Ÿ“ ๋ฏธ๋””์–ด</div>
854
  <div class="library-content">
855
- <div class="upload-zone" onclick="document.getElementById('fileInput').click()">
856
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
857
- <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M17 8l-5-5-5 5M12 3v12"/>
858
- </svg>
859
- <p>ํด๋ฆญ ๋˜๋Š” ๋“œ๋ž˜๊ทธ<br>์˜์ƒ/์ด๋ฏธ์ง€/์˜ค๋””์˜ค</p>
860
  </div>
861
  <div class="media-grid" id="mediaGrid"></div>
862
  </div>
@@ -875,27 +853,27 @@ EDITOR_HTML = """
875
  </div>
876
 
877
  <div class="playback-controls">
878
- <button class="control-btn" onclick="skipToStart()" title="์ฒ˜์Œ์œผ๋กœ">
879
  <svg viewBox="0 0 24 24" fill="currentColor">
880
  <path d="M6 6h2v12H6zm3.5 6l8.5 6V6z"/>
881
  </svg>
882
  </button>
883
- <button class="control-btn" onclick="skipBackward()" title="-5์ดˆ">
884
  <svg viewBox="0 0 24 24" fill="currentColor">
885
  <path d="M11 18V6l-8.5 6 8.5 6zm.5-6l8.5 6V6l-8.5 6z"/>
886
  </svg>
887
  </button>
888
- <button class="control-btn play-btn" onclick="togglePlay()" id="playBtn" title="Space">
889
  <svg viewBox="0 0 24 24" fill="currentColor" id="playIcon">
890
  <polygon points="5 3 19 12 5 21 5 3"/>
891
  </svg>
892
  </button>
893
- <button class="control-btn" onclick="skipForward()" title="+5์ดˆ">
894
  <svg viewBox="0 0 24 24" fill="currentColor">
895
  <path d="M4 18l8.5-6L4 6v12zm9-12v12l8.5-6L13 6z"/>
896
  </svg>
897
  </button>
898
- <button class="control-btn" onclick="skipToEnd()" title="๋์œผ๋กœ">
899
  <svg viewBox="0 0 24 24" fill="currentColor">
900
  <path d="M6 18l8.5-6L6 6v12zM16 6v12h2V6h-2z"/>
901
  </svg>
@@ -903,7 +881,7 @@ EDITOR_HTML = """
903
  <div class="time-display">
904
  <span id="currentTimeDisplay">00:00.00</span> / <span id="durationDisplay">00:00.00</span>
905
  </div>
906
- <button class="control-btn" onclick="toggleMute()" id="muteBtn" title="์Œ์†Œ๊ฑฐ">
907
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" id="volumeIcon">
908
  <polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/>
909
  <path d="M19.07 4.93a10 10 0 0 1 0 14.14M15.54 8.46a5 5 0 0 1 0 7.07"/>
@@ -926,7 +904,7 @@ EDITOR_HTML = """
926
  <!-- ========== ํƒ€์ž„๋ผ์ธ ========== -->
927
  <div class="timeline-area">
928
  <div class="timeline-toolbar">
929
- <button class="btn btn-secondary" onclick="splitSelectedClip()" title="์„ ํƒ ํด๋ฆฝ ์ž๋ฅด๊ธฐ">
930
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
931
  <circle cx="6" cy="6" r="3"/>
932
  <circle cx="6" cy="18" r="3"/>
@@ -936,14 +914,14 @@ EDITOR_HTML = """
936
  </svg>
937
  ์ž๋ฅด๊ธฐ
938
  </button>
939
- <button class="btn btn-secondary" onclick="duplicateSelectedClip()" title="Ctrl+D">
940
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
941
  <rect x="9" y="9" width="13" height="13" rx="2"/>
942
  <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
943
  </svg>
944
  ๋ณต์ œ
945
  </button>
946
- <button class="btn btn-danger" onclick="deleteSelectedClip()" title="Delete">
947
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
948
  <polyline points="3 6 5 6 21 6"/>
949
  <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
@@ -956,7 +934,7 @@ EDITOR_HTML = """
956
  <line x1="21" y1="21" x2="16.65" y2="16.65"/>
957
  <line x1="8" y1="11" x2="14" y2="11"/>
958
  </svg>
959
- <input type="range" min="0.5" max="3" step="0.1" value="1" oninput="setZoom(this.value)" id="zoomSlider">
960
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
961
  <circle cx="11" cy="11" r="8"/>
962
  <line x1="21" y1="21" x2="16.65" y2="16.65"/>
@@ -965,7 +943,7 @@ EDITOR_HTML = """
965
  </svg>
966
  </div>
967
  </div>
968
- <div class="timeline-container" id="timelineContainer" onclick="onTimelineClick(event)">
969
  <div class="timeline-ruler" id="timelineRuler"></div>
970
  <div class="timeline-tracks" id="timelineTracks">
971
  <div class="timeline-track" data-track="0">
@@ -976,7 +954,7 @@ EDITOR_HTML = """
976
  </svg>
977
  ์˜์ƒ
978
  </div>
979
- <div class="track-content" ondragover="handleDragOver(event)" ondrop="handleDrop(event, 0)" ondragleave="handleDragLeave(event)"></div>
980
  </div>
981
  <div class="timeline-track" data-track="1">
982
  <div class="track-label">
@@ -987,17 +965,19 @@ EDITOR_HTML = """
987
  </svg>
988
  ์˜ค๋””์˜ค
989
  </div>
990
- <div class="track-content" ondragover="handleDragOver(event)" ondrop="handleDrop(event, 1)" ondragleave="handleDragLeave(event)"></div>
991
  </div>
992
  </div>
993
  <div class="playhead" id="playhead" style="left: 70px;"></div>
994
  </div>
995
  </div>
 
 
996
  </div>
997
 
998
  <!-- ========== ์ปจํ…์ŠคํŠธ ๋ฉ”๋‰ด ========== -->
999
  <div class="context-menu" id="contextMenu" style="display:none;">
1000
- <div class="context-menu-item" onclick="splitSelectedClip()">
1001
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1002
  <circle cx="6" cy="6" r="3"/>
1003
  <circle cx="6" cy="18" r="3"/>
@@ -1005,7 +985,7 @@ EDITOR_HTML = """
1005
  </svg>
1006
  ์ž๋ฅด๊ธฐ
1007
  </div>
1008
- <div class="context-menu-item" onclick="duplicateSelectedClip()">
1009
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1010
  <rect x="9" y="9" width="13" height="13" rx="2"/>
1011
  <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
@@ -1013,7 +993,7 @@ EDITOR_HTML = """
1013
  ๋ณต์ œ
1014
  </div>
1015
  <div class="context-menu-divider"></div>
1016
- <div class="context-menu-item danger" onclick="deleteSelectedClip()">
1017
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1018
  <polyline points="3 6 5 6 21 6"/>
1019
  <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/>
@@ -1030,878 +1010,893 @@ EDITOR_HTML = """
1030
  <div class="progress-bar">
1031
  <div class="progress-fill" id="exportProgress" style="width: 0%"></div>
1032
  </div>
1033
- <button class="btn btn-secondary" onclick="cancelExport()" id="cancelExportBtn">์ทจ์†Œ</button>
1034
  </div>
1035
  </div>
1036
-
1037
- <!-- ๋””๋ฒ„๊ทธ ๋ฉ”์‹œ์ง€ -->
1038
- <div class="debug-msg" id="debugMsg">Ready</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1039
 
1040
- <script>
1041
- // ========================================
1042
- // ์ „์—ญ ์ƒํƒœ ๋ณ€์ˆ˜
1043
- // ========================================
1044
- var mediaLibrary = [];
1045
- var timelineClips = [];
1046
- var selectedClipId = null;
1047
- var isPlaying = false;
1048
- var isMuted = false;
1049
- var currentTime = 0;
1050
- var totalDuration = 0;
1051
- var zoom = 1;
1052
- var pixelsPerSecond = 80;
1053
- var undoStack = [];
1054
- var animationId = null;
1055
- var trimData = null;
1056
-
1057
- // ========================================
1058
- // ๋””๋ฒ„๊ทธ ํ•จ์ˆ˜
1059
- // ========================================
1060
- function debug(msg) {
1061
- console.log('[VideoEditor]', msg);
1062
- var el = document.getElementById('debugMsg');
1063
- if (el) {
1064
- el.textContent = msg;
1065
- el.style.display = 'block';
1066
- setTimeout(function() { el.style.display = 'none'; }, 3000);
 
 
 
 
 
 
 
 
 
1067
  }
1068
- }
1069
 
1070
- // ========================================
1071
- // ์œ ํ‹ธ๋ฆฌํ‹ฐ ํ•จ์ˆ˜
1072
- // ========================================
1073
- function generateId() {
1074
- return Date.now().toString(36) + Math.random().toString(36).substr(2);
1075
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1076
 
1077
- function formatTime(seconds) {
1078
- var mins = Math.floor(seconds / 60);
1079
- var secs = Math.floor(seconds % 60);
1080
- var ms = Math.floor((seconds % 1) * 100);
1081
- return (mins < 10 ? '0' : '') + mins + ':' + (secs < 10 ? '0' : '') + secs + '.' + (ms < 10 ? '0' : '') + ms;
1082
- }
1083
 
1084
- function saveState() {
1085
- undoStack.push(JSON.stringify(timelineClips));
1086
- if (undoStack.length > 50) undoStack.shift();
1087
- }
1088
 
1089
- function undoAction() {
1090
- if (undoStack.length > 0) {
1091
- timelineClips = JSON.parse(undoStack.pop());
1092
- renderTimeline();
1093
- updateDuration();
1094
- debug('์‹คํ–‰์ทจ์†Œ');
1095
- }
1096
  }
1097
 
1098
- // ========================================
1099
- // ํŒŒ์ผ ์—…๋กœ๋“œ - ํ•ต์‹ฌ ์ˆ˜์ •!
1100
- // ========================================
1101
- function handleFileUpload(input) {
1102
- debug('ํŒŒ์ผ ์—…๋กœ๋“œ ์‹œ์ž‘!');
1103
-
1104
- var files = input.files;
1105
- if (!files || files.length === 0) {
1106
- debug('ํŒŒ์ผ ์—†์Œ');
1107
- return;
1108
- }
1109
-
1110
- debug('ํŒŒ์ผ ' + files.length + '๊ฐœ ์ฒ˜๋ฆฌ ์ค‘...');
1111
-
1112
- for (var i = 0; i < files.length; i++) {
1113
- processFile(files[i]);
1114
- }
1115
-
1116
- // ์ž…๋ ฅ ๋ฆฌ์…‹
1117
- input.value = '';
1118
- }
1119
-
1120
- function processFile(file) {
1121
- var type = null;
1122
- if (file.type.indexOf('video') === 0) {
1123
- type = 'video';
1124
- } else if (file.type.indexOf('image') === 0) {
1125
- type = 'image';
1126
- } else if (file.type.indexOf('audio') === 0) {
1127
- type = 'audio';
1128
- }
1129
-
1130
- if (!type) {
1131
- debug('์ง€์› ์•ˆ ํ•จ: ' + file.type);
1132
- return;
1133
- }
1134
-
1135
- var url = URL.createObjectURL(file);
1136
- debug('์ถ”๊ฐ€: ' + file.name + ' (' + type + ')');
1137
-
1138
- var media = {
1139
- id: generateId(),
1140
- name: file.name,
1141
- type: type,
1142
- url: url,
1143
- file: file,
1144
- duration: (type === 'image') ? 5 : 0,
1145
- thumbnail: (type === 'image') ? url : null
1146
- };
1147
-
1148
- mediaLibrary.push(media);
1149
- renderMediaLibrary();
1150
-
1151
- // ๋น„๋””์˜ค/์˜ค๋””์˜ค ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ๋กœ๋“œ
1152
- if (type === 'video' || type === 'audio') {
1153
- loadMetadata(media);
1154
- }
1155
- }
1156
 
1157
- function loadMetadata(media) {
1158
- var el = document.createElement(media.type);
1159
- el.src = media.url;
1160
- el.preload = 'metadata';
1161
-
1162
- el.onloadedmetadata = function() {
1163
- media.duration = el.duration;
1164
- debug('๊ธธ์ด: ' + formatTime(el.duration));
1165
- renderMediaLibrary();
1166
-
1167
- if (media.type === 'video') {
1168
- el.currentTime = Math.min(1, el.duration / 2);
1169
- }
1170
- };
1171
-
1172
- el.onseeked = function() {
1173
- if (media.type === 'video') {
1174
- try {
1175
- var canvas = document.createElement('canvas');
1176
- canvas.width = 160;
1177
- canvas.height = 90;
1178
- canvas.getContext('2d').drawImage(el, 0, 0, 160, 90);
1179
- media.thumbnail = canvas.toDataURL();
1180
- renderMediaLibrary();
1181
- } catch(e) {
1182
- debug('์ธ๋„ค์ผ ์‹คํŒจ');
1183
- }
1184
- }
1185
- };
1186
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1187
 
1188
- // ========================================
1189
- // ๋ฏธ๋””์–ด ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๋ Œ๋”๋ง
1190
- // ========================================
1191
- function renderMediaLibrary() {
1192
- var grid = document.getElementById('mediaGrid');
1193
- if (!grid) return;
1194
-
1195
- grid.innerHTML = '';
1196
-
1197
- var icons = {
1198
- video: '<svg viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21"/></svg>',
1199
- image: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>',
1200
- audio: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/></svg>'
1201
- };
1202
-
1203
- for (var i = 0; i < mediaLibrary.length; i++) {
1204
- var media = mediaLibrary[i];
1205
- var item = document.createElement('div');
1206
- item.className = 'media-item';
1207
- item.draggable = true;
1208
- item.setAttribute('data-media-id', media.id);
1209
-
1210
- // ํด๋กœ์ €๋กœ media ์บก์ฒ˜
1211
- (function(m) {
1212
- item.ondragstart = function(e) {
1213
- e.dataTransfer.setData('mediaId', m.id);
1214
- this.style.opacity = '0.5';
1215
- };
1216
- item.ondragend = function() {
1217
- this.style.opacity = '1';
1218
- };
1219
- item.ondblclick = function() {
1220
- addToTimeline(m);
1221
- };
1222
- })(media);
1223
-
1224
- var thumbHtml = '';
1225
- if (media.thumbnail) {
1226
- thumbHtml = '<img src="' + media.thumbnail + '" alt="' + media.name + '">';
1227
- } else {
1228
- thumbHtml = '<div style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;background:#e5e7eb;color:#9ca3af">' + icons[media.type] + '</div>';
1229
- }
1230
-
1231
- item.innerHTML = thumbHtml +
1232
- '<div class="media-item-type">' + icons[media.type] + '</div>' +
1233
- (media.duration > 0 ? '<div class="media-item-duration">' + formatTime(media.duration) + '</div>' : '') +
1234
- '<div class="media-item-overlay"><span class="media-item-name">' + media.name + '</span></div>';
1235
-
1236
- grid.appendChild(item);
1237
- }
1238
-
1239
- debug('๋ฏธ๋””์–ด ' + mediaLibrary.length + '๊ฐœ');
1240
- }
1241
 
1242
- // ========================================
1243
- // ํƒ€์ž„๋ผ์ธ ๊ด€๋ฆฌ
1244
- // ========================================
1245
- function addToTimeline(media, startTime) {
1246
- saveState();
1247
-
1248
- var track = (media.type === 'audio') ? 1 : 0;
1249
- var start = (startTime !== undefined) ? startTime : getTrackEndTime(track);
1250
-
1251
- var clip = {
1252
- id: generateId(),
1253
- mediaId: media.id,
1254
- name: media.name,
1255
- type: media.type,
1256
- url: media.url,
1257
- thumbnail: media.thumbnail,
1258
- track: track,
1259
- startTime: start,
1260
- duration: media.duration,
1261
- trimStart: 0,
1262
- trimEnd: media.duration,
1263
- volume: 1
1264
- };
1265
-
1266
- timelineClips.push(clip);
1267
- debug('ํด๋ฆฝ ์ถ”๊ฐ€: ' + clip.name);
1268
-
1269
- renderTimeline();
1270
- updateDuration();
1271
- }
1272
 
1273
- function getTrackEndTime(track) {
1274
- var maxEnd = 0;
1275
- for (var i = 0; i < timelineClips.length; i++) {
1276
- var c = timelineClips[i];
1277
- if (c.track === track) {
1278
- var end = c.startTime + (c.trimEnd - c.trimStart);
1279
- if (end > maxEnd) maxEnd = end;
1280
- }
1281
- }
1282
- return maxEnd;
1283
- }
1284
 
1285
- function renderTimeline() {
1286
- var tracks = document.querySelectorAll('.track-content');
1287
- for (var t = 0; t < tracks.length; t++) {
1288
- tracks[t].innerHTML = '';
1289
- }
1290
-
1291
- for (var i = 0; i < timelineClips.length; i++) {
1292
- var clip = timelineClips[i];
1293
- var trackEl = document.querySelector('.timeline-track[data-track="' + clip.track + '"] .track-content');
1294
- if (!trackEl) continue;
1295
-
1296
- var clipEl = document.createElement('div');
1297
- clipEl.className = 'timeline-clip ' + clip.type + (selectedClipId === clip.id ? ' selected' : '');
1298
- clipEl.setAttribute('data-clip-id', clip.id);
1299
-
1300
- var clipDuration = clip.trimEnd - clip.trimStart;
1301
- var left = 70 + (clip.startTime * pixelsPerSecond * zoom);
1302
- var width = Math.max(40, clipDuration * pixelsPerSecond * zoom);
1303
-
1304
- clipEl.style.left = left + 'px';
1305
- clipEl.style.width = width + 'px';
1306
- clipEl.draggable = true;
1307
-
1308
- // ํด๋กœ์ €
1309
- (function(c) {
1310
- clipEl.onclick = function(e) {
1311
- e.stopPropagation();
1312
- selectClip(c.id);
1313
- };
1314
- clipEl.oncontextmenu = function(e) {
1315
- e.preventDefault();
1316
- selectClip(c.id);
1317
- showContextMenu(e.clientX, e.clientY);
1318
- };
1319
- clipEl.ondragstart = function(e) {
1320
- e.dataTransfer.setData('clipId', c.id);
1321
- e.dataTransfer.setData('offsetX', e.offsetX.toString());
1322
- };
1323
- })(clip);
1324
-
1325
- var thumbHtml = clip.thumbnail ? '<img class="clip-thumbnail" src="' + clip.thumbnail + '">' : '';
1326
-
1327
- clipEl.innerHTML = thumbHtml +
1328
- '<div class="clip-info"><div class="clip-name">' + clip.name + '</div><div class="clip-duration">' + formatTime(clipDuration) + '</div></div>' +
1329
- '<div class="clip-handles clip-handle-left" onmousedown="startTrim(event, \'' + clip.id + '\', \'left\')"></div>' +
1330
- '<div class="clip-handles clip-handle-right" onmousedown="startTrim(event, \'' + clip.id + '\', \'right\')"></div>';
1331
-
1332
- trackEl.appendChild(clipEl);
1333
- }
1334
-
1335
- renderRuler();
1336
- }
1337
 
1338
- function renderRuler() {
1339
- var ruler = document.getElementById('timelineRuler');
1340
- if (!ruler) return;
1341
-
1342
- var width = Math.max(totalDuration * pixelsPerSecond * zoom + 300, 1000);
1343
- ruler.style.width = width + 'px';
1344
-
1345
- var html = '<svg width="100%" height="22" style="position:absolute;left:70px">';
1346
- var step = (zoom < 0.7) ? 5 : (zoom < 1.5) ? 2 : 1;
1347
-
1348
- for (var i = 0; i <= Math.ceil(totalDuration) + 10; i += step) {
1349
- var x = i * pixelsPerSecond * zoom;
1350
- html += '<line x1="' + x + '" y1="17" x2="' + x + '" y2="22" stroke="#d1d5db" stroke-width="1"/>';
1351
- html += '<text x="' + x + '" y="12" fill="#9ca3af" font-size="9" text-anchor="middle">' + formatTime(i) + '</text>';
1352
- }
1353
-
1354
- html += '</svg>';
1355
- ruler.innerHTML = html;
1356
- }
1357
 
1358
- // ========================================
1359
- // ๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋กญ
1360
- // ========================================
1361
- function handleDragOver(event) {
1362
- event.preventDefault();
1363
- event.currentTarget.classList.add('drop-highlight');
1364
- }
1365
 
1366
- function handleDragLeave(event) {
1367
- event.currentTarget.classList.remove('drop-highlight');
1368
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1369
 
1370
- function handleDrop(event, track) {
1371
- event.preventDefault();
1372
- event.currentTarget.classList.remove('drop-highlight');
1373
 
1374
- var rect = event.currentTarget.getBoundingClientRect();
1375
- var x = event.clientX - rect.left;
1376
- var time = Math.max(0, x / (pixelsPerSecond * zoom));
1377
 
1378
- var mediaId = event.dataTransfer.getData('mediaId');
1379
- var clipId = event.dataTransfer.getData('clipId');
1380
- var offsetX = parseFloat(event.dataTransfer.getData('offsetX') || 0);
1381
 
1382
  if (mediaId) {
1383
- // ๋ฏธ๋””์–ด ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์—์„œ ๋“œ๋กญ
1384
- var media = null;
1385
- for (var i = 0; i < mediaLibrary.length; i++) {
1386
- if (mediaLibrary[i].id === mediaId) {
1387
- media = mediaLibrary[i];
1388
- break;
1389
- }
1390
- }
1391
  if (media) {
1392
- var targetTrack = (media.type === 'audio') ? 1 : track;
1393
  addToTimeline(media, time);
1394
- if (targetTrack !== track) {
1395
- timelineClips[timelineClips.length - 1].track = targetTrack;
1396
  renderTimeline();
1397
  }
1398
  }
1399
  } else if (clipId) {
1400
- // ํƒ€์ž„๋ผ์ธ ๋‚ด ์ด๋™
1401
  saveState();
1402
- for (var i = 0; i < timelineClips.length; i++) {
1403
- if (timelineClips[i].id === clipId) {
1404
- var clip = timelineClips[i];
1405
- clip.startTime = Math.max(0, time - (offsetX / (pixelsPerSecond * zoom)));
1406
- clip.track = (clip.type === 'audio') ? 1 : track;
1407
- break;
1408
- }
1409
  }
1410
- renderTimeline();
1411
- updateDuration();
1412
  }
1413
- }
1414
-
1415
- // ========================================
1416
- // ํด๋ฆฝ ์„ ํƒ ๋ฐ ์†์„ฑ
1417
- // ========================================
1418
- function selectClip(clipId) {
1419
- selectedClipId = clipId;
1420
- renderTimeline();
1421
- renderProperties();
1422
- }
1423
-
1424
- function renderProperties() {
1425
- var container = document.getElementById('propertiesContent');
1426
- if (!container) return;
1427
-
1428
- var clip = null;
1429
- for (var i = 0; i < timelineClips.length; i++) {
1430
- if (timelineClips[i].id === selectedClipId) {
1431
- clip = timelineClips[i];
1432
- break;
1433
- }
1434
- }
1435
-
1436
- if (!clip) {
1437
- container.innerHTML = '<div class="no-selection">ํด๋ฆฝ์„ ์„ ํƒํ•˜๋ฉด<br>์†์„ฑ์„ ํŽธ์ง‘ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค</div>';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1438
  return;
1439
  }
1440
-
1441
- var clipDuration = clip.trimEnd - clip.trimStart;
1442
-
1443
- var html = '<div class="property-group">' +
1444
- '<div class="property-label">์ด๋ฆ„</div>' +
1445
- '<input type="text" class="property-input" value="' + clip.name + '" onchange="updateClipProperty(\'name\', this.value)">' +
1446
- '</div>' +
1447
- '<div class="property-group">' +
1448
- '<div class="property-label">์‹œ์ž‘ ์‹œ๊ฐ„</div>' +
1449
- '<input type="number" class="property-input" value="' + clip.startTime.toFixed(2) + '" step="0.1" min="0" onchange="updateClipProperty(\'startTime\', parseFloat(this.value))">' +
1450
- '</div>' +
1451
- '<div class="property-group">' +
1452
- '<div class="property-row">' +
1453
- '<div><div class="property-label">ํŠธ๋ฆผ ์‹œ์ž‘</div><input type="number" class="property-input" value="' + clip.trimStart.toFixed(2) + '" step="0.1" min="0" max="' + clip.duration + '" onchange="updateClipProperty(\'trimStart\', parseFloat(this.value))"></div>' +
1454
- '<div><div class="property-label">ํŠธ๋ฆผ ๋</div><input type="number" class="property-input" value="' + clip.trimEnd.toFixed(2) + '" step="0.1" min="0" max="' + clip.duration + '" onchange="updateClipProperty(\'trimEnd\', parseFloat(this.value))"></div>' +
1455
- '</div></div>' +
1456
- '<div class="property-group"><div class="property-label">๊ธธ์ด</div><div style="padding:6px 0;font-size:12px;color:#374151">' + formatTime(clipDuration) + '</div></div>';
1457
-
1458
- if (clip.type !== 'image') {
1459
- html += '<div class="property-group">' +
1460
- '<div class="property-label">๋ณผ๋ฅจ (' + Math.round(clip.volume * 100) + '%)</div>' +
1461
- '<input type="range" class="property-input" style="padding:0" min="0" max="1" step="0.05" value="' + clip.volume + '" oninput="updateClipProperty(\'volume\', parseFloat(this.value))">' +
1462
- '</div>';
1463
- }
1464
-
1465
- container.innerHTML = html;
1466
  }
1467
 
1468
- function updateClipProperty(prop, value) {
1469
- saveState();
1470
- for (var i = 0; i < timelineClips.length; i++) {
1471
- if (timelineClips[i].id === selectedClipId) {
1472
- timelineClips[i][prop] = value;
1473
- break;
1474
- }
1475
- }
1476
- renderTimeline();
1477
- updateDuration();
1478
- renderProperties();
1479
- }
1480
-
1481
- // ========================================
1482
- // ํด๋ฆฝ ํŽธ์ง‘
1483
- // ========================================
1484
- function splitSelectedClip() {
1485
- if (!selectedClipId) return;
1486
-
1487
- var clip = null;
1488
- var clipIndex = -1;
1489
- for (var i = 0; i < timelineClips.length; i++) {
1490
- if (timelineClips[i].id === selectedClipId) {
1491
- clip = timelineClips[i];
1492
- clipIndex = i;
1493
- break;
1494
- }
1495
- }
1496
- if (!clip) return;
1497
-
1498
- saveState();
1499
-
1500
- var clipDuration = clip.trimEnd - clip.trimStart;
1501
- var splitPoint = clipDuration / 2;
1502
-
1503
- var clip1 = JSON.parse(JSON.stringify(clip));
1504
- clip1.trimEnd = clip.trimStart + splitPoint;
1505
-
1506
- var clip2 = JSON.parse(JSON.stringify(clip));
1507
- clip2.id = generateId();
1508
- clip2.startTime = clip.startTime + splitPoint;
1509
- clip2.trimStart = clip.trimStart + splitPoint;
1510
-
1511
- timelineClips[clipIndex] = clip1;
1512
- timelineClips.push(clip2);
1513
-
1514
- renderTimeline();
1515
- hideContextMenu();
1516
- debug('ํด๋ฆฝ ๋ถ„ํ• ');
1517
- }
1518
-
1519
- function duplicateSelectedClip() {
1520
- if (!selectedClipId) return;
1521
-
1522
- var clip = null;
1523
- for (var i = 0; i < timelineClips.length; i++) {
1524
- if (timelineClips[i].id === selectedClipId) {
1525
- clip = timelineClips[i];
1526
- break;
1527
- }
1528
- }
1529
- if (!clip) return;
1530
-
1531
- saveState();
1532
-
1533
- var clipDuration = clip.trimEnd - clip.trimStart;
1534
- var newClip = JSON.parse(JSON.stringify(clip));
1535
- newClip.id = generateId();
1536
- newClip.startTime = clip.startTime + clipDuration;
1537
-
1538
- timelineClips.push(newClip);
1539
- renderTimeline();
1540
- updateDuration();
1541
- hideContextMenu();
1542
- debug('ํด๋ฆฝ ๋ณต์ œ');
1543
- }
1544
-
1545
- function deleteSelectedClip() {
1546
- if (!selectedClipId) return;
1547
-
1548
- saveState();
1549
-
1550
- var newClips = [];
1551
- for (var i = 0; i < timelineClips.length; i++) {
1552
- if (timelineClips[i].id !== selectedClipId) {
1553
- newClips.push(timelineClips[i]);
1554
- }
1555
- }
1556
- timelineClips = newClips;
1557
- selectedClipId = null;
1558
-
1559
- renderTimeline();
1560
- renderProperties();
1561
- updateDuration();
1562
- hideContextMenu();
1563
- debug('ํด๋ฆฝ ์‚ญ์ œ');
1564
- }
1565
-
1566
- // ========================================
1567
- // ํŠธ๋ฆผ ํ•ธ๋“ค
1568
- // ========================================
1569
- function startTrim(event, clipId, side) {
1570
- event.stopPropagation();
1571
- event.preventDefault();
1572
-
1573
- var clip = null;
1574
- for (var i = 0; i < timelineClips.length; i++) {
1575
- if (timelineClips[i].id === clipId) {
1576
- clip = timelineClips[i];
1577
- break;
1578
- }
1579
- }
1580
- if (!clip) return;
1581
-
1582
- saveState();
1583
-
1584
- trimData = {
1585
- clipId: clipId,
1586
- side: side,
1587
- startX: event.clientX,
1588
- originalTrimStart: clip.trimStart,
1589
- originalTrimEnd: clip.trimEnd,
1590
- originalStartTime: clip.startTime
1591
- };
1592
-
1593
- document.addEventListener('mousemove', handleTrim);
1594
- document.addEventListener('mouseup', endTrim);
1595
- }
1596
-
1597
- function handleTrim(event) {
1598
- if (!trimData) return;
1599
-
1600
- var clip = null;
1601
- for (var i = 0; i < timelineClips.length; i++) {
1602
- if (timelineClips[i].id === trimData.clipId) {
1603
- clip = timelineClips[i];
1604
- break;
1605
- }
1606
- }
1607
- if (!clip) return;
1608
-
1609
- var deltaX = event.clientX - trimData.startX;
1610
- var deltaTime = deltaX / (pixelsPerSecond * zoom);
1611
-
1612
- if (trimData.side === 'left') {
1613
- var newTrimStart = Math.max(0, Math.min(clip.trimEnd - 0.1, trimData.originalTrimStart + deltaTime));
1614
- var trimDelta = newTrimStart - trimData.originalTrimStart;
1615
- clip.trimStart = newTrimStart;
1616
- clip.startTime = trimData.originalStartTime + trimDelta;
1617
- } else {
1618
- clip.trimEnd = Math.max(clip.trimStart + 0.1, Math.min(clip.duration, trimData.originalTrimEnd + deltaTime));
1619
- }
1620
-
1621
- renderTimeline();
1622
- updateDuration();
1623
- }
1624
-
1625
- function endTrim() {
1626
- trimData = null;
1627
- document.removeEventListener('mousemove', handleTrim);
1628
- document.removeEventListener('mouseup', endTrim);
1629
- }
1630
-
1631
- // ========================================
1632
- // ์žฌ์ƒ ์ปจํŠธ๋กค
1633
- // ========================================
1634
- function updateDuration() {
1635
- var maxEnd = 0;
1636
- for (var i = 0; i < timelineClips.length; i++) {
1637
- var c = timelineClips[i];
1638
- var end = c.startTime + (c.trimEnd - c.trimStart);
1639
- if (end > maxEnd) maxEnd = end;
1640
- }
1641
- totalDuration = maxEnd;
1642
- document.getElementById('durationDisplay').textContent = formatTime(totalDuration);
1643
- }
1644
-
1645
- function togglePlay() {
1646
- isPlaying = !isPlaying;
1647
- var icon = document.getElementById('playIcon');
1648
-
1649
- if (isPlaying) {
1650
- icon.innerHTML = '<rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/>';
1651
- startPlayback();
1652
- } else {
1653
- icon.innerHTML = '<polygon points="5 3 19 12 5 21 5 3"/>';
1654
- stopPlayback();
1655
- }
1656
- }
1657
-
1658
- function startPlayback() {
1659
- var lastTime = performance.now();
1660
-
1661
- function animate(now) {
1662
- if (!isPlaying) return;
1663
-
1664
- var delta = (now - lastTime) / 1000;
1665
- lastTime = now;
1666
-
1667
- currentTime += delta;
1668
-
1669
- if (currentTime >= totalDuration) {
1670
- currentTime = 0;
1671
- if (totalDuration === 0) {
1672
- isPlaying = false;
1673
- document.getElementById('playIcon').innerHTML = '<polygon points="5 3 19 12 5 21 5 3"/>';
1674
- return;
1675
- }
1676
- }
1677
-
1678
- updatePlayhead();
1679
- updatePreview();
1680
-
1681
- animationId = requestAnimationFrame(animate);
1682
- }
1683
-
1684
- animationId = requestAnimationFrame(animate);
1685
- }
1686
-
1687
- function stopPlayback() {
1688
- if (animationId) {
1689
- cancelAnimationFrame(animationId);
1690
- animationId = null;
1691
- }
1692
- var video = document.querySelector('#previewContainer video');
1693
- if (video && !video.paused) video.pause();
1694
- }
1695
-
1696
- function updatePlayhead() {
1697
- var playhead = document.getElementById('playhead');
1698
- if (playhead) {
1699
- playhead.style.left = (70 + currentTime * pixelsPerSecond * zoom) + 'px';
1700
- }
1701
- document.getElementById('currentTimeDisplay').textContent = formatTime(currentTime);
1702
- }
1703
 
1704
- function updatePreview() {
1705
- var container = document.getElementById('previewContainer');
1706
- if (!container) return;
1707
-
1708
- var currentClips = [];
1709
- for (var i = 0; i < timelineClips.length; i++) {
1710
- var c = timelineClips[i];
1711
- var clipEnd = c.startTime + (c.trimEnd - c.trimStart);
1712
- if (currentTime >= c.startTime && currentTime < clipEnd) {
1713
- currentClips.push(c);
1714
- }
1715
  }
1716
-
1717
- var visualClip = null;
1718
- for (var i = 0; i < currentClips.length; i++) {
1719
- if (currentClips[i].type === 'video' || currentClips[i].type === 'image') {
1720
- visualClip = currentClips[i];
1721
- break;
1722
- }
1723
  }
1724
 
1725
- if (visualClip) {
1726
- var clipTime = currentTime - visualClip.startTime + visualClip.trimStart;
1727
-
1728
- if (visualClip.type === 'image') {
1729
- if (!container.querySelector('img[data-clip-id="' + visualClip.id + '"]')) {
1730
- container.innerHTML = '<img src="' + visualClip.url + '" data-clip-id="' + visualClip.id + '">';
1731
- }
1732
- } else if (visualClip.type === 'video') {
1733
- var video = container.querySelector('video[data-clip-id="' + visualClip.id + '"]');
1734
- if (!video) {
1735
- container.innerHTML = '<video src="' + visualClip.url + '" data-clip-id="' + visualClip.id + '"' + (isMuted ? ' muted' : '') + '></video>';
1736
- video = container.querySelector('video');
1737
- }
1738
-
1739
- if (Math.abs(video.currentTime - clipTime) > 0.2) {
1740
- video.currentTime = clipTime;
1741
- }
1742
-
1743
- if (isPlaying && video.paused) {
1744
- video.play().catch(function(){});
1745
- } else if (!isPlaying && !video.paused) {
1746
- video.pause();
1747
- }
1748
-
1749
- video.volume = isMuted ? 0 : visualClip.volume;
1750
- video.muted = isMuted;
1751
- }
1752
- } else {
1753
- var hasAudio = false;
1754
- for (var i = 0; i < currentClips.length; i++) {
1755
- if (currentClips[i].type === 'audio') hasAudio = true;
1756
- }
1757
-
1758
- if (!container.querySelector('.preview-placeholder')) {
1759
- container.innerHTML = '<div class="preview-placeholder">' +
1760
- '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1"><rect x="2" y="2" width="20" height="20" rx="2"/><polygon points="10 8 16 12 10 16 10 8"/></svg>' +
1761
- '<p>' + (hasAudio ? '๐ŸŽต ์˜ค๋””์˜ค ์žฌ์ƒ ์ค‘' : 'ํƒ€์ž„๋ผ์ธ์— ๋ฏธ๋””์–ด๋ฅผ ์ถ”๊ฐ€ํ•˜์„ธ์š”') + '</p></div>';
1762
- }
1763
  }
1764
- }
1765
-
1766
- function skipToStart() { currentTime = 0; updatePlayhead(); updatePreview(); }
1767
- function skipToEnd() { currentTime = totalDuration; updatePlayhead(); updatePreview(); }
1768
- function skipBackward() { currentTime = Math.max(0, currentTime - 5); updatePlayhead(); updatePreview(); }
1769
- function skipForward() { currentTime = Math.min(totalDuration, currentTime + 5); updatePlayhead(); updatePreview(); }
1770
-
1771
- function toggleMute() {
1772
- isMuted = !isMuted;
1773
- var icon = document.getElementById('volumeIcon');
1774
- if (isMuted) {
1775
- icon.innerHTML = '<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/><line x1="23" y1="9" x2="17" y2="15"/><line x1="17" y1="9" x2="23" y2="15"/>';
1776
- } else {
1777
- icon.innerHTML = '<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/><path d="M19.07 4.93a10 10 0 0 1 0 14.14M15.54 8.46a5 5 0 0 1 0 7.07"/>';
1778
- }
1779
- var video = document.querySelector('#previewContainer video');
1780
- if (video) video.muted = isMuted;
1781
- }
1782
-
1783
- function setZoom(value) {
1784
- zoom = parseFloat(value);
1785
- renderTimeline();
1786
- updatePlayhead();
1787
- }
1788
-
1789
- // ========================================
1790
- // ์ปจํ…์ŠคํŠธ ๋ฉ”๋‰ด
1791
- // ========================================
1792
- function showContextMenu(x, y) {
1793
- var menu = document.getElementById('contextMenu');
1794
- menu.style.display = 'block';
1795
- menu.style.left = x + 'px';
1796
- menu.style.top = y + 'px';
1797
- }
1798
-
1799
- function hideContextMenu() {
1800
- document.getElementById('contextMenu').style.display = 'none';
1801
- }
1802
-
1803
- // ========================================
1804
- // ํƒ€์ž„๋ผ์ธ ํด๋ฆญ
1805
- // ========================================
1806
- function onTimelineClick(e) {
1807
- if (e.target.closest('.timeline-clip')) return;
1808
 
1809
- var container = document.getElementById('timelineContainer');
1810
- var rect = container.getBoundingClientRect();
1811
- var x = e.clientX - rect.left - 70 + container.scrollLeft;
1812
- currentTime = Math.max(0, Math.min(totalDuration, x / (pixelsPerSecond * zoom)));
1813
- updatePlayhead();
1814
- updatePreview();
1815
- }
1816
-
1817
- // ========================================
1818
- // ๋‚ด๋ณด๋‚ด๊ธฐ
1819
- // ========================================
1820
- function exportVideo() {
1821
- if (timelineClips.length === 0) {
1822
- alert('ํƒ€์ž„๋ผ์ธ์— ํด๋ฆฝ์„ ์ถ”๊ฐ€ํ•ด์ฃผ์„ธ์š”.');
1823
- return;
1824
  }
1825
 
1826
- document.getElementById('exportModal').style.display = 'flex';
1827
- document.getElementById('exportProgress').style.width = '0%';
1828
- document.getElementById('exportStatus').textContent = '์˜์ƒ์„ ์ฒ˜๋ฆฌํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค...';
1829
-
1830
- var progress = 0;
1831
- var interval = setInterval(function() {
1832
- progress += 2;
1833
- document.getElementById('exportProgress').style.width = progress + '%';
1834
-
1835
- if (progress === 30) document.getElementById('exportStatus').textContent = 'ํด๋ฆฝ ๋ณ‘ํ•ฉ ์ค‘...';
1836
- if (progress === 60) document.getElementById('exportStatus').textContent = '์˜ค๋””์˜ค ์ฒ˜๋ฆฌ ์ค‘...';
1837
- if (progress === 90) document.getElementById('exportStatus').textContent = '์ตœ์ข… ๋ Œ๋”๋ง ์ค‘...';
1838
-
1839
- if (progress >= 100) {
1840
- clearInterval(interval);
1841
- document.getElementById('exportStatus').textContent = 'โœ… ์™„๋ฃŒ!';
1842
- document.getElementById('cancelExportBtn').textContent = '๋‹ซ๊ธฐ';
1843
- }
1844
- }, 50);
1845
  }
1846
-
1847
- function cancelExport() {
1848
- document.getElementById('exportModal').style.display = 'none';
1849
- document.getElementById('cancelExportBtn').textContent = '์ทจ์†Œ';
 
 
1850
  }
1851
-
1852
- // ========================================
1853
- // ํ‚ค๋ณด๋“œ ๋‹จ์ถ•ํ‚ค
1854
- // ========================================
1855
- document.onkeydown = function(e) {
1856
- if (e.target.tagName === 'INPUT') return;
1857
-
1858
- if (e.code === 'Space') {
1859
- e.preventDefault();
1860
- togglePlay();
1861
- } else if (e.code === 'Delete' || e.code === 'Backspace') {
1862
- e.preventDefault();
1863
- if (selectedClipId) deleteSelectedClip();
1864
- } else if (e.code === 'KeyD' && (e.ctrlKey || e.metaKey)) {
1865
- e.preventDefault();
1866
- if (selectedClipId) duplicateSelectedClip();
1867
- } else if (e.code === 'KeyZ' && (e.ctrlKey || e.metaKey)) {
1868
- e.preventDefault();
1869
- undoAction();
1870
- } else if (e.code === 'ArrowLeft') {
1871
- e.preventDefault();
1872
- currentTime = Math.max(0, currentTime - (e.shiftKey ? 1 : 0.1));
1873
- updatePlayhead();
1874
- updatePreview();
1875
- } else if (e.code === 'ArrowRight') {
1876
- e.preventDefault();
1877
- currentTime = Math.min(totalDuration, currentTime + (e.shiftKey ? 1 : 0.1));
1878
- updatePlayhead();
1879
- updatePreview();
1880
- }
1881
- };
1882
-
1883
- // ํด๋ฆญ์œผ๋กœ ์ปจํ…์ŠคํŠธ ๋ฉ”๋‰ด ๋‹ซ๊ธฐ
1884
- document.onclick = function(e) {
1885
- if (!e.target.closest('.context-menu')) {
1886
- hideContextMenu();
1887
- }
1888
- };
1889
-
1890
- // ========================================
1891
- // ์ดˆ๊ธฐํ™” - ์ฆ‰์‹œ ์‹คํ–‰!
1892
- // ========================================
1893
- renderTimeline();
1894
- debug('์—๋””ํ„ฐ ์ค€๋น„ ์™„๋ฃŒ!');
1895
- </script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1896
  </body>
1897
  </html>
1898
  """
1899
 
1900
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1901
  def create_interface():
1902
  """Gradio ์ธํ„ฐํŽ˜์ด์Šค ์ƒ์„ฑ"""
 
1903
  with gr.Blocks(title="Simple Video Editor") as demo:
1904
- gr.HTML(EDITOR_HTML)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1905
  return demo
1906
 
1907
 
 
1
  """
2
  Simple Video Editor - ํ—ˆ๊น…ํŽ˜์ด์Šค ์ŠคํŽ˜์ด์Šค์šฉ
3
  CapCut/VEED ์Šคํƒ€์ผ ๊ฐ„๋‹จ ์˜์ƒ ํŽธ์ง‘๊ธฐ (ํฐ์ƒ‰ ํ…Œ๋งˆ)
4
+ Gradio ๋„ค์ดํ‹ฐ๋ธŒ ํŒŒ์ผ ์—…๋กœ๋“œ ์‚ฌ์šฉ
5
  """
6
 
7
  import gradio as gr
8
+ import base64
9
+ import os
10
 
11
  EDITOR_HTML = """
12
  <!DOCTYPE html>
 
23
  box-sizing: border-box;
24
  }
25
 
26
+ #editor-root {
27
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Noto Sans KR', sans-serif;
28
  background: #f5f5f7;
29
  color: #1d1d1f;
 
30
  font-size: 13px;
31
  }
32
 
 
34
  .editor-container {
35
  display: flex;
36
  flex-direction: column;
37
+ height: 750px;
38
+ background: #f5f5f7;
39
+ border-radius: 12px;
40
+ overflow: hidden;
41
+ border: 1px solid #e0e0e0;
42
  }
43
 
44
  /* ========== ์ƒ๋‹จ ํˆด๋ฐ” ========== */
 
167
  padding: 8px;
168
  }
169
 
170
+ .library-hint {
 
 
 
 
171
  text-align: center;
172
+ padding: 20px 10px;
 
 
 
 
 
 
 
 
 
 
 
 
 
173
  color: #9ca3af;
 
 
 
 
174
  font-size: 11px;
175
+ line-height: 1.5;
 
176
  }
177
 
178
  /* ========== ๋ฏธ๋””์–ด ๊ทธ๋ฆฌ๋“œ ========== */
 
263
  color: white;
264
  }
265
 
266
+ .media-item-icon {
267
+ width: 100%;
268
+ height: 100%;
269
+ display: flex;
270
+ align-items: center;
271
+ justify-content: center;
272
+ font-size: 24px;
273
+ color: #9ca3af;
274
+ background: #e5e7eb;
275
+ }
276
+
277
  /* ========== ํ”„๋ฆฌ๋ทฐ ์˜์—ญ ========== */
278
  .preview-area {
279
  flex: 1;
 
784
  margin: 4px 0;
785
  }
786
 
787
+ /* ========== ์ƒํƒœ๋ฐ” ========== */
788
+ .status-bar {
789
+ height: 24px;
790
+ background: #f0f0f0;
791
+ border-top: 1px solid #e0e0e0;
792
+ display: flex;
793
+ align-items: center;
794
+ padding: 0 12px;
 
 
 
 
 
795
  font-size: 11px;
796
+ color: #666;
 
 
797
  }
798
  </style>
799
  </head>
800
  <body>
801
+ <div id="editor-root">
802
  <div class="editor-container">
803
  <!-- ========== ์ƒ๋‹จ ํˆด๋ฐ” ========== -->
804
  <div class="toolbar">
 
812
  Simple Video Editor
813
  </div>
814
  <div class="toolbar-actions">
815
+ <button class="btn btn-secondary" onclick="editorUndo()" title="Ctrl+Z">
816
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
817
  <path d="M3 7v6h6M3 13a9 9 0 1 0 2.5-6.5"/>
818
  </svg>
819
  ์‹คํ–‰์ทจ์†Œ
820
  </button>
821
+ <button class="btn btn-success" onclick="editorExport()" id="exportBtn">
 
 
 
 
 
 
 
822
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
823
  <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3"/>
824
  </svg>
 
833
  <div class="media-library">
834
  <div class="library-header">๐Ÿ“ ๋ฏธ๋””์–ด</div>
835
  <div class="library-content">
836
+ <div class="library-hint" id="libraryHint">
837
+ โฌ†๏ธ ์œ„์˜ ํŒŒ์ผ ์—…๋กœ๋“œ๋ฅผ<br>์‚ฌ์šฉํ•˜์„ธ์š”
 
 
 
838
  </div>
839
  <div class="media-grid" id="mediaGrid"></div>
840
  </div>
 
853
  </div>
854
 
855
  <div class="playback-controls">
856
+ <button class="control-btn" onclick="editorSkipStart()" title="์ฒ˜์Œ์œผ๋กœ">
857
  <svg viewBox="0 0 24 24" fill="currentColor">
858
  <path d="M6 6h2v12H6zm3.5 6l8.5 6V6z"/>
859
  </svg>
860
  </button>
861
+ <button class="control-btn" onclick="editorSkipBack()" title="-5์ดˆ">
862
  <svg viewBox="0 0 24 24" fill="currentColor">
863
  <path d="M11 18V6l-8.5 6 8.5 6zm.5-6l8.5 6V6l-8.5 6z"/>
864
  </svg>
865
  </button>
866
+ <button class="control-btn play-btn" onclick="editorTogglePlay()" id="playBtn" title="Space">
867
  <svg viewBox="0 0 24 24" fill="currentColor" id="playIcon">
868
  <polygon points="5 3 19 12 5 21 5 3"/>
869
  </svg>
870
  </button>
871
+ <button class="control-btn" onclick="editorSkipForward()" title="+5์ดˆ">
872
  <svg viewBox="0 0 24 24" fill="currentColor">
873
  <path d="M4 18l8.5-6L4 6v12zm9-12v12l8.5-6L13 6z"/>
874
  </svg>
875
  </button>
876
+ <button class="control-btn" onclick="editorSkipEnd()" title="๋์œผ๋กœ">
877
  <svg viewBox="0 0 24 24" fill="currentColor">
878
  <path d="M6 18l8.5-6L6 6v12zM16 6v12h2V6h-2z"/>
879
  </svg>
 
881
  <div class="time-display">
882
  <span id="currentTimeDisplay">00:00.00</span> / <span id="durationDisplay">00:00.00</span>
883
  </div>
884
+ <button class="control-btn" onclick="editorToggleMute()" id="muteBtn" title="์Œ์†Œ๊ฑฐ">
885
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" id="volumeIcon">
886
  <polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/>
887
  <path d="M19.07 4.93a10 10 0 0 1 0 14.14M15.54 8.46a5 5 0 0 1 0 7.07"/>
 
904
  <!-- ========== ํƒ€์ž„๋ผ์ธ ========== -->
905
  <div class="timeline-area">
906
  <div class="timeline-toolbar">
907
+ <button class="btn btn-secondary" onclick="editorSplit()" title="์„ ํƒ ํด๋ฆฝ ์ž๋ฅด๊ธฐ">
908
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
909
  <circle cx="6" cy="6" r="3"/>
910
  <circle cx="6" cy="18" r="3"/>
 
914
  </svg>
915
  ์ž๋ฅด๊ธฐ
916
  </button>
917
+ <button class="btn btn-secondary" onclick="editorDuplicate()" title="Ctrl+D">
918
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
919
  <rect x="9" y="9" width="13" height="13" rx="2"/>
920
  <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
921
  </svg>
922
  ๋ณต์ œ
923
  </button>
924
+ <button class="btn btn-danger" onclick="editorDelete()" title="Delete">
925
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
926
  <polyline points="3 6 5 6 21 6"/>
927
  <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
 
934
  <line x1="21" y1="21" x2="16.65" y2="16.65"/>
935
  <line x1="8" y1="11" x2="14" y2="11"/>
936
  </svg>
937
+ <input type="range" min="0.5" max="3" step="0.1" value="1" oninput="editorSetZoom(this.value)" id="zoomSlider">
938
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
939
  <circle cx="11" cy="11" r="8"/>
940
  <line x1="21" y1="21" x2="16.65" y2="16.65"/>
 
943
  </svg>
944
  </div>
945
  </div>
946
+ <div class="timeline-container" id="timelineContainer" onclick="editorTimelineClick(event)">
947
  <div class="timeline-ruler" id="timelineRuler"></div>
948
  <div class="timeline-tracks" id="timelineTracks">
949
  <div class="timeline-track" data-track="0">
 
954
  </svg>
955
  ์˜์ƒ
956
  </div>
957
+ <div class="track-content" id="track0"></div>
958
  </div>
959
  <div class="timeline-track" data-track="1">
960
  <div class="track-label">
 
965
  </svg>
966
  ์˜ค๋””์˜ค
967
  </div>
968
+ <div class="track-content" id="track1"></div>
969
  </div>
970
  </div>
971
  <div class="playhead" id="playhead" style="left: 70px;"></div>
972
  </div>
973
  </div>
974
+
975
+ <div class="status-bar" id="statusBar">์ค€๋น„๋จ | ๋ฏธ๋””์–ด: 0๊ฐœ | ํด๋ฆฝ: 0๊ฐœ</div>
976
  </div>
977
 
978
  <!-- ========== ์ปจํ…์ŠคํŠธ ๋ฉ”๋‰ด ========== -->
979
  <div class="context-menu" id="contextMenu" style="display:none;">
980
+ <div class="context-menu-item" onclick="editorSplit()">
981
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
982
  <circle cx="6" cy="6" r="3"/>
983
  <circle cx="6" cy="18" r="3"/>
 
985
  </svg>
986
  ์ž๋ฅด๊ธฐ
987
  </div>
988
+ <div class="context-menu-item" onclick="editorDuplicate()">
989
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
990
  <rect x="9" y="9" width="13" height="13" rx="2"/>
991
  <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
 
993
  ๋ณต์ œ
994
  </div>
995
  <div class="context-menu-divider"></div>
996
+ <div class="context-menu-item danger" onclick="editorDelete()">
997
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
998
  <polyline points="3 6 5 6 21 6"/>
999
  <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/>
 
1010
  <div class="progress-bar">
1011
  <div class="progress-fill" id="exportProgress" style="width: 0%"></div>
1012
  </div>
1013
+ <button class="btn btn-secondary" onclick="editorCancelExport()" id="cancelExportBtn">์ทจ์†Œ</button>
1014
  </div>
1015
  </div>
1016
+ </div>
1017
+
1018
+ <script>
1019
+ // ========================================
1020
+ // ์ „์—ญ ์ƒํƒœ ๋ณ€์ˆ˜
1021
+ // ========================================
1022
+ window.editorState = {
1023
+ mediaLibrary: [],
1024
+ timelineClips: [],
1025
+ selectedClipId: null,
1026
+ isPlaying: false,
1027
+ isMuted: false,
1028
+ currentTime: 0,
1029
+ totalDuration: 0,
1030
+ zoom: 1,
1031
+ pixelsPerSecond: 80,
1032
+ undoStack: [],
1033
+ animationId: null,
1034
+ trimData: null
1035
+ };
1036
+
1037
+ // ========================================
1038
+ // ์œ ํ‹ธ๋ฆฌํ‹ฐ ํ•จ์ˆ˜
1039
+ // ========================================
1040
+ function generateId() {
1041
+ return Date.now().toString(36) + Math.random().toString(36).substr(2);
1042
+ }
1043
+
1044
+ function formatTime(seconds) {
1045
+ if (!seconds || isNaN(seconds)) seconds = 0;
1046
+ var mins = Math.floor(seconds / 60);
1047
+ var secs = Math.floor(seconds % 60);
1048
+ var ms = Math.floor((seconds % 1) * 100);
1049
+ return (mins < 10 ? '0' : '') + mins + ':' + (secs < 10 ? '0' : '') + secs + '.' + (ms < 10 ? '0' : '') + ms;
1050
+ }
1051
+
1052
+ function updateStatus(msg) {
1053
+ var el = document.getElementById('statusBar');
1054
+ if (el) {
1055
+ var state = window.editorState;
1056
+ el.textContent = msg + ' | ๋ฏธ๋””์–ด: ' + state.mediaLibrary.length + '๊ฐœ | ํด๋ฆฝ: ' + state.timelineClips.length + '๊ฐœ';
1057
+ }
1058
+ }
1059
 
1060
+ function saveState() {
1061
+ var state = window.editorState;
1062
+ state.undoStack.push(JSON.stringify(state.timelineClips));
1063
+ if (state.undoStack.length > 50) state.undoStack.shift();
1064
+ }
1065
+
1066
+ // ========================================
1067
+ // ์™ธ๋ถ€์—์„œ ํ˜ธ์ถœ๋˜๋Š” ๋ฏธ๋””์–ด ์ถ”๊ฐ€ ํ•จ์ˆ˜
1068
+ // ========================================
1069
+ window.addMediaToEditor = function(name, type, dataUrl) {
1070
+ console.log('Adding media:', name, type);
1071
+
1072
+ var state = window.editorState;
1073
+ var media = {
1074
+ id: generateId(),
1075
+ name: name,
1076
+ type: type,
1077
+ url: dataUrl,
1078
+ duration: (type === 'image') ? 5 : 0,
1079
+ thumbnail: (type === 'image') ? dataUrl : null
1080
+ };
1081
+
1082
+ state.mediaLibrary.push(media);
1083
+
1084
+ // ๋น„๋””์˜ค/์˜ค๋””์˜ค ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ๋กœ๋“œ
1085
+ if (type === 'video' || type === 'audio') {
1086
+ var el = document.createElement(type);
1087
+ el.src = dataUrl;
1088
+ el.preload = 'metadata';
1089
+
1090
+ el.onloadedmetadata = function() {
1091
+ media.duration = el.duration;
1092
+ renderMediaLibrary();
1093
+
1094
+ if (type === 'video') {
1095
+ el.currentTime = Math.min(1, el.duration / 2);
1096
  }
1097
+ };
1098
 
1099
+ el.onseeked = function() {
1100
+ if (type === 'video') {
1101
+ try {
1102
+ var canvas = document.createElement('canvas');
1103
+ canvas.width = 160;
1104
+ canvas.height = 90;
1105
+ canvas.getContext('2d').drawImage(el, 0, 0, 160, 90);
1106
+ media.thumbnail = canvas.toDataURL();
1107
+ renderMediaLibrary();
1108
+ } catch(e) {
1109
+ console.log('Thumbnail failed');
1110
+ }
1111
+ }
1112
+ };
1113
+ }
1114
+
1115
+ renderMediaLibrary();
1116
+ updateStatus('๋ฏธ๋””์–ด ์ถ”๊ฐ€๋จ: ' + name);
1117
+ };
1118
+
1119
+ // ========================================
1120
+ // ๋ฏธ๋””์–ด ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๋ Œ๋”๋ง
1121
+ // ========================================
1122
+ function renderMediaLibrary() {
1123
+ var state = window.editorState;
1124
+ var grid = document.getElementById('mediaGrid');
1125
+ var hint = document.getElementById('libraryHint');
1126
+
1127
+ if (!grid) return;
1128
+
1129
+ if (hint) {
1130
+ hint.style.display = state.mediaLibrary.length === 0 ? 'block' : 'none';
1131
+ }
1132
+
1133
+ grid.innerHTML = '';
1134
+
1135
+ var typeIcons = {
1136
+ video: '<svg viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21"/></svg>',
1137
+ image: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>',
1138
+ audio: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/></svg>'
1139
+ };
1140
+
1141
+ state.mediaLibrary.forEach(function(media) {
1142
+ var item = document.createElement('div');
1143
+ item.className = 'media-item';
1144
+ item.draggable = true;
1145
+ item.setAttribute('data-id', media.id);
1146
+
1147
+ item.ondblclick = function() {
1148
+ addToTimeline(media);
1149
+ };
1150
 
1151
+ item.ondragstart = function(e) {
1152
+ e.dataTransfer.setData('mediaId', media.id);
1153
+ this.style.opacity = '0.5';
1154
+ };
 
 
1155
 
1156
+ item.ondragend = function() {
1157
+ this.style.opacity = '1';
1158
+ };
 
1159
 
1160
+ var thumbHtml = '';
1161
+ if (media.thumbnail) {
1162
+ thumbHtml = '<img src="' + media.thumbnail + '" alt="' + media.name + '">';
1163
+ } else {
1164
+ thumbHtml = '<div class="media-item-icon">' + (media.type === 'video' ? '๐ŸŽฌ' : media.type === 'audio' ? '๐ŸŽต' : '๐Ÿ–ผ๏ธ') + '</div>';
 
 
1165
  }
1166
 
1167
+ item.innerHTML = thumbHtml +
1168
+ '<div class="media-item-type">' + typeIcons[media.type] + '</div>' +
1169
+ (media.duration > 0 ? '<div class="media-item-duration">' + formatTime(media.duration) + '</div>' : '') +
1170
+ '<div class="media-item-overlay"><span class="media-item-name">' + media.name + '</span></div>';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1171
 
1172
+ grid.appendChild(item);
1173
+ });
1174
+ }
1175
+
1176
+ // ========================================
1177
+ // ํƒ€์ž„๋ผ์ธ์— ์ถ”๊ฐ€
1178
+ // ========================================
1179
+ function addToTimeline(media, startTime) {
1180
+ var state = window.editorState;
1181
+ saveState();
1182
+
1183
+ var track = (media.type === 'audio') ? 1 : 0;
1184
+ var start = (startTime !== undefined) ? startTime : getTrackEndTime(track);
1185
+
1186
+ var clip = {
1187
+ id: generateId(),
1188
+ mediaId: media.id,
1189
+ name: media.name,
1190
+ type: media.type,
1191
+ url: media.url,
1192
+ thumbnail: media.thumbnail,
1193
+ track: track,
1194
+ startTime: start,
1195
+ duration: media.duration,
1196
+ trimStart: 0,
1197
+ trimEnd: media.duration,
1198
+ volume: 1
1199
+ };
1200
+
1201
+ state.timelineClips.push(clip);
1202
+ renderTimeline();
1203
+ updateDuration();
1204
+ updateStatus('ํด๋ฆฝ ์ถ”๊ฐ€๋จ: ' + clip.name);
1205
+ }
1206
+
1207
+ function getTrackEndTime(track) {
1208
+ var state = window.editorState;
1209
+ var maxEnd = 0;
1210
+ state.timelineClips.forEach(function(c) {
1211
+ if (c.track === track) {
1212
+ var end = c.startTime + (c.trimEnd - c.trimStart);
1213
+ if (end > maxEnd) maxEnd = end;
1214
+ }
1215
+ });
1216
+ return maxEnd;
1217
+ }
1218
+
1219
+ // ========================================
1220
+ // ํƒ€์ž„๋ผ์ธ ๋ Œ๋”๋ง
1221
+ // ========================================
1222
+ function renderTimeline() {
1223
+ var state = window.editorState;
1224
+
1225
+ var track0 = document.getElementById('track0');
1226
+ var track1 = document.getElementById('track1');
1227
+ if (track0) track0.innerHTML = '';
1228
+ if (track1) track1.innerHTML = '';
1229
+
1230
+ state.timelineClips.forEach(function(clip) {
1231
+ var trackEl = document.getElementById('track' + clip.track);
1232
+ if (!trackEl) return;
1233
 
1234
+ var clipEl = document.createElement('div');
1235
+ clipEl.className = 'timeline-clip ' + clip.type + (state.selectedClipId === clip.id ? ' selected' : '');
1236
+ clipEl.setAttribute('data-clip-id', clip.id);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1237
 
1238
+ var clipDuration = clip.trimEnd - clip.trimStart;
1239
+ var left = clip.startTime * state.pixelsPerSecond * state.zoom;
1240
+ var width = Math.max(40, clipDuration * state.pixelsPerSecond * state.zoom);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1241
 
1242
+ clipEl.style.left = left + 'px';
1243
+ clipEl.style.width = width + 'px';
1244
+ clipEl.draggable = true;
 
 
 
 
 
 
 
 
1245
 
1246
+ clipEl.onclick = function(e) {
1247
+ e.stopPropagation();
1248
+ selectClip(clip.id);
1249
+ };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1250
 
1251
+ clipEl.oncontextmenu = function(e) {
1252
+ e.preventDefault();
1253
+ selectClip(clip.id);
1254
+ showContextMenu(e.clientX, e.clientY);
1255
+ };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1256
 
1257
+ clipEl.ondragstart = function(e) {
1258
+ e.dataTransfer.setData('clipId', clip.id);
1259
+ e.dataTransfer.setData('offsetX', e.offsetX.toString());
1260
+ };
 
 
 
1261
 
1262
+ var thumbHtml = clip.thumbnail ? '<img class="clip-thumbnail" src="' + clip.thumbnail + '">' : '';
1263
+
1264
+ clipEl.innerHTML = thumbHtml +
1265
+ '<div class="clip-info"><div class="clip-name">' + clip.name + '</div><div class="clip-duration">' + formatTime(clipDuration) + '</div></div>' +
1266
+ '<div class="clip-handles clip-handle-left" onmousedown="startTrim(event, \'' + clip.id + '\', \'left\')"></div>' +
1267
+ '<div class="clip-handles clip-handle-right" onmousedown="startTrim(event, \'' + clip.id + '\', \'right\')"></div>';
1268
+
1269
+ trackEl.appendChild(clipEl);
1270
+ });
1271
+
1272
+ renderRuler();
1273
+ setupTrackDropZones();
1274
+ }
1275
+
1276
+ function renderRuler() {
1277
+ var state = window.editorState;
1278
+ var ruler = document.getElementById('timelineRuler');
1279
+ if (!ruler) return;
1280
+
1281
+ var width = Math.max(state.totalDuration * state.pixelsPerSecond * state.zoom + 300, 1000);
1282
+ ruler.style.width = width + 'px';
1283
+
1284
+ var html = '<svg width="100%" height="22" style="position:absolute;left:70px">';
1285
+ var step = state.zoom < 0.7 ? 5 : state.zoom < 1.5 ? 2 : 1;
1286
+
1287
+ for (var i = 0; i <= Math.ceil(state.totalDuration) + 10; i += step) {
1288
+ var x = i * state.pixelsPerSecond * state.zoom;
1289
+ html += '<line x1="' + x + '" y1="17" x2="' + x + '" y2="22" stroke="#d1d5db" stroke-width="1"/>';
1290
+ html += '<text x="' + x + '" y="12" fill="#9ca3af" font-size="9" text-anchor="middle">' + formatTime(i) + '</text>';
1291
+ }
1292
+
1293
+ html += '</svg>';
1294
+ ruler.innerHTML = html;
1295
+ }
1296
+
1297
+ function setupTrackDropZones() {
1298
+ var state = window.editorState;
1299
+
1300
+ ['track0', 'track1'].forEach(function(trackId, trackIdx) {
1301
+ var track = document.getElementById(trackId);
1302
+ if (!track) return;
1303
+
1304
+ track.ondragover = function(e) {
1305
+ e.preventDefault();
1306
+ track.classList.add('drop-highlight');
1307
+ };
1308
+
1309
+ track.ondragleave = function() {
1310
+ track.classList.remove('drop-highlight');
1311
+ };
1312
 
1313
+ track.ondrop = function(e) {
1314
+ e.preventDefault();
1315
+ track.classList.remove('drop-highlight');
1316
 
1317
+ var rect = track.getBoundingClientRect();
1318
+ var x = e.clientX - rect.left;
1319
+ var time = Math.max(0, x / (state.pixelsPerSecond * state.zoom));
1320
 
1321
+ var mediaId = e.dataTransfer.getData('mediaId');
1322
+ var clipId = e.dataTransfer.getData('clipId');
1323
+ var offsetX = parseFloat(e.dataTransfer.getData('offsetX') || 0);
1324
 
1325
  if (mediaId) {
1326
+ var media = state.mediaLibrary.find(function(m) { return m.id === mediaId; });
 
 
 
 
 
 
 
1327
  if (media) {
1328
+ var targetTrack = (media.type === 'audio') ? 1 : trackIdx;
1329
  addToTimeline(media, time);
1330
+ if (targetTrack !== trackIdx) {
1331
+ state.timelineClips[state.timelineClips.length - 1].track = targetTrack;
1332
  renderTimeline();
1333
  }
1334
  }
1335
  } else if (clipId) {
 
1336
  saveState();
1337
+ var clip = state.timelineClips.find(function(c) { return c.id === clipId; });
1338
+ if (clip) {
1339
+ clip.startTime = Math.max(0, time - offsetX / (state.pixelsPerSecond * state.zoom));
1340
+ clip.track = (clip.type === 'audio') ? 1 : trackIdx;
1341
+ renderTimeline();
1342
+ updateDuration();
 
1343
  }
 
 
1344
  }
1345
+ };
1346
+ });
1347
+ }
1348
+
1349
+ // ========================================
1350
+ // ํด๋ฆฝ ์„ ํƒ ๋ฐ ์†์„ฑ
1351
+ // ========================================
1352
+ function selectClip(clipId) {
1353
+ var state = window.editorState;
1354
+ state.selectedClipId = clipId;
1355
+ renderTimeline();
1356
+ renderProperties();
1357
+ }
1358
+
1359
+ function renderProperties() {
1360
+ var state = window.editorState;
1361
+ var container = document.getElementById('propertiesContent');
1362
+ if (!container) return;
1363
+
1364
+ var clip = state.timelineClips.find(function(c) { return c.id === state.selectedClipId; });
1365
+
1366
+ if (!clip) {
1367
+ container.innerHTML = '<div class="no-selection">ํด๋ฆฝ์„ ์„ ํƒํ•˜๋ฉด<br>์†์„ฑ์„ ํŽธ์ง‘ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค</div>';
1368
+ return;
1369
+ }
1370
+
1371
+ var clipDuration = clip.trimEnd - clip.trimStart;
1372
+
1373
+ var html = '<div class="property-group">' +
1374
+ '<div class="property-label">์ด๋ฆ„</div>' +
1375
+ '<input type="text" class="property-input" value="' + clip.name + '" onchange="updateClipProp(\'name\', this.value)">' +
1376
+ '</div>' +
1377
+ '<div class="property-group">' +
1378
+ '<div class="property-label">์‹œ์ž‘ ์‹œ๊ฐ„</div>' +
1379
+ '<input type="number" class="property-input" value="' + clip.startTime.toFixed(2) + '" step="0.1" min="0" onchange="updateClipProp(\'startTime\', parseFloat(this.value))">' +
1380
+ '</div>' +
1381
+ '<div class="property-group">' +
1382
+ '<div class="property-row">' +
1383
+ '<div><div class="property-label">ํŠธ๋ฆผ ์‹œ์ž‘</div><input type="number" class="property-input" value="' + clip.trimStart.toFixed(2) + '" step="0.1" min="0" max="' + clip.duration + '" onchange="updateClipProp(\'trimStart\', parseFloat(this.value))"></div>' +
1384
+ '<div><div class="property-label">ํŠธ๋ฆผ ๋</div><input type="number" class="property-input" value="' + clip.trimEnd.toFixed(2) + '" step="0.1" min="0" max="' + clip.duration + '" onchange="updateClipProp(\'trimEnd\', parseFloat(this.value))"></div>' +
1385
+ '</div></div>' +
1386
+ '<div class="property-group"><div class="property-label">๊ธธ์ด</div><div style="padding:6px 0;font-size:12px;color:#374151">' + formatTime(clipDuration) + '</div></div>';
1387
+
1388
+ if (clip.type !== 'image') {
1389
+ html += '<div class="property-group">' +
1390
+ '<div class="property-label">๋ณผ๋ฅจ (' + Math.round(clip.volume * 100) + '%)</div>' +
1391
+ '<input type="range" class="property-input" style="padding:0" min="0" max="1" step="0.05" value="' + clip.volume + '" oninput="updateClipProp(\'volume\', parseFloat(this.value))">' +
1392
+ '</div>';
1393
+ }
1394
+
1395
+ container.innerHTML = html;
1396
+ }
1397
+
1398
+ function updateClipProp(prop, value) {
1399
+ var state = window.editorState;
1400
+ saveState();
1401
+ var clip = state.timelineClips.find(function(c) { return c.id === state.selectedClipId; });
1402
+ if (clip) {
1403
+ clip[prop] = value;
1404
+ renderTimeline();
1405
+ updateDuration();
1406
+ renderProperties();
1407
+ }
1408
+ }
1409
+
1410
+ // ========================================
1411
+ // ํŠธ๋ฆผ ํ•ธ๋“ค
1412
+ // ========================================
1413
+ window.startTrim = function(event, clipId, side) {
1414
+ event.stopPropagation();
1415
+ event.preventDefault();
1416
+
1417
+ var state = window.editorState;
1418
+ var clip = state.timelineClips.find(function(c) { return c.id === clipId; });
1419
+ if (!clip) return;
1420
+
1421
+ saveState();
1422
+
1423
+ state.trimData = {
1424
+ clipId: clipId,
1425
+ side: side,
1426
+ startX: event.clientX,
1427
+ originalTrimStart: clip.trimStart,
1428
+ originalTrimEnd: clip.trimEnd,
1429
+ originalStartTime: clip.startTime
1430
+ };
1431
+
1432
+ document.addEventListener('mousemove', handleTrim);
1433
+ document.addEventListener('mouseup', endTrim);
1434
+ };
1435
+
1436
+ function handleTrim(event) {
1437
+ var state = window.editorState;
1438
+ if (!state.trimData) return;
1439
+
1440
+ var clip = state.timelineClips.find(function(c) { return c.id === state.trimData.clipId; });
1441
+ if (!clip) return;
1442
+
1443
+ var deltaX = event.clientX - state.trimData.startX;
1444
+ var deltaTime = deltaX / (state.pixelsPerSecond * state.zoom);
1445
+
1446
+ if (state.trimData.side === 'left') {
1447
+ var newTrimStart = Math.max(0, Math.min(clip.trimEnd - 0.1, state.trimData.originalTrimStart + deltaTime));
1448
+ var trimDelta = newTrimStart - state.trimData.originalTrimStart;
1449
+ clip.trimStart = newTrimStart;
1450
+ clip.startTime = state.trimData.originalStartTime + trimDelta;
1451
+ } else {
1452
+ clip.trimEnd = Math.max(clip.trimStart + 0.1, Math.min(clip.duration, state.trimData.originalTrimEnd + deltaTime));
1453
+ }
1454
+
1455
+ renderTimeline();
1456
+ updateDuration();
1457
+ }
1458
+
1459
+ function endTrim() {
1460
+ var state = window.editorState;
1461
+ state.trimData = null;
1462
+ document.removeEventListener('mousemove', handleTrim);
1463
+ document.removeEventListener('mouseup', endTrim);
1464
+ }
1465
+
1466
+ // ========================================
1467
+ // ํŽธ์ง‘ ๊ธฐ๋Šฅ
1468
+ // ========================================
1469
+ window.editorSplit = function() {
1470
+ var state = window.editorState;
1471
+ if (!state.selectedClipId) return;
1472
+
1473
+ var idx = -1;
1474
+ var clip = null;
1475
+ for (var i = 0; i < state.timelineClips.length; i++) {
1476
+ if (state.timelineClips[i].id === state.selectedClipId) {
1477
+ idx = i;
1478
+ clip = state.timelineClips[i];
1479
+ break;
1480
+ }
1481
+ }
1482
+ if (!clip) return;
1483
+
1484
+ saveState();
1485
+
1486
+ var clipDuration = clip.trimEnd - clip.trimStart;
1487
+ var splitPoint = clipDuration / 2;
1488
+
1489
+ var clip2 = JSON.parse(JSON.stringify(clip));
1490
+ clip2.id = generateId();
1491
+ clip2.startTime = clip.startTime + splitPoint;
1492
+ clip2.trimStart = clip.trimStart + splitPoint;
1493
+
1494
+ clip.trimEnd = clip.trimStart + splitPoint;
1495
+
1496
+ state.timelineClips.push(clip2);
1497
+ renderTimeline();
1498
+ hideContextMenu();
1499
+ updateStatus('ํด๋ฆฝ ๋ถ„ํ• ๋จ');
1500
+ };
1501
+
1502
+ window.editorDuplicate = function() {
1503
+ var state = window.editorState;
1504
+ if (!state.selectedClipId) return;
1505
+
1506
+ var clip = state.timelineClips.find(function(c) { return c.id === state.selectedClipId; });
1507
+ if (!clip) return;
1508
+
1509
+ saveState();
1510
+
1511
+ var clipDuration = clip.trimEnd - clip.trimStart;
1512
+ var newClip = JSON.parse(JSON.stringify(clip));
1513
+ newClip.id = generateId();
1514
+ newClip.startTime = clip.startTime + clipDuration;
1515
+
1516
+ state.timelineClips.push(newClip);
1517
+ renderTimeline();
1518
+ updateDuration();
1519
+ hideContextMenu();
1520
+ updateStatus('ํด๋ฆฝ ๋ณต์ œ๋จ');
1521
+ };
1522
+
1523
+ window.editorDelete = function() {
1524
+ var state = window.editorState;
1525
+ if (!state.selectedClipId) return;
1526
+
1527
+ saveState();
1528
+ state.timelineClips = state.timelineClips.filter(function(c) { return c.id !== state.selectedClipId; });
1529
+ state.selectedClipId = null;
1530
+
1531
+ renderTimeline();
1532
+ renderProperties();
1533
+ updateDuration();
1534
+ hideContextMenu();
1535
+ updateStatus('ํด๋ฆฝ ์‚ญ์ œ๋จ');
1536
+ };
1537
+
1538
+ window.editorUndo = function() {
1539
+ var state = window.editorState;
1540
+ if (state.undoStack.length > 0) {
1541
+ state.timelineClips = JSON.parse(state.undoStack.pop());
1542
+ renderTimeline();
1543
+ updateDuration();
1544
+ updateStatus('์‹คํ–‰์ทจ์†Œ');
1545
+ }
1546
+ };
1547
+
1548
+ // ========================================
1549
+ // ์žฌ์ƒ ์ปจํŠธ๋กค
1550
+ // ========================================
1551
+ function updateDuration() {
1552
+ var state = window.editorState;
1553
+ var maxEnd = 0;
1554
+ state.timelineClips.forEach(function(c) {
1555
+ var end = c.startTime + (c.trimEnd - c.trimStart);
1556
+ if (end > maxEnd) maxEnd = end;
1557
+ });
1558
+ state.totalDuration = maxEnd;
1559
+ document.getElementById('durationDisplay').textContent = formatTime(maxEnd);
1560
+ }
1561
+
1562
+ window.editorTogglePlay = function() {
1563
+ var state = window.editorState;
1564
+ state.isPlaying = !state.isPlaying;
1565
+ var icon = document.getElementById('playIcon');
1566
+
1567
+ if (state.isPlaying) {
1568
+ icon.innerHTML = '<rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/>';
1569
+ startPlayback();
1570
+ } else {
1571
+ icon.innerHTML = '<polygon points="5 3 19 12 5 21 5 3"/>';
1572
+ stopPlayback();
1573
+ }
1574
+ };
1575
+
1576
+ function startPlayback() {
1577
+ var state = window.editorState;
1578
+ var lastTime = performance.now();
1579
+
1580
+ function animate(now) {
1581
+ if (!state.isPlaying) return;
1582
+
1583
+ var delta = (now - lastTime) / 1000;
1584
+ lastTime = now;
1585
+ state.currentTime += delta;
1586
+
1587
+ if (state.currentTime >= state.totalDuration) {
1588
+ state.currentTime = 0;
1589
+ if (state.totalDuration === 0) {
1590
+ state.isPlaying = false;
1591
+ document.getElementById('playIcon').innerHTML = '<polygon points="5 3 19 12 5 21 5 3"/>';
1592
  return;
1593
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1594
  }
1595
 
1596
+ updatePlayhead();
1597
+ updatePreview();
1598
+ state.animationId = requestAnimationFrame(animate);
1599
+ }
1600
+
1601
+ state.animationId = requestAnimationFrame(animate);
1602
+ }
1603
+
1604
+ function stopPlayback() {
1605
+ var state = window.editorState;
1606
+ if (state.animationId) {
1607
+ cancelAnimationFrame(state.animationId);
1608
+ state.animationId = null;
1609
+ }
1610
+ var video = document.querySelector('#previewContainer video');
1611
+ if (video && !video.paused) video.pause();
1612
+ }
1613
+
1614
+ function updatePlayhead() {
1615
+ var state = window.editorState;
1616
+ var playhead = document.getElementById('playhead');
1617
+ if (playhead) {
1618
+ playhead.style.left = (70 + state.currentTime * state.pixelsPerSecond * state.zoom) + 'px';
1619
+ }
1620
+ document.getElementById('currentTimeDisplay').textContent = formatTime(state.currentTime);
1621
+ }
1622
+
1623
+ function updatePreview() {
1624
+ var state = window.editorState;
1625
+ var container = document.getElementById('previewContainer');
1626
+ if (!container) return;
1627
+
1628
+ var currentClips = state.timelineClips.filter(function(c) {
1629
+ var clipEnd = c.startTime + (c.trimEnd - c.trimStart);
1630
+ return state.currentTime >= c.startTime && state.currentTime < clipEnd;
1631
+ });
1632
+
1633
+ var visualClip = currentClips.find(function(c) { return c.type === 'video' || c.type === 'image'; });
1634
+
1635
+ if (visualClip) {
1636
+ var clipTime = state.currentTime - visualClip.startTime + visualClip.trimStart;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1637
 
1638
+ if (visualClip.type === 'image') {
1639
+ if (!container.querySelector('img[data-clip-id="' + visualClip.id + '"]')) {
1640
+ container.innerHTML = '<img src="' + visualClip.url + '" data-clip-id="' + visualClip.id + '">';
 
 
 
 
 
 
 
 
1641
  }
1642
+ } else if (visualClip.type === 'video') {
1643
+ var video = container.querySelector('video[data-clip-id="' + visualClip.id + '"]');
1644
+ if (!video) {
1645
+ container.innerHTML = '<video src="' + visualClip.url + '" data-clip-id="' + visualClip.id + '"' + (state.isMuted ? ' muted' : '') + '></video>';
1646
+ video = container.querySelector('video');
 
 
1647
  }
1648
 
1649
+ if (Math.abs(video.currentTime - clipTime) > 0.2) {
1650
+ video.currentTime = clipTime;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1651
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1652
 
1653
+ if (state.isPlaying && video.paused) {
1654
+ video.play().catch(function(){});
1655
+ } else if (!state.isPlaying && !video.paused) {
1656
+ video.pause();
 
 
 
 
 
 
 
 
 
 
 
1657
  }
1658
 
1659
+ video.volume = state.isMuted ? 0 : visualClip.volume;
1660
+ video.muted = state.isMuted;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1661
  }
1662
+ } else {
1663
+ var hasAudio = currentClips.some(function(c) { return c.type === 'audio'; });
1664
+ if (!container.querySelector('.preview-placeholder')) {
1665
+ container.innerHTML = '<div class="preview-placeholder">' +
1666
+ '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1"><rect x="2" y="2" width="20" height="20" rx="2"/><polygon points="10 8 16 12 10 16 10 8"/></svg>' +
1667
+ '<p>' + (hasAudio ? '๐ŸŽต ์˜ค๋””์˜ค ์žฌ์ƒ ์ค‘' : 'ํƒ€์ž„๋ผ์ธ์— ๋ฏธ๋””์–ด๋ฅผ ์ถ”๊ฐ€ํ•˜์„ธ์š”') + '</p></div>';
1668
  }
1669
+ }
1670
+ }
1671
+
1672
+ window.editorSkipStart = function() { window.editorState.currentTime = 0; updatePlayhead(); updatePreview(); };
1673
+ window.editorSkipEnd = function() { window.editorState.currentTime = window.editorState.totalDuration; updatePlayhead(); updatePreview(); };
1674
+ window.editorSkipBack = function() { window.editorState.currentTime = Math.max(0, window.editorState.currentTime - 5); updatePlayhead(); updatePreview(); };
1675
+ window.editorSkipForward = function() { window.editorState.currentTime = Math.min(window.editorState.totalDuration, window.editorState.currentTime + 5); updatePlayhead(); updatePreview(); };
1676
+
1677
+ window.editorToggleMute = function() {
1678
+ var state = window.editorState;
1679
+ state.isMuted = !state.isMuted;
1680
+ var icon = document.getElementById('volumeIcon');
1681
+
1682
+ if (state.isMuted) {
1683
+ icon.innerHTML = '<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/><line x1="23" y1="9" x2="17" y2="15"/><line x1="17" y1="9" x2="23" y2="15"/>';
1684
+ } else {
1685
+ icon.innerHTML = '<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/><path d="M19.07 4.93a10 10 0 0 1 0 14.14M15.54 8.46a5 5 0 0 1 0 7.07"/>';
1686
+ }
1687
+
1688
+ var video = document.querySelector('#previewContainer video');
1689
+ if (video) video.muted = state.isMuted;
1690
+ };
1691
+
1692
+ window.editorSetZoom = function(value) {
1693
+ window.editorState.zoom = parseFloat(value);
1694
+ renderTimeline();
1695
+ updatePlayhead();
1696
+ };
1697
+
1698
+ window.editorTimelineClick = function(e) {
1699
+ if (e.target.closest('.timeline-clip')) return;
1700
+ var state = window.editorState;
1701
+ var container = document.getElementById('timelineContainer');
1702
+ var rect = container.getBoundingClientRect();
1703
+ var x = e.clientX - rect.left - 70 + container.scrollLeft;
1704
+ state.currentTime = Math.max(0, Math.min(state.totalDuration, x / (state.pixelsPerSecond * state.zoom)));
1705
+ updatePlayhead();
1706
+ updatePreview();
1707
+ };
1708
+
1709
+ // ========================================
1710
+ // ์ปจํ…์Šค๏ฟฝ๏ฟฝ๏ฟฝ ๋ฉ”๋‰ด
1711
+ // ========================================
1712
+ function showContextMenu(x, y) {
1713
+ var menu = document.getElementById('contextMenu');
1714
+ menu.style.display = 'block';
1715
+ menu.style.left = x + 'px';
1716
+ menu.style.top = y + 'px';
1717
+ }
1718
+
1719
+ function hideContextMenu() {
1720
+ document.getElementById('contextMenu').style.display = 'none';
1721
+ }
1722
+
1723
+ document.addEventListener('click', function(e) {
1724
+ if (!e.target.closest('.context-menu')) {
1725
+ hideContextMenu();
1726
+ }
1727
+ });
1728
+
1729
+ // ========================================
1730
+ // ๋‚ด๋ณด๋‚ด๊ธฐ
1731
+ // ========================================
1732
+ window.editorExport = function() {
1733
+ var state = window.editorState;
1734
+ if (state.timelineClips.length === 0) {
1735
+ alert('ํƒ€์ž„๋ผ์ธ์— ํด๋ฆฝ์„ ์ถ”๊ฐ€ํ•ด์ฃผ์„ธ์š”.');
1736
+ return;
1737
+ }
1738
+
1739
+ document.getElementById('exportModal').style.display = 'flex';
1740
+ document.getElementById('exportProgress').style.width = '0%';
1741
+ document.getElementById('exportStatus').textContent = '์˜์ƒ์„ ์ฒ˜๋ฆฌํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค...';
1742
+
1743
+ var progress = 0;
1744
+ var interval = setInterval(function() {
1745
+ progress += 2;
1746
+ document.getElementById('exportProgress').style.width = progress + '%';
1747
+
1748
+ if (progress === 30) document.getElementById('exportStatus').textContent = 'ํด๋ฆฝ ๋ณ‘ํ•ฉ ์ค‘...';
1749
+ if (progress === 60) document.getElementById('exportStatus').textContent = '์˜ค๋””์˜ค ์ฒ˜๋ฆฌ ์ค‘...';
1750
+ if (progress === 90) document.getElementById('exportStatus').textContent = '์ตœ์ข… ๋ Œ๋”๋ง ์ค‘...';
1751
+
1752
+ if (progress >= 100) {
1753
+ clearInterval(interval);
1754
+ document.getElementById('exportStatus').textContent = 'โœ… ์™„๋ฃŒ!';
1755
+ document.getElementById('cancelExportBtn').textContent = '๋‹ซ๊ธฐ';
1756
+ }
1757
+ }, 50);
1758
+ };
1759
+
1760
+ window.editorCancelExport = function() {
1761
+ document.getElementById('exportModal').style.display = 'none';
1762
+ document.getElementById('cancelExportBtn').textContent = '์ทจ์†Œ';
1763
+ };
1764
+
1765
+ // ========================================
1766
+ // ํ‚ค๋ณด๋“œ ๋‹จ์ถ•ํ‚ค
1767
+ // ========================================
1768
+ document.addEventListener('keydown', function(e) {
1769
+ if (e.target.tagName === 'INPUT') return;
1770
+
1771
+ if (e.code === 'Space') {
1772
+ e.preventDefault();
1773
+ editorTogglePlay();
1774
+ } else if (e.code === 'Delete' || e.code === 'Backspace') {
1775
+ e.preventDefault();
1776
+ editorDelete();
1777
+ } else if (e.code === 'KeyD' && (e.ctrlKey || e.metaKey)) {
1778
+ e.preventDefault();
1779
+ editorDuplicate();
1780
+ } else if (e.code === 'KeyZ' && (e.ctrlKey || e.metaKey)) {
1781
+ e.preventDefault();
1782
+ editorUndo();
1783
+ } else if (e.code === 'ArrowLeft') {
1784
+ e.preventDefault();
1785
+ window.editorState.currentTime = Math.max(0, window.editorState.currentTime - (e.shiftKey ? 1 : 0.1));
1786
+ updatePlayhead();
1787
+ updatePreview();
1788
+ } else if (e.code === 'ArrowRight') {
1789
+ e.preventDefault();
1790
+ window.editorState.currentTime = Math.min(window.editorState.totalDuration, window.editorState.currentTime + (e.shiftKey ? 1 : 0.1));
1791
+ updatePlayhead();
1792
+ updatePreview();
1793
+ }
1794
+ });
1795
+
1796
+ // ========================================
1797
+ // ์ดˆ๊ธฐํ™”
1798
+ // ========================================
1799
+ renderTimeline();
1800
+ updateStatus('์ค€๋น„๋จ');
1801
+ console.log('Video Editor initialized');
1802
+ </script>
1803
  </body>
1804
  </html>
1805
  """
1806
 
1807
 
1808
+ def process_file(file):
1809
+ """์—…๋กœ๋“œ๋œ ํŒŒ์ผ์„ ์ฒ˜๋ฆฌํ•˜์—ฌ JavaScript๋กœ ์ „๋‹ฌ"""
1810
+ if file is None:
1811
+ return ""
1812
+
1813
+ results = []
1814
+
1815
+ # file์ด ๋ฆฌ์ŠคํŠธ์ธ ๊ฒฝ์šฐ ์ฒ˜๋ฆฌ
1816
+ files = file if isinstance(file, list) else [file]
1817
+
1818
+ for f in files:
1819
+ if f is None:
1820
+ continue
1821
+
1822
+ # ํŒŒ์ผ ๊ฒฝ๋กœ ๊ฐ€์ ธ์˜ค๊ธฐ
1823
+ file_path = f.name if hasattr(f, 'name') else f
1824
+ file_name = os.path.basename(file_path)
1825
+
1826
+ # ํŒŒ์ผ ํƒ€์ž… ๊ฒฐ์ •
1827
+ ext = file_name.lower().split('.')[-1]
1828
+ if ext in ['mp4', 'webm', 'mov', 'avi', 'mkv']:
1829
+ file_type = 'video'
1830
+ mime = f'video/{ext}'
1831
+ elif ext in ['jpg', 'jpeg', 'png', 'gif', 'webp']:
1832
+ file_type = 'image'
1833
+ mime = f'image/{ext}'
1834
+ elif ext in ['mp3', 'wav', 'ogg', 'm4a', 'aac']:
1835
+ file_type = 'audio'
1836
+ mime = f'audio/{ext}'
1837
+ else:
1838
+ continue
1839
+
1840
+ # Base64๋กœ ์ธ์ฝ”๋”ฉ
1841
+ with open(file_path, 'rb') as fp:
1842
+ data = base64.b64encode(fp.read()).decode('utf-8')
1843
+
1844
+ data_url = f'data:{mime};base64,{data}'
1845
+
1846
+ results.append({
1847
+ 'name': file_name,
1848
+ 'type': file_type,
1849
+ 'dataUrl': data_url
1850
+ })
1851
+
1852
+ return results
1853
+
1854
+
1855
  def create_interface():
1856
  """Gradio ์ธํ„ฐํŽ˜์ด์Šค ์ƒ์„ฑ"""
1857
+
1858
  with gr.Blocks(title="Simple Video Editor") as demo:
1859
+
1860
+ gr.Markdown("## ๐ŸŽฌ Simple Video Editor - ํŒŒ์ผ์„ ์•„๋ž˜์—์„œ ์—…๋กœ๋“œํ•˜์„ธ์š”")
1861
+
1862
+ with gr.Row():
1863
+ file_input = gr.File(
1864
+ label="๐Ÿ“ ํŒŒ์ผ ์—…๋กœ๋“œ (์˜์ƒ/์ด๋ฏธ์ง€/์˜ค๋””์˜ค) - ์—ฌ๋Ÿฌ ํŒŒ์ผ ์„ ํƒ ๊ฐ€๋Šฅ",
1865
+ file_count="multiple",
1866
+ file_types=["video", "image", "audio"],
1867
+ height=100
1868
+ )
1869
+
1870
+ # ์—๋””ํ„ฐ HTML
1871
+ editor = gr.HTML(EDITOR_HTML)
1872
+
1873
+ # ์ˆจ๊ฒจ์ง„ ์ถœ๋ ฅ (JavaScript ์‹คํ–‰์šฉ)
1874
+ js_output = gr.HTML(visible=False)
1875
+
1876
+ def on_file_upload(files):
1877
+ if not files:
1878
+ return ""
1879
+
1880
+ results = process_file(files)
1881
+ if not results:
1882
+ return ""
1883
+
1884
+ # JavaScript ์ฝ”๋“œ ์ƒ์„ฑ
1885
+ js_code = "<script>"
1886
+ for r in results:
1887
+ # ์ด์Šค์ผ€์ดํ”„ ์ฒ˜๋ฆฌ
1888
+ name = r['name'].replace("'", "\\'").replace('"', '\\"')
1889
+ js_code += f"window.addMediaToEditor('{name}', '{r['type']}', '{r['dataUrl']}');"
1890
+ js_code += "</script>"
1891
+
1892
+ return js_code
1893
+
1894
+ file_input.change(
1895
+ fn=on_file_upload,
1896
+ inputs=[file_input],
1897
+ outputs=[js_output]
1898
+ )
1899
+
1900
  return demo
1901
 
1902