seawolf2357 commited on
Commit
9142e02
Β·
verified Β·
1 Parent(s): ac61d23

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +283 -183
app.py CHANGED
@@ -2,6 +2,7 @@ from flask import Flask, render_template, request, jsonify, session
2
  import requests
3
  import os
4
  from datetime import timedelta
 
5
 
6
  app = Flask(__name__)
7
  app.secret_key = os.urandom(24)
@@ -103,6 +104,10 @@ def login():
103
  session['token'] = token
104
  session['username'] = username
105
 
 
 
 
 
106
  return jsonify({
107
  'success': True,
108
  'username': username
@@ -112,6 +117,7 @@ def login():
112
  def logout():
113
  session.pop('token', None)
114
  session.pop('username', None)
 
115
  return jsonify({'success': True})
116
 
117
  @app.route('/api/urls', methods=['GET'])
@@ -124,18 +130,68 @@ def get_urls():
124
  if not model_info:
125
  continue
126
 
 
 
 
 
 
127
  results.append({
128
  'url': url,
129
  'title': title,
130
- 'model_info': model_info
 
131
  })
132
 
133
  return jsonify(results)
134
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
135
  @app.route('/api/session-status', methods=['GET'])
136
  def session_status():
137
  return jsonify({
138
- 'logged_in': 'token' in session,
139
  'username': session.get('username')
140
  })
141
 
@@ -157,6 +213,7 @@ if __name__ == '__main__':
157
  margin: 0;
158
  padding: 0;
159
  color: #333;
 
160
  }
161
 
162
  .container {
@@ -166,25 +223,11 @@ if __name__ == '__main__':
166
  }
167
 
168
  .header {
169
- background-color: #f0f0f0;
170
- padding: 1rem;
171
- border-radius: 5px;
172
- margin-bottom: 1rem;
173
- }
174
-
175
- .stats-bar {
176
- background-color: #e9f7ef;
177
  padding: 1rem;
178
- border-radius: 5px;
179
  margin-bottom: 1rem;
180
- display: flex;
181
- justify-content: space-between;
182
- align-items: center;
183
- }
184
-
185
- .stats-bar .liked-count {
186
- font-weight: bold;
187
- color: #e74c3c;
188
  }
189
 
190
  .user-controls {
@@ -195,16 +238,20 @@ if __name__ == '__main__':
195
  }
196
 
197
  .filter-controls {
198
- background-color: #f8f9fa;
199
  padding: 1rem;
200
- border-radius: 5px;
201
  margin-bottom: 1rem;
 
 
 
 
202
  }
203
 
204
  input[type="password"],
205
  input[type="text"] {
206
  padding: 0.5rem;
207
- border: 1px solid #ccc;
208
  border-radius: 4px;
209
  margin-right: 5px;
210
  }
@@ -216,6 +263,7 @@ if __name__ == '__main__':
216
  border: none;
217
  border-radius: 4px;
218
  cursor: pointer;
 
219
  }
220
 
221
  button:hover {
@@ -268,8 +316,19 @@ if __name__ == '__main__':
268
  }
269
 
270
  .card.liked {
271
- border-color: #e74c3c;
272
- background-color: #fff9f9;
 
 
 
 
 
 
 
 
 
 
 
273
  }
274
 
275
  .card a {
@@ -277,30 +336,29 @@ if __name__ == '__main__':
277
  color: #2980b9;
278
  word-break: break-all;
279
  display: block;
280
- margin-top: 0.5rem;
281
  }
282
 
283
  .card a:hover {
284
  text-decoration: underline;
285
  }
286
 
287
- .card-title {
288
- margin-top: 0;
289
- color: #333;
290
- font-size: 1.2rem;
291
- padding-right: 30px; /* μ’‹μ•„μš” λ²„νŠΌ 곡간 확보 */
292
- }
293
-
294
  .like-button {
295
  position: absolute;
296
  top: 1rem;
297
  right: 1rem;
 
 
 
 
 
 
298
  border: none;
299
  background: transparent;
300
- font-size: 1.8rem;
301
  cursor: pointer;
302
- transition: all 0.2s;
303
- z-index: 2;
304
  }
305
 
306
  .like-button:hover {
@@ -308,46 +366,54 @@ if __name__ == '__main__':
308
  }
309
 
310
  .like-button.liked {
311
- color: #e74c3c;
312
- text-shadow: 0 0 5px rgba(231, 76, 60, 0.3);
313
  }
314
 
315
- .like-button.not-liked {
316
- color: #ccc;
317
- }
318
-
319
- .like-label {
320
  position: absolute;
321
- top: 10px;
322
- left: 10px;
323
- background-color: #e74c3c;
324
  color: white;
325
- padding: 2px 8px;
326
- border-radius: 10px;
327
  font-size: 0.7rem;
328
  font-weight: bold;
 
 
 
 
 
 
 
 
329
  display: none;
330
  }
331
 
332
- .card.liked .like-label {
333
- display: block;
334
  }
335
 
336
  .status-message {
 
 
 
337
  padding: 1rem;
338
- border-radius: 4px;
339
- margin-bottom: 1rem;
340
  display: none;
 
 
 
341
  }
