openfree commited on
Commit
9008d92
·
verified ·
1 Parent(s): 18a8ab6

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +162 -80
app.py CHANGED
@@ -23,7 +23,7 @@ def generate_dummy_spaces(count):
23
  'createdAt': '2023-01-01T00:00:00.000Z',
24
  'hardware': 'cpu',
25
  'user': {
26
- 'avatar_url': None, # 어차피 안 쓰임 (이모지 대체)
27
  'name': 'dummyUser'
28
  }
29
  })
@@ -32,15 +32,15 @@ def generate_dummy_spaces(count):
32
  # Function to fetch Zero-GPU (CPU-based) Spaces from Huggingface with pagination
33
  def fetch_trending_spaces(offset=0, limit=72):
34
  """
35
- hardware=cpu & sort=trending 파라미터로:
36
- - GPU 없는 스페이스만, 트렌딩 순 정렬로 최대 10000개
37
  """
38
  try:
39
  url = "https://huggingface.co/api/spaces"
40
  params = {
41
- "limit": 10000,
42
- "hardware": "cpu",
43
- "sort": "trending" # 트렌딩 기준
44
  }
45
  response = requests.get(url, params=params, timeout=30)
46
 
@@ -54,10 +54,11 @@ def fetch_trending_spaces(offset=0, limit=72):
54
  and space.get('id', '').split('/', 1)[0] != 'None'
55
  ]
56
 
 
57
  start = min(offset, len(filtered_spaces))
58
  end = min(offset + limit, len(filtered_spaces))
59
 
60
- print(f"[fetch_trending_spaces] CPU기반 스페이스 총 {len(filtered_spaces)}개(트렌딩 정렬), "
61
  f"요청 구간 {start}~{end-1} 반환")
62
 
63
  return {
@@ -65,7 +66,7 @@ def fetch_trending_spaces(offset=0, limit=72):
65
  'total': len(filtered_spaces),
66
  'offset': offset,
67
  'limit': limit,
68
- 'all_spaces': filtered_spaces # 통계용 (전체)
69
  }
70
  else:
71
  print(f"Error fetching spaces: {response.status_code}")
@@ -108,8 +109,8 @@ def transform_url(owner, name):
108
  # Get space details
109
  def get_space_details(space_data, index, offset):
110
  """
111
- Zero-GPU 스페이스 상세 정보 추출
112
- (description, user info) + rank 계산
113
  """
114
  try:
115
  if '/' in space_data.get('id', ''):
@@ -135,9 +136,9 @@ def get_space_details(space_data, index, offset):
135
  # Description
136
  short_desc = space_data.get('description', '')
137
 
138
- # User info
139
  user_info = space_data.get('user', {})
140
- # avatar_url = user_info.get('avatar_url', '') # <-- 사용하지 않음 (이모지로 대체)
141
  author_name = user_info.get('name') or owner
142
 
143
  return {
@@ -148,7 +149,7 @@ def get_space_details(space_data, index, offset):
148
  'name': name,
149
  'likes_count': likes_count,
150
  'description': short_desc,
151
- # 'avatar_url': avatar_url, # 이모지로 대체
152
  'author_name': author_name,
153
  'rank': offset + index + 1
154
  }
@@ -163,7 +164,7 @@ def get_space_details(space_data, index, offset):
163
  'name': 'error',
164
  'likes_count': 0,
165
  'description': '',
166
- # 'avatar_url': '',
167
  'author_name': 'huggingface',
168
  'rank': offset + index + 1
169
  }
@@ -171,15 +172,10 @@ def get_space_details(space_data, index, offset):
171
  # Get owner statistics from all spaces
172
  def get_owner_stats(all_spaces):
173
  """
174
- all_spaces는 이미 트렌딩 순으로 정렬된 상태.
175
- -> 상위 1000개만 골라서(owner) 개수를 세어
176
- 상위 30명을 산출.
177
  """
178
- # TOP 1000만 추려서 rank<=1000로 간주
179
- top_1000_spaces = all_spaces[:1000]
180
-
181
  owners = []
182
- for space in top_1000_spaces:
183
  if '/' in space.get('id', ''):
184
  owner, _ = space.get('id', '').split('/', 1)
185
  else:
@@ -188,7 +184,10 @@ def get_owner_stats(all_spaces):
188
  if owner != 'None':
