GitHub Actions commited on
Commit
a2e3783
ยท
1 Parent(s): 78fece2

Auto-deploy from GitHub Actions - 2025-12-18 08:56:34

Browse files
app/routes.py CHANGED
@@ -6487,6 +6487,21 @@ def save_webtoon_milestone_workflow_settings():
6487
  def admin_webtoon_milestones():
6488
  # ๊ณต๊ฐœ ์„ค์ •๋œ ์›น์†Œ์„ค ๋ชฉ๋ก ์กฐํšŒ (์›๋ณธ ํŒŒ์ผ๋งŒ)
6489
  files = UploadedFile.query.filter_by(is_public=True, parent_file_id=None).order_by(UploadedFile.uploaded_at.desc()).all()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6490
  return render_template('admin_webtoon_milestones.html', files=files)
6491
 
6492
  @main_bp.route('/api/webtoons/milestones', methods=['POST'])
@@ -7049,6 +7064,10 @@ def admin_webtoon_milestone_export_xlsx(milestone_id):
7049
  except Exception:
7050
  return None
7051
 
 
 
 
 
7052
  row = 2
7053
  for ep in (schedule_data.get("episodes") or []):
7054
  ep_num = ep.get("episode_num")
@@ -7068,6 +7087,45 @@ def admin_webtoon_milestone_export_xlsx(milestone_id):
7068
  ws2.cell(row=row, column=6, value=int(ep_dur) if ep_dur is not None else None)
7069
  row += 1
7070
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7071
  for st in (ep.get("stages") or []):
7072
  st_key = st.get("stage_key")
7073
  # '์ž‘์—…๋ช…'์€ ๋‹จ๊ณ„ํ‚ค๋ฅผ ํ‘œ์‹œ๋ช…์œผ๋กœ ๋ณ€ํ™˜ํ•œ ๊ฐ’์œผ๋กœ ์ €์žฅ
@@ -7110,7 +7168,37 @@ def admin_webtoon_milestone_export_xlsx(milestone_id):
7110
  "start": ep_start,
7111
  "end": ep_end,
7112
  "is_group": True,
 
7113
  })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7114
  for st in (ep.get("stages") or []):
7115
  st_key = st.get("stage_key")
7116
  st_name = stage_label_map.get(st_key) or st_key
@@ -7123,6 +7211,7 @@ def admin_webtoon_milestone_export_xlsx(milestone_id):
7123
  "start": st_start,
7124
  "end": st_end,
7125
  "is_group": False,
 
7126
  })
7127
 
7128
  if gantt_rows:
@@ -7141,6 +7230,7 @@ def admin_webtoon_milestone_export_xlsx(milestone_id):
7141
  gantt_header_fill = PatternFill("solid", fgColor="EEF2FF")
7142
  gantt_group_fill = PatternFill("solid", fgColor="D2E3FC")
7143
  gantt_stage_fill = PatternFill("solid", fgColor="BFC5CC")
 
7144
 
7145
  def _fmt_md(d):
7146
  return f"{d.month}/{d.day}"
@@ -7189,8 +7279,16 @@ def admin_webtoon_milestone_export_xlsx(milestone_id):
7189
  ws.cell(row=r_idx, column=2, value=gr["stage"])
7190
  if gr["is_group"]:
7191
  ws.cell(row=r_idx, column=1).font = Font(bold=True)
 
 
7192
 
7193
- fill = gantt_group_fill if gr["is_group"] else gantt_stage_fill
 
 
 
 
 
 
7194
 
7195
  if timeline:
7196
  for i, td in enumerate(timeline):
 
6487
  def admin_webtoon_milestones():
6488
  # ๊ณต๊ฐœ ์„ค์ •๋œ ์›น์†Œ์„ค ๋ชฉ๋ก ์กฐํšŒ (์›๋ณธ ํŒŒ์ผ๋งŒ)
6489
  files = UploadedFile.query.filter_by(is_public=True, parent_file_id=None).order_by(UploadedFile.uploaded_at.desc()).all()
6490
+
6491
+ # ๊ฐ ํŒŒ์ผ์— ๋Œ€ํ•ด ์ผ๋ฐ˜/ํœด์ผ ๋งˆ์ผ์Šคํ†ค ๊ตฌ๋ถ„
6492
+ for f in files:
6493
+ f.normal_milestones = []
6494
+ f.holiday_milestones = []
6495
+ for ms in f.milestones:
6496
+ try:
6497
+ data = json.loads(ms.schedule_json) if ms.schedule_json else {}
6498
+ if data.get('apply_holidays'):
6499
+ f.holiday_milestones.append(ms)
6500
+ else:
6501
+ f.normal_milestones.append(ms)
6502
+ except:
6503
+ f.normal_milestones.append(ms)
6504
+
6505
  return render_template('admin_webtoon_milestones.html', files=files)
