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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +80 -184
app.py CHANGED
@@ -1,78 +1,41 @@
1
  from flask import Flask, render_template, request, jsonify, session
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)
9
  app.permanent_session_lifetime = timedelta(days=7)
10
 
11
- # Hugging Face URL λͺ©λ‘
 
12
  HUGGINGFACE_URLS = [
13
- "https://huggingface.co/spaces/ginipick/Tech_Hangman_Game",
14
- "https://huggingface.co/spaces/openfree/deepseek_r1_API",
15
- "https://huggingface.co/spaces/ginipick/open_Deep-Research",
16
- "https://huggingface.co/spaces/aiqmaster/open-deep-research",
17
- "https://huggingface.co/spaces/seawolf2357/DeepSeek-R1-32b-search",
18
- "https://huggingface.co/spaces/ginigen/LLaDA",
19
- "https://huggingface.co/spaces/VIDraft/PHI4-Multimodal",
20
- "https://huggingface.co/spaces/ginigen/Ovis2-8B",
21
- "https://huggingface.co/spaces/ginigen/Graph-Mind",
22
- "https://huggingface.co/spaces/ginigen/Workflow-Canvas",
23
- "https://huggingface.co/spaces/ginigen/Design",
24
- "https://huggingface.co/spaces/ginigen/Diagram",
25
- "https://huggingface.co/spaces/ginigen/Mockup",
26
- "https://huggingface.co/spaces/ginigen/Infographic",
27
- "https://huggingface.co/spaces/ginigen/Flowchart",
28
- "https://huggingface.co/spaces/aiqcamp/FLUX-Vision",
29
- "https://huggingface.co/spaces/ginigen/VoiceClone-TTS",
30
- "https://huggingface.co/spaces/openfree/Perceptron-Network",
31
- "https://huggingface.co/spaces/openfree/Article-Generator",
32
  ]
33
 
34
- # URLμ—μ„œ λͺ¨λΈ/슀페이슀 정보 μΆ”μΆœ
35
- def extract_model_info(url):
36
- parts = url.split('/')
37
- if len(parts) < 6:
38
- return None
39
-
40
- if parts[3] == 'spaces' or parts[3] == 'models':
41
- return {
42
- 'type': parts[3],
43
- 'owner': parts[4],
44
- 'repo': parts[5],
45
- 'full_id': f"{parts[4]}/{parts[5]}"
46
- }
47
- elif len(parts) >= 5:
48
- return {
49
- 'type': 'models',
50
- 'owner': parts[3],
51
- 'repo': parts[4],
52
- 'full_id': f"{parts[3]}/{parts[4]}"
53
- }
54
-
55
- return None
56
-
57
  # URL의 λ§ˆμ§€λ§‰ 뢀뢄을 제λͺ©μœΌλ‘œ μΆ”μΆœ
58
  def extract_title(url):
59
  parts = url.split("/")
60
  title = parts[-1] if parts else ""
61
  return title.replace("_", " ").replace("-", " ")
62
 
63
- # ν—ˆκΉ…νŽ˜μ΄μŠ€ μ‚¬μš©μž 인증
64
- def validate_token(token):
65
- headers = {"Authorization": f"Bearer {token}"}
66
-
67
- try:
68
- response = requests.get("https://huggingface.co/api/whoami-v2", headers=headers)
69
- if response.ok:
70
- return True, response.json()
71
- except Exception as e:
72
- print(f"토큰 검증 였λ₯˜: {e}")
73
-
74
- return False, None
75
-
76
  @app.route('/')
77
  def home():
78
  return render_template('index.html')
@@ -84,30 +47,16 @@ def login():
84
  if not token:
85
  return jsonify({'success': False, 'message': '토큰을 μž…λ ₯ν•΄μ£Όμ„Έμš”.'})
86
 
87
- is_valid, user_info = validate_token(token)
88
-
89
- if not is_valid or not user_info:
90
  return jsonify({'success': False, 'message': 'μœ νš¨ν•˜μ§€ μ•Šμ€ ν† ν°μž…λ‹ˆλ‹€.'})
91
 
92
- # μ‚¬μš©μž 이름 μ°ΎκΈ°
93
- username = None
94
- if 'name' in user_info:
95
- username = user_info['name']
96
- elif 'user' in user_info and 'username' in user_info['user']:
97
- username = user_info['user']['username']
98
- elif 'username' in user_info:
99
- username = user_info['username']
100
- else:
101
- username = '인증된 μ‚¬μš©μž'
102
 
