SOY NV AI commited on
Commit
673f124
·
1 Parent(s): 47fd3af

Update photo album and database logic for better Hugging Face integration

Browse files
app/database.py CHANGED
@@ -820,17 +820,20 @@ class PhotoAlbum(db.Model):
820
  id = db.Column(db.Integer, primary_key=True)
821
  title = db.Column(db.String(255), nullable=False)
822
  is_public = db.Column(db.Boolean, default=False, nullable=False)
 
823
  created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
824
  updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
825
 
826
  # 관계
827
- photos = db.relationship('Photo', backref='album', lazy=True, cascade='all, delete-orphan')
 
828
 
829
  def to_dict(self):
830
  return {
831
  'id': self.id,
832
  'title': self.title,
833
  'is_public': self.is_public,
 
834
  'created_at': self.created_at.isoformat() if self.created_at else None,
835
  'updated_at': self.updated_at.isoformat() if self.updated_at else None,
836
  'photo_count': len(self.photos) if self.photos else 0
 
820
  id = db.Column(db.Integer, primary_key=True)
821
  title = db.Column(db.String(255), nullable=False)
822
  is_public = db.Column(db.Boolean, default=False, nullable=False)
823
+ representative_photo_id = db.Column(db.Integer, db.ForeignKey('photo.id'), nullable=True)
824
  created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
825
  updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
826
 
827
  # 관계
828
+ photos = db.relationship('Photo', foreign_keys='Photo.album_id', backref='album', lazy=True, cascade='all, delete-orphan')
829
+ representative_photo = db.relationship('Photo', foreign_keys=[representative_photo_id], post_update=True)
830
 
831
  def to_dict(self):
832
  return {
833
  'id': self.id,
834
  'title': self.title,
835
  'is_public': self.is_public,
836
+ 'representative_photo_id': self.representative_photo_id,
837
  'created_at': self.created_at.isoformat() if self.created_at else None,
838
  'updated_at': self.updated_at.isoformat() if self.updated_at else None,
839
  'photo_count': len(self.photos) if self.photos else 0
app/i18n.py CHANGED
@@ -39,7 +39,10 @@ TRANSLATIONS = {
39
  'file_too_large': 'Vượt quá dung lượng tải lên tối đa (500MB). Vui lòng chia nhỏ tệp để tải lên.',
40
  'db_error': 'Kết nối máy chủ DB đang chậm lại. Chúng tôi sẽ phản hồi nhanh chóng. Vui lòng liên hệ Lee Chang-woo / rarcissus@soymedia.kr',
41
  'no_resource': 'Không tìm thấy tài nguyên.',
42
- 'upload_limit_exceeded': 'Đã vượt quá dung lượng tải lên tối đa.'
 
 
 
43
  },
44
  'ko': {
45
  'photo_album': '사진첩',
@@ -81,7 +84,10 @@ TRANSLATIONS = {
81
  'file_too_large': '업로드 가능한 최대 용량(500MB)을 초과했습니다. 파일을 나누어 업로드해주세요.',
82
  'db_error': 'DB 서버 연결이 느려지고 있습니다. 빠르게 대응하겠습니다. 담당자 이창우 / rarcissus@soymedia.kr 로 연락 주시길 바랍니다',
83
  'no_resource': '리소스를 찾을 수 없습니다.',
84
- 'upload_limit_exceeded': '업로드 가능한 최대 용량을 초과했습니다.'
 
 
 
85
  }
86
  }
87
 
 
39
  'file_too_large': 'Vượt quá dung lượng tải lên tối đa (500MB). Vui lòng chia nhỏ tệp để tải lên.',
40
  'db_error': 'Kết nối máy chủ DB đang chậm lại. Chúng tôi sẽ phản hồi nhanh chóng. Vui lòng liên hệ Lee Chang-woo / rarcissus@soymedia.kr',
41
  'no_resource': 'Không tìm thấy tài nguyên.',
42
+ 'upload_limit_exceeded': 'Đã vượt quá dung lượng tải lên tối đa.',
43
+ 'representative': 'Đại diện',
44
+ 'set_representative': 'Đặt làm đại diện',
45
+ 'thumbnail': 'Ảnh thu nhỏ'
46
  },
47
  'ko': {
48
  'photo_album': '사진첩',
 
84
  'file_too_large': '업로드 가능한 최대 용량(500MB)을 초과했습니다. 파일을 나누어 업로드해주세요.',
85
  'db_error': 'DB 서버 연결이 느려지고 있습니다. 빠르게 대응하겠습니다. 담당자 이창우 / rarcissus@soymedia.kr 로 연락 주시길 바랍니다',
86
  'no_resource': '리소스를 찾을 수 없습니다.',
87
+ 'upload_limit_exceeded': '업로드 가능한 최대 용량을 초과했습니다.',
88
+ 'representative': '대표',
89
+ 'set_representative': '대표 설정',
90
+ 'thumbnail': '썸네일'
91
  }
92
  }
