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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
<
|
| 191 |
-
|
| 192 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
<
|
| 208 |
-
<button class="btn
|
| 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 = '
|
| 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.
|
| 69 |
-
{% set
|
| 70 |
-
<a href="{{ url_for('main.admin_webtoon_milestone_result', milestone_id=
|
| 71 |
-
|
| 72 |
-
{%
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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>
|