342
 
343
  .success {
344
- background-color: #dff0d8;
345
- color: #3c763d;
346
  }
347
 
348
  .error {
349
- background-color: #f2dede;
350
- color: #a94442;
351
  }
352
 
353
  .loading {
@@ -356,40 +422,51 @@ if __name__ == '__main__':
356
  left: 0;
357
  right: 0;
358
  bottom: 0;
359
- background-color: rgba(255, 255, 255, 0.7);
360
  display: none;
361
  justify-content: center;
362
  align-items: center;
363
  z-index: 1000;
364
- font-size: 1.5rem;
365
  }
366
 
367
- .login-section {
368
- margin-top: 1rem;
 
 
 
 
 
369
  }
370
 
371
- .logged-in-section {
372
- display: none;
373
- margin-top: 1rem;
374
  }
375
 
376
- .view-toggle {
377
- margin-bottom: 1rem;
378
  display: flex;
379
- gap: 0.5rem;
380
  }
381
 
382
- .view-toggle button {
383
- background-color: #f8f9fa;
 
384
  color: #333;
385
- border: 1px solid #ddd;
386
  }
387
 
388
- .view-toggle button.active {
389
  background-color: #4CAF50;
390
  color: white;
391
  }
392
 
 
 
 
 
 
 
 
 
 
393
  @media (max-width: 768px) {
394
  .user-controls {
395
  flex-direction: column;
@@ -400,14 +477,16 @@ if __name__ == '__main__':
400
  margin-bottom: 1rem;
401
  }
402
 
403
- .card {
404
- width: 100%;
405
  }
406
 
407
- .stats-bar {
408
- flex-direction: column;
409
- gap: 0.5rem;
410
- align-items: flex-start;
 
 
411
  }
412
  }
413
  </style>
@@ -435,26 +514,25 @@ if __name__ == '__main__':
435
  </div>
436
  </div>
437
 
438
- <!-- μ’‹μ•„μš” 톡계 및 ν‘œμ‹œ ν† κΈ€ -->
439
- <div id="statsBar" class="stats-bar" style="display: none;">
440
- <div>
441
- 총 <span id="totalCount">0</span>개 쀑 <span id="likedCount" class="liked-count">0</span>개 μ’‹μ•„μš” 함
442
- </div>
443
- <div class="view-toggle">
444
- <button id="allViewBtn" class="active">전체 보기</button>
445
- <button id="likedViewBtn">μ’‹μ•„μš”λ§Œ 보기</button>
446
- </div>
447
  </div>
448
 
449
  <div class="filter-controls">
450
- <label>
451
- <input type="text" id="searchInput" placeholder="URL λ˜λŠ” 제λͺ©μœΌλ‘œ 검색" style="width: 250px;" />
452
- </label>
 
 
 
 
453
  </div>
454
 
455
  <div id="statusMessage" class="status-message"></div>
456
 
457
- <div id="loadingIndicator" class="loading">처리 쀑...</div>
 
 
458
 
459
  <div id="cardsContainer" class="cards-container"></div>
460
  </div>
@@ -472,11 +550,11 @@ if __name__ == '__main__':
472
  searchInput: document.getElementById('searchInput'),
473
  loginSection: document.getElementById('loginSection'),
474
  loggedInSection: document.getElementById('loggedInSection'),
475
- statsBar: document.getElementById('statsBar'),
476
- totalCount: document.getElementById('totalCount'),
477
- likedCount: document.getElementById('likedCount'),
478
- allViewBtn: document.getElementById('allViewBtn'),
479
- likedViewBtn: document.getElementById('likedViewBtn')
480
  };
481
 
482
  // μ• ν”Œλ¦¬μΌ€μ΄μ…˜ μƒνƒœ
@@ -488,37 +566,6 @@ if __name__ == '__main__':
488
  viewMode: 'all' // 'all' λ˜λŠ” 'liked'
489
  };
490
 
491
- // 둜컬 μŠ€ν† λ¦¬μ§€ ν‚€
492
- function getLikesStorageKey(username) {
493
- return `hf_local_likes_${username}`;
494
- }
495
-
496
- // 둜컬 μŠ€ν† λ¦¬μ§€μ—μ„œ μ’‹μ•„μš” 정보 λ‘œλ“œ
497
- function loadLikesFromStorage() {
498
- if (!state.username) return {};
499
-
500
- const key = getLikesStorageKey(state.username);
501
- const savedLikes = localStorage.getItem(key);
502
- return savedLikes ? JSON.parse(savedLikes) : {};
503
- }
504
-
505
- // 둜컬 μŠ€ν† λ¦¬μ§€μ— μ’‹μ•„μš” 정보 μ €μž₯
506
- function saveLikesToStorage() {
507
- if (!state.username) return;
508
-
509
- const key = getLikesStorageKey(state.username);
510
- localStorage.setItem(key, JSON.stringify(state.likedURLs));
511
- }
512
-
513
- // μ’‹μ•„μš” 톡계 μ—…λ°μ΄νŠΈ
514
- function updateLikeStats() {
515
- const totalCount = state.allURLs.length;
516
- const likedCount = Object.keys(state.likedURLs).length;
517
-
518
- elements.totalCount.textContent = totalCount;
519
- elements.likedCount.textContent = likedCount;
520
- }
521
-
522
  // λ‘œλ”© μƒνƒœ ν‘œμ‹œ ν•¨μˆ˜
