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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +454 -536
app.py CHANGED
@@ -792,7 +792,21 @@ EDITOR_HTML = """
792
 
793
  /* ========== ํŒŒ์ผ ์ž…๋ ฅ ์ˆจ๊ธฐ๊ธฐ ========== */
794
  #fileInput {
795
- display: none;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
796
  }
797
  </style>
798
  </head>
@@ -816,13 +830,13 @@ EDITOR_HTML = """
816
  </svg>
817
  ์‹คํ–‰์ทจ์†Œ
818
  </button>
819
- <label class="btn btn-primary" for="fileInput" style="cursor:pointer">
820
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
821
  <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M17 8l-5-5-5 5M12 3v12"/>
822
  </svg>
823
  ํŒŒ์ผ ์ถ”๊ฐ€
824
- </label>
825
- <input type="file" id="fileInput" multiple accept="video/*,image/*,audio/*">
826
  <button class="btn btn-success" onclick="exportVideo()" id="exportBtn">
827
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
828
  <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3"/>
@@ -838,12 +852,12 @@ EDITOR_HTML = """
838
  <div class="media-library">
839
  <div class="library-header">๐Ÿ“ ๋ฏธ๋””์–ด</div>
840
  <div class="library-content">
841
- <label class="upload-zone" for="fileInput">
842
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
843
  <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M17 8l-5-5-5 5M12 3v12"/>
844
  </svg>
845
  <p>ํด๋ฆญ ๋˜๋Š” ๋“œ๋ž˜๊ทธ<br>์˜์ƒ/์ด๋ฏธ์ง€/์˜ค๋””์˜ค</p>
846
- </label>
847
  <div class="media-grid" id="mediaGrid"></div>
848
  </div>
849
  </div>
@@ -951,7 +965,7 @@ EDITOR_HTML = """
951
  </svg>
952
  </div>
953
  </div>
954
- <div class="timeline-container" id="timelineContainer">
955
  <div class="timeline-ruler" id="timelineRuler"></div>
956
  <div class="timeline-tracks" id="timelineTracks">
957
  <div class="timeline-track" data-track="0">