93
 
app/routes.py CHANGED
@@ -184,12 +184,22 @@ def check_db_schema():
184
  try:
185
  from sqlalchemy import inspect, text
186
  inspector = inspect(db.engine)
 
 
187
  columns = [c['name'] for c in inspector.get_columns('user')]
188
  if 'role' not in columns:
189
  print("Adding 'role' column to user table...")
190
  with db.engine.connect() as conn:
191
  conn.execute(text("ALTER TABLE user ADD COLUMN role VARCHAR(20) DEFAULT 'user' NOT NULL"))
192
  conn.commit()
 
 
 
 
 
 
 
 
193
  except Exception as e:
194
  print(f"Schema check error: {e}")
195
 
@@ -6332,6 +6342,11 @@ def admin_photo_album_photo_delete(photo_id):
6332
  photo = Photo.query.get_or_404(photo_id)
6333
  album_id = photo.album_id
6334
 
 
 
 
 
 
6335
  # 실제 파일 삭제
6336
  base_path = os.path.dirname(os.path.dirname(__file__))
6337
  full_file_path = os.path.join(base_path, photo.file_path)
@@ -6363,6 +6378,19 @@ def admin_photo_album_photo_delete(photo_id):
6363
  flash('사진이 삭제되었습니다.', 'success')
6364
  return redirect(url_for('main.admin_photo_album_photos', album_id=album_id))
6365
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6366
  # -----------------------------
6367
  # 사진첩 (사용자용)
6368
  # -----------------------------
 
184
  try:
185
  from sqlalchemy import inspect, text
186
  inspector = inspect(db.engine)
187
+
188
+ # User 테이블
189
  columns = [c['name'] for c in inspector.get_columns('user')]
190
  if 'role' not in columns:
191
  print("Adding 'role' column to user table...")
192
  with db.engine.connect() as conn:
193
  conn.execute(text("ALTER TABLE user ADD COLUMN role VARCHAR(20) DEFAULT 'user' NOT NULL"))
194
  conn.commit()
195
+
196
+ # PhotoAlbum 테이블
197
+ columns = [c['name'] for c in inspector.get_columns('photo_album')]
198
+ if 'representative_photo_id' not in columns:
199
+ print("Adding 'representative_photo_id' column to photo_album table...")
200
+ with db.engine.connect() as conn:
201
+ conn.execute(text("ALTER TABLE photo_album ADD COLUMN representative_photo_id INTEGER REFERENCES photo(id)"))
202
+ conn.commit()
203
  except Exception as e:
204
  print(f"Schema check error: {e}")
205
 
 
6342
  photo = Photo.query.get_or_404(photo_id)
6343
  album_id = photo.album_id
6344
 
6345
+ # 만약 삭제하는 사진이 대표 사진이면 null 처리
6346
+ album = PhotoAlbum.query.get(album_id)
6347
+ if album and album.representative_photo_id == photo_id:
6348
+ album.representative_photo_id = None
6349
+
6350
  # 실제 파일 삭제
6351
  base_path = os.path.dirname(os.path.dirname(__file__))
6352
  full_file_path = os.path.join(base_path, photo.file_path)
 
6378
  flash('사진이 삭제되었습니다.', 'success')
6379
  return redirect(url_for('main.admin_photo_album_photos', album_id=album_id))
6380
 
6381
+ @main_bp.route('/admin/photo-album/photo/<int:photo_id>/representative', methods=['POST'])
6382
+ @admin_required
6383
+ def admin_photo_album_photo_representative(photo_id):
6384
+ """대표 사진 설정"""
6385
+ photo = Photo.query.get_or_404(photo_id)
6386
+ album = PhotoAlbum.query.get_or_404(photo.album_id)
6387
+
6388
+ album.representative_photo_id = photo.id
6389
+ db.session.commit()
6390
+
6391
+ flash('대표 사진이 설정되었습니다.', 'success')
6392
+ return redirect(url_for('main.admin_photo_album_photos', album_id=album.id))
6393
+
6394
  # -----------------------------
6395
  # 사진첩 (사용자용)
6396
  # -----------------------------
templates/admin_photo_album.html CHANGED
@@ -41,6 +41,14 @@
41
  background-color: #fce8e6;
42
  color: #d93025;
43
  }
 
 
 
 
 
 
 
 
44
  </style>
45
  </head>
46
  <body>
@@ -90,6 +98,7 @@
90
  <thead>
91
  <tr>
92
  <th>ID</th>
 
93
  <th>{{ gettext('album_title') }}</th>
94
  <th>{{ gettext('photos_count', count='')|replace(' ', '')|replace('{}', '') }}</th>
95
  <th>{{ gettext('public_status') }}</th>