6506
 
6507
  @main_bp.route('/api/webtoons/milestones', methods=['POST'])
 
7064
  except Exception:
7065
  return None
7066
 
7067
+ # ํœด์ผ ์Šคํƒ€์ผ (๋นจ๊ฐ„์ƒ‰ ํ…์ŠคํŠธ, ์—ฐํ•œ ๋นจ๊ฐ„ ๋ฐฐ๊ฒฝ)
7068
+ holiday_font = Font(color="C62828", italic=True)
7069
+ holiday_fill = PatternFill("solid", fgColor="FFEBEE")
7070
+
7071
  row = 2
7072
  for ep in (schedule_data.get("episodes") or []):
7073
  ep_num = ep.get("episode_num")
 
7087
  ws2.cell(row=row, column=6, value=int(ep_dur) if ep_dur is not None else None)
7088
  row += 1
7089
 
7090
+ # ํšŒ์ฐจ ๊ธฐ๊ฐ„ ๋‚ด ํœด์ผ ์ฐพ๊ธฐ ๋ฐ ์—ฐ์† ๋ธ”๋ก์œผ๋กœ ๋ฌถ๊ธฐ
7091
+ if ep_start and ep_end:
7092
+ ep_holidays = get_holidays_in_range(ep_start, ep_end)
7093
+ if ep_holidays:
7094
+ # ์—ฐ์†๋œ ํœด์ผ์„ ๋ธ”๋ก์œผ๋กœ ๋ฌถ๊ธฐ
7095
+ holiday_blocks = []
7096
+ current_block = None
7097
+ for h_date in sorted(ep_holidays):
7098
+ if current_block is None:
7099
+ current_block = {'start': h_date, 'end': h_date}
7100
+ else:
7101
+ diff = (h_date - current_block['end']).days
7102
+ if diff <= 1: # ์—ฐ์†๋œ ๋‚ ์งœ
7103
+ current_block['end'] = h_date
7104
+ else:
7105
+ holiday_blocks.append(current_block)
7106
+ current_block = {'start': h_date, 'end': h_date}
7107
+ if current_block:
7108
+ holiday_blocks.append(current_block)
7109
+
7110
+ # ํœด์ผ ๋ธ”๋ก ํ–‰ ์ถ”๊ฐ€
7111
+ for block in holiday_blocks:
7112
+ h_start = block['start']
7113
+ h_end = block['end']
7114
+ h_dur = (h_end - h_start).days + 1
7115
+
7116
+ for col in range(1, 7):
7117
+ cell = ws2.cell(row=row, column=col)
7118
+ cell.font = holiday_font
7119
+ cell.fill = holiday_fill
7120
+
7121
+ ws2.cell(row=row, column=1, value=ep_name)
7122
+ ws2.cell(row=row, column=2, value="ํœด์ผ (์ฃผ๋ง/๊ณตํœด์ผ)")
7123
+ ws2.cell(row=row, column=3, value="HOLIDAY")
7124
+ ws2.cell(row=row, column=4, value=h_start)
7125
+ ws2.cell(row=row, column=5, value=h_end)
7126
+ ws2.cell(row=row, column=6, value=h_dur)
7127
+ row += 1
7128
+
7129
  for st in (ep.get("stages") or []):
7130
  st_key = st.get("stage_key")
7131
  # '์ž‘์—…๋ช…'์€ ๋‹จ๊ณ„ํ‚ค๋ฅผ ํ‘œ์‹œ๋ช…์œผ๋กœ ๋ณ€ํ™˜ํ•œ ๊ฐ’์œผ๋กœ ์ €์žฅ
 
7168
  "start": ep_start,
7169
  "end": ep_end,
7170
  "is_group": True,
7171
+ "is_holiday": False,
7172
  })
7173
+
7174
+ # ํšŒ์ฐจ ๊ธฐ๊ฐ„ ๋‚ด ํœด์ผ ๋ธ”๋ก ์ถ”๊ฐ€
7175
+ ep_holidays = get_holidays_in_range(ep_start, ep_end)
7176
+ if ep_holidays:
7177
+ holiday_blocks = []
7178
+ current_block = None
7179
+ for h_date in sorted(ep_holidays):
7180
+ if current_block is None:
7181
+ current_block = {'start': h_date, 'end': h_date}
7182
+ else:
7183
+ diff = (h_date - current_block['end']).days
7184
+ if diff <= 1:
7185
+ current_block['end'] = h_date
7186
+ else:
7187
+ holiday_blocks.append(current_block)
7188
+ current_block = {'start': h_date, 'end': h_date}
7189
+ if current_block:
7190
+ holiday_blocks.append(current_block)
7191
+
7192
+ for block in holiday_blocks:
7193
+ gantt_rows.append({
7194
+ "episode": ep_name,
7195
+ "stage": "ํœด์ผ (์ฃผ๋ง/๊ณตํœด์ผ)",
7196
+ "start": block['start'],
7197
+ "end": block['end'],
7198
+ "is_group": False,
7199
+ "is_holiday": True,
7200
+ })
7201
+
7202
  for st in (ep.get("stages") or []):