@@ -1019,287 +1033,222 @@ EDITOR_HTML = """
1019
  <button class="btn btn-secondary" onclick="cancelExport()" id="cancelExportBtn">์ทจ์†Œ</button>
1020
  </div>
1021
  </div>
 
 
 
1022
 
1023
  <script>
1024
  // ========================================
1025
- // ์ƒํƒœ ๊ด€๋ฆฌ ๋ณ€์ˆ˜
1026
  // ========================================
1027
- let mediaLibrary = []; // ์—…๋กœ๋“œ๋œ ๋ฏธ๋””์–ด ๋ชฉ๋ก
1028
- let timelineClips = []; // ํƒ€์ž„๋ผ์ธ ํด๋ฆฝ ๋ชฉ๋ก
1029
- let selectedClipId = null; // ์„ ํƒ๋œ ํด๋ฆฝ ID
1030
- let isPlaying = false; // ์žฌ์ƒ ์ƒํƒœ
1031
- let isMuted = false; // ์Œ์†Œ๊ฑฐ ์ƒํƒœ
1032
- let currentTime = 0; // ํ˜„์žฌ ์žฌ์ƒ ์‹œ๊ฐ„
1033
- let totalDuration = 0; // ์ „์ฒด ๊ธธ์ด
1034
- let zoom = 1; // ํƒ€์ž„๋ผ์ธ ์คŒ ๋ ˆ๋ฒจ
1035
- let pixelsPerSecond = 80; // ์ดˆ๋‹น ํ”ฝ์…€
1036
- let undoStack = []; // ์‹คํ–‰์ทจ์†Œ ์Šคํƒ
1037
- let animationId = null; // ์• ๋‹ˆ๋ฉ”์ด์…˜ ํ”„๋ ˆ์ž„ ID
1038
- let trimData = null; // ํŠธ๋ฆผ ์ž‘์—… ๋ฐ์ดํ„ฐ
1039
 
1040
  // ========================================
1041
- // ์ดˆ๊ธฐํ™”
1042
  // ========================================
1043
- document.addEventListener('DOMContentLoaded', function() {
1044
- // ํŒŒ์ผ ์ž…๋ ฅ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ
1045
- const fileInput = document.getElementById('fileInput');
1046
- fileInput.addEventListener('change', handleFileUpload);
1047
-
1048
- // ๋“œ๋ž˜๊ทธ์•ค๋“œ๋กญ ์ „์—ญ ์„ค์ •
1049
- document.addEventListener('dragover', function(e) {
1050
- e.preventDefault();
1051
- });
1052
-
1053
- document.addEventListener('drop', function(e) {
1054
- e.preventDefault();
1055
- if (e.target.closest('.upload-zone') || e.target.closest('.library-content')) {
1056
- handleFileDrop(e);
1057
- }
1058
- });
1059
-
1060
- // ์ดˆ๊ธฐ ํƒ€์ž„๋ผ์ธ ๋ Œ๋”๋ง
1061
- renderTimeline();
1062
-
1063
- console.log('๐ŸŽฌ Simple Video Editor ์ดˆ๊ธฐํ™” ์™„๋ฃŒ');
1064
- });
1065
 
1066
  // ========================================
1067
  // ์œ ํ‹ธ๋ฆฌํ‹ฐ ํ•จ์ˆ˜
1068
  // ========================================
1069
-
1070
- /**
1071
- * ๊ณ ์œ  ID ์ƒ์„ฑ
1072
- */
1073
  function generateId() {
1074
  return Date.now().toString(36) + Math.random().toString(36).substr(2);
1075
  }
1076
 
1077
- /**
1078
- * ์‹œ๊ฐ„ ํฌ๋งทํŒ… (์ดˆ -> mm:ss.ms)
1079
- */
1080
  function formatTime(seconds) {
1081
- const mins = Math.floor(seconds / 60);
1082
- const secs = Math.floor(seconds % 60);
1083
- const ms = Math.floor((seconds % 1) * 100);
1084
- return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}.${ms.toString().padStart(2, '0')}`;
1085
  }
1086
 
1087
- /**
1088
- * ํ˜„์žฌ ์ƒํƒœ ์ €์žฅ (์‹คํ–‰์ทจ์†Œ์šฉ)
1089
- */
1090
  function saveState() {
1091
  undoStack.push(JSON.stringify(timelineClips));
1092
- if (undoStack.length > 50) {
1093
- undoStack.shift();
1094
- }
1095
  }
1096
 
1097
- /**
1098
- * ์‹คํ–‰์ทจ์†Œ
1099
- */
1100
  function undoAction() {
1101
  if (undoStack.length > 0) {
1102
  timelineClips = JSON.parse(undoStack.pop());
1103
  renderTimeline();
1104
  updateDuration();
 
1105
  }
1106
  }
1107
 
1108
  // ========================================
1109
- // ํŒŒ์ผ ์—…๋กœ๋“œ ์ฒ˜๋ฆฌ
1110
  // ========================================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1111
 
1112
- /**
1113
- * ํŒŒ์ผ ์ž…๋ ฅ ํ•ธ๋“ค๋Ÿฌ
1114
- */
1115
- function handleFileUpload(event) {
1116
- const files = event.target.files;
1117
- if (!files || files.length === 0) return;
1118
-
1119
- processFiles(Array.from(files));
1120
- event.target.value = ''; // ๋ฆฌ์…‹
1121
- }
1122
-
1123
- /**
1124
- * ํŒŒ์ผ ๋“œ๋กญ ํ•ธ๋“ค๋Ÿฌ
1125
- */
1126
- function handleFileDrop(event) {
1127
- const files = event.dataTransfer.files;
1128
- if (!files || files.length === 0) return;
1129
-
1130
- processFiles(Array.from(files));
1131
- }
1132
-
1133
- /**
1134
- * ํŒŒ์ผ ์ฒ˜๋ฆฌ
1135
- */
1136
- function processFiles(files) {
1137
- console.log('๐Ÿ“ ํŒŒ์ผ ์ฒ˜๋ฆฌ ์‹œ์ž‘:', files.length, '๊ฐœ');
1138
-
1139
- files.forEach(file => {
1140
- // ํŒŒ์ผ ํƒ€์ž… ํ™•์ธ
1141
- let type = null;
1142
- if (file.type.startsWith('video')) {
1143
- type = 'video';
1144
- } else if (file.type.startsWith('image')) {
1145
- type = 'image';
1146
- } else if (file.type.startsWith('audio')) {
1147
- type = 'audio';
1148
- }
1149
-
1150
- if (!type) {
1151
- console.log('โš ๏ธ ์ง€์›ํ•˜์ง€ ์•Š๋Š” ํŒŒ์ผ ํ˜•์‹:', file.type);
1152
- return;
1153
- }
1154
-
1155
- // Blob URL ์ƒ์„ฑ
1156
- const url = URL.createObjectURL(file);
1157
- console.log('โœ… ๏ฟฝ๏ฟฝ๏ฟฝ์ผ ์ถ”๊ฐ€:', file.name, type);
1158
-
1159
- // ๋ฏธ๋””์–ด ๊ฐ์ฒด ์ƒ์„ฑ
1160
- const media = {
1161
- id: generateId(),
1162
- name: file.name,
1163
- type: type,
1164
- url: url,
1165
- file: file,
1166
- duration: type === 'image' ? 5 : 0, // ์ด๋ฏธ์ง€๋Š” ๊ธฐ๋ณธ 5์ดˆ
1167
- thumbnail: type === 'image' ? url : null
1168
- };
1169
-
1170
- // ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์— ์ถ”๊ฐ€
1171
- mediaLibrary.push(media);
1172
- renderMediaLibrary();
1173
-
1174
- // ๋น„๋””์˜ค/์˜ค๋””์˜ค ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ๋กœ๋“œ
1175
- if (type === 'video' || type === 'audio') {
1176
- loadMediaMetadata(media, type);
1177
- }
1178
- });
1179
  }
1180
 
1181
- /**
1182
- * ๋ฏธ๋””์–ด ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ๋กœ๋“œ
1183
- */
1184
- function loadMediaMetadata(media, type) {
1185
- const el = document.createElement(type);
1186
  el.src = media.url;
1187
  el.preload = 'metadata';
1188
 
1189
  el.onloadedmetadata = function() {
1190
  media.duration = el.duration;
1191
- console.log('๐Ÿ“Š ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ๋กœ๋“œ:', media.name, formatTime(el.duration));
1192
  renderMediaLibrary();
1193
 
1194
- // ๋น„๋””์˜ค ์ธ๋„ค์ผ ์ƒ์„ฑ
1195
- if (type === 'video') {
1196
  el.currentTime = Math.min(1, el.duration / 2);
1197
  }
1198
  };
1199
 
1200
  el.onseeked = function() {
1201
- if (type === 'video') {
1202
- generateVideoThumbnail(el, media);
 
 
 
 
 
 
 
 
 
1203
  }
1204
  };
1205
-
1206
- el.onerror = function(e) {
1207
- console.error('โŒ ๋ฏธ๋””์–ด ๋กœ๋“œ ์˜ค๋ฅ˜:', media.name, e);
1208
- };
1209
- }
1210
-
1211
- /**
1212
- * ๋น„๋””์˜ค ์ธ๋„ค์ผ ์ƒ์„ฑ
1213
- */
1214
- function generateVideoThumbnail(videoEl, media) {
1215
- try {
1216
- const canvas = document.createElement('canvas');
1217
- canvas.width = 160;
1218
- canvas.height = 90;
1219
- const ctx = canvas.getContext('2d');
1220
- ctx.drawImage(videoEl, 0, 0, 160, 90);
1221
- media.thumbnail = canvas.toDataURL();
1222
- renderMediaLibrary();
1223
- } catch(e) {
1224
- console.log('โš ๏ธ ์ธ๋„ค์ผ ์ƒ์„ฑ ์‹คํŒจ:', e);
1225
- }
1226
  }
1227
 
1228
  // ========================================
1229
  // ๋ฏธ๋””์–ด ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๋ Œ๋”๋ง
1230
  // ========================================
1231
  function renderMediaLibrary() {
1232
- const grid = document.getElementById('mediaGrid');
 
 
1233
  grid.innerHTML = '';
1234
 
1235
- // ํƒ€์ž…๋ณ„ ์•„์ด์ฝ˜
1236
- const typeIcons = {
1237
  video: '<svg viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21"/></svg>',
1238
  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>',
1239
  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>'
1240
  };
1241
 
1242
- mediaLibrary.forEach(media => {
1243
- const item = document.createElement('div');
 
1244
  item.className = 'media-item';
1245
  item.draggable = true;
1246
- item.dataset.mediaId = media.id;
1247
-
1248
- // ๋“œ๋ž˜๊ทธ ์ด๋ฒคํŠธ
1249
- item.ondragstart = function(e) {
1250
- e.dataTransfer.setData('mediaId', media.id);
1251
- item.style.opacity = '0.5';
1252
- };
1253
 
1254
- item.ondragend = function() {
1255
- item.style.opacity = '1';
1256
- };
 
 
 
 
 
 
 
 
 
 
1257
 
1258
- // ๋”๋ธ”ํด๋ฆญ์œผ๋กœ ํƒ€์ž„๋ผ์ธ์— ์ถ”๊ฐ€
1259
- item.ondblclick = function() {
1260
- addToTimeline(media);
1261
- };
1262
-
1263
- // ์ธ๋„ค์ผ HTML
1264
- let thumbnailHtml = '';
1265
  if (media.thumbnail) {
1266
- thumbnailHtml = `<img src="${media.thumbnail}" alt="${media.name}">`;
1267
  } else {
1268
- thumbnailHtml = `
1269
- <div style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;background:#e5e7eb;color:#9ca3af">
1270
- ${typeIcons[media.type]}
1271
- </div>
1272
- `;
1273
  }
1274
 
1275
- item.innerHTML = `
1276
- ${thumbnailHtml}
1277
- <div class="media-item-type">${typeIcons[media.type]}</div>
1278
- ${media.duration > 0 ? `<div class="media-item-duration">${formatTime(media.duration)}</div>` : ''}
1279
- <div class="media-item-overlay">
1280
- <span class="media-item-name">${media.name}</span>
1281
- </div>
1282
- `;
1283
 
1284
  grid.appendChild(item);
1285
- });
 
 
1286
  }
1287
 
1288
  // ========================================
1289
  // ํƒ€์ž„๋ผ์ธ ๊ด€๋ฆฌ
1290
  // ========================================
1291
-
1292
- /**
1293
- * ๋ฏธ๋””์–ด๋ฅผ ํƒ€์ž„๋ผ์ธ์— ์ถ”๊ฐ€
1294
- */
1295
- function addToTimeline(media, startTime = null) {
1296
  saveState();
1297
 
1298
- // ์˜ค๋””์˜ค๋Š” ํŠธ๋ž™ 1, ๋‚˜๋จธ์ง€๋Š” ํŠธ๋ž™ 0
1299
- const track = media.type === 'audio' ? 1 : 0;
1300
- const start = startTime !== null ? startTime : getTrackEndTime(track);
1301
 
1302
- const clip = {
1303
  id: generateId(),
1304
  mediaId: media.id,
1305
  name: media.name,
@@ -1315,104 +1264,91 @@ EDITOR_HTML = """
1315
  };