@@ -101,6 +110,16 @@
101
  {% for album in albums %}
102
  <tr>
103
  <td>{{ album.id }}</td>
 
 
 
 
 
 
 
 
 
 
104
  <td>
105
  <a href="{{ url_for('main.admin_photo_album_photos', album_id=album.id) }}" class="text-decoration-none fw-medium text-dark">
106
  {{ album.title }}
 
41
  background-color: #fce8e6;
42
  color: #d93025;
43
  }
44
+ .admin-thumbnail {
45
+ width: 60px;
46
+ height: 60px;
47
+ object-fit: cover;
48
+ border-radius: 6px;
49
+ background-color: #f1f3f4;
50
+ border: 1px solid #dee2e6;
51
+ }
52
  </style>
53
  </head>
54
  <body>
 
98
  <thead>
99
  <tr>
100
  <th>ID</th>
101
+ <th>{{ gettext('thumbnail') }}</th>
102
  <th>{{ gettext('album_title') }}</th>
103
  <th>{{ gettext('photos_count', count='')|replace(' ', '')|replace('{}', '') }}</th>
104
  <th>{{ gettext('public_status') }}</th>
 
110
  {% for album in albums %}
111
  <tr>
112
  <td>{{ album.id }}</td>
113
+ <td>
114
+ {% set display_photo = album.representative_photo if album.representative_photo else (album.photos[0] if album.photos else None) %}
115
+ {% if display_photo %}
116
+ <img src="{{ url_for('static', filename=display_photo.thumbnail_path.replace('static/', '')) if display_photo.thumbnail_path.startswith('static/') else '/' + display_photo.thumbnail_path }}" class="admin-thumbnail">
117
+ {% else %}
118
+ <div class="admin-thumbnail d-flex align-items-center justify-content-center">
119
+ <span class="text-muted" style="font-size: 0.6rem;">N/A</span>
120
+ </div>
121
+ {% endif %}
122
+ </td>
123
  <td>
124
  <a href="{{ url_for('main.admin_photo_album_photos', album_id=album.id) }}" class="text-decoration-none fw-medium text-dark">
125
  {{ album.title }}
templates/admin_photo_album_photos.html CHANGED
@@ -31,6 +31,15 @@
31
  overflow: hidden;
32
  box-shadow: 0 1px 3px rgba(0,0,0,0.1);
33
  position: relative;
 
 
 
 
 
 
 
 
 
34
  }
35
  .photo-card img {
36
  width: 100%;
@@ -39,6 +48,11 @@
39
  }
40
  .photo-actions {
41
  padding: 10px;
 
 
 
 
 
42
  display: flex;
43
  justify-content: space-between;
44
  align-items: center;
@@ -49,10 +63,23 @@
49
  background: none;
50
  border: none;
51
  font-size: 0.8rem;
 
52
  }
53
  .delete-btn:hover {
54
  text-decoration: underline;
55
  }
 
 
 
 
 
 
 
 
 
 
 
 
56
  .drop-zone {
57
  border: 2px dashed #dadce0;
58
  border-radius: 8px;
@@ -143,17 +170,34 @@
143
 
144
  <div class="photo-grid">
145
  {% for photo in album.photos %}
146
- <div class="photo-card">
 
 
 
 
147
  {% if photo.thumbnail_path %}
148
  <img src="{{ url_for('static', filename=photo.thumbnail_path.replace('static/', '')) if photo.thumbnail_path.startswith('static/') else '/' + photo.thumbnail_path }}" alt="{{ photo.original_filename }}">
149
  {% else %}
150
  <img src="{{ url_for('static', filename=photo.file_path.replace('static/', '')) if photo.file_path.startswith('static/') else '/' + photo.file_path }}" alt="{{ photo.original_filename }}">
151
  {% endif %}
 
152
  <div class="photo-actions">
153
- <span class="text-truncate small text-muted" style="max-width: 120px;" title="{{ photo.original_filename }}">{{ photo.original_filename }}</span>
154
- <form action="{{ url_for('main.admin_photo_album_photo_delete', photo_id=photo.id) }}" method="POST" onsubmit="return confirm('{{ gettext('delete') }}?')">
155
- <button type="submit" class="delete-btn">{{ gettext('delete') }}</button>
156
- </form>
 
 
 
 
 
 
 
 
 
 
 
 
157
  </div>
158
  </div>
159
  {% endfor %}
 
31
  overflow: hidden;
32
  box-shadow: 0 1px 3px rgba(0,0,0,0.1);
33
  position: relative;
34
+ transition: transform 0.2s;
35
+ }
36
+ .photo-card:hover {
37
+ transform: translateY(-2px);
38
+ box-shadow: 0 4px 6px rgba(0,0,0,0.1);
39
+ }
40
+ .photo-card.representative {
41
+ outline: 3px solid #1a73e8;
42
+ outline-offset: -3px;
43
  }