523
  function setLoading(isLoading) {
524
  state.isLoading = isLoading;
@@ -546,6 +593,15 @@ if __name__ == '__main__':
546
  return response.json();
547
  }
548
 
 
 
 
 
 
 
 
 
 
549
  // μ„Έμ…˜ μƒνƒœ 확인
550
  async function checkSessionStatus() {
551
  try {
@@ -557,10 +613,10 @@ if __name__ == '__main__':
557
  elements.currentUser.textContent = data.username;
558
  elements.loginSection.style.display = 'none';
559
  elements.loggedInSection.style.display = 'block';
560
- elements.statsBar.style.display = 'flex';
561
 
562
- // 둜컬 μŠ€ν† λ¦¬μ§€μ—μ„œ μ’‹μ•„μš” 정보 λ‘œλ“œ
563
- state.likedURLs = loadLikesFromStorage();
564
 
565
  // URL λͺ©λ‘ λ‘œλ“œ
566
  loadUrls();
@@ -570,6 +626,23 @@ if __name__ == '__main__':
570
  }
571
  }
572
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
573
  // 둜그인 처리
574
  async function login(token) {
575
  if (!token.trim()) {
@@ -593,16 +666,16 @@ if __name__ == '__main__':
593
  if (data.success) {
594
  state.username = data.username;
595
 
596
- // 둜컬 μŠ€ν† λ¦¬μ§€μ—μ„œ μ’‹μ•„μš” 정보 λ‘œλ“œ
597
- state.likedURLs = loadLikesFromStorage();
598
-
599
  elements.currentUser.textContent = state.username;
600
  elements.loginSection.style.display = 'none';
601
  elements.loggedInSection.style.display = 'block';
602
- elements.statsBar.style.display = 'flex';
603
 
604
  showMessage(`${state.username}λ‹˜μœΌλ‘œ λ‘œκ·ΈμΈλ˜μ—ˆμŠ΅λ‹ˆλ‹€.`);
605
 
 
 
 
606
  // URL λͺ©λ‘ λ‘œλ“œ
607
  loadUrls();
608
  } else {
@@ -636,7 +709,7 @@ if __name__ == '__main__':
636
  elements.tokenInput.value = '';
637
  elements.loginSection.style.display = 'block';
638
  elements.loggedInSection.style.display = 'none';
639
- elements.statsBar.style.display = 'none';
640
 
641
  showMessage('λ‘œκ·Έμ•„μ›ƒλ˜μ—ˆμŠ΅λ‹ˆλ‹€.');
642
 
@@ -659,8 +732,16 @@ if __name__ == '__main__':
659
  const response = await fetch('/api/urls');
660
  const urls = await handleApiResponse(response);
661
 
 
662
  state.allURLs = urls;
663
 
 
 
 
 
 
 
 
664
  // 필터링 및 λ Œλ”λ§
665
  filterAndRenderCards();
666
 
@@ -699,7 +780,7 @@ if __name__ == '__main__':
699
  }
700
 
701
  // μ’‹μ•„μš” ν† κΈ€
702
- function toggleLike(url, card) {
703
  if (!state.username) {
704
  showMessage('μ’‹μ•„μš”λ₯Ό ν•˜λ €λ©΄ ν—ˆκΉ…νŽ˜μ΄μŠ€ API ν† ν°μœΌλ‘œ 인증이 ν•„μš”ν•©λ‹ˆλ‹€.', true);
705
  return;
@@ -708,39 +789,47 @@ if __name__ == '__main__':
708
  setLoading(true);
709
 
710
  try {
711
- // ν˜„μž¬ μ’‹μ•„μš” μƒνƒœ 확인
712
- const isCurrentlyLiked = state.likedURLs[url] || false;
 
 
 
 
 
 
 
713
 
714
- // μƒνƒœ ν† κΈ€
715
- if (isCurrentlyLiked) {
716
- delete state.likedURLs[url];
717
- card.classList.remove('liked');
718
- const likeBtn = card.querySelector('.like-button');
719
- if (likeBtn) {
720
- likeBtn.classList.remove('liked');
721
- likeBtn.classList.add('not-liked');
 
 
 
 
 
 
 
 
 
 
722
  }
723
- showMessage('μ’‹μ•„μš”λ₯Ό μ·¨μ†Œν–ˆμŠ΅λ‹ˆλ‹€.');
724
- } else {
725
- state.likedURLs[url] = true;
726
- card.classList.add('liked');
727
- const likeBtn = card.querySelector('.like-button');
728
- if (likeBtn) {
729
- likeBtn.classList.add('liked');
730
- likeBtn.classList.remove('not-liked');
 
731
  }
732
- showMessage('μ’‹μ•„μš”λ₯Ό μΆ”κ°€ν–ˆμŠ΅λ‹ˆλ‹€.');
733
- }
734
-
735
- // 둜컬 μŠ€ν† λ¦¬μ§€μ— μ €μž₯
736
- saveLikesToStorage();
737
-
738
- // μ’‹μ•„μš” 톡계 μ—…λ°μ΄νŠΈ
739
- updateLikeStats();
740
-
741
- // μ’‹μ•„μš”λ§Œ 보기 λͺ¨λ“œμΈ 경우 λͺ©λ‘ λ‹€μ‹œ λ Œλ”λ§
742
- if (state.viewMode === 'liked') {
743
- filterAndRenderCards();
744
  }
745
  } catch (error) {
746
  console.error('μ’‹μ•„μš” ν† κΈ€ 였λ₯˜:', error);
@@ -771,49 +860,60 @@ if __name__ == '__main__':
771
  const card = document.createElement('div');
772
  card.className = `card ${isLiked ? 'liked' : ''}`;
773
 
774
- // μ’‹μ•„μš” 라벨
775
- const likeLabel = document.createElement('span');
776
- likeLabel.className = 'like-label';
777
- likeLabel.textContent = 'μ’‹μ•„μš”';
778
- card.appendChild(likeLabel);
779
 
780
  // 제λͺ©
781
  const titleEl = document.createElement('h3');
782
  titleEl.className = 'card-title';
783
  titleEl.textContent = title;
784
- card.appendChild(titleEl);
 
 
785
 
786
  // URL 링크
787
  const linkEl = document.createElement('a');
788
- linkEl.href = url;
 
 
789
  linkEl.textContent = url;
790
  linkEl.target = '_blank';
791
  card.appendChild(linkEl);
792
 
793
- // μ’‹μ•„μš” λ²„νŠΌ (β™₯ μ•„μ΄μ½˜)
794
  const likeBtn = document.createElement('button');
795
- likeBtn.className = `like-button ${isLiked ? 'liked' : 'not-liked'}`;
796
- likeBtn.textContent = 'β™₯';
797
  likeBtn.title = isLiked ? 'μ’‹μ•„μš” μ·¨μ†Œ' : 'μ’‹μ•„μš”';
798
 
799
- likeBtn.addEventListener('click', function(e) {
800
  e.preventDefault();
801
  toggleLike(url, card);
802
  });
 
803
  card.appendChild(likeBtn);
804
 
 
 
 
 
 
 
 
 
805
  // μΉ΄λ“œ μΆ”κ°€
806
  elements.cardsContainer.appendChild(card);
807
  });
808
  }
809
 
810
- // λͺ¨λ“œ λ³€κ²½ ν•¨μˆ˜
811
  function changeViewMode(mode) {
812
  state.viewMode = mode;
813
 
814
- // λ²„νŠΌ μƒνƒœ μ—…λ°μ΄νŠΈ
815
- elements.allViewBtn.classList.toggle('active', mode === 'all');
816
- elements.likedViewBtn.classList.toggle('active', mode === 'liked');
817
 
818
  // μΉ΄λ“œ λ‹€μ‹œ λ Œλ”λ§
819
  filterAndRenderCards();
@@ -835,14 +935,14 @@ if __name__ == '__main__':
835
 
836
  // 검색 이벀트 λ¦¬μŠ€λ„ˆ
837
  elements.searchInput.addEventListener('input', () => {
838
- // μž…λ ₯ μ§€μ—° 처리 (타이핑할 λ•Œλ§ˆλ‹€ 필터링 λ°©μ§€)
839
  clearTimeout(state.searchTimeout);
840
  state.searchTimeout = setTimeout(filterAndRenderCards, 300);
841
  });
842
 
843
- // 보기 λͺ¨λ“œ μ „ν™˜ λ²„νŠΌ
844
- elements.allViewBtn.addEventListener('click', () => changeViewMode('all'));
845
- elements.likedViewBtn.addEventListener('click', () => changeViewMode('liked'));
846
 
847
  // μ΄ˆκΈ°ν™”
848
  checkSessionStatus();
@@ -852,4 +952,4 @@ if __name__ == '__main__':
852
  ''')
853
 
854
  # ν—ˆκΉ…νŽ˜μ΄μŠ€ μŠ€νŽ˜μ΄μŠ€μ—μ„œλŠ” 7860 포트 μ‚¬μš©
855
- app.run(host='0.0.0.0', port=7860)
 
2
  import requests
3
  import os
4
  from datetime import timedelta
5
+ import json
6
 
7
  app = Flask(__name__)
8
  app.secret_key = os.urandom(24)
 
104
  session['token'] = token
105
  session['username'] = username
106
 
107
+ # μ‚¬μš©μžκ°€ μ’‹μ•„μš”ν•œ URL μƒνƒœλ₯Ό μ„Έμ…˜μ— μ΄ˆκΈ°ν™”
108
+ if 'liked_urls' not in session:
109
+ session['liked_urls'] = {}
110
+
111
  return jsonify({
112
  'success': True,
113
  'username': username
 
117
  def logout():
118
  session.pop('token', None)
119
  session.pop('username', None)
120
+ session.pop('liked_urls', None)
121
  return jsonify({'success': True})
122
 
123
  @app.route('/api/urls', methods=['GET'])
 
130
  if not model_info:
131
  continue
132
 
133
+ # μ‚¬μš©μžμ˜ μ’‹μ•„μš” μƒνƒœ 확인
134
+ is_liked = False
135
+ if 'liked_urls' in session and url in session['liked_urls']:
136
+ is_liked = True
137
+
138
  results.append({
139
  'url': url,
140
  'title': title,
141
+ 'model_info': model_info,
142
+ 'is_liked': is_liked
143
  })
144
 
145
  return jsonify(results)
146
 
147
+ @app.route('/api/toggle-like', methods=['POST'])
148
+ def toggle_like():
149
+ if 'username' not in session:
150
+ return jsonify({'success': False, 'message': '둜그인이 ν•„μš”ν•©λ‹ˆλ‹€.'})
151
+
152
+ data = request.json
153
+ url = data.get('url')
154
+
155
+ if not url:
156
+ return jsonify({'success': False, 'message': 'URL이 ν•„μš”ν•©λ‹ˆλ‹€.'})
157
+
158
+ # μ„Έμ…˜μ—μ„œ μ’‹μ•„μš” 정보 κ°€μ Έμ˜€κΈ°
159
+ liked_urls = session.get('liked_urls', {})
160
+
161
+ # μ’‹μ•„μš” μƒνƒœ ν† κΈ€
162
+ if url in liked_urls:
163
+ del liked_urls[url]
164
+ is_liked = False
165
+ message = 'μ’‹μ•„μš”λ₯Ό μ·¨μ†Œν–ˆμŠ΅λ‹ˆλ‹€.'
166
+ else:
167
+ liked_urls[url] = True
168
+ is_liked = True
169
+ message = 'μ’‹μ•„μš”λ₯Ό μΆ”κ°€ν–ˆμŠ΅λ‹ˆλ‹€.'
170
+
171
+ # μ„Έμ…˜μ— μ’‹μ•„μš” 정보 μ—…λ°μ΄νŠΈ
172
+ session['liked_urls'] = liked_urls
173
+
174
+ return jsonify({
175
+ 'success': True,
176
+ 'is_liked': is_liked,
177
+ 'message': message
178
+ })
179
+
180
+ @app.route('/api/get-likes', methods=['GET'])
181
+ def get_likes():
182
+ if 'username' not in session:
183
+ return jsonify({'success': False, 'message': '둜그인이 ν•„μš”ν•©λ‹ˆλ‹€.'})
184
+
185
+ liked_urls = session.get('liked_urls', {})
186
+ return jsonify({
187
+ 'success': True,
188
+ 'liked_urls': liked_urls
189
+ })
190
+
191
  @app.route('/api/session-status', methods=['GET'])
192
  def session_status():
193
  return jsonify({
194
+ 'logged_in': 'username' in session,
195
  'username': session.get('username')
196
  })
197
 
 
213
  margin: 0;
214
  padding: 0;
215
  color: #333;
216
+ background-color: #f4f5f7;
217
  }
218
 
219
  .container {
 
223
  }
224
 
225
  .header {
226
+ background-color: #fff;
 
 
 
 
 
 
 
227
  padding: 1rem;
228
+ border-radius: 8px;
229
  margin-bottom: 1rem;
230
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
 
 
 
 
 
 
 
231
  }
232
 
233
  .user-controls {
 
238
  }
239
 
240
  .filter-controls {
241
+ background-color: #fff;
242
  padding: 1rem;
243
+ border-radius: 8px;
244
  margin-bottom: 1rem;
245
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
246
+ display: flex;
247
+ justify-content: space-between;
248
+ align-items: center;
249
  }
250
 
251
  input[type="password"],
252
  input[type="text"] {
253
  padding: 0.5rem;
254
+ border: 1px solid #ddd;
255
  border-radius: 4px;
256
  margin-right: 5px;
257
  }
 
263
  border: none;
264
  border-radius: 4px;
265
  cursor: pointer;
266
+ transition: background-color 0.2s;
267
  }
268
 
269
  button:hover {
 
316
  }
317
 
318
  .card.liked {
319
+ border-color: #ff4757;
320
+ background-color: #ffebee;
321
+ }
322
+
323
+ .card-header {
324
+ margin-bottom: 0.5rem;
325
+ padding-right: 40px; /* μ’‹μ•„μš” λ²„νŠΌ 곡간 */
326
+ }
327
+
328
+ .card-title {
329
+ font-size: 1.2rem;
330
+ margin: 0 0 0.5rem 0;
331
+ color: #333;
332
  }
333
 
334
  .card a {
 
336
  color: #2980b9;
337
  word-break: break-all;
338
  display: block;
339
+ font-size: 0.9rem;
340
  }
341
 
342
  .card a:hover {
343
  text-decoration: underline;
344
  }
345
 
 
 
 
 
 
 
 
346
  .like-button {
347
  position: absolute;
348
  top: 1rem;
349
  right: 1rem;
350
+ width: 30px;
351
+ height: 30px;
352
+ display: flex;
353
+ align-items: center;
354
+ justify-content: center;
355
+ border-radius: 50%;
356
  border: none;
357
  background: transparent;
358
+ font-size: 1.5rem;
359
  cursor: pointer;
360
+ transition: all 0.3s ease;
361
+ color: #ddd;
362
  }
363
 
364
  .like-button:hover {
 
366
  }
367
 
368
  .like-button.liked {
369
+ color: #ff4757;
 
370
  }
371
 
372
+ .like-badge {
 
 
 
 
373
  position: absolute;
374
+ top: -5px;
375
+ left: -5px;
376
+ background-color: #ff4757;
377
  color: white;
378
+ padding: 0.2rem 0.5rem;
379
+ border-radius: 4px;
380
  font-size: 0.7rem;
381
  font-weight: bold;
382
+ }
383
+
384
+ .like-status {
385
+ background-color: #fff;
386
+ padding: 1rem;
387
+ border-radius: 8px;
388
+ margin-bottom: 1rem;
389
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
390
  display: none;
391
  }
392
 
393
+ .like-status strong {
394
+ color: #ff4757;
395
  }
396
 
397
  .status-message {
398
+ position: fixed;
399
+ bottom: 20px;
400
+ right: 20px;
401
  padding: 1rem;
402
+ border-radius: 8px;
 
403
  display: none;
404
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15);
405
+ z-index: 1000;
406
+ max-width: 300px;
407
  }
408
 
409
  .success {
410
+ background-color: #4CAF50;
411
+ color: white;
412
  }
413
 
414
  .error {
415
+ background-color: #f44336;
416
+ color: white;
417
  }
418
 
419
  .loading {
 
422
  left: 0;
423
  right: 0;
424
  bottom: 0;
425
+ background-color: rgba(255, 255, 255, 0.8);
426
  display: none;
427
  justify-content: center;
428
  align-items: center;
429
  z-index: 1000;
 
430
  }
431
 
432
+ .spinner {
433
+ width: 40px;
434
+ height: 40px;
435
+ border: 4px solid #f3f3f3;
436
+ border-top: 4px solid #3498db;
437
+ border-radius: 50%;
438
+ animation: spin 1s linear infinite;
439
  }
440
 
441
+ @keyframes spin {
442
+ 0% { transform: rotate(0deg); }
443
+ 100% { transform: rotate(360deg); }
444
  }
445
 
446
+ .filter-toggle {
 
447
  display: flex;
 
448
  }
449
 
450
+ .filter-toggle button {
451
+ margin-right: 0.5rem;
452
+ background-color: #f0f0f0;
453
  color: #333;
 
454
  }
455
 
456
+ .filter-toggle button.active {
457
  background-color: #4CAF50;
458
  color: white;
459
  }
460
 
461
+ .login-section {
462
+ margin-top: 1rem;
463
+ }
464
+
465
+ .logged-in-section {
466
+ display: none;
467
+ margin-top: 1rem;
468
+ }
469
+
470
  @media (max-width: 768px) {
471
  .user-controls {
472
  flex-direction: column;
 
477
  margin-bottom: 1rem;
478
  }
479
 
480
+ .filter-controls {
481
+ flex-direction: column;
482
  }
483
 
484
+ .filter-controls > div {
485
+ margin-bottom: 0.5rem;
486
+ }
487
+
488
+ .card {
489
+ width: 100%;
490
  }
491
  }
492
  </style>
 
514
  </div>
515
  </div>
516
 
517
+ <div id="likeStatus" class="like-status">
518
+ <div id="likeStatsText">총 <span id="totalUrlCount">0</span>개 쀑 <strong><span id="likedUrlCount">0</span>개</strong>의 URL을 μ’‹μ•„μš” ν–ˆμŠ΅λ‹ˆλ‹€.</div>
 
 
 
 
 
 
 
519
  </div>
520
 
521
  <div class="filter-controls">
522
+ <div>
523
+ <input type="text" id="searchInput" placeholder="URL λ˜λŠ” 제λͺ©μœΌλ‘œ 검색" style="width: 300px;" />
524
+ </div>
525
+ <div class="filter-toggle">
526
+ <button id="allUrlsBtn" class="active">전체 보기</button>
527
+ <button id="likedUrlsBtn">μ’‹μ•„μš”λ§Œ 보기</button>
528
+ </div>
529
  </div>
530
 
531
  <div id="statusMessage" class="status-message"></div>
532
 
533
+ <div id="loadingIndicator" class="loading">
534
+ <div class="spinner"></div>
535
+ </div>
536
 
537
  <div id="cardsContainer" class="cards-container"></div>
538
  </div>
 
550
  searchInput: document.getElementById('searchInput'),
551
  loginSection: document.getElementById('loginSection'),
552
  loggedInSection: document.getElementById('loggedInSection'),
553
+ likeStatus: document.getElementById('likeStatus'),
554
+ totalUrlCount: document.getElementById('totalUrlCount'),
555
+ likedUrlCount: document.getElementById('likedUrlCount'),
556
+ allUrlsBtn: document.getElementById('allUrlsBtn'),
557
+ likedUrlsBtn: document.getElementById('likedUrlsBtn')
558
  };
559
 
560
  // μ• ν”Œλ¦¬μΌ€μ΄μ…˜ μƒνƒœ
 
566
  viewMode: 'all' // 'all' λ˜λŠ” 'liked'
567
  };
568
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
569
  // λ‘œλ”© μƒνƒœ ν‘œμ‹œ ν•¨μˆ˜
570
  function setLoading(isLoading) {
571
  state.isLoading = isLoading;
 
593
  return response.json();
594
  }
595
 
596
+ // μ’‹μ•„μš” 톡계 μ—…λ°μ΄νŠΈ
597
+ function updateLikeStats() {
598
+ const totalCount = state.allURLs.length;
599
+ const likedCount = Object.keys(state.likedURLs).length;
600
+
601
+ elements.totalUrlCount.textContent = totalCount;
602
+ elements.likedUrlCount.textContent = likedCount;
603
+ }
604
+
605
  // μ„Έμ…˜ μƒνƒœ 확인
606
  async function checkSessionStatus() {
607
  try {
 
613
  elements.currentUser.textContent = data.username;
614
  elements.loginSection.style.display = 'none';
615
  elements.loggedInSection.style.display = 'block';
616
+ elements.likeStatus.style.display = 'block';
617
 
618
+ // μ’‹μ•„μš” λͺ©λ‘ κ°€μ Έμ˜€κΈ°
619
+ await getLikedUrls();
620
 
621
  // URL λͺ©λ‘ λ‘œλ“œ
622
  loadUrls();
 
626
  }
627
  }
628
 
629
+ // μ’‹μ•„μš” URL κ°€μ Έμ˜€κΈ°
630
+ async function getLikedUrls() {
631
+ try {
632
+ const response = await fetch('/api/get-likes');
633
+ const data = await handleApiResponse(response);
634
+
635
+ if (data.success) {
636
+ state.likedURLs = data.liked_urls || {};
637
+ return true;
638
+ }
639
+ } catch (error) {
640
+ console.error('μ’‹μ•„μš” λͺ©λ‘ κ°€μ Έμ˜€κΈ° 였λ₯˜:', error);
641
+ }
642
+
643
+ return false;
644
+ }
645
+
646
  // 둜그인 처리
647
  async function login(token) {
648
  if (!token.trim()) {
 
666
  if (data.success) {
667
  state.username = data.username;
668
 
 
 
 
669
  elements.currentUser.textContent = state.username;
670
  elements.loginSection.style.display = 'none';
671
  elements.loggedInSection.style.display = 'block';
672
+ elements.likeStatus.style.display = 'block';
673
 
674
  showMessage(`${state.username}λ‹˜μœΌλ‘œ λ‘œκ·ΈμΈλ˜μ—ˆμŠ΅λ‹ˆλ‹€.`);
675
 
676
+ // μ’‹μ•„μš” λͺ©λ‘ κ°€μ Έμ˜€κΈ°
677
+ await getLikedUrls();
678
+
679
  // URL λͺ©λ‘ λ‘œλ“œ
680
  loadUrls();
681
  } else {
 
709
  elements.tokenInput.value = '';
710
  elements.loginSection.style.display = 'block';
711
  elements.loggedInSection.style.display = 'none';
712
+ elements.likeStatus.style.display = 'none';
713
 
714
  showMessage('λ‘œκ·Έμ•„μ›ƒλ˜μ—ˆμŠ΅λ‹ˆλ‹€.');
715
 
 
732
  const response = await fetch('/api/urls');
733
  const urls = await handleApiResponse(response);
734
 
735
+ // URL 및 μ’‹μ•„μš” μƒνƒœ μ €μž₯
736
  state.allURLs = urls;
737
 
738
+ // μ„œλ²„μ—μ„œ 받은 μ’‹μ•„μš” μƒνƒœ μ—…λ°μ΄νŠΈ
739
+ urls.forEach(item => {
740
+ if (item.is_liked) {
741
+ state.likedURLs[item.url] = true;
742
+ }
743
+ });
744
+
745
  // 필터링 및 λ Œλ”λ§
746
  filterAndRenderCards();
747
 
 
780
  }
781
 
782
  // μ’‹μ•„μš” ν† κΈ€
783
+ async function toggleLike(url, card) {
784
  if (!state.username) {
785
  showMessage('μ’‹μ•„μš”λ₯Ό ν•˜λ €λ©΄ ν—ˆκΉ…νŽ˜μ΄μŠ€ API ν† ν°μœΌλ‘œ 인증이 ν•„μš”ν•©λ‹ˆλ‹€.', true);
786
  return;
 
789
  setLoading(true);
790
 
791
  try {
792
+ const response = await fetch('/api/toggle-like', {
793
+ method: 'POST',
794
+ headers: {
795
+ 'Content-Type': 'application/json'
796
+ },
797
+ body: JSON.stringify({ url })
798
+ });
799
+
800
+ const data = await handleApiResponse(response);
801
 
802
+ if (data.success) {
803
+ // μ’‹μ•„μš” μƒνƒœ μ—…λ°μ΄νŠΈ
804
+ if (data.is_liked) {
805
+ state.likedURLs[url] = true;
806
+ card.classList.add('liked');
807
+ const likeBtn = card.querySelector('.like-button');
808
+ if (likeBtn) likeBtn.classList.add('liked');
809
+ const likeBadge = document.createElement('div');
810
+ likeBadge.className = 'like-badge';
811
+ likeBadge.textContent = 'μ’‹μ•„μš”';
812
+ card.appendChild(likeBadge);
813
+ } else {
814
+ delete state.likedURLs[url];
815
+ card.classList.remove('liked');
816
+ const likeBtn = card.querySelector('.like-button');
817
+ if (likeBtn) likeBtn.classList.remove('liked');
818
+ const likeBadge = card.querySelector('.like-badge');
819
+ if (likeBadge) card.removeChild(likeBadge);
820
  }
821
+
822
+ showMessage(data.message);
823
+
824
+ // μ’‹μ•„μš” 톡계 μ—…λ°μ΄νŠΈ
825
+ updateLikeStats();
826
+
827
+ // μ’‹μ•„μš”λ§Œ 보기 λͺ¨λ“œμΈ 경우 λͺ©λ‘ λ‹€μ‹œ 필터링
828
+ if (state.viewMode === 'liked') {
829
+ filterAndRenderCards();
830
  }
831
+ } else {
832
+ showMessage(data.message || 'μ’‹μ•„μš” μ²˜λ¦¬μ— μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€.', true);
 
 
 
 
 
 
 
 
 
 
833
  }
834
  } catch (error) {
835
  console.error('μ’‹μ•„μš” ν† κΈ€ 였λ₯˜:', error);
 
860
  const card = document.createElement('div');
861
  card.className = `card ${isLiked ? 'liked' : ''}`;
862
 
863
+ // μΉ΄λ“œ 헀더
864
+ const cardHeader = document.createElement('div');
865
+ cardHeader.className = 'card-header';
 
 
866
 
867
  // 제λͺ©
868
  const titleEl = document.createElement('h3');
869
  titleEl.className = 'card-title';
870
  titleEl.textContent = title;
871
+ cardHeader.appendChild(titleEl);
872
+
873
+ card.appendChild(cardHeader);
874
 
875
  // URL 링크
876
  const linkEl = document.createElement('a');
877
+
878
+
879
+ linkEl.href = url;
880
  linkEl.textContent = url;
881
  linkEl.target = '_blank';
882
  card.appendChild(linkEl);
883
 
884
+ // μ’‹μ•„μš” λ²„νŠΌ
885
  const likeBtn = document.createElement('button');
886
+ likeBtn.className = `like-button ${isLiked ? 'liked' : ''}`;
887
+ likeBtn.innerHTML = 'β™₯';
888
  likeBtn.title = isLiked ? 'μ’‹μ•„μš” μ·¨μ†Œ' : 'μ’‹μ•„μš”';
889
 
890
+ likeBtn.addEventListener('click', (e) => {
891
  e.preventDefault();
892
  toggleLike(url, card);
893
  });
894
+
895
  card.appendChild(likeBtn);
896
 
897
+ // μ’‹μ•„μš” λ°°μ§€ (μ’‹μ•„μš” μƒνƒœμΌ λ•Œλ§Œ)
898
+ if (isLiked) {
899
+ const likeBadge = document.createElement('div');
900
+ likeBadge.className = 'like-badge';
901
+ likeBadge.textContent = 'μ’‹μ•„μš”';
902
+ card.appendChild(likeBadge);
903
+ }
904
+
905
  // μΉ΄λ“œ μΆ”κ°€
906
  elements.cardsContainer.appendChild(card);
907
  });
908
  }
909
 
910
+ // λ·° λͺ¨λ“œ λ³€κ²½
911
  function changeViewMode(mode) {
912
  state.viewMode = mode;
913
 
914
+ // λ²„νŠΌ ν™œμ„±ν™” μƒνƒœ μ—…λ°μ΄νŠΈ
915
+ elements.allUrlsBtn.classList.toggle('active', mode === 'all');
916
+ elements.likedUrlsBtn.classList.toggle('active', mode === 'liked');
917
 
918
  // μΉ΄λ“œ λ‹€μ‹œ λ Œλ”λ§
919
  filterAndRenderCards();
 
935
 
936
  // 검색 이벀트 λ¦¬μŠ€λ„ˆ
937
  elements.searchInput.addEventListener('input', () => {
938
+ // λ””λ°”μš΄μ‹± (μž…λ ₯ μ§€μ—° 처리)
939
  clearTimeout(state.searchTimeout);
940
  state.searchTimeout = setTimeout(filterAndRenderCards, 300);
941
  });
942
 
943
+ // ν•„ν„° λ²„νŠΌ 이벀트 λ¦¬μŠ€λ„ˆ
944
+ elements.allUrlsBtn.addEventListener('click', () => changeViewMode('all'));
945
+ elements.likedUrlsBtn.addEventListener('click', () => changeViewMode('liked'));
946
 
947
  // μ΄ˆκΈ°ν™”
948
  checkSessionStatus();
 
952
  ''')
953
 
954
  # ν—ˆκΉ…νŽ˜μ΄μŠ€ μŠ€νŽ˜μ΄μŠ€μ—μ„œλŠ” 7860 포트 μ‚¬μš©
955
+ app.run(host='0.0.0.0', port=7860)