1316
 
1317
  timelineClips.push(clip);
1318
- console.log('๐ŸŽฌ ํด๋ฆฝ ์ถ”๊ฐ€:', clip.name, 'ํŠธ๋ž™:', track);
1319
 
1320
  renderTimeline();
1321
  updateDuration();
1322
  }
1323
 
1324
- /**
1325
- * ํŠธ๋ž™์˜ ๋ ์‹œ๊ฐ„ ๊ณ„์‚ฐ
1326
- */
1327
  function getTrackEndTime(track) {
1328
- const trackClips = timelineClips.filter(c => c.track === track);
1329
- if (trackClips.length === 0) return 0;
1330
- return Math.max(...trackClips.map(c => c.startTime + (c.trimEnd - c.trimStart)));
 
 
 
 
 
 
1331
  }
1332
 
1333
- /**
1334
- * ํƒ€์ž„๋ผ์ธ ๋ Œ๋”๋ง
1335
- */
1336
  function renderTimeline() {
1337
- // ๋ชจ๋“  ํŠธ๋ž™ ๋‚ด์šฉ ์ดˆ๊ธฐํ™”
1338
- const tracks = document.querySelectorAll('.track-content');
1339
- tracks.forEach(track => {
1340
- track.innerHTML = '';
1341
- });
1342
-
1343
- // ํด๋ฆฝ ๋ Œ๋”๋ง
1344
- timelineClips.forEach(clip => {
1345
- const trackEl = document.querySelector(`.timeline-track[data-track="${clip.track}"] .track-content`);
1346
- if (!trackEl) return;
1347
-
1348
- const clipEl = document.createElement('div');
1349
- clipEl.className = `timeline-clip ${clip.type}${selectedClipId === clip.id ? ' selected' : ''}`;
1350
- clipEl.dataset.clipId = clip.id;
1351
 
1352
- // ํด๋ฆฝ ์œ„์น˜ ๋ฐ ํฌ๊ธฐ ๊ณ„์‚ฐ
1353
- const clipDuration = clip.trimEnd - clip.trimStart;
1354
- const left = 70 + (clip.startTime * pixelsPerSecond * zoom);
1355
- const width = Math.max(40, clipDuration * pixelsPerSecond * zoom);
1356
 
1357
- clipEl.style.left = `${left}px`;
1358
- clipEl.style.width = `${width}px`;
 
1359
 
1360
- // ๋“œ๋ž˜๊ทธ ์ด๋ฒคํŠธ
 
1361
  clipEl.draggable = true;
1362
 
1363
- clipEl.onclick = function(e) {
1364
- e.stopPropagation();
1365
- selectClip(clip.id);
1366
- };
1367
-
1368
- clipEl.oncontextmenu = function(e) {
1369
- e.preventDefault();
1370
- selectClip(clip.id);
1371
- showContextMenu(e.clientX, e.clientY);
1372
- };
1373
-
1374
- clipEl.ondragstart = function(e) {
1375
- e.dataTransfer.setData('clipId', clip.id);
1376
- e.dataTransfer.setData('offsetX', e.offsetX.toString());
1377
- };
 
1378
 
1379
- // ํด๋ฆฝ ๋‚ด์šฉ
1380
- let thumbnailHtml = '';
1381
- if (clip.thumbnail) {
1382
- thumbnailHtml = `<img class="clip-thumbnail" src="${clip.thumbnail}">`;
1383
- }
1384
 
1385
- clipEl.innerHTML = `
1386
- ${thumbnailHtml}
1387
- <div class="clip-info">
1388
- <div class="clip-name">${clip.name}</div>
1389
- <div class="clip-duration">${formatTime(clipDuration)}</div>
1390
- </div>
1391
- <div class="clip-handles clip-handle-left" onmousedown="startTrim(event, '${clip.id}', 'left')"></div>
1392
- <div class="clip-handles clip-handle-right" onmousedown="startTrim(event, '${clip.id}', 'right')"></div>
1393
- `;
1394
 
1395
  trackEl.appendChild(clipEl);
1396
- });
1397
 
1398
  renderRuler();
1399
  }
