2022-10-29 17:21:14 +09:00
{% extends "base.html" %}
{% block content %}
2026-01-02 17:48:58 +09:00
< link rel = "stylesheet" href = "{{ url_for('.static', filename='css/mobile_custom.css') }}" / >
< link rel = "stylesheet" href = "{{ url_for('.static', filename='css/' ~ arg['sub'] ~ '.css') }}" / >
2026-01-04 01:00:17 +09:00
< div id = "ohli24_setting_wrapper" class = "ohli24-common-wrapper container-fluid mt-4 content-cloak" >
2025-12-29 23:08:08 +09:00
< div class = "glass-card p-4" >
2026-01-04 01:00:17 +09:00
< div class = "ohli24-header" >
< div class = "ohli24-header-left" >
< div class = "ohli24-icon-box" >
< i class = "bi bi-gear-fill text-primary" style = "font-size: 1.5rem;" > < / i >
< / div >
< div >
< h3 class = "ohli24-header-title" > Ohli24 설정< / h3 >
< span class = "ohli24-header-subtitle" > 플러그인 설정 및 시스템 상태< / span >
< / div >
< / div >
< div class = "ohli24-header-right" >
2026-01-09 22:18:48 +09:00
< button type = "button" class = "btn btn-outline-info btn-sm mr-2" id = "btn-self-update" title = "최신 버전으로 업데이트" >
< i class = "bi bi-arrow-repeat" > < / i > 업데이트
< / button >
2026-01-04 01:00:17 +09:00
{{ macros.m_button_group([['globalSettingSaveBtn', '설정 저장']])}}
< / div >
2022-10-29 17:21:14 +09:00
< / div >
2025-12-29 23:08:08 +09:00
{{ macros.m_row_start('5') }}
{{ macros.m_row_end() }}
< nav >
{{ macros.m_tab_head_start() }}
{{ macros.m_tab_head('normal', '일반', true) }}
2026-01-08 16:38:24 +09:00
{{ macros.m_tab_head('auto', '자동등록', false) }}
2025-12-29 23:08:08 +09:00
{{ macros.m_tab_head('action', '기타', false) }}
{{ macros.m_tab_head_end() }}
< / nav >
< form id = "setting" class = "mt-4" >
< div class = "tab-content" id = "nav-tabContent" >
{{ macros.m_tab_content_start('normal', true) }}
{{ macros.setting_input_text_and_buttons('ohli24_url', 'ohli24 URL', [['go_btn', 'GO']], value=arg['ohli24_url']) }}
2026-01-01 16:57:48 +09:00
<!-- 저장 폴더 (탐색 버튼 포함) -->
< div class = "row" style = "padding-top: 10px; padding-bottom:10px; align-items: center;" >
< div class = "col-sm-3 set-left" >
< strong > 저장 폴더< / strong >
< / div >
< div class = "col-sm-9" >
< div class = "input-group col-sm-9" >
< input type = "text" class = "form-control form-control-sm" id = "ohli24_download_path" name = "ohli24_download_path" value = "{{arg['ohli24_download_path']}}" >
< div class = "btn-group btn-group-sm flex-wrap mr-2" role = "group" style = "padding-left:5px; padding-top:0px" >
< button type = "button" class = "btn btn-sm btn-outline-primary" id = "browse_folder_btn" title = "폴더 탐색" >
< i class = "bi bi-folder2-open" > < / i > 탐색
< / button >
< / div >
< / div >
< div style = "padding-left:20px; padding-top:5px;" >
< em > 정상적으로 다운 완료 된 파일이 이동할 폴더 입니다.< / em >
< / div >
< / div >
< / div >
2025-12-29 23:08:08 +09:00
{{ macros.setting_input_int('ohli24_max_ffmpeg_process_count', '동시 다운로드 수', value=arg['ohli24_max_ffmpeg_process_count'], desc='동시에 다운로드 할 에피소드 갯수입니다.') }}
2025-12-30 20:34:02 +09:00
{{ macros.setting_input_text('ohli24_proxy_url', 'Proxy URL', value=arg.get('ohli24_proxy_url', ''), desc=['프록시 서버 URL (예: http://192.168.0.2:3138)', '비어있으면 사용 안 함']) }}
2025-12-31 16:01:15 +09:00
{{ macros.setting_input_text('ohli24_discord_webhook_url', 'Discord Webhook URL', value=arg.get('ohli24_discord_webhook_url', ''), desc=['디스코드 알림을 받을 웹후크 주소입니다.', '다운로드 시작 시 알림을 보냅니다.']) }}
{{ macros.setting_select('ohli24_download_method', '다운로드 방법', [['cdndania', 'cdndania (최적화, 기본)'], ['ffmpeg', 'ffmpeg'], ['ytdlp', 'yt-dlp'], ['aria2c', 'aria2c (yt-dlp)']], value=arg.get('ohli24_download_method', 'cdndania'), desc='m3u8 다운로드에 사용할 도구를 선택합니다.') }}
2025-12-30 00:50:13 +09:00
< div id = "ohli24_download_threads_div" >
2025-12-31 16:01:15 +09:00
{{ macros.setting_select('ohli24_download_threads', '다운로드 속도', [['1', '1배속 (1개, 안정)'], ['2', '2배속 (2개, 권장)'], ['4', '4배속 (4개)'], ['8', '8배속 (8개)'], ['16', '16배속 (16개, 불안정)']], value=arg.get('ohli24_download_threads', '2'), desc='cdndania/yt-dlp/aria2c 모드에서 사용할 동시 다운로드 수입니다. CDN 차단 시 1-2개 권장.') }}
2025-12-30 00:50:13 +09:00
< / div >
2025-12-29 23:08:08 +09:00
{{ macros.setting_checkbox('ohli24_order_desc', '요청 화면 최신순 정렬', value=arg['ohli24_order_desc'], desc='On : 최신화부터, Off : 1화부터') }}
{{ macros.setting_checkbox('ohli24_auto_make_folder', '제목 폴더 생성', value=arg['ohli24_auto_make_folder'], desc='제목으로 폴더를 생성하고 폴더 안에 다운로드합니다.') }}
< div id = "ohli24_auto_make_folder_div" class = "collapse pl-4 border-left ml-3" style = "border-color: rgba(255,255,255,0.1) !important;" >
{{ macros.setting_input_text('ohli24_finished_insert', '완결 표시', col='3', value=arg['ohli24_finished_insert'], desc=['완결된 컨텐츠 폴더명 앞에 넣을 문구입니다.']) }}
{{ macros.setting_checkbox('ohli24_auto_make_season_folder', '시즌 폴더 생성', value=arg['ohli24_auto_make_season_folder'], desc=['On : Season 번호 폴더를 만듭니다.']) }}
< / div >
{{ macros.setting_checkbox('ohli24_uncompleted_auto_enqueue', '자동으로 다시 받기', value=arg['ohli24_uncompleted_auto_enqueue'], desc=['On : 플러그인 로딩시 미완료인 항목은 자동으로 다시 받습니다.']) }}
2026-01-06 19:25:59 +09:00
{{ macros.setting_select('ohli24_cache_minutes', 'HTML 캐시 시간', [['0', '캐시 없음'], ['5', '5분'], ['10', '10분'], ['15', '15분'], ['30', '30분'], ['60', '1시간']], value=arg.get('ohli24_cache_minutes', '5'), desc=['브라우징(요청, 검색) 페이지의 HTML을 캐시합니다.', '0으로 설정하면 캐시를 사용하지 않습니다.', '다운로드 루틴은 캐시를 사용하지 않습니다.']) }}
2025-12-29 23:08:08 +09:00
{{ macros.m_tab_content_end() }}
{{ macros.m_tab_content_start('auto', false) }}
{{ macros.global_setting_scheduler_button(arg['scheduler'], arg['is_running']) }}
{{ macros.setting_input_text('ohli24_interval', '스케쥴링 실행 정보', value=arg['ohli24_interval'], col='3', desc=['Inverval(minute 단위)이나 Cron 설정']) }}
{{ macros.setting_checkbox('ohli24_auto_start', '시작시 자동실행', value=arg['ohli24_auto_start'], desc='On : 시작시 자동으로 스케쥴러에 등록됩니다.') }}
2026-01-08 16:38:24 +09:00
<!-- 자동 다운로드 작품 코드 - Tag Chips UI -->
< div class = "row" style = "padding-top: 10px; padding-bottom:10px;" >
< div class = "col-sm-3 set-left" >
< strong > 자동 다운로드할 작품 코드< / strong >
< / div >
< div class = "col-sm-9" >
<!-- 숨겨진 실제 값 필드 (DB 저장용, | 구분) -->
< input type = "hidden" id = "ohli24_auto_code_list" name = "ohli24_auto_code_list" value = "{{arg['ohli24_auto_code_list']}}" >
<!-- Tag Chips 컨테이너 -->
< div id = "tag_chips_container" class = "tag-chips-wrapper mb-2" >
<!-- 태그들이 여기에 동적으로 추가됨 -->
< / div >
<!-- 새 태그 입력 -->
< div class = "input-group input-group-sm" >
< input type = "text" id = "new_tag_input" class = "form-control" placeholder = "작품명 입력 후 Enter 또는 추가 버튼" >
< div class = "input-group-append" >
< button type = "button" class = "btn btn-outline-primary" id = "add_tag_btn" >
< i class = "bi bi-plus-lg" > < / i > 추가
< / button >
< / div >
< / div >
< div style = "padding-top:5px;" >
< em class = "text-muted" > Enter로 추가, 태그 X로 삭제, 드래그로 순서 변경 가능< / em >
< / div >
< / div >
< / div >
2025-12-29 23:08:08 +09:00
{{ macros.setting_checkbox('ohli24_auto_mode_all', '에피소드 모두 받기', value=arg['ohli24_auto_mode_all'], desc=['On : 이전 에피소드를 모두 받습니다.', 'Off : 최신 에피소드만 받습니다.']) }}
{{ macros.m_tab_content_end() }}
{{ macros.m_tab_content_start('action', false) }}
< div class = "p-3" style = "background: rgba(0,0,0,0.2); border-radius: 8px;" >
< h5 class = "text-info mb-3" > < i class = "bi bi-lightning-charge-fill mr-2" > < / i > Actions< / h5 >
{{ macros.setting_buttons([['global_one_execute_btn', '1회 실행']], left='1회 실행' ) }}
< hr style = "border-color: rgba(255,255,255,0.1);" >
{{ macros.setting_buttons([['global_reset_db_btn', 'DB 초기화']], left='DB정리' ) }}
2026-01-03 20:52:39 +09:00
< hr style = "border-color: rgba(255,255,255,0.1);" >
< h5 class = "text-info mb-3" > < i class = "bi bi-cpu-fill mr-2" > < / i > 시스템 상태 및 의존성< / h5 >
< div id = "system_check_result" class = "mb-3 p-3 rounded" style = "background: rgba(0,0,0,0.3); border: 1px solid rgba(255,255,255,0.05);" >
< div class = "d-flex align-items-center mb-2" >
< span class = "mr-2" > Chromium/Chrome:< / span >
< span id = "browser_status_badge" class = "badge badge-secondary" > 확인 중...< / span >
< / div >
< div id = "browser_path_display" class = "small text-muted mb-2" style = "font-family: monospace;" > < / div >
< div id = "install_guide_section" style = "display:none;" >
< p class = "small text-warning mb-2" > < i class = "bi bi-exclamation-triangle-fill mr-1" > < / i > 브라우저가 발견되지 않았습니다. Zendriver 기능을 위해 설치가 필요합니다.< / p >
< div id = "auto_install_div" style = "display:none;" >
< button type = "button" id = "auto_install_btn" class = "btn btn-sm btn-outline-info mb-2" >
< i class = "bi bi-download mr-1" > < / i > 자동 설치 (Ubuntu/Docker)
< / button >
< / div >
< div class = "mt-2" >
< small class = "d-block text-muted mb-1" > 수동 설치 명령어:< / small >
< div class = "input-group input-group-sm" >
< input type = "text" id = "manual_install_cmd" class = "form-control form-control-sm bg-dark border-secondary text-info" readonly value = "apt-get update && apt-get install -y chromium-browser" >
< div class = "input-group-append" >
< button class = "btn btn-outline-secondary" type = "button" id = "copy_cmd_btn" > < i class = "bi bi-clipboard" > < / i > < / button >
< / div >
< / div >
< / div >
< / div >
< / div >
2026-01-03 20:45:58 +09:00
< hr style = "border-color: rgba(255,255,255,0.1);" >
< h5 class = "text-info mb-3" > < i class = "bi bi-browser-chrome mr-2" > < / i > Zendriver 설정< / h5 >
2026-01-03 20:52:39 +09:00
{{ macros.setting_input_text('ohli24_zendriver_browser_path', '브라우저 경로', value=arg.get('ohli24_zendriver_browser_path', ''), desc=['Zendriver가 사용할 Chrome/Chromium 실행 파일 경로입니다.', '위의 시스템 상태에서 자동으로 찾은 경우 비워두셔도 됩니다 (수동 설정 시 우선 적용).']) }}
2025-12-29 23:08:08 +09:00
< / div >
{{ macros.m_tab_content_end() }}
< / div > <!-- tab - content -->
< / form >
< / div >
2022-10-29 17:21:14 +09:00
< / div > <!-- 전체 -->
2026-01-01 16:57:48 +09:00
<!-- 폴더 탐색 모달 -->
< div class = "modal fade" id = "folderBrowserModal" tabindex = "-1" role = "dialog" aria-labelledby = "folderBrowserModalLabel" aria-hidden = "true" >
< div class = "modal-dialog modal-lg" role = "document" >
< div class = "modal-content" style = "background: #1e293b; border: 1px solid rgba(255,255,255,0.1);" >
< div class = "modal-header" style = "border-color: rgba(255,255,255,0.1);" >
< h5 class = "modal-title text-white" id = "folderBrowserModalLabel" >
< i class = "bi bi-folder2-open mr-2" > < / i > 폴더 선택
< / h5 >
< button type = "button" class = "close text-white" data-dismiss = "modal" aria-label = "Close" >
< span aria-hidden = "true" > × < / span >
< / button >
< / div >
< div class = "modal-body" >
<!-- 현재 경로 표시 -->
< div class = "d-flex align-items-center mb-3" >
< button type = "button" class = "btn btn-sm btn-outline-secondary mr-2" id = "folder_go_up" title = "상위 폴더" >
< i class = "bi bi-arrow-up" > < / i >
< / button >
< div class = "flex-grow-1 px-3 py-2 rounded" style = "background: rgba(0,0,0,0.3); font-family: monospace; color: #94a3b8;" >
< span id = "current_path_display" > /< / span >
< / div >
< / div >
<!-- 폴더 목록 -->
< div id = "folder_list" style = "min-height: 300px; max-height: 600px; overflow-y: auto; background: rgba(0,0,0,0.2); border-radius: 8px; padding: 4px;" >
< div class = "text-center text-muted py-4" >
< i class = "bi bi-arrow-repeat spin" > < / i > 로딩 중...
< / div >
< / div >
< / div >
< div class = "modal-footer" style = "border-color: rgba(255,255,255,0.1);" >
< button type = "button" class = "btn btn-secondary" data-dismiss = "modal" > 취소< / button >
< button type = "button" class = "btn btn-primary" id = "folder_select_btn" >
< i class = "bi bi-check-lg mr-1" > < / i > 선택
< / button >
< / div >
< / div >
< / div >
< / div >
2025-12-29 23:08:08 +09:00
< link rel = "stylesheet" href = "https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css" >
< style >
. nav-tabs . nav-link . active {
color : #60a5fa !important ;
background : rgba ( 30 , 41 , 59 , 0.8 ) !important ;
border-bottom : 2 px solid #60a5fa !important ;
}
/* Form Controls */
. form-control , . custom-select , textarea {
background-color : rgba ( 0 , 0 , 0 , 0.3 ) !important ;
border : 1 px solid rgba ( 255 , 255 , 255 , 0.1 ) !important ;
color : #f1f5f9 !important ;
border-radius : 8 px !important ;
}
. form-control : focus , . custom-select : focus , textarea : focus {
background-color : rgba ( 0 , 0 , 0 , 0.5 ) !important ;
border-color : #3b82f6 !important ;
box-shadow : 0 0 0 2 px rgba ( 59 , 130 , 246 , 0.25 ) !important ;
}
/* Labels & Text */
label , . col-form-label {
font-weight : 600 ;
color : #cbd5e1 ;
}
. text-muted {
color : #94a3b8 !important ;
}
/* Buttons */
. btn {
border : none ;
border-radius : 8 px ;
font-weight : 600 ;
transition : all 0.3 s ease ;
padding : 8 px 16 px ;
text-transform : uppercase ;
letter-spacing : 0.5 px ;
}
. btn-primary , # globalSettingSaveBtn {
background : linear-gradient ( 135 deg , #3b82f6 0 % , #2563eb 100 % ) ;
color : white ;
box-shadow : 0 4 px 15 px rgba ( 37 , 99 , 235 , 0.4 ) ;
}
. btn-primary : hover , # globalSettingSaveBtn : hover {
background : linear-gradient ( 135 deg , #60a5fa 0 % , #3b82f6 100 % ) ;
transform : translateY ( -2 px ) ;
box-shadow : 0 6 px 20 px rgba ( 37 , 99 , 235 , 0.6 ) ;
}
/* GO Button specific (Input Group) */
# go_btn {
background : linear-gradient ( 135 deg , #10b981 0 % , #059669 100 % ) ;
color : white ;
box-shadow : 0 4 px 15 px rgba ( 16 , 185 , 129 , 0.4 ) ;
border-radius : 0 8 px 8 px 0 !important ; /* Fix for input group */
margin-left : -1 px ;
}
# go_btn : hover {
background : linear-gradient ( 135 deg , #34d399 0 % , #10b981 100 % ) ;
transform : translateY ( -1 px ) ;
box-shadow : 0 6 px 20 px rgba ( 16 , 185 , 129 , 0.6 ) ;
z-index : 5 ;
}
. btn-outline-primary {
color : #60a5fa ;
border : 1 px solid #60a5fa ;
background : transparent ;
}
. btn-outline-primary : hover {
background : rgba ( 96 , 165 , 250 , 0.1 ) ;
color : #93c5fd ;
box-shadow : 0 0 15 px rgba ( 96 , 165 , 250 , 0.3 ) ;
}
. btn : active {
transform : translateY ( 0 ) !important ;
box-shadow : inset 0 2 px 4 px rgba ( 0 , 0 , 0 , 0.2 ) !important ;
}
/* Custom Checkbox/Switch Override (if Bootstrap switch is used) */
. custom-control-label :: before {
background-color : rgba ( 0 , 0 , 0 , 0.3 ) ;
border-color : rgba ( 255 , 255 , 255 , 0.2 ) ;
}
. custom-control-input : checked ~ . custom-control-label :: before {
background-color : #3b82f6 ;
border-color : #3b82f6 ;
}
/* Collapse Borders */
. border-left {
border-left : 3 px solid rgba ( 255 , 255 , 255 , 0.1 ) !important ;
}
2026-01-01 16:57:48 +09:00
/* Folder Browser Modal Styles */
. folder-item {
2026-01-01 22:58:25 +09:00
cursor : pointer ;
transition : background 0.2 s ;
border-bottom : 1 px solid rgba ( 255 , 255 , 255 , 0.05 ) ;
display : flex !important ;
align-items : center ;
width : 100 % ;
overflow : hidden ;
2026-01-01 16:57:48 +09:00
font-size : 0.95 rem ;
margin-bottom : 2 px ;
}
. folder-item : hover {
2026-01-01 22:58:25 +09:00
background : rgba ( 59 , 130 , 246 , 0.2 ) !important ;
}
. folder-item span {
white-space : nowrap ;
overflow : hidden ;
text-overflow : ellipsis ;
flex : 1 ;
min-width : 0 ;
}
. folder-item . selected {
background : rgba ( 59 , 130 , 246 , 0.3 ) !important ;
2026-01-01 16:57:48 +09:00
}
. folder-item . folder-parent ,
. folder-item . folder-current {
font-weight : 600 ;
}
. folder-item i {
font-size : 1.1 rem ;
}
2026-01-08 16:38:24 +09:00
/* Tag Chips Styles */
. tag-chips-wrapper {
display : flex ;
flex-wrap : wrap ;
gap : 8 px ;
padding : 12 px ;
min-height : 60 px ;
background : rgba ( 0 , 0 , 0 , 0.2 ) ;
border : 1 px dashed rgba ( 255 , 255 , 255 , 0.15 ) ;
border-radius : 8 px ;
transition : all 0.3 s ease ;
}
. tag-chips-wrapper : empty :: before {
content : '작품이 없습니다. 아래에서 추가하세요.' ;
color : #64748b ;
font-style : italic ;
}
. tag-chips-wrapper . drag-over {
border-color : #3b82f6 ;
background : rgba ( 59 , 130 , 246 , 0.1 ) ;
}
. tag-chip {
display : inline-flex ;
align-items : center ;
gap : 8 px ;
padding : 8 px 12 px ;
background : linear-gradient ( 135 deg , rgba ( 59 , 130 , 246 , 0.3 ) 0 % , rgba ( 37 , 99 , 235 , 0.4 ) 100 % ) ;
border : 1 px solid rgba ( 96 , 165 , 250 , 0.4 ) ;
border-radius : 20 px ;
font-size : 0.9 rem ;
color : #e2e8f0 ;
cursor : grab ;
transition : all 0.2 s ease ;
user-select : none ;
}
. tag-chip : hover {
background : linear-gradient ( 135 deg , rgba ( 59 , 130 , 246 , 0.5 ) 0 % , rgba ( 37 , 99 , 235 , 0.6 ) 100 % ) ;
transform : translateY ( -2 px ) ;
box-shadow : 0 4 px 12 px rgba ( 59 , 130 , 246 , 0.3 ) ;
}
. tag-chip . dragging {
opacity : 0.5 ;
cursor : grabbing ;
}
. tag-chip . tag-text {
max-width : 200 px ;
overflow : hidden ;
text-overflow : ellipsis ;
white-space : nowrap ;
}
. tag-chip . tag-remove {
display : flex ;
align-items : center ;
justify-content : center ;
width : 18 px ;
height : 18 px ;
background : rgba ( 239 , 68 , 68 , 0.5 ) ;
border-radius : 50 % ;
cursor : pointer ;
transition : all 0.2 s ease ;
font-size : 0.75 rem ;
}
. tag-chip . tag-remove : hover {
background : rgba ( 239 , 68 , 68 , 0.9 ) ;
transform : scale ( 1.1 ) ;
}
. tag-chip . tag-index {
display : flex ;
align-items : center ;
justify-content : center ;
width : 20 px ;
height : 20 px ;
background : rgba ( 0 , 0 , 0 , 0.3 ) ;
border-radius : 50 % ;
font-size : 0.7 rem ;
color : #94a3b8 ;
}
2025-12-29 23:08:08 +09:00
< / style >
2022-10-29 17:21:14 +09:00
< script type = "text/javascript" >
var package _name = "{{arg['package_name'] }}" ;
var sub = "{{arg['sub'] }}" ;
var current _data = null ;
$ ( document ) . ready ( function ( ) {
2025-12-29 23:08:08 +09:00
// Width Fix
$ ( "#main_container" ) . removeClass ( "container" ) . addClass ( "container-fluid" ) ;
2025-12-30 00:50:13 +09:00
// Smooth Load Trigger
setTimeout ( function ( ) {
$ ( '.content-cloak, #menu_module_div, #menu_page_div' ) . addClass ( 'visible' ) ;
} , 100 ) ;
2022-10-29 17:21:14 +09:00
use _collapse ( 'ohli24_auto_make_folder' ) ;
2026-01-08 16:38:24 +09:00
// Tag Chips 초기화
initTagChips ( ) ;
2022-10-29 17:21:14 +09:00
} ) ;
$ ( '#ani365_auto_make_folder' ) . change ( function ( ) {
use _collapse ( 'ohli24_auto_make_folder' ) ;
} ) ;
2025-12-30 00:50:13 +09:00
function toggle _download _threads ( ) {
var method = $ ( '#ohli24_download_method' ) . val ( ) ;
2025-12-31 16:01:15 +09:00
if ( method == 'cdndania' || method == 'ytdlp' || method == 'aria2c' ) {
2025-12-30 00:50:13 +09:00
$ ( '#ohli24_download_threads_div' ) . slideDown ( ) ;
} else {
$ ( '#ohli24_download_threads_div' ) . slideUp ( ) ;
}
}
$ ( '#ohli24_download_method' ) . change ( function ( ) {
toggle _download _threads ( ) ;
} ) ;
// Initial check
toggle _download _threads ( ) ;
2022-10-29 17:21:14 +09:00
$ ( "body" ) . on ( 'click' , '#go_btn' , function ( e ) {
e . preventDefault ( ) ;
let url = document . getElementById ( "ohli24_url" ) . value
window . open ( url , "_blank" ) ;
} ) ;
2025-12-29 20:13:05 +09:00
// 1회 실행 버튼
$ ( "body" ) . on ( 'click' , '#global_one_execute_btn' , function ( e ) {
e . preventDefault ( ) ;
$ . ajax ( {
url : '/' + package _name + '/ajax/' + sub + '/immediately_execute' ,
type : "POST" ,
cache : false ,
dataType : "json" ,
success : function ( ret ) {
if ( ret . ret == 'success' ) {
2025-12-29 23:08:08 +09:00
$ . notify ( '스케줄러 1회 실행을 시작합니다.' , { type : 'success' } ) ;
2025-12-29 20:13:05 +09:00
} else {
2025-12-29 23:08:08 +09:00
$ . notify ( ret . msg || '실행 실패' , { type : 'danger' } ) ;
2025-12-29 20:13:05 +09:00
}
} ,
error : function ( xhr , status , error ) {
2025-12-29 23:08:08 +09:00
$ . notify ( '에러: ' + error , { type : 'danger' } ) ;
2025-12-29 20:13:05 +09:00
}
} ) ;
} ) ;
// DB 초기화 버튼
$ ( "body" ) . on ( 'click' , '#global_reset_db_btn' , function ( e ) {
e . preventDefault ( ) ;
if ( ! confirm ( '정말 DB를 초기화하시겠습니까?' ) ) return ;
$ . ajax ( {
url : '/' + package _name + '/ajax/' + sub + '/reset_db' ,
type : "POST" ,
cache : false ,
dataType : "json" ,
success : function ( ret ) {
if ( ret . ret == 'success' ) {
2025-12-29 23:08:08 +09:00
$ . notify ( 'DB가 초기화되었습니다.' , { type : 'success' } ) ;
2025-12-29 20:13:05 +09:00
} else {
2025-12-29 23:08:08 +09:00
$ . notify ( ret . msg || '초기화 실패' , { type : 'danger' } ) ;
2025-12-29 20:13:05 +09:00
}
} ,
error : function ( xhr , status , error ) {
2025-12-29 23:08:08 +09:00
$ . notify ( '에러: ' + error , { type : 'danger' } ) ;
2025-12-29 20:13:05 +09:00
}
} ) ;
} ) ;
2026-01-01 16:57:48 +09:00
// ======================================
// 폴더 탐색 기능
// ======================================
var currentBrowsePath = '' ;
var parentPath = null ;
// 탐색 버튼 클릭
$ ( '#browse_folder_btn' ) . on ( 'click' , function ( ) {
var initialPath = $ ( '#ohli24_download_path' ) . val ( ) || '' ;
loadFolderList ( initialPath ) ;
$ ( '#folderBrowserModal' ) . modal ( 'show' ) ;
} ) ;
// 폴더 목록 로드
function loadFolderList ( path ) {
$ ( '#folder_list' ) . html ( '<div class="text-center text-muted py-4"><i class="bi bi-arrow-repeat"></i> 로딩 중...</div>' ) ;
$ . ajax ( {
url : '/' + package _name + '/ajax/' + sub + '/browse_dir' ,
type : 'POST' ,
data : { path : path } ,
dataType : 'json' ,
success : function ( ret ) {
if ( ret . ret === 'success' ) {
currentBrowsePath = ret . current _path ;
parentPath = ret . parent _path ;
$ ( '#current_path_display' ) . text ( currentBrowsePath ) ;
// 상위 폴더 버튼 활성화/비활성화
if ( parentPath ) {
$ ( '#folder_go_up' ) . prop ( 'disabled' , false ) ;
} else {
$ ( '#folder_go_up' ) . prop ( 'disabled' , true ) ;
}
// 폴더 목록 렌더링
var html = '' ;
// 상위 폴더 (..) - 루트가 아닐 때만 표시
if ( parentPath ) {
html += '<div class="folder-item folder-parent d-flex align-items-center p-2 rounded" data-path="' + escapeHtml ( parentPath ) + '" style="cursor: pointer; border-bottom: 1px solid rgba(255,255,255,0.1);">' ;
html += '<i class="bi bi-folder-symlink text-info mr-2"></i>' ;
html += '<span class="text-light">..</span>' ;
html += '<span class="text-muted ml-2">(상위 폴더)</span>' ;
html += '</div>' ;
}
// 현재 폴더 (.)
html += '<div class="folder-item folder-current d-flex align-items-center p-2 rounded" data-path="' + escapeHtml ( currentBrowsePath ) + '" style="cursor: pointer; border-bottom: 1px solid rgba(255,255,255,0.05);">' ;
html += '<i class="bi bi-folder-check text-success mr-2"></i>' ;
html += '<span class="text-light">.</span>' ;
html += '<span class="text-muted ml-2">(현재 폴더)</span>' ;
html += '</div>' ;
// 하위 폴더 목록
if ( ret . directories . length === 0 ) {
html += '<div class="text-center text-muted py-3"><small>하위 폴더 없음</small></div>' ;
} else {
for ( var i = 0 ; i < ret . directories . length ; i ++ ) {
var dir = ret . directories [ i ] ;
html += '<div class="folder-item d-flex align-items-center p-2 rounded" data-path="' + escapeHtml ( dir . path ) + '" style="cursor: pointer;">' ;
html += '<i class="bi bi-folder-fill text-warning mr-2"></i>' ;
html += '<span class="text-light">' + escapeHtml ( dir . name ) + '</span>' ;
html += '</div>' ;
}
}
$ ( '#folder_list' ) . html ( html ) ;
} else {
$ ( '#folder_list' ) . html ( '<div class="text-center text-danger py-4">로드 실패: ' + ( ret . error || '알 수 없는 오류' ) + '</div>' ) ;
}
} ,
error : function ( xhr , status , error ) {
$ ( '#folder_list' ) . html ( '<div class="text-center text-danger py-4">에러: ' + error + '</div>' ) ;
}
} ) ;
}
// 폴더 항목 더블클릭 -> 진입
$ ( '#folder_list' ) . on ( 'dblclick' , '.folder-item' , function ( ) {
var path = $ ( this ) . data ( 'path' ) ;
loadFolderList ( path ) ;
} ) ;
// 폴더 항목 클릭 -> 선택 표시
$ ( '#folder_list' ) . on ( 'click' , '.folder-item' , function ( ) {
$ ( '.folder-item' ) . removeClass ( 'selected' ) . css ( 'background' , '' ) ;
$ ( this ) . addClass ( 'selected' ) . css ( 'background' , 'rgba(59, 130, 246, 0.3)' ) ;
currentBrowsePath = $ ( this ) . data ( 'path' ) ;
$ ( '#current_path_display' ) . text ( currentBrowsePath ) ;
} ) ;
// 상위 폴더 버튼
$ ( '#folder_go_up' ) . on ( 'click' , function ( ) {
if ( parentPath ) {
loadFolderList ( parentPath ) ;
}
} ) ;
// 선택 버튼
$ ( '#folder_select_btn' ) . on ( 'click' , function ( ) {
$ ( '#ohli24_download_path' ) . val ( currentBrowsePath ) ;
$ ( '#folderBrowserModal' ) . modal ( 'hide' ) ;
$ . notify ( '저장 폴더가 설정되었습니다: ' + currentBrowsePath , { type : 'success' } ) ;
} ) ;
// HTML 이스케이프 함수
function escapeHtml ( text ) {
var div = document . createElement ( 'div' ) ;
div . appendChild ( document . createTextNode ( text ) ) ;
return div . innerHTML ;
}
2026-01-03 20:52:39 +09:00
// ======================================
// 시스템 체크 및 브라우저 설치
// ======================================
function runSystemCheck ( ) {
$ . ajax ( {
url : '/' + package _name + '/ajax/' + sub + '/system_check' ,
type : 'POST' ,
success : function ( ret ) {
if ( ret . browser _found ) {
2026-01-03 22:10:19 +09:00
$ ( '#browser_status_badge' ) . removeClass ( 'badge-secondary badge-danger badge-warning' ) . addClass ( 'badge-success' ) . text ( '발견됨' ) ;
2026-01-03 20:52:39 +09:00
$ ( '#browser_path_display' ) . text ( '경로: ' + ret . browser _path ) ;
$ ( '#install_guide_section' ) . hide ( ) ;
} else {
2026-01-03 22:10:19 +09:00
if ( ret . snap _error ) {
$ ( '#browser_status_badge' ) . removeClass ( 'badge-secondary badge-success badge-danger' ) . addClass ( 'badge-warning' ) . text ( '스냅 오류' ) ;
$ ( '#browser_path_display' ) . html ( '<span class="text-warning">발견되었으나 Snap 버전입니다. 도커에서 작동하지 않습니다.</span>' ) ;
} else {
$ ( '#browser_status_badge' ) . removeClass ( 'badge-secondary badge-success badge-warning' ) . addClass ( 'badge-danger' ) . text ( '미설치' ) ;
$ ( '#browser_path_display' ) . text ( '' ) ;
}
2026-01-03 20:52:39 +09:00
$ ( '#install_guide_section' ) . show ( ) ;
$ ( '#manual_install_cmd' ) . val ( ret . install _cmd ) ;
if ( ret . can _install ) {
$ ( '#auto_install_div' ) . show ( ) ;
} else {
$ ( '#auto_install_div' ) . hide ( ) ;
}
}
}
} ) ;
}
// 자동 설치 버튼
$ ( '#auto_install_btn' ) . on ( 'click' , function ( ) {
if ( ! confirm ( '시스템 브라우저 설치를 시작하시겠습니까?\n(Ubuntu/Debian 기반 도커 환경에서만 작동합니다)' ) ) return ;
var btn = $ ( this ) ;
btn . prop ( 'disabled' , true ) . html ( '<i class="bi bi-arrow-repeat spin mr-1"></i>설치 중 (최대 10분 소요)...' ) ;
$ . ajax ( {
url : '/' + package _name + '/ajax/' + sub + '/install_browser' ,
type : 'POST' ,
success : function ( ret ) {
if ( ret . ret === 'success' ) {
$ . notify ( ret . msg , { type : 'success' } ) ;
if ( ret . path ) {
$ ( '#ohli24_zendriver_browser_path' ) . val ( ret . path ) ;
}
runSystemCheck ( ) ;
} else {
$ . notify ( ret . msg , { type : 'danger' } ) ;
}
} ,
error : function ( ) {
$ . notify ( '설치 요청 중 오류가 발생했습니다.' , { type : 'danger' } ) ;
} ,
complete : function ( ) {
btn . prop ( 'disabled' , false ) . html ( '<i class="bi bi-download mr-1"></i>자동 설치 (Ubuntu/Docker)' ) ;
}
} ) ;
} ) ;
// 명령어 복사 버튼
$ ( '#copy_cmd_btn' ) . on ( 'click' , function ( ) {
var copyText = document . getElementById ( "manual_install_cmd" ) ;
copyText . select ( ) ;
copyText . setSelectionRange ( 0 , 99999 ) ;
document . execCommand ( "copy" ) ;
$ . notify ( '명령어가 복사되었습니다.' , { type : 'info' } ) ;
} ) ;
// 초기 실행
$ ( document ) . ready ( function ( ) {
// Action 탭이 활성화될 때 체크 (또는 그냥 로딩 시 한 번)
runSystemCheck ( ) ;
} ) ;
2026-01-08 16:38:24 +09:00
// ======================================
// Tag Chips 기능
// ======================================
function initTagChips ( ) {
var hiddenField = $ ( '#ohli24_auto_code_list' ) ;
var container = $ ( '#tag_chips_container' ) ;
// 초기 값 파싱 (| 또는 줄바꿈으로 구분)
var value = hiddenField . val ( ) . trim ( ) ;
if ( value ) {
var items = value . split ( /[|\n]/ ) . map ( s => s . trim ( ) ) . filter ( s => s . length > 0 ) ;
items . forEach ( function ( item , index ) {
addTagChip ( item , index ) ;
} ) ;
}
updateTagIndices ( ) ;
}
function addTagChip ( text , index ) {
var container = $ ( '#tag_chips_container' ) ;
var chip = $ ( `
<div class="tag-chip" draggable="true" data-value=" ${ escapeHtml ( text ) } ">
<span class="tag-index"> ${ index + 1 } </span>
<span class="tag-text" title=" ${ escapeHtml ( text ) } "> ${ escapeHtml ( text ) } </span>
<span class="tag-remove" title="삭제"><i class="bi bi-x"></i></span>
</div>
` ) ;
container . append ( chip ) ;
}
function updateHiddenField ( ) {
var container = $ ( '#tag_chips_container' ) ;
var values = [ ] ;
container . find ( '.tag-chip' ) . each ( function ( ) {
values . push ( $ ( this ) . data ( 'value' ) ) ;
} ) ;
$ ( '#ohli24_auto_code_list' ) . val ( values . join ( '|' ) ) ;
}
function updateTagIndices ( ) {
$ ( '#tag_chips_container .tag-chip' ) . each ( function ( index ) {
$ ( this ) . find ( '.tag-index' ) . text ( index + 1 ) ;
} ) ;
}
// 태그 삭제
$ ( '#tag_chips_container' ) . on ( 'click' , '.tag-remove' , function ( e ) {
e . stopPropagation ( ) ;
var chip = $ ( this ) . closest ( '.tag-chip' ) ;
var text = chip . data ( 'value' ) ;
chip . fadeOut ( 200 , function ( ) {
$ ( this ) . remove ( ) ;
updateHiddenField ( ) ;
updateTagIndices ( ) ;
} ) ;
$ . notify ( '"' + text + '" 삭제됨' , { type : 'info' } ) ;
} ) ;
// 새 태그 추가 (버튼)
$ ( '#add_tag_btn' ) . on ( 'click' , function ( ) {
addNewTag ( ) ;
} ) ;
// 새 태그 추가 (엔터키)
$ ( '#new_tag_input' ) . on ( 'keypress' , function ( e ) {
if ( e . which === 13 ) {
e . preventDefault ( ) ;
addNewTag ( ) ;
}
} ) ;
function addNewTag ( ) {
var input = $ ( '#new_tag_input' ) ;
var text = input . val ( ) . trim ( ) ;
if ( ! text ) {
$ . notify ( '작품명을 입력하세요' , { type : 'warning' } ) ;
return ;
}
// 중복 체크
var exists = false ;
$ ( '#tag_chips_container .tag-chip' ) . each ( function ( ) {
if ( $ ( this ) . data ( 'value' ) === text ) {
exists = true ;
return false ;
}
} ) ;
if ( exists ) {
$ . notify ( '이미 등록된 작품입니다' , { type : 'warning' } ) ;
return ;
}
var count = $ ( '#tag_chips_container .tag-chip' ) . length ;
addTagChip ( text , count ) ;
updateHiddenField ( ) ;
input . val ( '' ) ;
$ . notify ( '"' + text + '" 추가됨' , { type : 'success' } ) ;
}
// 드래그 앤 드롭 순서 변경
var draggedChip = null ;
$ ( '#tag_chips_container' ) . on ( 'dragstart' , '.tag-chip' , function ( e ) {
draggedChip = this ;
$ ( this ) . addClass ( 'dragging' ) ;
e . originalEvent . dataTransfer . effectAllowed = 'move' ;
} ) ;
$ ( '#tag_chips_container' ) . on ( 'dragend' , '.tag-chip' , function ( e ) {
$ ( this ) . removeClass ( 'dragging' ) ;
draggedChip = null ;
updateHiddenField ( ) ;
updateTagIndices ( ) ;
} ) ;
$ ( '#tag_chips_container' ) . on ( 'dragover' , function ( e ) {
e . preventDefault ( ) ;
e . originalEvent . dataTransfer . dropEffect = 'move' ;
$ ( this ) . addClass ( 'drag-over' ) ;
var afterElement = getDragAfterElement ( this , e . originalEvent . clientX ) ;
if ( afterElement == null ) {
this . appendChild ( draggedChip ) ;
} else {
this . insertBefore ( draggedChip , afterElement ) ;
}
} ) ;
$ ( '#tag_chips_container' ) . on ( 'dragleave' , function ( e ) {
$ ( this ) . removeClass ( 'drag-over' ) ;
} ) ;
$ ( '#tag_chips_container' ) . on ( 'drop' , function ( e ) {
e . preventDefault ( ) ;
$ ( this ) . removeClass ( 'drag-over' ) ;
} ) ;
function getDragAfterElement ( container , x ) {
var chips = [ ... container . querySelectorAll ( '.tag-chip:not(.dragging)' ) ] ;
return chips . reduce ( ( closest , child ) => {
var box = child . getBoundingClientRect ( ) ;
var offset = x - box . left - box . width / 2 ;
if ( offset < 0 && offset > closest . offset ) {
return { offset : offset , element : child } ;
} else {
return closest ;
}
} , { offset : Number . NEGATIVE _INFINITY } ) . element ;
}
2026-01-09 22:18:48 +09:00
// ======================================
// 자가 업데이트 기능
// ======================================
2026-01-11 14:00:27 +09:00
$ ( document ) . on ( 'click' , '#btn-self-update' , function ( ) {
$ ( '#updateConfirmModal' ) . modal ( 'show' ) ;
} ) ;
// 실제 업데이트 실행 (이벤트 위임 - 모달이 스크립트 이후에 있으므로)
$ ( document ) . on ( 'click' , '#confirmUpdateBtn' , function ( ) {
$ ( '#updateConfirmModal' ) . modal ( 'hide' ) ;
2026-01-09 22:18:48 +09:00
2026-01-11 14:00:27 +09:00
var btn = $ ( '#btn-self-update' ) ;
2026-01-09 22:18:48 +09:00
var originalHTML = btn . html ( ) ;
btn . prop ( 'disabled' , true ) . html ( '<i class="bi bi-arrow-repeat spin"></i> 업데이트 중...' ) ;
$ . ajax ( {
url : '/' + package _name + '/ajax/' + sub + '/self_update' ,
type : 'POST' ,
dataType : 'json' ,
success : function ( ret ) {
if ( ret . ret === 'success' ) {
2026-01-11 14:00:27 +09:00
if ( ret . needs _restart ) {
$ . notify ( '<strong>⚠️ 모델 변경 감지!</strong><br>서버 재시작이 필요합니다.' , { type : 'warning' , delay : 10000 } ) ;
} else {
$ . notify ( '<strong>✅ 업데이트 완료!</strong><br>페이지를 새로고침하세요.' , { type : 'success' , delay : 5000 } ) ;
}
2026-01-09 22:18:48 +09:00
} else {
$ . notify ( '<strong>업데이트 실패: ' + ret . msg + '</strong>' , { type : 'danger' } ) ;
}
} ,
error : function ( ) {
$ . notify ( '<strong>업데이트 중 오류 발생</strong>' , { type : 'danger' } ) ;
} ,
complete : function ( ) {
btn . prop ( 'disabled' , false ) . html ( originalHTML ) ;
}
} ) ;
} ) ;
2022-10-29 17:21:14 +09:00
< / script >
2026-01-01 16:57:48 +09:00
2026-01-11 14:00:27 +09:00
<!-- Update Confirmation Modal -->
< div class = "modal fade" id = "updateConfirmModal" tabindex = "-1" role = "dialog" aria-hidden = "true" >
< div class = "modal-dialog modal-dialog-centered" role = "document" >
< div class = "modal-content animate__animated animate__zoomIn" style = "background: linear-gradient(145deg, #1e293b 0%, #0f172a 100%); border: 1px solid rgba(59, 130, 246, 0.3); border-radius: 20px; box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);" >
< div class = "modal-body text-center" style = "padding: 40px 30px;" >
< div style = "width: 80px; height: 80px; background: linear-gradient(135deg, rgba(59, 130, 246, 0.2) 0%, rgba(99, 102, 241, 0.2) 100%); border-radius: 50%; display: flex; align-items: center; justify-content: center; margin: 0 auto 24px; border: 2px solid rgba(59, 130, 246, 0.3);" >
< i class = "bi bi-arrow-repeat" style = "color: #3b82f6; font-size: 36px;" > < / i >
< / div >
< h4 style = "color: #f1f5f9; font-weight: 700; margin-bottom: 12px;" > 플러그인 업데이트< / h4 >
< p style = "color: #94a3b8; font-size: 15px; margin-bottom: 8px;" > 최신 코드를 다운로드하고 플러그인을 리로드합니다.< / p >
< p style = "color: #64748b; font-size: 13px; margin-bottom: 32px;" > < i class = "bi bi-info-circle" > < / i > 서버 재시작 없이 즉시 적용됩니다.< / p >
< div style = "display: flex; gap: 12px; justify-content: center;" >
< button type = "button" class = "btn" data-dismiss = "modal" style = "width: 120px; background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.2); color: #94a3b8; border-radius: 10px; padding: 12px 24px; font-weight: 600;" > 취소< / button >
< button type = "button" id = "confirmUpdateBtn" class = "btn" style = "width: 140px; background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%); border: none; color: white; border-radius: 10px; padding: 12px 24px; font-weight: 600; box-shadow: 0 4px 15px rgba(59, 130, 246, 0.4);" >
< i class = "bi bi-download" > < / i > 업데이트
< / button >
< / div >
< / div >
< / div >
< / div >
< / div >
< style >
/* Update Button Enhanced Visibility */
# btn-self-update {
background : linear-gradient ( 135 deg , #0ea5e9 0 % , #0284c7 100 % ) !important ;
border : none !important ;
color : white !important ;
font-weight : 600 ;
padding : 8 px 16 px ;
border-radius : 8 px ;
box-shadow : 0 2 px 8 px rgba ( 14 , 165 , 233 , 0.3 ) ;
transition : all 0.2 s ease ;
}
# btn-self-update : hover : not ( : disabled ) {
background : linear-gradient ( 135 deg , #0284c7 0 % , #0369a1 100 % ) !important ;
transform : translateY ( -1 px ) ;
box-shadow : 0 4 px 12 px rgba ( 14 , 165 , 233 , 0.4 ) ;
}
# btn-self-update : disabled {
background : linear-gradient ( 135 deg , #475569 0 % , #334155 100 % ) !important ;
color : #94a3b8 !important ;
cursor : not-allowed ;
box-shadow : none ;
opacity : 0.7 ;
}
# btn-self-update . bi-arrow-repeat . spin {
animation : spin 1 s linear infinite ;
}
@ keyframes spin {
from { transform : rotate ( 0 deg ) ; }
to { transform : rotate ( 360 deg ) ; }
}
/* Animate.css for modal */
. animate__zoomIn {
animation-duration : 0.3 s ;
}
< / style >
2022-10-29 17:21:14 +09:00
{% endblock %}