189
  owners.append(owner)
190
 
 
191
  owner_counts = Counter(owners)
 
 
192
  top_owners = owner_counts.most_common(30)
193
 
194
  return top_owners
@@ -196,28 +195,32 @@ def get_owner_stats(all_spaces):
196
  # Homepage route
197
  @app.route('/')
198
  def home():
 
 
 
199
  return render_template('index.html')
200
 
201
- # Zero-GPU spaces API
202
  @app.route('/api/trending-spaces', methods=['GET'])
203
  def trending_spaces():
204
  """
205
- hardware=cpu & sort=trending 가져온 리스트에서
206
- 검색, 페이징 적용 + top 1000 집계
207
  """
208
  search_query = request.args.get('search', '').lower()
209
  offset = int(request.args.get('offset', 0))
210
  limit = int(request.args.get('limit', 72)) # Default 72
211
 
 
212
  spaces_data = fetch_trending_spaces(offset, limit)
213
 
 
214
  results = []
215
  for index, space_data in enumerate(spaces_data['spaces']):
216
  space_info = get_space_details(space_data, index, offset)
217
  if not space_info:
218
  continue
219
 
220
- # 검색 필터
221
  if search_query:
222
  if (search_query not in space_info['title'].lower()
223
  and search_query not in space_info['owner'].lower()
@@ -227,7 +230,7 @@ def trending_spaces():
227
 
228
  results.append(space_info)
229
 
230
- # 상위 1000 기준 top 30 owner 집계
231
  top_owners = get_owner_stats(spaces_data.get('all_spaces', []))
232
 
233
  return jsonify({
@@ -245,7 +248,7 @@ if __name__ == '__main__':
245
  # Create templates folder if not exists
246
  os.makedirs('templates', exist_ok=True)
247
 
248
- # index.html 전체 작성 (이모지로 대체, 상위 1000 랭크 통계)
249
  with open('templates/index.html', 'w', encoding='utf-8') as f:
250
  f.write('''<!DOCTYPE html>
251
  <html lang="en">
@@ -808,20 +811,88 @@ if __name__ == '__main__':
808
  }
809
  }
810
 
811
- /* 이모지로 대체하기 위한 클래스 (아바타 대신) */
812
- .emoji-avatar {
813
- font-size: 1.5rem;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
814
  border: 1px solid #ccc;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
815
  width: 32px;
816
  height: 32px;
817
  border-radius: 50%;
 
818
  display: flex;
819
  align-items: center;
820
  justify-content: center;
821
- margin-right: 8px;
822
  }
823
-
824
- /* 기존 아바타 관련 CSS는 제거/미사용 */
825
  </style>
826
  </head>
827
  <body>
@@ -838,14 +909,15 @@ if __name__ == '__main__':
838
 
839
  <div class="mac-content">
840
  <div class="header">
841
- <h1>ZeroGPU Spaces Leaderboard</h1>
842
- <p>Discover CPU-based (No GPU) spaces from Hugging Face</p>
 
843
  </div>
844
 
845
  <!-- Tab Navigation -->
846
  <div class="tab-nav">
847
- <button id="tabTrendingButton" class="tab-button active">Trending</button>
848
- <button id="tabFixedButton" class="tab-button">Picks</button>
849
  </div>
850
 
851
  <!-- Trending(Zero GPU) Tab Content -->
@@ -861,8 +933,7 @@ if __name__ == '__main__':
861
  </div>
862
  <div class="mac-content">
863
  <div class="stats-header">
864
- <!-- 변경된 타이틀(Top 30 Creators by Count Number of TOP 1000 RANK) -->
865
- <div class="stats-title">Top 30 Creators by Count Number of TOP 1000 Rank</div>
866
  <button id="statsToggle" class="stats-toggle">Show Stats</button>
867
  </div>
868
  <div id="statsContent" class="stats-content">
@@ -886,7 +957,7 @@ if __name__ == '__main__':
886
  <div id="pagination" class="pagination"></div>
887
  </div>
888
 
889
- <!-- Fixed Tab Content -->
890
  <div id="fixedTab" class="tab-content">
891
  <div id="fixedGrid" class="grid-container"></div>
892
  </div>
@@ -1232,7 +1303,7 @@ if __name__ == '__main__':
1232
  container.appendChild(errorPlaceholder);
1233
  }
1234
 
1235
- // (1) Trending Tab(Zero GPU) - 이모지로 대체
1236
  function renderGrid(spaces) {
1237
  elements.gridContainer.innerHTML = '';
1238
 
@@ -1251,7 +1322,7 @@ if __name__ == '__main__':
1251
  try {
1252
  const {
1253
  url, title, likes_count, owner, name, rank,
1254
- description, author_name, embedUrl
1255
  } = item;
1256
 
1257
  const gridItem = document.createElement('div');
@@ -1261,49 +1332,64 @@ if __name__ == '__main__':
1261
  const headerDiv = document.createElement('div');
1262
  headerDiv.className = 'grid-header';
1263
 
1264
- // 첫줄: 이모지 아바타 + 제목 + ZERO GPU 배지
1265
- // => 코드에서는 ZERO GPU 배지 대신, 본래 코드를 약간 단순화
1266
- const headerTop = document.createElement('div');
1267
- headerTop.className = 'grid-header-top';
1268
 
1269
- // 왼쪽: 이모지
1270
- const leftWrapper = document.createElement('div');
1271
- leftWrapper.style.display = 'flex';
1272
- leftWrapper.style.alignItems = 'center';
1273
-
1274
  const emojiAvatar = document.createElement('div');
1275
  emojiAvatar.className = 'emoji-avatar';
1276
- emojiAvatar.textContent = '🤖'; // 이모지
1277
- leftWrapper.appendChild(emojiAvatar);
 
 
 
 
 
1278
 
1279
  const titleEl = document.createElement('h3');
 
1280
  titleEl.textContent = title;
1281
  titleEl.title = title;
1282
- leftWrapper.appendChild(titleEl);
1283
 
1284
- headerTop.appendChild(leftWrapper);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1285
 
1286
- // 오른쪽: rank
1287
  const rankBadge = document.createElement('div');
1288
  rankBadge.className = 'rank-badge';
1289
  rankBadge.textContent = `#${rank}`;
1290
- headerTop.appendChild(rankBadge);
1291
 
1292
- headerDiv.appendChild(headerTop);
 
 
 
 
1293
 
1294
- // 둘째줄: owner + likes
1295
- const metaInfo = document.createElement('div');
1296
- metaInfo.className = 'grid-meta';
1297
 
1298
- const ownerEl = document.createElement('div');
1299
- ownerEl.className = 'owner-info';
1300
- ownerEl.textContent = `by ${author_name}`;
1301
- metaInfo.appendChild(ownerEl);
1302
-
1303
- const likesCounter = document.createElement('div');
1304
- likesCounter.className = 'likes-counter';
1305
- likesCounter.innerHTML = `♥ <span>${likes_count}</span>`;
1306
- metaInfo.appendChild(likesCounter);
1307
 
1308
  headerDiv.appendChild(metaInfo);
1309
  gridItem.appendChild(headerDiv);
@@ -1311,10 +1397,7 @@ if __name__ == '__main__':
1311
  // description
1312
  if (description) {
1313
  const descP = document.createElement('p');
1314
- descP.style.padding = '0 15px';
1315
- descP.style.fontSize = '0.85rem';
1316
- descP.style.color = '#444';
1317
- descP.style.marginTop = '6px';
1318
  descP.textContent = description;
1319
  gridItem.appendChild(descP);
1320
  }
@@ -1327,7 +1410,7 @@ if __name__ == '__main__':
1327
  iframeContainer.className = 'iframe-container';
1328
 
1329
  const iframe = document.createElement('iframe');
1330
- iframe.src = embedUrl;
1331
  iframe.title = title;
1332
  iframe.allow = 'accelerometer; camera; encrypted-media; geolocation; gyroscope;';
1333
  iframe.setAttribute('allowfullscreen', '');
@@ -1376,8 +1459,8 @@ if __name__ == '__main__':
1376
  });
1377
  }
1378
 
1379
- // (2) Fixed Tab - 이모지로 대체
1380
  function renderFixedGrid() {
 
1381
  fixedGridContainer.innerHTML = '';
1382
 
1383
  const staticSpaces = [
@@ -1421,17 +1504,16 @@ if __name__ == '__main__':
1421
  staticSpaces.forEach((item) => {
1422
  try {
1423
  const { url, title, likes_count, owner, name, rank } = item;
1424
-
1425
  const gridItem = document.createElement('div');
1426
  gridItem.className = 'grid-item';
1427
 
1428
  const header = document.createElement('div');
1429
  header.className = 'grid-header';
1430
 
1431
- // 첫줄(top): 이모지 + 제목 + rank
1432
  const headerTop = document.createElement('div');
1433
  headerTop.className = 'grid-header-top';
1434
 
 
1435
  const leftWrapper = document.createElement('div');
1436
  leftWrapper.style.display = 'flex';
1437
  leftWrapper.style.alignItems = 'center';
@@ -1455,7 +1537,6 @@ if __name__ == '__main__':
1455
 
1456
  header.appendChild(headerTop);
1457
 
1458
- // 둘째줄(meta): owner + likes
1459
  const metaInfo = document.createElement('div');
1460
  metaInfo.className = 'grid-meta';
1461
 
@@ -1510,12 +1591,13 @@ if __name__ == '__main__':
1510
  linkEl.target = '_blank';
1511
  linkEl.className = 'open-link';
1512
  linkEl.textContent = 'Open in new window';
1513
-
1514
  actions.appendChild(linkEl);
 
1515
  gridItem.appendChild(content);
1516
  gridItem.appendChild(actions);
1517
 
1518
  fixedGridContainer.appendChild(gridItem);
 
1519
  } catch (error) {
1520
  console.error('Fixed tab rendering error:', error);
1521
  }
 
23
  'createdAt': '2023-01-01T00:00:00.000Z',
24
  'hardware': 'cpu',
25
  'user': {
26
+ 'avatar_url': 'https://huggingface.co/front/thumbnails/huggingface/default-avatar.svg',
27
  'name': 'dummyUser'
28
  }
29
  })
 
32
  # Function to fetch Zero-GPU (CPU-based) Spaces from Huggingface with pagination
33
  def fetch_trending_spaces(offset=0, limit=72):
34
  """
35
+ 원본 코드에서 수정: hardware=cpu 파라미터를 추가하여
36
+ GPU 없는(=CPU 전용) 스페이스만 필터링
37
  """
38
  try:
39
  url = "https://huggingface.co/api/spaces"
40
  params = {
41
+ "limit": 10000, # 더 많이 가져오기
42
+ "hardware": "cpu" # <-- Zero GPU(=CPU) 필터 적용
43
+ # "sort": "trending", # 필요시 추가 (HF API 지원 여부에 따라)
44
  }
45
  response = requests.get(url, params=params, timeout=30)
46
 
 
54
  and space.get('id', '').split('/', 1)[0] != 'None'
55
  ]
56
 
57
+ # Slice according to requested offset and limit
58
  start = min(offset, len(filtered_spaces))
59
  end = min(offset + limit, len(filtered_spaces))
60
 
61
+ print(f"[fetch_trending_spaces] CPU기반 스페이스 총 {len(filtered_spaces)}개, "
62
  f"요청 구간 {start}~{end-1} 반환")
63
 
64
  return {
 
66
  'total': len(filtered_spaces),
67
  'offset': offset,
68
  'limit': limit,
69
+ 'all_spaces': filtered_spaces # 통계 산출용
70
  }
71
  else:
72
  print(f"Error fetching spaces: {response.status_code}")
 
109
  # Get space details
110
  def get_space_details(space_data, index, offset):
111
  """
112
+ 원본 코드에서 수정:
113
+ - description, avatar_url, author_name추가 필드를 추출
114
  """
115
  try:
116
  if '/' in space_data.get('id', ''):
 
136
  # Description
137
  short_desc = space_data.get('description', '')
138
 
139
+ # User info (avatar, name)
140
  user_info = space_data.get('user', {})
141
+ avatar_url = user_info.get('avatar_url', '')
142
  author_name = user_info.get('name') or owner
143
 
144
  return {
 
149
  'name': name,
150
  'likes_count': likes_count,
151
  'description': short_desc,
152
+ 'avatar_url': avatar_url,
153
  'author_name': author_name,
154
  'rank': offset + index + 1
155
  }
 
164
  'name': 'error',
165
  'likes_count': 0,
166
  'description': '',
167
+ 'avatar_url': '',
168
  'author_name': 'huggingface',
169
  'rank': offset + index + 1
170
  }
 
172
  # Get owner statistics from all spaces
173
  def get_owner_stats(all_spaces):
174
  """
175
+ 모든 스페이스에서 owner 등장 빈도 상위 30명 추출
 
 
176
  """
 
 
 
177
  owners = []
178
+ for space in all_spaces:
179
  if '/' in space.get('id', ''):
180
  owner, _ = space.get('id', '').split('/', 1)
181
  else:
 
184
  if owner != 'None':
185
  owners.append(owner)
186
 
187
+ # Count occurrences of each owner
188
  owner_counts = Counter(owners)
189
+
190
+ # Get top 30 owners by count
191
  top_owners = owner_counts.most_common(30)
192
 
193
  return top_owners
 
195
  # Homepage route
196
  @app.route('/')
197
  def home():
198
+ """
199
+ index.html 템플릿 렌더링 (���인 페이지)
200
+ """
201
  return render_template('index.html')
202
 
203
+ # Zero-GPU spaces API (원본: 'trending-spaces' -> 이제 CPU only)
204
  @app.route('/api/trending-spaces', methods=['GET'])
205
  def trending_spaces():
206
  """
207
+ hardware=cpu 스페이스 목록을 불러와 검색, 페이징, 통계 등을 적용
 
208
  """
209
  search_query = request.args.get('search', '').lower()
210
  offset = int(request.args.get('offset', 0))
211
  limit = int(request.args.get('limit', 72)) # Default 72
212
 
213
+ # Fetch zero-gpu (cpu) spaces
214
  spaces_data = fetch_trending_spaces(offset, limit)
215
 
216
+ # Process and filter spaces
217
  results = []
218
  for index, space_data in enumerate(spaces_data['spaces']):
219
  space_info = get_space_details(space_data, index, offset)
220
  if not space_info:
221
  continue
222
 
223
+ # Apply search filter if needed
224
  if search_query:
225
  if (search_query not in space_info['title'].lower()
226
  and search_query not in space_info['owner'].lower()
 
230
 
231
  results.append(space_info)
232
 
233
+ # Get owner statistics for all spaces
234
  top_owners = get_owner_stats(spaces_data.get('all_spaces', []))
235
 
236
  return jsonify({
 
248
  # Create templates folder if not exists
249
  os.makedirs('templates', exist_ok=True)
250
 
251
+ # index.html 전체를 새로 작성
252
  with open('templates/index.html', 'w', encoding='utf-8') as f:
253
  f.write('''<!DOCTYPE html>
254
  <html lang="en">
 
811
  }
812
  }
813
 
814
+ .error-emoji-detector {
815
+ position: fixed;
816
+ top: -9999px;
817
+ left: -9999px;
818
+ z-index: -1;
819
+ opacity: 0;
820
+ }
821
+
822
+ /* 추가 레이아웃 수정(아바타, ZERO GPU 뱃지 등)을 위해
823
+ 아래 클래스들을 일부 추가/수정해도 좋지만
824
+ 예시는 위 영역에서 이미 충분히 처리하므로 생략 */
825
+
826
+ /* 다음 부분은 Zero GPU Spaces용 카드 구조에서 활용 */
827
+ .space-header {
828
+ display: flex;
829
+ align-items: center;
830
+ gap: 10px;
831
+ margin-bottom: 4px;
832
+ }
833
+ .avatar-img {
834
+ width: 32px;
835
+ height: 32px;
836
+ border-radius: 50%;
837
+ object-fit: cover;
838
  border: 1px solid #ccc;
839
+ }
840
+ .space-title {
841
+ font-size: 1rem;
842
+ font-weight: 600;
843
+ margin: 0;
844
+ overflow: hidden;
845
+ text-overflow: ellipsis;
846
+ white-space: nowrap;
847
+ max-width: 200px;
848
+ }
849
+ .zero-gpu-badge {
850
+ font-size: 0.7rem;
851
+ background-color: #e6fffa;
852
+ color: #319795;
853
+ border: 1px solid #81e6d9;
854
+ border-radius: 6px;
855
+ padding: 2px 6px;
856
+ font-weight: 600;
857
+ margin-left: 8px;
858
+ }
859
+ .desc-text {
860
+ font-size: 0.85rem;
861
+ color: #444;
862
+ margin: 4px 0;
863
+ line-clamp: 2;
864
+ display: -webkit-box;
865
+ -webkit-box-orient: vertical;
866
+ overflow: hidden;
867
+ }
868
+ .author-name {
869
+ font-size: 0.8rem;
870
+ color: #666;
871
+ }
872
+ .likes-wrapper {
873
+ display: flex;
874
+ align-items: center;
875
+ gap: 4px;
876
+ color: #e53e3e;
877
+ font-weight: bold;
878
+ font-size: 0.85rem;
879
+ }
880
+ .likes-heart {
881
+ font-size: 1rem;
882
+ line-height: 1rem;
883
+ color: #f56565;
884
+ }
885
+ /* 이모지 전용 스타일 (선택사항) */
886
+ .emoji-avatar {
887
+ font-size: 1.2rem;
888
  width: 32px;
889
  height: 32px;
890
  border-radius: 50%;
891
+ border: 1px solid #ccc;
892
  display: flex;
893
  align-items: center;
894
  justify-content: center;
 
895
  }
 
 
896
  </style>
897
  </head>
898
  <body>
 
909
 
910
  <div class="mac-content">
911
  <div class="header">
912
+ <!-- 첫 번째 탭 제목을 Zero GPU Spaces 변경 -->
913
+ <h1>Zero GPU Spaces</h1>
914
+ <p>Discover 'TOP 1,000' Zero GPU(Shared A100) spaces from Hugging Face</p>
915
  </div>
916
 
917
  <!-- Tab Navigation -->
918
  <div class="tab-nav">
919
+ <button id="tabTrendingButton" class="tab-button active">Zero GPU Spaces</button>
920
+ <button id="tabFixedButton" class="tab-button">Fixed Tab</button>
921
  </div>
922
 
923
  <!-- Trending(Zero GPU) Tab Content -->
 
933
  </div>
934
  <div class="mac-content">
935
  <div class="stats-header">
936
+ <div class="stats-title">Top 30 Creators by Count Number of TOP 1000 RANK</div>
 
937
  <button id="statsToggle" class="stats-toggle">Show Stats</button>
938
  </div>
939
  <div id="statsContent" class="stats-content">
 
957
  <div id="pagination" class="pagination"></div>
958
  </div>
959
 
960
+ <!-- Fixed Tab Content (기존 예시 유지) -->
961
  <div id="fixedTab" class="tab-content">
962
  <div id="fixedGrid" class="grid-container"></div>
963
  </div>
 
1303
  container.appendChild(errorPlaceholder);
1304
  }
1305
 
1306
+ // 여기서부터 카드 HTML 구조 (Zero-GPU 전용) 렌더링
1307
  function renderGrid(spaces) {
1308
  elements.gridContainer.innerHTML = '';
1309
 
 
1322
  try {
1323
  const {
1324
  url, title, likes_count, owner, name, rank,
1325
+ description, avatar_url, author_name, embedUrl
1326
  } = item;
1327
 
1328
  const gridItem = document.createElement('div');
 
1332
  const headerDiv = document.createElement('div');
1333
  headerDiv.className = 'grid-header';
1334
 
1335
+ // space-header (로봇 이모지 + 제목 + Zero GPU 배지)
1336
+ const spaceHeader = document.createElement('div');
1337
+ spaceHeader.className = 'space-header';
 
1338
 
1339
+ // 로봇 이모지 대체
 
 
 
 
1340
  const emojiAvatar = document.createElement('div');
1341
  emojiAvatar.className = 'emoji-avatar';
1342
+ emojiAvatar.textContent = '🤖';
1343
+ spaceHeader.appendChild(emojiAvatar);
1344
+
1345
+ // 제목+배지
1346
+ const titleWrapper = document.createElement('div');
1347
+ titleWrapper.style.display = 'flex';
1348
+ titleWrapper.style.alignItems = 'center';
1349
 
1350
  const titleEl = document.createElement('h3');
1351
+ titleEl.className = 'space-title';
1352
  titleEl.textContent = title;
1353
  titleEl.title = title;
1354
+ titleWrapper.appendChild(titleEl);
1355
 
1356
+ const zeroGpuBadge = document.createElement('span');
1357
+ zeroGpuBadge.className = 'zero-gpu-badge';
1358
+ zeroGpuBadge.textContent = 'ZERO GPU';
1359
+ titleWrapper.appendChild(zeroGpuBadge);
1360
+
1361
+ spaceHeader.appendChild(titleWrapper);
1362
+ headerDiv.appendChild(spaceHeader);
1363
+
1364
+ // rank + author + likes
1365
+ const metaInfo = document.createElement('div');
1366
+ metaInfo.className = 'grid-meta';
1367
+ metaInfo.style.display = 'flex';
1368
+ metaInfo.style.justifyContent = 'space-between';
1369
+ metaInfo.style.alignItems = 'center';
1370
+ metaInfo.style.marginTop = '6px';
1371
+
1372
+ // 왼쪽: rank + author
1373
+ const leftMeta = document.createElement('div');
1374
 
 
1375
  const rankBadge = document.createElement('div');
1376
  rankBadge.className = 'rank-badge';
1377
  rankBadge.textContent = `#${rank}`;
1378
+ leftMeta.appendChild(rankBadge);
1379
 
1380
+ const authorSpan = document.createElement('span');
1381
+ authorSpan.className = 'author-name';
1382
+ authorSpan.style.marginLeft = '8px';
1383
+ authorSpan.textContent = `by ${author_name}`;
1384
+ leftMeta.appendChild(authorSpan);
1385
 
1386
+ metaInfo.appendChild(leftMeta);
 
 
1387
 
1388
+ // 오른쪽: likes
1389
+ const likesDiv = document.createElement('div');
1390
+ likesDiv.className = 'likes-wrapper';
1391
+ likesDiv.innerHTML = `<span class="likes-heart">♥</span><span>${likes_count}</span>`;
1392
+ metaInfo.appendChild(likesDiv);
 
 
 
 
1393
 
1394
  headerDiv.appendChild(metaInfo);
1395
  gridItem.appendChild(headerDiv);
 
1397
  // description
1398
  if (description) {
1399
  const descP = document.createElement('p');
1400
+ descP.className = 'desc-text';
 
 
 
1401
  descP.textContent = description;
1402
  gridItem.appendChild(descP);
1403
  }
 
1410
  iframeContainer.className = 'iframe-container';
1411
 
1412
  const iframe = document.createElement('iframe');
1413
+ iframe.src = embedUrl; // transformed direct URL
1414
  iframe.title = title;
1415
  iframe.allow = 'accelerometer; camera; encrypted-media; geolocation; gyroscope;';
1416
  iframe.setAttribute('allowfullscreen', '');
 
1459
  });
1460
  }