1400
 
1401
- /**
1402
- * ํƒ€์ž„๋ผ์ธ ๋ˆˆ๊ธˆ์ž ๋ Œ๋”๋ง
1403
- */
1404
  function renderRuler() {
1405
- const ruler = document.getElementById('timelineRuler');
1406
- const width = Math.max(totalDuration * pixelsPerSecond * zoom + 300, 1000);
1407
- ruler.style.width = `${width}px`;
 
 
1408
 
1409
- let html = '<svg width="100%" height="22" style="position:absolute;left:70px">';
1410
- const step = zoom < 0.7 ? 5 : zoom < 1.5 ? 2 : 1;
1411
 
1412
- for (let i = 0; i <= Math.ceil(totalDuration) + 10; i += step) {
1413
- const x = i * pixelsPerSecond * zoom;
1414
- html += `<line x1="${x}" y1="17" x2="${x}" y2="22" stroke="#d1d5db" stroke-width="1"/>`;
1415
- html += `<text x="${x}" y="12" fill="#9ca3af" font-size="9" text-anchor="middle">${formatTime(i)}</text>`;
1416
  }
1417
 
1418
  html += '</svg>';
@@ -1422,7 +1358,6 @@ EDITOR_HTML = """
1422
  // ========================================
1423
  // ๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋กญ
1424
  // ========================================
1425
-
1426
  function handleDragOver(event) {
1427
  event.preventDefault();
1428
  event.currentTarget.classList.add('drop-highlight');
@@ -1436,37 +1371,41 @@ EDITOR_HTML = """
1436
  event.preventDefault();
1437
  event.currentTarget.classList.remove('drop-highlight');
1438
 
1439
- const rect = event.currentTarget.getBoundingClientRect();
1440
- const x = event.clientX - rect.left;
1441
- const time = Math.max(0, x / (pixelsPerSecond * zoom));
1442
 
1443
- const mediaId = event.dataTransfer.getData('mediaId');
1444
- const clipId = event.dataTransfer.getData('clipId');
1445
- const offsetX = parseFloat(event.dataTransfer.getData('offsetX') || 0);
1446
 
1447
  if (mediaId) {
1448
  // ๋ฏธ๋””์–ด ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์—์„œ ๋“œ๋กญ
1449
- const media = mediaLibrary.find(m => m.id === mediaId);
 
 
 
 
 
 
1450
  if (media) {
1451
- const targetTrack = media.type === 'audio' ? 1 : track;
1452
  addToTimeline(media, time);
1453
-
1454
- // ๋“œ๋กญ๋œ ํŠธ๋ž™์ด ๋‹ค๋ฅด๋ฉด ์ˆ˜์ •
1455
  if (targetTrack !== track) {
1456
- const lastClip = timelineClips[timelineClips.length - 1];
1457
- lastClip.track = targetTrack;
1458
  renderTimeline();
1459
  }
1460
  }
1461
  } else if (clipId) {
1462
- // ํƒ€์ž„๋ผ์ธ ๋‚ด ํด๋ฆฝ ์ด๋™
1463
  saveState();
1464
- const clip = timelineClips.find(c => c.id === clipId);
1465
- if (clip) {
1466
- const adjustedTime = Math.max(0, time - (offsetX / (pixelsPerSecond * zoom)));
1467
- clip.startTime = adjustedTime;
1468
- // ์˜ค๋””์˜ค ํด๋ฆฝ์€ ์˜ค๋””์˜ค ํŠธ๋ž™์—๋งŒ
1469
- clip.track = clip.type === 'audio' ? 1 : track;
 
1470
  }
1471
  renderTimeline();
1472
  updateDuration();
@@ -1476,7 +1415,6 @@ EDITOR_HTML = """
1476
  // ========================================
1477
  // ํด๋ฆฝ ์„ ํƒ ๋ฐ ์†์„ฑ
1478
  // ========================================
1479
-
1480
  function selectClip(clipId) {
1481
  selectedClipId = clipId;
1482
  renderTimeline();
@@ -1484,145 +1422,161 @@ EDITOR_HTML = """
1484
  }
1485
 
1486
  function renderProperties() {
1487
- const container = document.getElementById('propertiesContent');
1488
- const clip = timelineClips.find(c => c.id === selectedClipId);
 
 
 
 
 
 
 
 
1489
 
1490
  if (!clip) {
1491
  container.innerHTML = '<div class="no-selection">ํด๋ฆฝ์„ ์„ ํƒํ•˜๋ฉด<br>์†์„ฑ์„ ํŽธ์ง‘ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค</div>';
1492
  return;
1493
  }
1494
 
1495
- const clipDuration = clip.trimEnd - clip.trimStart;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1496
 
1497
- container.innerHTML = `
1498
- <div class="property-group">
1499
- <div class="property-label">์ด๋ฆ„</div>
1500
- <input type="text" class="property-input" value="${clip.name}" onchange="updateClipProperty('name', this.value)">
1501
- </div>
1502
- <div class="property-group">
1503
- <div class="property-label">์‹œ์ž‘ ์‹œ๊ฐ„</div>
1504
- <input type="number" class="property-input" value="${clip.startTime.toFixed(2)}" step="0.1" min="0" onchange="updateClipProperty('startTime', parseFloat(this.value))">
1505
- </div>
1506
- <div class="property-group">
1507
- <div class="property-row">
1508
- <div>
1509
- <div class="property-label">ํŠธ๋ฆผ ์‹œ์ž‘</div>
1510
- <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))">
1511
- </div>
1512
- <div>
1513
- <div class="property-label">ํŠธ๋ฆผ ๋</div>
1514
- <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))">
1515
- </div>
1516
- </div>
1517
- </div>
1518
- <div class="property-group">
1519
- <div class="property-label">๊ธธ์ด</div>
1520
- <div style="padding:6px 0;font-size:12px;color:#374151">${formatTime(clipDuration)}</div>
1521
- </div>
1522
- ${clip.type !== 'image' ? `
1523
- <div class="property-group">
1524
- <div class="property-label">๋ณผ๋ฅจ (${Math.round(clip.volume * 100)}%)</div>
1525
- <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))">
1526
- </div>
1527
- ` : ''}
1528
- `;
1529
  }