103
  # μ„Έμ…˜μ— μ €μž₯
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
@@ -115,30 +64,20 @@ def login():
115
 
116
  @app.route('/api/logout', methods=['POST'])
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'])
124
  def get_urls():
125
  results = []
126
- for url in HUGGINGFACE_URLS:
 
 
127
  title = extract_title(url)
128
- model_info = extract_model_info(url)
129
-
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
 
@@ -155,38 +94,19 @@ def toggle_like():
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():
@@ -560,7 +480,6 @@ if __name__ == '__main__':
560
  // μ• ν”Œλ¦¬μΌ€μ΄μ…˜ μƒνƒœ
561
  const state = {
562
  username: null,
563
- likedURLs: {},
564
  allURLs: [],
565
  isLoading: false,
566
  viewMode: 'all' // 'all' λ˜λŠ” 'liked'
@@ -596,7 +515,7 @@ if __name__ == '__main__':
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;
@@ -615,9 +534,6 @@ if __name__ == '__main__':
615
  elements.loggedInSection.style.display = 'block';
616
  elements.likeStatus.style.display = 'block';
617
 
618
- // μ’‹μ•„μš” λͺ©λ‘ κ°€μ Έμ˜€κΈ°
619
- await getLikedUrls();
620
-
621
  // URL λͺ©λ‘ λ‘œλ“œ
622
  loadUrls();
623
  }