44
  .photo-card img {
45
  width: 100%;
 
48
  }
49
  .photo-actions {
50
  padding: 10px;
51
+ display: flex;
52
+ flex-direction: column;
53
+ gap: 8px;
54
+ }
55
+ .photo-info {
56
  display: flex;
57
  justify-content: space-between;
58
  align-items: center;
 
63
  background: none;
64
  border: none;
65
  font-size: 0.8rem;
66
+ padding: 0;
67
  }
68
  .delete-btn:hover {
69
  text-decoration: underline;
70
  }
71
+ .representative-badge {
72
+ position: absolute;
73
+ top: 10px;
74
+ right: 10px;
75
+ background: #1a73e8;
76
+ color: white;
77
+ padding: 2px 8px;
78
+ border-radius: 4px;
79
+ font-size: 0.75rem;
80
+ font-weight: 500;
81
+ box-shadow: 0 2px 4px rgba(0,0,0,0.2);
82
+ }
83
  .drop-zone {
84
  border: 2px dashed #dadce0;
85
  border-radius: 8px;
 
170
 
171
  <div class="photo-grid">
172
  {% for photo in album.photos %}
173
+ <div class="photo-card {{ 'representative' if album.representative_photo_id == photo.id else '' }}">
174
+ {% if album.representative_photo_id == photo.id %}
175
+ <div class="representative-badge">{{ gettext('representative') }}</div>
176
+ {% endif %}
177
+
178
  {% if photo.thumbnail_path %}
179
  <img src="{{ url_for('static', filename=photo.thumbnail_path.replace('static/', '')) if photo.thumbnail_path.startswith('static/') else '/' + photo.thumbnail_path }}" alt="{{ photo.original_filename }}">
180
  {% else %}
181
  <img src="{{ url_for('static', filename=photo.file_path.replace('static/', '')) if photo.file_path.startswith('static/') else '/' + photo.file_path }}" alt="{{ photo.original_filename }}">
182
  {% endif %}
183
+
184
  <div class="photo-actions">
185
+ <div class="photo-info">
186
+ <span class="text-truncate small text-muted" style="max-width: 150px;" title="{{ photo.original_filename }}">{{ photo.original_filename }}</span>
187
+ </div>
188
+ <div class="d-flex justify-content-between align-items-center mt-1">
189
+ {% if album.representative_photo_id != photo.id %}
190
+ <form action="{{ url_for('main.admin_photo_album_photo_representative', photo_id=photo.id) }}" method="POST">
191
+ <button type="submit" class="btn btn-sm btn-outline-primary py-0 px-2" style="font-size: 0.75rem;">{{ gettext('set_representative') }}</button>
192
+ </form>
193
+ {% else %}
194
+ <span></span>
195
+ {% endif %}
196
+
197
+ <form action="{{ url_for('main.admin_photo_album_photo_delete', photo_id=photo.id) }}" method="POST" onsubmit="return confirm('{{ gettext('delete') }}?')">
198
+ <button type="submit" class="delete-btn">{{ gettext('delete') }}</button>
199
+ </form>
200
+ </div>
201
  </div>
202
  </div>
203
  {% endfor %}
templates/photo_album_list.html CHANGED
@@ -116,11 +116,11 @@
116
  <a href="{{ url_for('main.photo_album_view', album_id=album.id) }}" class="album-card">
117
  <div class="album-thumbnail">
118
  {% if album.photos %}
119
- {% set first_photo = album.photos[0] %}
120
- {% if first_photo.thumbnail_path %}
121
- <img src="/{{ first_photo.thumbnail_path }}" alt="{{ album.title }}">
122
  {% else %}
123
- <img src="/{{ first_photo.file_path }}" alt="{{ album.title }}">
124
  {% endif %}
125
  {% else %}
126
  <div class="text-muted">{{ gettext('no_photos') }}</div>
 
116
  <a href="{{ url_for('main.photo_album_view', album_id=album.id) }}" class="album-card">
117
  <div class="album-thumbnail">
118
  {% if album.photos %}
119
+ {% set display_photo = album.representative_photo if album.representative_photo else album.photos[0] %}
120
+ {% if display_photo.thumbnail_path %}
121
+ <img src="{{ url_for('static', filename=display_photo.thumbnail_path.replace('static/', '')) if display_photo.thumbnail_path.startswith('static/') else '/' + display_photo.thumbnail_path }}" alt="{{ album.title }}">
122
  {% else %}
123
+ <img src="{{ url_for('static', filename=display_photo.file_path.replace('static/', '')) if display_photo.file_path.startswith('static/') else '/' + display_photo.file_path }}" alt="{{ album.title }}">
124
  {% endif %}
125
  {% else %}
126
  <div class="text-muted">{{ gettext('no_photos') }}</div>