1461
 
 
1462
  function renderFixedGrid() {
1463
+ // Fixed Tab 예시용 (원본 코드 예시 유지)
1464
  fixedGridContainer.innerHTML = '';
1465
 
1466
  const staticSpaces = [
 
1504
  staticSpaces.forEach((item) => {
1505
  try {
1506
  const { url, title, likes_count, owner, name, rank } = item;
 
1507
  const gridItem = document.createElement('div');
1508
  gridItem.className = 'grid-item';
1509
 
1510
  const header = document.createElement('div');
1511
  header.className = 'grid-header';
1512
 
 
1513
  const headerTop = document.createElement('div');
1514
  headerTop.className = 'grid-header-top';
1515
 
1516
+ // 로봇 이모지 + 타이틀 함께 표시
1517
  const leftWrapper = document.createElement('div');
1518
  leftWrapper.style.display = 'flex';
1519
  leftWrapper.style.alignItems = 'center';
 
1537
 
1538
  header.appendChild(headerTop);
1539
 
 
1540
  const metaInfo = document.createElement('div');
1541
  metaInfo.className = 'grid-meta';
1542
 
 
1591
  linkEl.target = '_blank';
1592
  linkEl.className = 'open-link';
1593
  linkEl.textContent = 'Open in new window';
 
1594
  actions.appendChild(linkEl);
1595
+
1596
  gridItem.appendChild(content);
1597
  gridItem.appendChild(actions);
1598
 
1599
  fixedGridContainer.appendChild(gridItem);
1600
+
1601
  } catch (error) {
1602
  console.error('Fixed tab rendering error:', error);
1603
  }