1530
 
1531
  function updateClipProperty(prop, value) {
1532
  saveState();
1533
- timelineClips = timelineClips.map(c =>
1534
- c.id === selectedClipId ? { ...c, [prop]: value } : c
1535
- );
 
 
 
1536
  renderTimeline();
1537
  updateDuration();
1538
  renderProperties();
1539
  }
1540
 
1541
  // ========================================
1542
- // ํด๋ฆฝ ํŽธ์ง‘ ๊ธฐ๋Šฅ
1543
  // ========================================
1544
-
1545
- /**
1546
- * ์„ ํƒ๋œ ํด๋ฆฝ ์ž๋ฅด๊ธฐ (์ค‘๊ฐ„ ์ง€์ ์—์„œ)
1547
- */
1548
  function splitSelectedClip() {
1549
  if (!selectedClipId) return;
1550
 
1551
- const clip = timelineClips.find(c => c.id === selectedClipId);
 
 
 
 
 
 
 
 
1552
  if (!clip) return;
1553
 
1554
  saveState();
1555
 
1556
- const clipDuration = clip.trimEnd - clip.trimStart;
1557
- const splitPoint = clipDuration / 2;
1558
 
1559
- // ์ฒซ ๋ฒˆ์งธ ํด๋ฆฝ (์•ž๋ถ€๋ถ„)
1560
- const clip1 = { ...clip, trimEnd: clip.trimStart + splitPoint };
1561
 
1562
- // ๋‘ ๋ฒˆ์งธ ํด๋ฆฝ (๋’ท๋ถ€๋ถ„)
1563
- const clip2 = {
1564
- ...clip,
1565
- id: generateId(),
1566
- startTime: clip.startTime + splitPoint,
1567
- trimStart: clip.trimStart + splitPoint
1568
- };
1569
 
1570
- timelineClips = timelineClips.map(c => c.id === clip.id ? clip1 : c);
1571
  timelineClips.push(clip2);
1572
 
1573
  renderTimeline();
1574
  hideContextMenu();
 
1575
  }
1576
 