@@ -626,23 +542,6 @@ if __name__ == '__main__':
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()) {
@@ -673,9 +572,6 @@ if __name__ == '__main__':
673
 
674
  showMessage(`${state.username}λ‹˜μœΌλ‘œ λ‘œκ·ΈμΈλ˜μ—ˆμŠ΅λ‹ˆλ‹€.`);
675
 
676
- // μ’‹μ•„μš” λͺ©λ‘ κ°€μ Έμ˜€κΈ°
677
- await getLikedUrls();
678
-
679
  // URL λͺ©λ‘ λ‘œλ“œ
680
  loadUrls();
681
  } else {
@@ -702,7 +598,6 @@ if __name__ == '__main__':
702
 
703
  if (data.success) {
704
  state.username = null;
705
- state.likedURLs = {};
706
  state.allURLs = [];
707
 
708
  elements.currentUser.textContent = 'λ‘œκ·ΈμΈλ˜μ§€ μ•ŠμŒ';
@@ -735,13 +630,6 @@ if __name__ == '__main__':
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
 
@@ -761,10 +649,10 @@ if __name__ == '__main__':
761
 
762
  // 필터링 적용
763
  const filteredUrls = state.allURLs.filter(item => {
764
- const { url, title } = item;
765
 
766
  // μ’‹μ•„μš” 필터링 (μ’‹μ•„μš”λ§Œ 보기 λͺ¨λ“œ)
767
- if (state.viewMode === 'liked' && !state.likedURLs[url]) {
768
  return false;
769
  }
770
 
@@ -800,23 +688,34 @@ if __name__ == '__main__':
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);
@@ -853,12 +752,11 @@ if __name__ == '__main__':
853
  }
854
 
855
  urls.forEach(item => {
856
- const { url, title } = item;
857
- const isLiked = state.likedURLs[url] || false;
858
 
859
  // μΉ΄λ“œ 생성
860
  const card = document.createElement('div');
861
- card.className = `card ${isLiked ? 'liked' : ''}`;
862
 
863
  // μΉ΄λ“œ 헀더
864
  const cardHeader = document.createElement('div');
@@ -874,18 +772,16 @@ if __name__ == '__main__':
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();
@@ -895,7 +791,7 @@ linkEl.href = url;
895
  card.appendChild(likeBtn);
896
 
897
  // μ’‹μ•„μš” λ°°μ§€ (μ’‹μ•„μš” μƒνƒœμΌ λ•Œλ§Œ)
898
- if (isLiked) {
899
  const likeBadge = document.createElement('div');
900
  likeBadge.className = 'like-badge';
901
  likeBadge.textContent = 'μ’‹μ•„μš”';
@@ -919,7 +815,7 @@ linkEl.href = url;
919
  filterAndRenderCards();
920
  }
921
 
922
- // 이벀트 λ¦¬μŠ€λ„ˆ μ„€μ •
923
  elements.loginButton.addEventListener('click', () => {
924
  login(elements.tokenInput.value);
925
  });
@@ -952,4 +848,4 @@ linkEl.href = url;
952
  ''')
953
 
954
  # ν—ˆκΉ…νŽ˜μ΄μŠ€ μŠ€νŽ˜μ΄μŠ€μ—μ„œλŠ” 7860 포트 μ‚¬μš©
955
- app.run(host='0.0.0.0', port=7860)
 
1
  from flask import Flask, render_template, request, jsonify, session
 
2
  import os
3
  from datetime import timedelta
 
4
 
5
  app = Flask(__name__)
6
  app.secret_key = os.urandom(24)
7
  app.permanent_session_lifetime = timedelta(days=7)
8
 
9
+ # Hugging Face URL λͺ©λ‘ - 일뢀 URL은 미리 μ’‹μ•„μš” μƒνƒœλ‘œ μ„€μ •
10
+ # 'is_liked'λ₯Ό true둜 μ„€μ •ν•œ URL은 항상 μ’‹μ•„μš” μƒνƒœλ‘œ ν‘œμ‹œλ¨
11
  HUGGINGFACE_URLS = [
12
+ {"url": "https://huggingface.co/spaces/ginipick/Tech_Hangman_Game", "is_liked": True},
13
+ {"url": "https://huggingface.co/spaces/openfree/deepseek_r1_API", "is_liked": False},
14
+ {"url": "https://huggingface.co/spaces/ginipick/open_Deep-Research", "is_liked": True},
15
+ {"url": "https://huggingface.co/spaces/aiqmaster/open-deep-research", "is_liked": False},
16
+ {"url": "https://huggingface.co/spaces/seawolf2357/DeepSeek-R1-32b-search", "is_liked": True},
17
+ {"url": "https://huggingface.co/spaces/ginigen/LLaDA", "is_liked": False},
18
+ {"url": "https://huggingface.co/spaces/VIDraft/PHI4-Multimodal", "is_liked": True},
19
+ {"url": "https://huggingface.co/spaces/ginigen/Ovis2-8B", "is_liked": False},
20
+ {"url": "https://huggingface.co/spaces/ginigen/Graph-Mind", "is_liked": True},
21
+ {"url": "https://huggingface.co/spaces/ginigen/Workflow-Canvas", "is_liked": False},
22
+ {"url": "https://huggingface.co/spaces/ginigen/Design", "is_liked": True},
23
+ {"url": "https://huggingface.co/spaces/ginigen/Diagram", "is_liked": False},
24
+ {"url": "https://huggingface.co/spaces/ginigen/Mockup", "is_liked": True},
25
+ {"url": "https://huggingface.co/spaces/ginigen/Infographic", "is_liked": False},
26
+ {"url": "https://huggingface.co/spaces/ginigen/Flowchart", "is_liked": True},
27
+ {"url": "https://huggingface.co/spaces/aiqcamp/FLUX-Vision", "is_liked": False},
28
+ {"url": "https://huggingface.co/spaces/ginigen/VoiceClone-TTS", "is_liked": True},
29
+ {"url": "https://huggingface.co/spaces/openfree/Perceptron-Network", "is_liked": False},
30
+ {"url": "https://huggingface.co/spaces/openfree/Article-Generator", "is_liked": True},
31
  ]
32
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  # URL의 λ§ˆμ§€λ§‰ 뢀뢄을 제λͺ©μœΌλ‘œ μΆ”μΆœ
34
  def extract_title(url):
35
  parts = url.split("/")
36
  title = parts[-1] if parts else ""
37
  return title.replace("_", " ").replace("-", " ")
38
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  @app.route('/')
40
  def home():
41
  return render_template('index.html')
 
47
  if not token:
48
  return jsonify({'success': False, 'message': '토큰을 μž…λ ₯ν•΄μ£Όμ„Έμš”.'})
49
 
50
+ # κ°„λ‹¨νžˆ 토큰 길이만 검사 (μ‹€μ œλ‘œλŠ” 더 λ³΅μž‘ν•œ 검증 ν•„μš”)
51
+ if len(token) < 5:
 
52
  return jsonify({'success': False, 'message': 'μœ νš¨ν•˜μ§€ μ•Šμ€ ν† ν°μž…λ‹ˆλ‹€.'})
53
 
54
+ # ν† ν°μ˜ 첫 κΈ€μžλ₯Ό μ‚¬μš©μž μ΄λ¦„μœΌλ‘œ μ„€μ • (ν…ŒμŠ€νŠΈμš©)
55
+ username = f"μ‚¬μš©μž_{token[:3]}"
 
 
 
 
 
 
 
 
56
 
57
  # μ„Έμ…˜μ— μ €μž₯
 
58
  session['username'] = username
59
 
 
 
 
 
60
  return jsonify({
61
  'success': True,
62
  'username': username
 
64
 
65
  @app.route('/api/logout', methods=['POST'])
66
  def logout():
 
67
  session.pop('username', None)
 
68
  return jsonify({'success': True})
69
 
70
  @app.route('/api/urls', methods=['GET'])
71
  def get_urls():
72
  results = []
73
+ for url_item in HUGGINGFACE_URLS:
74
+ url = url_item["url"]
75
+ is_liked = url_item["is_liked"]
76
  title = extract_title(url)
 
 
 
 
 
 
 
 
 
77
 
78
  results.append({
79
  'url': url,
80
  'title': title,
 
81
  'is_liked': is_liked
82
  })
83
 
 
94
  if not url:
95
  return jsonify({'success': False, 'message': 'URL이 ν•„μš”ν•©λ‹ˆλ‹€.'})
96
 
97
+ # URL λͺ©λ‘μ—μ„œ ν•΄λ‹Ή URL μ°ΎκΈ°
98
+ for url_item in HUGGINGFACE_URLS:
99
+ if url_item["url"] == url:
100
+ # μ’‹μ•„μš” μƒνƒœ ν† κΈ€
101
+ url_item["is_liked"] = not url_item["is_liked"]
102
+
103
+ return jsonify({
104
+ 'success': True,
105
+ 'is_liked': url_item["is_liked"],
106
+ 'message': 'μ’‹μ•„μš”λ₯Ό μΆ”κ°€ν–ˆμŠ΅λ‹ˆλ‹€.' if url_item["is_liked"] else 'μ’‹μ•„μš”λ₯Ό μ·¨μ†Œν–ˆμŠ΅λ‹ˆλ‹€.'
107
+ })
108
+
109
+ return jsonify({'success': False, 'message': 'ν•΄λ‹Ή URL을 찾을 수 μ—†μŠ΅λ‹ˆλ‹€.'})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
 
111
  @app.route('/api/session-status', methods=['GET'])
112
  def session_status():
 
480
  // μ• ν”Œλ¦¬μΌ€μ΄μ…˜ μƒνƒœ
481
  const state = {
482
  username: null,
 
483
  allURLs: [],
484
  isLoading: false,
485
  viewMode: 'all' // 'all' λ˜λŠ” 'liked'
 
515
  // μ’‹μ•„μš” 톡계 μ—…λ°μ΄νŠΈ
516
  function updateLikeStats() {
517
  const totalCount = state.allURLs.length;
518
+ const likedCount = state.allURLs.filter(item => item.is_liked).length;
519
 
520
  elements.totalUrlCount.textContent = totalCount;
521
  elements.likedUrlCount.textContent = likedCount;
 
534
  elements.loggedInSection.style.display = 'block';
535
  elements.likeStatus.style.display = 'block';
536
 
 
 
 
537
  // URL λͺ©λ‘ λ‘œλ“œ
538
  loadUrls();
539
  }
 
542
  }
543
  }
544
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
545
  // 둜그인 처리
546
  async function login(token) {
547
  if (!token.trim()) {
 
572
 
573
  showMessage(`${state.username}λ‹˜μœΌλ‘œ λ‘œκ·ΈμΈλ˜μ—ˆμŠ΅λ‹ˆλ‹€.`);
574
 
 
 
 
575
  // URL λͺ©λ‘ λ‘œλ“œ
576
  loadUrls();
577
  } else {
 
598
 
599
  if (data.success) {
600
  state.username = null;
 
601
  state.allURLs = [];
602
 
603
  elements.currentUser.textContent = 'λ‘œκ·ΈμΈλ˜μ§€ μ•ŠμŒ';
 
630
  // URL 및 μ’‹μ•„μš” μƒνƒœ μ €μž₯
631
  state.allURLs = urls;
632
 
 
 
 
 
 
 
 
633
  // 필터링 및 λ Œλ”λ§
634
  filterAndRenderCards();
635
 
 
649
 
650
  // 필터링 적용
651
  const filteredUrls = state.allURLs.filter(item => {
652
+ const { url, title, is_liked } = item;
653
 
654
  // μ’‹μ•„μš” 필터링 (μ’‹μ•„μš”λ§Œ 보기 λͺ¨λ“œ)
655
+ if (state.viewMode === 'liked' && !is_liked) {
656
  return false;
657
  }
658
 
 
688
  const data = await handleApiResponse(response);
689
 
690
  if (data.success) {
691
+ // μƒνƒœ κ°μ²΄μ—μ„œ URL μ°ΎκΈ°
692
+ const urlItem = state.allURLs.find(item => item.url === url);
693
+ if (urlItem) {
694
+ // μ’‹μ•„μš” μƒνƒœ μ—…λ°μ΄νŠΈ
695
+ urlItem.is_liked = data.is_liked;
696
+
697
+ // μΉ΄λ“œ UI μ—…λ°μ΄νŠΈ
698
+ if (data.is_liked) {
699
+ card.classList.add('liked');
700
+ const likeBtn = card.querySelector('.like-button');
701
+ if (likeBtn) likeBtn.classList.add('liked');
702
+
703
+ // μ’‹μ•„μš” λ°°μ§€ μΆ”κ°€
704
+ if (!card.querySelector('.like-badge')) {
705
+ const likeBadge = document.createElement('div');
706
+ likeBadge.className = 'like-badge';
707
+ likeBadge.textContent = 'μ’‹μ•„μš”';
708
+ card.appendChild(likeBadge);
709
+ }
710
+ } else {
711
+ card.classList.remove('liked');
712
+ const likeBtn = card.querySelector('.like-button');
713
+ if (likeBtn) likeBtn.classList.remove('liked');
714
+
715
+ // μ’‹μ•„μš” λ°°μ§€ 제거
716
+ const likeBadge = card.querySelector('.like-badge');
717
+ if (likeBadge) card.removeChild(likeBadge);
718
+ }
719
  }
720
 
721
  showMessage(data.message);
 
752
  }
753
 
754
  urls.forEach(item => {
755
+ const { url, title, is_liked } = item;
 
756
 
757
  // μΉ΄λ“œ 생성
758
  const card = document.createElement('div');
759
+ card.className = `card ${is_liked ? 'liked' : ''}`;
760
 
761
  // μΉ΄λ“œ 헀더
762
  const cardHeader = document.createElement('div');
 
772
 
773
  // URL 링크
774
  const linkEl = document.createElement('a');
775
+ linkEl.href = url;
 
 
776
  linkEl.textContent = url;
777
  linkEl.target = '_blank';
778
  card.appendChild(linkEl);
779
 
780
  // μ’‹μ•„μš” λ²„νŠΌ
781
  const likeBtn = document.createElement('button');
782
+ likeBtn.className = `like-button ${is_liked ? 'liked' : ''}`;
783
  likeBtn.innerHTML = 'β™₯';
784
+ likeBtn.title = is_liked ? 'μ’‹μ•„μš” μ·¨μ†Œ' : 'μ’‹μ•„μš”';
785
 
786
  likeBtn.addEventListener('click', (e) => {
787
  e.preventDefault();
 
791
  card.appendChild(likeBtn);
792
 
793
  // μ’‹μ•„μš” λ°°μ§€ (μ’‹μ•„μš” μƒνƒœμΌ λ•Œλ§Œ)
794
+ if (is_liked) {
795
  const likeBadge = document.createElement('div');
796
  likeBadge.className = 'like-badge';
797
  likeBadge.textContent = 'μ’‹μ•„μš”';
 
815
  filterAndRenderCards();
816
  }
817
 
818
+ // 이벀트 λ¦¬μŠ€λ„ˆ μ„€μ •
819
  elements.loginButton.addEventListener('click', () => {
820
  login(elements.tokenInput.value);
821
  });
 
848
  ''')
849
 
850
  # ν—ˆκΉ…νŽ˜μ΄μŠ€ μŠ€νŽ˜μ΄μŠ€μ—μ„œλŠ” 7860 포트 μ‚¬μš©
851
+ app.run(host='0.0.0.0', port=7860)