7203
  st_key = st.get("stage_key")
7204
  st_name = stage_label_map.get(st_key) or st_key
 
7211
  "start": st_start,
7212
  "end": st_end,
7213
  "is_group": False,
7214
+ "is_holiday": False,
7215
  })
7216
 
7217
  if gantt_rows:
 
7230
  gantt_header_fill = PatternFill("solid", fgColor="EEF2FF")
7231
  gantt_group_fill = PatternFill("solid", fgColor="D2E3FC")
7232
  gantt_stage_fill = PatternFill("solid", fgColor="BFC5CC")
7233
+ gantt_holiday_fill = PatternFill("solid", fgColor="FFCDD2") # ์—ฐํ•œ ๋นจ๊ฐ•
7234
 
7235
  def _fmt_md(d):
7236
  return f"{d.month}/{d.day}"
 
7279
  ws.cell(row=r_idx, column=2, value=gr["stage"])
7280
  if gr["is_group"]:
7281
  ws.cell(row=r_idx, column=1).font = Font(bold=True)
7282
+ elif gr.get("is_holiday"):
7283
+ ws.cell(row=r_idx, column=2).font = Font(color="C62828", italic=True)
7284
 
7285
+ # ํœด์ผ/๊ทธ๋ฃน/์ผ๋ฐ˜ ์Šคํƒ€์ผ ๋ถ„๊ธฐ
7286
+ if gr.get("is_holiday"):
7287
+ fill = gantt_holiday_fill
7288
+ elif gr["is_group"]:
7289
+ fill = gantt_group_fill
7290
+ else:
7291
+ fill = gantt_stage_fill
7292
 
7293
  if timeline:
7294
  for i, td in enumerate(timeline):
templates/admin_webtoon_milestone_detail.html CHANGED
@@ -186,10 +186,15 @@
186
  <!-- ๋ฉ”์ธ ์ปจํ…์ธ  -->
187
  <div class="main-content">
188
  <div class="content-scroll">
189
- <div style="margin-bottom:24px;">
190
- <h1 style="font-size:22px; font-weight:700; margin-bottom:8px;">{{ milestone.file.original_filename }}</h1>
191
- <div style="font-size:13px; color:var(--muted);">
192
- ๋งˆ์ผ์Šคํ†ค ์ƒ์„ธ ({{ milestone.created_at.strftime('%Y-%m-%d') }})
 
 
 
 
 
193
  </div>
194
  </div>
195
 
@@ -204,8 +209,8 @@
204
  <div class="card-header">
205
  <div class="card-title">์ œ์ž‘ ๊ณต์ •ํ‘œ (Gantt)</div>
206
  <div class="view-controls" style="display:flex; gap:8px; align-items:center;">
207
- <a class="btn" href="/admin/webtoons/milestones/result/{{ milestone.id }}/export-xlsx">์—‘์…€ ์ €์žฅ</a>
208
- <button class="btn active" data-mode="Week" onclick="changeViewMode('Week')">Week</button>
209
  <button class="btn" data-mode="Month" onclick="changeViewMode('Month')">Month</button>
210
  </div>
211
  </div>
@@ -233,7 +238,7 @@
233
  : {};
234
  let gantt = null;
235
  let stageKeyMap = {};
236
- let currentViewMode = 'Week';
237
  let translationInterval = null;
238
 
239
  // CSS์™€ ์ผ์น˜ํ•ด์•ผ ํ•จ
@@ -291,7 +296,7 @@
291
 