1577
- /**
1578
- * ์„ ํƒ๋œ ํด๋ฆฝ ๋ณต์ œ
1579
- */
1580
  function duplicateSelectedClip() {
1581
  if (!selectedClipId) return;
1582
 
1583
- const clip = timelineClips.find(c => c.id === selectedClipId);
 
 
 
 
 
 
1584
  if (!clip) return;
1585
 
1586
  saveState();
1587
 
1588
- const clipDuration = clip.trimEnd - clip.trimStart;
1589
- const newClip = {
1590
- ...clip,
1591
- id: generateId(),
1592
- startTime: clip.startTime + clipDuration
1593
- };
1594
 
1595
  timelineClips.push(newClip);
1596
  renderTimeline();
1597
  updateDuration();
1598
  hideContextMenu();
 
1599
  }
1600
 
1601
- /**
1602
- * ์„ ํƒ๋œ ํด๋ฆฝ ์‚ญ์ œ
1603
- */
1604
  function deleteSelectedClip() {
1605
  if (!selectedClipId) return;
1606
 
1607
  saveState();
1608
- timelineClips = timelineClips.filter(c => c.id !== selectedClipId);
 
 
 
 
 
 
 
1609
  selectedClipId = null;
1610
 
1611
  renderTimeline();
1612
  renderProperties();
1613
  updateDuration();
1614
  hideContextMenu();
 
1615
  }
1616
 
1617
  // ========================================
1618
  // ํŠธ๋ฆผ ํ•ธ๋“ค
1619
  // ========================================
1620
-
1621
  function startTrim(event, clipId, side) {
1622
  event.stopPropagation();
1623
  event.preventDefault();
1624
 
1625
- const clip = timelineClips.find(c => c.id === clipId);
 
 
 
 
 
 
1626
  if (!clip) return;
1627
 
1628
  saveState();
@@ -1643,20 +1597,24 @@ EDITOR_HTML = """
1643
  function handleTrim(event) {
1644
  if (!trimData) return;
1645
 
1646
- const clip = timelineClips.find(c => c.id === trimData.clipId);
 
 
 
 
 
 
1647
  if (!clip) return;
1648
 
1649
- const deltaX = event.clientX - trimData.startX;
1650
- const deltaTime = deltaX / (pixelsPerSecond * zoom);
1651
 
1652
  if (trimData.side === 'left') {
1653
- // ์™ผ์ชฝ ํ•ธ๋“ค: ์‹œ์ž‘ ์ง€์  ์กฐ์ •
1654
- const newTrimStart = Math.max(0, Math.min(clip.trimEnd - 0.1, trimData.originalTrimStart + deltaTime));
1655
- const trimDelta = newTrimStart - trimData.originalTrimStart;
1656
  clip.trimStart = newTrimStart;
1657
  clip.startTime = trimData.originalStartTime + trimDelta;
1658
  } else {
1659
- // ์˜ค๋ฅธ์ชฝ ํ•ธ๋“ค: ๋ ์ง€์  ์กฐ์ •
1660
  clip.trimEnd = Math.max(clip.trimStart + 0.1, Math.min(clip.duration, trimData.originalTrimEnd + deltaTime));
1661
  }
1662
 
@@ -1673,19 +1631,20 @@ EDITOR_HTML = """
1673
  // ========================================
1674
  // ์žฌ์ƒ ์ปจํŠธ๋กค
1675
  // ========================================
1676
-
1677
  function updateDuration() {
1678
- if (timelineClips.length === 0) {
1679
- totalDuration = 0;
1680
- } else {
1681
- totalDuration = Math.max(...timelineClips.map(c => c.startTime + (c.trimEnd - c.trimStart)));
 
1682
  }
 
1683
  document.getElementById('durationDisplay').textContent = formatTime(totalDuration);
1684
  }
1685
 
1686
  function togglePlay() {
1687
  isPlaying = !isPlaying;
1688
- const icon = document.getElementById('playIcon');
1689
 
1690
  if (isPlaying) {
1691
  icon.innerHTML = '<rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/>';
@@ -1697,17 +1656,16 @@ EDITOR_HTML = """
1697
  }
1698
 
1699
  function startPlayback() {
1700
- let lastTime = performance.now();
1701
 
1702
  function animate(now) {
1703
  if (!isPlaying) return;
1704
 
1705
- const delta = (now - lastTime) / 1000;
1706
  lastTime = now;
1707
 
1708
  currentTime += delta;
1709
 
1710
- // ๋์— ๋„๋‹ฌํ•˜๋ฉด ์ฒ˜์Œ์œผ๋กœ
1711
  if (currentTime >= totalDuration) {
1712
  currentTime = 0;
1713
  if (totalDuration === 0) {
@@ -1731,57 +1689,59 @@ EDITOR_HTML = """
1731
  cancelAnimationFrame(animationId);
1732
  animationId = null;
1733
  }
1734
-
1735
- // ๋น„๋””์˜ค ์ผ์‹œ์ •์ง€
1736
- const video = document.querySelector('#previewContainer video');
1737
- if (video && !video.paused) {
1738
- video.pause();
1739
- }
1740
  }
1741
 
1742
  function updatePlayhead() {
1743
- const playhead = document.getElementById('playhead');
1744
- const left = 70 + (currentTime * pixelsPerSecond * zoom);
1745
- playhead.style.left = `${left}px`;
 
1746
  document.getElementById('currentTimeDisplay').textContent = formatTime(currentTime);
1747
  }
1748
 
1749
  function updatePreview() {
1750
- const container = document.getElementById('previewContainer');
1751
-
1752
- // ํ˜„์žฌ ์‹œ๊ฐ„์— ์žฌ์ƒ ์ค‘์ธ ํด๋ฆฝ ์ฐพ๊ธฐ
1753
- const currentClips = timelineClips.filter(clip => {
1754
- const clipEnd = clip.startTime + (clip.trimEnd - clip.trimStart);
1755
- return currentTime >= clip.startTime && currentTime < clipEnd;
1756
- });
 
 
 
 
1757
 
1758
- // ๋น„๋””์˜ค/์ด๋ฏธ์ง€ ํด๋ฆฝ ์ฐพ๊ธฐ
1759
- const visualClip = currentClips.find(c => c.type === 'video' || c.type === 'image');
 
 
 
 
 
1760
 
1761
  if (visualClip) {
1762
- const clipTime = currentTime - visualClip.startTime + visualClip.trimStart;
1763
 
1764
  if (visualClip.type === 'image') {
1765
- // ์ด๋ฏธ์ง€ ํ‘œ์‹œ
1766
- if (!container.querySelector(`img[data-clip-id="${visualClip.id}"]`)) {
1767
- container.innerHTML = `<img src="${visualClip.url}" data-clip-id="${visualClip.id}">`;
1768
  }
1769
  } else if (visualClip.type === 'video') {
1770
- // ๋น„๋””์˜ค ํ‘œ์‹œ
1771
- let video = container.querySelector(`video[data-clip-id="${visualClip.id}"]`);
1772
  if (!video) {
1773
- container.innerHTML = `<video src="${visualClip.url}" data-clip-id="${visualClip.id}" ${isMuted ? 'muted' : ''}></video>`;
1774
  video = container.querySelector('video');
1775
  }
1776
 
1777
- // ์‹œ๊ฐ„ ๋™๊ธฐํ™”
1778
  if (Math.abs(video.currentTime - clipTime) > 0.2) {
1779
  video.currentTime = clipTime;
1780
  }
1781
 
1782
- // ์žฌ์ƒ/์ผ์‹œ์ •์ง€
1783
  if (isPlaying && video.paused) {
1784
- video.play().catch(() => {});
1785
  } else if (!isPlaying && !video.paused) {
1786
  video.pause();
1787
  }
@@ -1789,60 +1749,35 @@ EDITOR_HTML = """
1789
  video.volume = isMuted ? 0 : visualClip.volume;
1790
  video.muted = isMuted;
1791
  }
1792
- } else if (!currentClips.some(c => c.type === 'video' || c.type === 'image')) {
1793
- // ์‹œ๊ฐ์  ํด๋ฆฝ์ด ์—†์„ ๋•Œ
 
 
 
 
1794
  if (!container.querySelector('.preview-placeholder')) {
1795
- container.innerHTML = `
1796
- <div class="preview-placeholder">
1797
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1">
1798
- <rect x="2" y="2" width="20" height="20" rx="2"/>
1799
- <polygon points="10 8 16 12 10 16 10 8"/>
1800
- </svg>
1801
- <p>${currentClips.length > 0 ? '๐ŸŽต ์˜ค๋””์˜ค ์žฌ์ƒ ์ค‘' : 'ํƒ€์ž„๋ผ์ธ์— ๋ฏธ๋””์–ด๋ฅผ ์ถ”๊ฐ€ํ•˜์„ธ์š”'}</p>
1802
- </div>
1803
- `;
1804
  }
1805
  }
1806
  }
1807
 
1808
- function skipToStart() {
1809
- currentTime = 0;
1810
- updatePlayhead();
1811
- updatePreview();
1812
- }
1813
-
1814
- function skipToEnd() {
1815
- currentTime = totalDuration;
1816
- updatePlayhead();
1817
- updatePreview();
1818
- }
1819
-
1820
- function skipBackward() {
1821
- currentTime = Math.max(0, currentTime - 5);
1822
- updatePlayhead();
1823
- updatePreview();
1824
- }
1825
-
1826
- function skipForward() {
1827
- currentTime = Math.min(totalDuration, currentTime + 5);
1828
- updatePlayhead();
1829
- updatePreview();
1830
- }
1831
 
1832
  function toggleMute() {
1833
  isMuted = !isMuted;
1834
- const icon = document.getElementById('volumeIcon');
1835
-
1836
  if (isMuted) {
1837
  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"/>';
1838
  } else {
1839
  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"/>';
1840
  }
1841
-
1842
- const video = document.querySelector('#previewContainer video');
1843
- if (video) {
1844
- video.muted = isMuted;
1845
- }
1846
  }
1847
 
1848
  function setZoom(value) {
@@ -1854,42 +1789,35 @@ EDITOR_HTML = """
1854
  // ========================================
1855
  // ์ปจํ…์ŠคํŠธ ๋ฉ”๋‰ด
1856
  // ========================================
1857
-
1858
  function showContextMenu(x, y) {
1859
- const menu = document.getElementById('contextMenu');
1860
  menu.style.display = 'block';
1861
- menu.style.left = `${x}px`;
1862
- menu.style.top = `${y}px`;
1863
  }
1864
 
1865
  function hideContextMenu() {
1866
  document.getElementById('contextMenu').style.display = 'none';
1867
  }
1868
 
1869
- // ๋‹ค๋ฅธ ๊ณณ ํด๋ฆญ์‹œ ์ปจํ…์ŠคํŠธ ๋ฉ”๋‰ด ๋‹ซ๊ธฐ
1870
- document.addEventListener('click', function(e) {
1871
- if (!e.target.closest('.context-menu')) {
1872
- hideContextMenu();
1873
- }
1874
- });
1875
-
1876
  // ========================================
1877
- // ํƒ€์ž„๋ผ์ธ ํด๋ฆญ์œผ๋กœ ์žฌ์ƒ ์œ„์น˜ ๋ณ€๊ฒฝ
1878
  // ========================================
1879
- document.getElementById('timelineContainer').addEventListener('click', function(e) {
1880
  if (e.target.closest('.timeline-clip')) return;
1881
 
1882
- const rect = this.getBoundingClientRect();
1883
- const x = e.clientX - rect.left - 70 + this.scrollLeft;
 
1884
  currentTime = Math.max(0, Math.min(totalDuration, x / (pixelsPerSecond * zoom)));
1885
  updatePlayhead();
1886
  updatePreview();
1887
- });
1888
 
1889
  // ========================================
1890
  // ๋‚ด๋ณด๋‚ด๊ธฐ
1891
  // ========================================
1892
- async function exportVideo() {
1893
  if (timelineClips.length === 0) {
1894
  alert('ํƒ€์ž„๋ผ์ธ์— ํด๋ฆฝ์„ ์ถ”๊ฐ€ํ•ด์ฃผ์„ธ์š”.');
1895
  return;
@@ -1899,24 +1827,21 @@ EDITOR_HTML = """
1899
  document.getElementById('exportProgress').style.width = '0%';
1900
  document.getElementById('exportStatus').textContent = '์˜์ƒ์„ ์ฒ˜๋ฆฌํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค...';
1901
 
1902
- // ์ง„ํ–‰๋ฅ  ์‹œ๋ฎฌ๋ ˆ์ด์…˜
1903
- for (let i = 0; i <= 100; i += 2) {
1904
- await new Promise(r => setTimeout(r, 50));
1905
- document.getElementById('exportProgress').style.width = `${i}%`;
1906
 
1907
- if (i === 30) {
1908
- document.getElementById('exportStatus').textContent = 'ํด๋ฆฝ ๋ณ‘ํ•ฉ ์ค‘...';
1909
- }
1910
- if (i === 60) {
1911
- document.getElementById('exportStatus').textContent = '์˜ค๋””์˜ค ์ฒ˜๋ฆฌ ์ค‘...';
1912
- }
1913
- if (i === 90) {
1914
- document.getElementById('exportStatus').textContent = '์ตœ์ข… ๋ Œ๋”๋ง ์ค‘...';
1915
  }
1916
- }
1917
-
1918
- document.getElementById('exportStatus').textContent = 'โœ… ์™„๋ฃŒ!';
1919
- document.getElementById('cancelExportBtn').textContent = '๋‹ซ๊ธฐ';
1920
  }
1921
 
1922
  function cancelExport() {
@@ -1927,51 +1852,46 @@ EDITOR_HTML = """
1927
  // ========================================
1928
  // ํ‚ค๋ณด๋“œ ๋‹จ์ถ•ํ‚ค
1929
  // ========================================
1930
- document.addEventListener('keydown', function(e) {
1931
- // ์ž…๋ ฅ ํ•„๋“œ์—์„œ๋Š” ๋‹จ์ถ•ํ‚ค ๋น„ํ™œ์„ฑํ™”
1932
  if (e.target.tagName === 'INPUT') return;
1933
 
1934
- switch(e.code) {
1935
- case 'Space':
1936
- e.preventDefault();
1937
- togglePlay();
1938
- break;
1939
-
1940
- case 'Delete':
1941
- case 'Backspace':
1942
- e.preventDefault();
1943
- if (selectedClipId) deleteSelectedClip();
1944
- break;
1945
-
1946
- case 'KeyD':
1947
- if (e.ctrlKey || e.metaKey) {
1948
- e.preventDefault();
1949
- if (selectedClipId) duplicateSelectedClip();
1950
- }
1951
- break;
1952
-
1953
- case 'KeyZ':
1954
- if (e.ctrlKey || e.metaKey) {
1955
- e.preventDefault();
1956
- undoAction();
1957
- }
1958
- break;
1959
-
1960
- case 'ArrowLeft':
1961
- e.preventDefault();
1962
- currentTime = Math.max(0, currentTime - (e.shiftKey ? 1 : 0.1));
1963
- updatePlayhead();
1964
- updatePreview();
1965
- break;
1966
-
1967
- case 'ArrowRight':
1968
- e.preventDefault();
1969
- currentTime = Math.min(totalDuration, currentTime + (e.shiftKey ? 1 : 0.1));
1970
- updatePlayhead();
1971
- updatePreview();
1972
- break;
1973
  }
1974
- });
 
 
 
 
 
 
1975
  </script>
1976
  </body>
1977
  </html>
@@ -1980,10 +1900,8 @@ EDITOR_HTML = """
1980
 
1981
  def create_interface():
1982
  """Gradio ์ธํ„ฐํŽ˜์ด์Šค ์ƒ์„ฑ"""
1983
-
1984
  with gr.Blocks(title="Simple Video Editor") as demo:
1985
  gr.HTML(EDITOR_HTML)
1986
-
1987
  return demo
1988
 
1989
 
 
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>
 
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"/>
 
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>
863
  </div>
 
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">
 
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,
 
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>';
 
1358
  // ========================================
1359
  // ๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋กญ
1360
  // ========================================
 
1361
  function handleDragOver(event) {
1362
  event.preventDefault();
1363
  event.currentTarget.classList.add('drop-highlight');
 
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();
 
1415
  // ========================================
1416
  // ํด๋ฆฝ ์„ ํƒ ๋ฐ ์†์„ฑ
1417
  // ========================================
 
1418
  function selectClip(clipId) {
1419
  selectedClipId = clipId;
1420
  renderTimeline();
 
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();
 
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
 
 
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"/>';
 
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) {
 
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
  }
 
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) {
 
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;
 
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() {
 
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>
 
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