292
  gantt = new Gantt("#gantt", tasks, {
293
  header_height: HEADER_HEIGHT,
294
- column_width: 38,
295
  step: 24,
296
  view_modes: ['Quarter Day', 'Half Day', 'Day', 'Week', 'Month'],
297
  bar_height: BAR_HEIGHT,
 
186
  <!-- ๋ฉ”์ธ ์ปจํ…์ธ  -->
187
  <div class="main-content">
188
  <div class="content-scroll">
189
+ <div style="margin-bottom:24px; display:flex; justify-content:space-between; align-items:flex-end;">
190
+ <div>
191
+ <h1 style="font-size:22px; font-weight:700; margin-bottom:8px;">{{ milestone.file.original_filename }}</h1>
192
+ <div style="font-size:13px; color:var(--muted);">
193
+ ๋งˆ์ผ์Šคํ†ค ์ƒ์„ธ ({{ milestone.created_at.strftime('%Y-%m-%d') }})
194
+ </div>
195
+ </div>
196
+ <div>
197
+ <a class="btn" href="/admin/webtoons/milestones/result/{{ milestone.id }}/export-xlsx">์—‘์…€ ์ €์žฅ</a>
198
  </div>
199
  </div>
200
 
 
209
  <div class="card-header">
210
  <div class="card-title">์ œ์ž‘ ๊ณต์ •ํ‘œ (Gantt)</div>
211
  <div class="view-controls" style="display:flex; gap:8px; align-items:center;">
212
+ <button class="btn active" data-mode="Day" onclick="changeViewMode('Day')">Day</button>
213
+ <button class="btn" data-mode="Week" onclick="changeViewMode('Week')">Week</button>
214
  <button class="btn" data-mode="Month" onclick="changeViewMode('Month')">Month</button>
215
  </div>
216
  </div>
 
238
  : {};
239
  let gantt = null;
240
  let stageKeyMap = {};
241
+ let currentViewMode = 'Day';
242
  let translationInterval = null;
243
 
244
  // CSS์™€ ์ผ์น˜ํ•ด์•ผ ํ•จ
 
296
 
297
  gantt = new Gantt("#gantt", tasks, {
298
  header_height: HEADER_HEIGHT,
299
+ column_width: currentViewMode === 'Day' ? 32 : 38,
300
  step: 24,
301
  view_modes: ['Quarter Day', 'Half Day', 'Day', 'Week', 'Month'],
302
  bar_height: BAR_HEIGHT,
templates/admin_webtoon_milestones.html CHANGED
@@ -65,11 +65,15 @@
65
  <button class="btn" style="background:#e8f0fe; color:#1a73e8; margin-left:4px;" onclick="openCreateModal({{ file.id }}, '{{ file.original_filename }}', true)">์ƒ์„ฑ(ํœด์ผ)</button>
66
  </td>
67
  <td style="text-align: center;">
68
- {% if file.milestones %}
69
- {% set latest_ms = file.milestones|sort(attribute='created_at', reverse=True)|first %}
70
- <a href="{{ url_for('main.admin_webtoon_milestone_result', milestone_id=latest_ms.id) }}" class="btn" style="background:#e6f4ea; color:#1e8e3e; padding:6px 10px; font-size:12px; display:inline-block;">ํ™•์ธ</a>
71
- <a href="{{ url_for('main.admin_webtoon_milestone_result_holidays', milestone_id=latest_ms.id) }}" class="btn" style="background:#fce8e6; color:#d93025; padding:6px 10px; font-size:12px; margin-left:4px; display:inline-block;">ํ™•์ธ(ํœด์ผ)</a>
72
- {% else %}
 
 
 
 
73
  <span style="color:#bdc1c6; font-size:12px;">-</span>
74
  {% endif %}
75
  </td>
 
65
  <button class="btn" style="background:#e8f0fe; color:#1a73e8; margin-left:4px;" onclick="openCreateModal({{ file.id }}, '{{ file.original_filename }}', true)">์ƒ์„ฑ(ํœด์ผ)</button>
66
  </td>
67
  <td style="text-align: center;">
68
+ {% if file.normal_milestones %}
69
+ {% set latest_normal = file.normal_milestones|sort(attribute='created_at', reverse=True)|first %}
70
+ <a href="{{ url_for('main.admin_webtoon_milestone_result', milestone_id=latest_normal.id) }}" class="btn" style="background:#e6f4ea; color:#1e8e3e; padding:6px 10px; font-size:12px; display:inline-block;">ํ™•์ธ</a>
71
+ {% endif %}
72
+ {% if file.holiday_milestones %}
73
+ {% set latest_holiday = file.holiday_milestones|sort(attribute='created_at', reverse=True)|first %}
74
+ <a href="{{ url_for('main.admin_webtoon_milestone_result_holidays', milestone_id=latest_holiday.id) }}" class="btn" style="background:#fce8e6; color:#d93025; padding:6px 10px; font-size:12px; {% if file.normal_milestones %}margin-left:4px; {% endif %}display:inline-block;">ํ™•์ธ(ํœด์ผ)</a>
75
+ {% endif %}
76
+ {% if not file.normal_milestones and not file.holiday_milestones %}
77
  <span style="color:#bdc1c6; font-size:12px;">-</span>
78
  {% endif %}
79
  </td>