Răsfoiți Sursa

BBS 기능 재정비

잉여개발기 (SPDV) 2 ani în urmă
părinte
comite
7104498843

+ 2 - 0
lang/en-US.json

@@ -252,6 +252,8 @@
             "bbs_set" : "BBS set",
             "comment" : "Comment",
             "reply" : "Reply",
+            "post_edit" : "Modify post",
+            "post_add" : "Add post",
             "_comment_" : "BBS ACL",
                 "bbs_view_acl" : "BBS ACL to view posts",
                 "bbs_acl" : "BBS ACL",

+ 3 - 1
lang/ko-KR.json

@@ -552,5 +552,7 @@
     "footnote_render": "각주 렌더링",
     "footnote_number": "각주 번호 출력",
     "only_number": "숫자만",
-    "footnote_real_num_view": "각주 실제 번호 보기"
+    "footnote_real_num_view": "각주 실제 번호 보기",
+    "post_edit" : "게시글 수정",
+    "post_add" : "게시글 추가"
 }

+ 1 - 1
route/api_bbs_w_comment.py

@@ -1,6 +1,6 @@
 from .tool.func import *
 
-def api_bbs_w_comment(sub_code : str = '') -> flask.Response:
+def api_bbs_w_comment(sub_code : str = '') -> typing.Union[str, werkzeug.wrappers.response.Response]:
     conn : typing.Union[sqlite3.Connection, pymysql.connections.Connection]
     with get_db_connect() as conn:
         curs : typing.Union[sqlite3.Cursor, pymysql.cursors.Cursor] = conn.cursor()

+ 1 - 1
route/api_bbs_w_post.py

@@ -1,6 +1,6 @@
 from .tool.func import *
 
-def api_bbs_w_post(sub_code : str = '') -> flask.Response:
+def api_bbs_w_post(sub_code : str = '') -> typing.Union[str, werkzeug.wrappers.response.Response]:
     sub_code_split : typing.List[str] = sub_code.split('-')
     if len(sub_code_split) < 2:
         sub_code_split = ['', '']

+ 0 - 1
route/api_skin_info.py

@@ -59,7 +59,6 @@ def api_skin_info(name = ''):
                                     "skin_ver" : get_data["skin_ver"]
                                 }}}
 
-
                     a_data = {**a_data, **{ i : json_data }}
 
             return flask.jsonify(a_data)

+ 2 - 13
route/api_topic.py

@@ -45,19 +45,8 @@ def api_topic(topic_num = 1, tool = 'normal', num = '', render = ''):
                     if data_v != '':
                         data_v = render_set(
                             doc_data = data_v, 
-                            data_type = 'api_view',
-                            data_in = 'topic_' + topic_num + '_' + i[0],
-                            doc_acl = 0
-                        )
-                        data_v[0] = re.sub(
-                            r'&lt;topic_a&gt;(?P<in>(?:(?!&lt;\/topic_a&gt;).)+)&lt;\/topic_a&gt;',
-                            '<a href="\g<in>">\g<in></a>',
-                            data_v[0]
-                        )
-                        data_v[0] = re.sub(
-                            r'&lt;topic_call&gt;@(?P<in>(?:(?!&lt;\/topic_call&gt;).)+)&lt;\/topic_call&gt;',
-                            '<a href="/w/user:\g<in>">@\g<in></a>',
-                            data_v[0]
+                            data_type = 'api_thread',
+                            data_in = 'topic_' + topic_num + '_' + i[0]
                         )
                     else:
                         data_v = ['', '']

+ 1 - 2
route/api_w.py

@@ -11,8 +11,7 @@ def api_w(name = 'Test', tool = '', rev = ''):
                 data_pas = render_set(
                     doc_name = name, 
                     doc_data = data_org, 
-                    data_type = 'api_view',
-                    data_in = ''
+                    data_type = 'api_view'
                 )
 
                 return flask.jsonify({

+ 51 - 23
route/bbs_edit.py

@@ -1,5 +1,7 @@
 from .tool.func import *
 
+from .api_bbs_w_post import api_bbs_w_post
+
 def bbs_edit(bbs_num = '', post_num = '', do_type = ''):
     with get_db_connect() as conn:
         curs = conn.cursor()
@@ -24,6 +26,8 @@ def bbs_edit(bbs_num = '', post_num = '', do_type = ''):
             
         if acl_check(bbs_num_str, 'bbs_edit') == 1:
             return redirect('/bbs/set/' + bbs_num_str)
+        
+        i_list = ['post_view_acl', 'post_comment_acl']
 
         if flask.request.method == 'POST' and do_type != 'preview':
             if captcha_post(flask.request.form.get('g-recaptcha-response', flask.request.form.get('g-recaptcha', ''))) == 1:
@@ -59,16 +63,18 @@ def bbs_edit(bbs_num = '', post_num = '', do_type = ''):
 
             return redirect('/bbs/w/' + bbs_num_str + '/' + id_data)
         else:
+            d_list = ['' for _ in range(0, len(i_list))]
             if do_type == 'preview':
                 title = flask.request.form.get('title', '')
                 data = flask.request.form.get('content', '')
                 data = data.replace('\r', '')
-
                 data_preview = render_set(
-                    doc_name = '', 
                     doc_data = data,
-                    data_in = 'from'
-                )
+                    data_type = 'thread',
+                    data_in = 'bbs'
+                ) + '<hr>'
+                for for_a in range(0, len(i_list)):
+                    d_list[for_a] = flask.request.form.get(i_list[for_a], 'normal')
             else:
                 if post_num == '':
                     title = ''
@@ -79,19 +85,22 @@ def bbs_edit(bbs_num = '', post_num = '', do_type = ''):
                     db_data = curs.fetchall()
                     db_data = list(db_data) if db_data else []
 
-                    temp_id = ''
-                    temp_dict = {}
-
-                    for for_a in db_data + [['', '', '']]:
-                        if temp_id != for_a[2]:
-                            temp_id = for_a[2]
-                            temp_dict['code'] = for_a[2]
-
-                        temp_dict[for_a[0]] = for_a[1]
+                    temp_dict = json.loads(api_bbs_w_post(bbs_num_str + '-' + post_num_str).data)
 
                     title = temp_dict['title']
                     data = temp_dict['data']
                     data_preview = ''
+
+            acl_div = ['' for _ in range(0, len(i_list))]
+            acl_list = get_acl_list()
+            for for_a in range(0, len(i_list)):
+                for data_list in acl_list:
+                    if data_list == d_list[for_a]:
+                        check = 'selected="selected"'
+                    else:
+                        check = ''
+
+                    acl_div[for_a] += '<option value="' + data_list + '" ' + check + '>' + (data_list if data_list != '' else 'normal') + '</option>'
             
             if post_num == '':
                 form_action = 'formaction="/bbs/edit/' + bbs_num_str + '"'
@@ -129,9 +138,14 @@ def bbs_edit(bbs_num = '', post_num = '', do_type = ''):
 
             if editor_top_text != '':
                 editor_top_text += '<hr class="main_hr">'
+
+            if post_num == '':
+                bbs_title = load_lang('post_add')
+            else:
+                bbs_title = load_lang('post_edit')
     
             return easy_minify(flask.render_template(skin_check(), 
-                imp = [load_lang('bbs_edit'), wiki_set(), wiki_custom(), wiki_css([0, 0])],
+                imp = [bbs_title, wiki_set(), wiki_custom(), wiki_css([0, 0])],
                 data =  editor_top_text + add_get_file + '''
                     <form method="post">
                         <textarea style="display: none;" id="opennamu_edit_origin" name="doc_data_org"></textarea>
@@ -149,16 +163,30 @@ def bbs_edit(bbs_num = '', post_num = '', do_type = ''):
                         
                         <button id="opennamu_save_button" type="submit" ''' + form_action + ''' onclick="do_monaco_to_textarea(); do_stop_exit_release();">''' + load_lang('save') + '''</button>
                         <button id="opennamu_preview_button" type="submit" ''' + form_action_preview + ''' onclick="do_monaco_to_textarea(); do_stop_exit_release();">''' + load_lang('preview') + '''</button>
-                    </form>
-                    
-                    <hr class="main_hr">
-                    <div id="opennamu_preview_area">''' + data_preview + '''</div>
                     
-                    <script>
-                        do_stop_exit();
-                        do_paste_image('opennamu_edit_textarea', 'opennamu_monaco_editor');
-                        ''' + add_script + '''
-                    </script>
+                        <hr class="main_hr">
+                        <div id="opennamu_preview_area">''' + data_preview + '''</div>
+                        
+                        <script>
+                            do_stop_exit();
+                            do_paste_image('opennamu_edit_textarea', 'opennamu_monaco_editor');
+                            ''' + add_script + '''
+                        </script>
+
+                        ''' + render_simple_set('''
+                            <hr class="main_hr">
+                            <a href="/acl/TEST#exp">(''' + load_lang('reference') + ''')</a>
+                            <h2>''' + load_lang('acl') + '''</h2>
+                            <h3>''' + load_lang('post_view_acl') + '''</h3>
+                            <select name="post_view_acl">''' + acl_div[0] + '''</select>
+
+                            <h4>''' + load_lang('post_comment_acl') + '''</h4>
+                            <select name="post_comment_acl">''' + acl_div[1] + '''</select>
+
+                            <h2>''' + load_lang('markup') + '''</h2>
+                            ''' + load_lang('not_working') + '''
+                        ''') + '''
+                    </form>
                 ''',
                 menu = [['bbs/w/' + bbs_num_str, load_lang('return')]]
             ))

+ 2 - 2
route/bbs_main.py

@@ -25,8 +25,8 @@ def bbs_main():
                 db_data_2 = curs.fetchall()
                 last_date = ('(' + db_data_2[0][0] + ')') if db_data_2 else ''
 
-                data += '<li><a href="/bbs/w/' + for_a[1] + '">' + for_a[0] + ' (' + bbs_type + ') ' + last_date + '</a></li>'
-                data += '<li></li>'
+                data += '<li><a href="/bbs/w/' + for_a[1] + '">' + html.escape(for_a[0]) + ' (' + bbs_type + ') ' + last_date + '</a></li>'
+                # data += '<li></li>'
 
             data += '</ul>'
 

+ 3 - 5
route/bbs_w.py

@@ -32,10 +32,9 @@ def bbs_w(bbs_num = ''):
         for for_a in db_data + [['', '', '']]:
             if temp_id != for_a[2]:
                 if temp_id != '':
-                    curs.execute(db_change('select set_data from bbs_data where set_name = "comment_date" and set_id = ? order by set_code + 0 desc'), [bbs_num_str + '-' + temp_dict['code']])
+                    curs.execute(db_change('select count(*) from bbs_data where set_name = "comment_date" and (set_id = ? or set_id like ?) order by set_code + 0 desc'), [bbs_num_str + '-' + temp_dict['code'], bbs_num_str + '-' + temp_dict['code'] + '-%'])
                     db_data = curs.fetchall()
-                    last_comment_date = db_data[0][0] if db_data else '0'
-                    comment_count = str(len(db_data)) if db_data else '0'
+                    comment_count = str(db_data[0][0]) if db_data else '0'
                     
                     data += '''
                         <tr>
@@ -45,9 +44,8 @@ def bbs_w(bbs_num = ''):
                         </tr>
                         <tr>
                             <td colspan="3">
-                                <a href="/bbs/w/''' + bbs_num_str + '/' + temp_dict['code'] + '">' + temp_dict['title'] + '''</a> 
+                                <a href="/bbs/w/''' + bbs_num_str + '/' + temp_dict['code'] + '">' + html.escape(temp_dict['title']) + '''</a> 
                                 (''' + comment_count + ''') 
-                                (''' + last_comment_date + ''')
                             </td>
                         </tr>
                     '''

+ 64 - 39
route/bbs_w_post.py

@@ -50,16 +50,18 @@ def bbs_w_post_comment(user_id : str, sub_code : str, comment_num : str, bbs_num
         sub_code_check : str = re.sub(r'^[0-9]+-[0-9]+-', '', sub_code + '-' + temp_dict['code'])
         margin_count : int = sub_code_check.count('-')
 
-        date : str = '<a href="/bbs/w/' + bbs_num_str + '/' + post_num_str + '/comment/' + sub_code_check + '/tool">(' + load_lang('tool') + ')</a> ' + temp_dict['comment_date']
+        date : str = ''
+        date += '<a href="javascript:opennamu_change_comment(\'' + sub_code_check + '\');">(' + load_lang('comment') + ')</a> '
+        date += '<a href="/bbs/w/' + bbs_num_str + '/' + post_num_str + '/comment/' + sub_code_check + '/tool">(' + load_lang('tool') + ')</a> '
+        date += temp_dict['comment_date']
 
         comment_data += '<span style="padding-left: 20px;"></span>' * margin_count
         comment_data += bbs_w_post_make_thread(
             ip_pas(temp_dict['comment_user_id']),
             date,
             render_set(
-                doc_name = '', 
                 doc_data = temp_dict['comment'],
-                data_in = 'from'
+                data_in = 'bbs_comment_' + sub_code_check
             ),
             sub_code_check,
             color = color,
@@ -81,7 +83,7 @@ def bbs_w_post_comment(user_id : str, sub_code : str, comment_num : str, bbs_num
 
     return (comment_data, comment_select, comment_count, comment_add_count)
 
-def bbs_w_post(bbs_num : typing.Union[int, str] = '', post_num : typing.Union[int, str] = '', do_type : str = '') -> flask.Response:
+def bbs_w_post(bbs_num : typing.Union[int, str] = '', post_num : typing.Union[int, str] = '', do_type : str = '') -> typing.Union[str, werkzeug.wrappers.response.Response]:
     conn : typing.Union[sqlite3.Connection, pymysql.connections.Connection]
     with get_db_connect() as conn:
         curs : typing.Union[sqlite3.Cursor, pymysql.cursors.Cursor] = conn.cursor()
@@ -93,11 +95,6 @@ def bbs_w_post(bbs_num : typing.Union[int, str] = '', post_num : typing.Union[in
         
         bbs_name : str = db_data_3[0][0]
 
-        curs.execute(db_change('select set_name, set_data, set_code from bbs_data where set_id = ? and set_code = ?'), [bbs_num, post_num])
-        db_data : typing.Optional[typing.List[typing.Tuple[str, str, str]]] = curs.fetchall()
-        if not db_data:
-            return redirect('/bbs/main')
-
         bbs_num_str : str = str(bbs_num)
         post_num_str : str = str(post_num)
         bbs_comment_acl : int = acl_check(bbs_num_str, 'bbs_comment')
@@ -112,6 +109,11 @@ def bbs_w_post(bbs_num : typing.Union[int, str] = '', post_num : typing.Union[in
         data : str
         date : str
         temp_dict : dict[str, str]
+        new_id_data : str
+
+        temp_dict = json.loads(api_bbs_w_post(bbs_num_str + '-' + post_num_str).data)
+        if temp_dict == {}:
+            return redirect('/bbs/main')
         
         curs.execute(db_change('select set_data from bbs_set where set_id = ? and set_name = "bbs_type"'), [bbs_num])
         db_data_2 : typing.Optional[typing.List[typing.Tuple[str]]] = curs.fetchall()
@@ -138,15 +140,20 @@ def bbs_w_post(bbs_num : typing.Union[int, str] = '', post_num : typing.Union[in
                     # re_error로 대체 예정
                     return redirect('/bbs/w/' + bbs_num_str + '/' + post_num_str)
                 
+                data = data.replace('\r', '')
+                data = get_thread_pre_render(data, id_data, ip, set_id, bbs_name, temp_dict['title'], 'post')
+                
                 date = get_time()
 
                 curs.execute(db_change("insert into bbs_data (set_name, set_code, set_id, set_data) values ('comment', ?, ?, ?)"), [id_data, set_id, data])
                 curs.execute(db_change("insert into bbs_data (set_name, set_code, set_id, set_data) values ('comment_date', ?, ?, ?)"), [id_data, set_id, date])
                 curs.execute(db_change("insert into bbs_data (set_name, set_code, set_id, set_data) values ('comment_user_id', ?, ?, ?)"), [id_data, set_id, ip])
 
+                add_alarm(temp_dict['user_id'], ip, 'BBS <a href="/bbs/w/' + bbs_num_str + '/' + post_num_str + '#' + id_data + '">' + html.escape(bbs_name) + ' - ' + html.escape(temp_dict['title']) + '#' + id_data + '</a>')
+
                 conn.commit()
 
-                return redirect('/bbs/w/' + bbs_num_str + '/' + post_num_str + '#' + str(int(id_data) + 1))
+                return redirect('/bbs/w/' + bbs_num_str + '/' + post_num_str + '#' + id_data)
             else:
                 if acl_check(bbs_num_str, 'bbs_view') == 1:
                     return re_error('/ban')
@@ -158,14 +165,14 @@ def bbs_w_post(bbs_num : typing.Union[int, str] = '', post_num : typing.Union[in
                     text = text.replace('\r', '')
 
                     data_preview = render_set(
-                        doc_name = '', 
                         doc_data = text,
-                        data_in = 'from'
+                        data_type = 'thread',
+                        data_in = 'bbs_comment_preview'
                     )
-                
-                temp_dict = json.loads(api_bbs_w_post(bbs_num_str + '-' + post_num_str).data)
 
-                date = '<a href="/bbs/w/' + bbs_num_str + '/' + post_num_str + '/tool">(' + load_lang('tool') + ')</a> ' + temp_dict['date']
+                date = ''
+                date += '<a href="/bbs/w/' + bbs_num_str + '/' + post_num_str + '/tool">(' + load_lang('tool') + ')</a> '
+                date += temp_dict['date']
 
                 data = ''
                 data += '<h2>' + html.escape(temp_dict['title']) + '</h2>'
@@ -173,9 +180,9 @@ def bbs_w_post(bbs_num : typing.Union[int, str] = '', post_num : typing.Union[in
                     ip_pas(temp_dict['user_id']),
                     date,
                     render_set(
-                        doc_name = '', 
                         doc_data = temp_dict['data'],
-                        data_in = 'from'
+                        data_type = 'thread',
+                        data_in = 'bbs'
                     ),
                     '1',
                     color = 'green'
@@ -193,15 +200,17 @@ def bbs_w_post(bbs_num : typing.Union[int, str] = '', post_num : typing.Union[in
                     else:
                         color = 'default'
                         
-                    date = '<a href="/bbs/w/' + bbs_num_str + '/' + post_num_str + '/comment/' + str(count) + '/tool">(' + load_lang('tool') + ')</a> ' + temp_dict['comment_date']
+                    date = ''
+                    date += '<a href="/bbs/w/' + bbs_num_str + '/' + post_num_str + '/comment/' + str(count) + '/tool">(' + load_lang('tool') + ')</a> '
+                    date += temp_dict['comment_date']
 
                     data += bbs_w_post_make_thread(
                         ip_pas(temp_dict['comment_user_id']),
                         date,
                         render_set(
-                            doc_name = '', 
                             doc_data = temp_dict['comment'],
-                            data_in = 'from'
+                            data_type = 'thread',
+                            data_in = 'bbs_comment_' + str(count)
                         ),
                         str(count),
                         color = color
@@ -244,27 +253,34 @@ def bbs_w_post(bbs_num : typing.Union[int, str] = '', post_num : typing.Union[in
                 else:
                     captcha_post('', 0)
                 
-                select : str = flask.request.form.get('comment_select', 'default')
-                select = '' if select == 'default' else select
+                select : str = flask.request.form.get('comment_select', '0')
+                select = '' if select == '0' else select
+
+                comment_user_name : str = ''
+
                 if select != '':
                     select_split : typing.List[str] = select.split('-')
                     if len(select_split) < 2:
-                        curs.execute(db_change('select set_code from bbs_data where set_name = "comment" and set_id = ? and set_code = ? limit 1'), [bbs_num_str + '-' + post_num_str, select_split[0]])
-                        if not curs.fetchall():
+                        curs.execute(db_change('select set_data from bbs_data where set_name = "comment_user_id" and set_id = ? and set_code = ? limit 1'), [bbs_num_str + '-' + post_num_str, select_split[0]])    
+                        db_data_6 : typing.Optional[typing.List[typing.Tuple[str]]] = curs.fetchall()
+                        if not db_data_6:
                             # re_error로 변경 예정
                             return redirect('/bbs/w/' + bbs_num_str + '/' + post_num_str)
                         else:
                             set_id = bbs_num_str + '-' + post_num_str + '-' + select_split[0]
+                            comment_user_name = db_data_6[0][0]
                     else:
-                        curs.execute(db_change('select set_code from bbs_data where set_name = "comment" and set_id = ? and set_code = ? limit 1'), [bbs_num_str + '-' + post_num_str + '-' + '-'.join(select_split[0:len(select) - 1]), select_split[len(select_split) - 1]])
-                        if not curs.fetchall():
+                        curs.execute(db_change('select set_data from bbs_data where set_name = "comment_user_id" and set_id = ? and set_code = ? limit 1'), [bbs_num_str + '-' + post_num_str + '-' + '-'.join(select_split[0:len(select_split) - 1]), select_split[len(select_split) - 1]])
+                        db_data_7 : typing.Optional[typing.List[typing.Tuple[str]]] = curs.fetchall()
+                        if not db_data_7:
                             return redirect('/bbs/w/' + bbs_num_str + '/' + post_num_str)
                         else:
                             set_id = bbs_num_str + '-' + post_num_str + '-' + '-'.join(select_split)
+                            comment_user_name = db_data_7[0][0]
                 else:
                     set_id = bbs_num_str + '-' + post_num_str
 
-                curs.execute(db_change('select set_code from bbs_data where set_name = "comment" and set_id = ? order by set_code + 0 desc'), [set_id])
+                curs.execute(db_change('select set_code from bbs_data where set_name = "comment" and set_id = ? order by set_code + 0 desc limit 1'), [set_id])
                 db_data_5 : typing.Optional[typing.List[typing.Tuple[str]]] = curs.fetchall()
                 id_data = str(int(db_data_5[0][0]) + 1) if db_data_5 else '1'
 
@@ -282,10 +298,17 @@ def bbs_w_post(bbs_num : typing.Union[int, str] = '', post_num : typing.Union[in
                 conn.commit()
             
                 if set_id == '':
-                    return redirect('/bbs/w/' + bbs_num_str + '/' + post_num_str + '#' + id_data)
+                    end_id = id_data
                 else:
                     set_id = re.sub(r'^[0-9]+-[0-9]+-?', '', set_id)
-                    return redirect('/bbs/w/' + bbs_num_str + '/' + post_num_str + '#' + set_id + '-' + id_data)
+                    set_id += '-' if set_id != '' else ''
+                    end_id = set_id + id_data
+
+                add_alarm(temp_dict['user_id'], ip, 'BBS <a href="/bbs/w/' + bbs_num_str + '/' + post_num_str + '#' + end_id + '">' + html.escape(bbs_name) + ' - ' + html.escape(temp_dict['title']) + '#' + end_id + '</a>')
+                if comment_user_name != '':
+                    add_alarm(comment_user_name, ip, 'BBS <a href="/bbs/w/' + bbs_num_str + '/' + post_num_str + '#' + end_id + '">' + html.escape(bbs_name) + ' - ' + html.escape(temp_dict['title']) + '#' + end_id + '</a>')
+
+                return redirect('/bbs/w/' + bbs_num_str + '/' + post_num_str + '#' + end_id)
             else:
                 if acl_check(bbs_num_str, 'bbs_view') == 1:
                     return re_error('/ban')
@@ -300,14 +323,14 @@ def bbs_w_post(bbs_num : typing.Union[int, str] = '', post_num : typing.Union[in
                     comment_num = flask.request.form.get('comment_select', '')
 
                     data_preview = render_set(
-                        doc_name = '', 
                         doc_data = text,
-                        data_in = 'from'
+                        data_in = 'bbs_comment_preview'
                     )
 
-                temp_dict = json.loads(api_bbs_w_post(bbs_num_str + '-' + post_num_str).data)
-
-                date = '<a href="/bbs/w/' + bbs_num_str + '/' + post_num_str + '/tool">(' + load_lang('tool') + ')</a> ' + temp_dict['date']
+                date = ''
+                date += '<a href="javascript:opennamu_change_comment(\'0\');">(' + load_lang('comment') + ')</a> '
+                date += '<a href="/bbs/w/' + bbs_num_str + '/' + post_num_str + '/tool">(' + load_lang('tool') + ')</a> '
+                date += temp_dict['date']
 
                 data = ''
                 data += '<h2>' + html.escape(temp_dict['title']) + '</h2>'
@@ -315,9 +338,8 @@ def bbs_w_post(bbs_num : typing.Union[int, str] = '', post_num : typing.Union[in
                     ip_pas(temp_dict['user_id']),
                     date,
                     render_set(
-                        doc_name = '', 
                         doc_data = temp_dict['data'],
-                        data_in = 'from'
+                        data_in = 'bbs'
                     ),
                     '0',
                     color = 'red'
@@ -326,8 +348,8 @@ def bbs_w_post(bbs_num : typing.Union[int, str] = '', post_num : typing.Union[in
                 user_id = temp_dict['user_id']
                 comment_data : str = ''
 
-                comment_select : str = '<hr class="main_hr"><select name="comment_select">'
-                comment_select += '<option value="default">' + load_lang('normal') + '</option>'
+                comment_select : str = '<select id="opennamu_comment_select" name="comment_select">'
+                comment_select += '<option value="0">' + load_lang('normal') + '</option>'
 
                 comment_count : int = 0
                 comment_add_count : int = 0
@@ -348,11 +370,13 @@ def bbs_w_post(bbs_num : typing.Union[int, str] = '', post_num : typing.Union[in
                     data += load_lang('comment') + ' : ' + str(comment_count) + '<hr class="main_hr">'
                     data += load_lang('reply') + ' : ' + str(comment_add_count) + '<hr class="main_hr">'
                     data += comment_data
+                else:
+                    data += '<hr class="main_hr">'
 
                 bbs_comment_form = ''
                 if bbs_comment_acl == 0:
                     bbs_comment_form += '''
-                        ''' + comment_select + '''
+                        ''' + comment_select + ''' <a href="javascript:opennamu_return_comment();">(R)</a>
                         <hr class="main_hr">
                         
                         <textarea name="content" id="opennamu_edit_textarea" class="opennamu_textarea_100">''' + html.escape(text) + '''</textarea>
@@ -370,6 +394,7 @@ def bbs_w_post(bbs_num : typing.Union[int, str] = '', post_num : typing.Union[in
                         ''' + bbs_comment_form + '''
                         ''' + data_preview + '''
                     </form>
+                    <script src="/views/main_css/js/route/bbs_w_post.js"></script>
                 '''
 
                 return easy_minify(flask.render_template(skin_check(),

+ 18 - 23
route/bbs_w_set.py

@@ -11,22 +11,17 @@ def bbs_w_set(bbs_num = ''):
         
         bbs_name = db_data[0][0]
 
-        i_list = {
-            1 : 'bbs_acl',
-            2 : 'bbs_edit_acl',
-            3 : 'bbs_comment_acl',
-            4 : 'bbs_view_acl'
-        }
+        i_list = ['bbs_acl', 'bbs_edit_acl', 'bbs_comment_acl', 'bbs_view_acl', 'bbs_markup']
         bbs_num_str = str(bbs_num)
 
         if flask.request.method == 'POST':
             if admin_check(None, 'bbs_set (acl)') != 1:
                 return re_error('/ban')
             else:
-                for i in i_list:
+                for for_a in range(len(i_list)):
                     curs.execute(db_change("update bbs_set set set_data = ? where set_name = ? and set_id = ?"), [
-                        flask.request.form.get(i_list[i], 'normal'),
-                        i_list[i],
+                        flask.request.form.get(i_list[for_a], 'normal'),
+                        i_list[for_a],
                         bbs_num
                     ])
 
@@ -34,37 +29,37 @@ def bbs_w_set(bbs_num = ''):
 
                 return redirect('/bbs/set/' + bbs_num_str)
         else:
-            d_list = {}
+            d_list = ['' for _ in range(0, len(i_list))]
 
             if admin_check() != 1:
                 disable = 'disabled'
             else:
                 disable = ''
 
-            for i in i_list:
-                curs.execute(db_change('select set_data from bbs_set where set_name = ? and set_id = ?'), [i_list[i], bbs_num])
+            for for_a in range(len(i_list)):
+                curs.execute(db_change('select set_data from bbs_set where set_name = ? and set_id = ?'), [i_list[for_a], bbs_num])
                 sql_d = curs.fetchall()
                 if sql_d:
-                    d_list[i] = sql_d[0][0]
+                    d_list[for_a] = sql_d[0][0]
                 else:
-                    curs.execute(db_change('insert into bbs_set (set_name, set_code, set_id, set_data) values (?, "", ?, ?)'), [i_list[i], bbs_num, 'normal'])
-                    d_list[i] = 'normal'
+                    curs.execute(db_change('insert into bbs_set (set_name, set_code, set_id, set_data) values (?, "", ?, ?)'), [i_list[for_a], bbs_num, 'normal'])
+                    d_list[for_a] = 'normal'
 
             conn.commit()
 
-            acl_div = []
-            for i in range(0, len(i_list)):
-                acl_div += ['']
-
+            acl_div = ['' for _ in range(0, len(i_list))]
             acl_list = get_acl_list()
-            for i in range(0, len(i_list)):
+            for for_a in range(0, len(i_list)):
+                if for_a == 4:
+                    acl_list = ['normal'] + get_init_set_list('markup')['list']
+
                 for data_list in acl_list:
-                    if data_list == d_list[i + 1]:
+                    if data_list == d_list[for_a]:
                         check = 'selected="selected"'
                     else:
                         check = ''
 
-                    acl_div[i] += '<option value="' + data_list + '" ' + check + '>' + (data_list if data_list != '' else 'normal') + '</option>'
+                    acl_div[for_a] += '<option value="' + data_list + '" ' + check + '>' + (data_list if data_list != '' else 'normal') + '</option>'
 
             return easy_minify(flask.render_template(skin_check(),
                 imp = [load_lang('bbs_set'), wiki_set(), wiki_custom(), wiki_css(['(' + bbs_name + ')', 0])],
@@ -87,7 +82,7 @@ def bbs_w_set(bbs_num = ''):
                         <select ''' + disable + ''' name="bbs_comment_acl">''' + acl_div[2] + '''</select>
 
                         <h2>''' + load_lang('markup') + '''</h2>
-                        ''' + load_lang('not_working') + '''
+                        <select ''' + disable + ''' name="bbs_markup">''' + acl_div[4] + '''</select>
                         
                         <hr class="main_hr">
                         <button id="opennamu_save_button" type="submit">''' + load_lang('save') + '''</button>

+ 3 - 4
route/edit.py

@@ -3,8 +3,7 @@ from .tool.func import *
 def edit_render_set(name, content):
     render_set(
         doc_name = name,
-        doc_data = content,
-        data_in = ''
+        doc_data = content
     )
 
 # https://stackoverflow.com/questions/13821156/timeout-function-using-threading-in-python-does-not-work
@@ -117,7 +116,7 @@ def edit(name = 'Test', section = 0, do_type = ''):
     
             curs.execute(db_change("select user from scan where title = ? and type = ''"), [name])
             for scan_user in curs.fetchall():
-                add_alarm(scan_user[0], ip + ' | <a href="/w/' + url_pas(name) + '">' + html.escape(name) + '</a> | Edit')
+                add_alarm(scan_user[0], ip, '<a href="/w/' + url_pas(name) + '">' + html.escape(name) + '</a>')
                     
             history_plus(
                 name,
@@ -236,7 +235,7 @@ def edit(name = 'Test', section = 0, do_type = ''):
                     data_preview = render_set(
                         doc_name = name, 
                         doc_data = data,
-                        data_in = 'from'
+                        data_type = 'from'
                     )
 
             if data_section == '':

+ 1 - 1
route/edit_delete_file.py

@@ -4,7 +4,7 @@ from .edit_delete import edit_delete
 
 # 처음으로 차세대 코드 방법론 적용
 # 앞으로 다 이렇게 작성할 예정
-def edit_delete_file(name : str = 'test.jpg') -> flask.Response:
+def edit_delete_file(name : str = 'test.jpg') -> typing.Union[str, werkzeug.wrappers.response.Response]:
     conn : typing.Union[sqlite3.Connection, pymysql.connections.Connection]
     with get_db_connect() as conn:
         curs : typing.Union[sqlite3.Cursor, pymysql.cursors.Cursor] = conn.cursor()

+ 1 - 1
route/login_register_submit.py

@@ -44,7 +44,7 @@ def login_register_submit_2():
             conn.commit()
             
             for for_a in get_admin_list():
-                add_alarm(for_a, flask.session['submit_id'] + ' | <a href="/app_submit">' + load_lang('new_application') + '</a>')
+                add_alarm(for_a, flask.session['submit_id'], '<a href="/app_submit">' + load_lang('new_application') + '</a>')
 
             return redirect('/')
         else:

+ 3 - 3
route/main_view_file.py

@@ -9,14 +9,14 @@ def main_view_file(data = ''):
             curs.execute(db_change("select data from other where name = 'robot_default'"))
             db_data = curs.fetchall()
             if db_data and db_data[0][0] != '':
-                return flask.Response(get_default_robots_txt(), mimetype = 'text/plain')
+                return typing.Union[str, werkzeug.wrappers.response.Response](get_default_robots_txt(), mimetype = 'text/plain')
             else:
                 curs.execute(db_change("select data from other where name = 'robot'"))
                 db_data = curs.fetchall()
                 if db_data:
-                    return flask.Response(db_data[0][0], mimetype = 'text/plain')
+                    return typing.Union[str, werkzeug.wrappers.response.Response](db_data[0][0], mimetype = 'text/plain')
                 else:
-                    return flask.Response(get_default_robots_txt(), mimetype = 'text/plain')
+                    return typing.Union[str, werkzeug.wrappers.response.Response](get_default_robots_txt(), mimetype = 'text/plain')
         elif os.path.exists(data):
             if re.search(r'\.txt$', data, flags = re.I):
                 return flask.send_from_directory('./', data, mimetype = 'text/plain')

+ 1 - 2
route/recent_history_add.py

@@ -47,8 +47,7 @@ def recent_history_add(name = 'Test', do_type = ''):
 
                 data_preview = render_set(
                     doc_name = name, 
-                    doc_data = data,
-                    data_in = ''
+                    doc_data = data
                 )
             
             return easy_minify(flask.render_template(skin_check(),

+ 77 - 7
route/tool/func.py

@@ -1336,8 +1336,14 @@ def load_skin(data = '', set_n = 0, default = 0):
 def render_set(doc_name = '', doc_data = '', data_type = 'view', data_in = '', doc_acl = ''):
     curs = conn.cursor()
 
-    # data_type in ['view', 'raw', 'api_view', 'backlink']
-    doc_acl = acl_check(doc_name, 'render') if doc_acl == '' else doc_acl
+    # data_type in ['view', 'from', 'thread', 'raw', 'api_view', 'api_thread', 'backlink']
+    # data_type을 list 형식으로 개편 필요할 듯
+    if doc_name != '':
+        doc_acl = acl_check(doc_name, 'render') if doc_acl == '' else doc_acl
+    else:
+        doc_acl = 0
+
+    data_type = 'view' if data_type == '' else data_type
     doc_data = 0 if doc_data == None else doc_data
 
     acl_dict = {}
@@ -1407,7 +1413,7 @@ def render_set(doc_name = '', doc_data = '', data_type = 'view', data_in = '', d
                     </style>
                 '''
 
-            if data_type == 'api_view':
+            if data_type == 'api_view' or data_type == 'api_thread':
                 return [
                     get_class_render[0], 
                     get_class_render[1]
@@ -1480,6 +1486,67 @@ def render_simple_set(data):
 
     return data
 
+def get_thread_pre_render(data, num, ip, topic_num = '', name = '', sub = '', do_type = 'thread'):
+    curs = conn.cursor()
+
+    call_thread_regex = r"( |\n|^)(?:#([0-9]+))( |\n|$)"
+    call_thread_count = len(re.findall(call_thread_regex, data)) * 3
+    while 1:
+        rd_data = re.search(call_thread_regex, data)
+        if call_thread_count < 0:
+            break
+        elif not rd_data:
+            break
+        else:
+            rd_data = rd_data.groups()
+
+            if do_type == 'thread':
+                curs.execute(db_change("select ip from topic where code = ? and id = ?"), [topic_num, rd_data[1]])
+            else:
+                curs.execute(db_change('select set_data from bbs_data where set_name = "comment_user_id" and set_id = ? and set_code = ?'), [topic_num, rd_data[1]])
+
+            ip_data = curs.fetchall()
+            if ip_data and ip_or_user(ip_data[0][0]) == 0:
+                if do_type == 'thread':
+                    add_alarm(ip_data[0][0], ip, '<a href="/thread/' + topic_num + '#' + num + '">' + html.escape(name) + ' - ' + html.escape(sub) + '#' + num + '</a>')
+                else:
+                    set_id = topic_num.split('-')
+                    add_alarm(ip_data[0][0], ip, 'BBS <a href="/bbs/w/' + set_id[0] + '/' + set_id[1] + '#' + num + '">' + html.escape(name) + ' - ' + html.escape(sub) + '#' + num + '</a>')
+
+            data = re.sub(call_thread_regex, rd_data[0] + '<topic_a>#' + rd_data[1] + '</topic_a>' + rd_data[2], data, 1)
+
+        call_thread_count -= 1
+
+    call_user_regex = r"( |\n|^)(?:@([^ \n]+))( |\n|$)"
+    call_user_count = len(re.findall(call_user_regex, data)) * 3
+    while 1:
+        rd_data = re.search(call_user_regex, data)
+        if call_user_count < 0:
+            break
+        elif not rd_data:
+            break
+        else:
+            rd_data = rd_data.groups()
+
+            curs.execute(db_change("select ip from history where ip = ? limit 1"), [rd_data[1]])
+            ip_data = curs.fetchall()
+            if not ip_data:
+                curs.execute(db_change("select ip from topic where ip = ? limit 1"), [rd_data[1]])
+                ip_data = curs.fetchall()
+
+            if ip_data and ip_or_user(ip_data[0][0]) == 0:
+                if do_type == 'thread':
+                    add_alarm(ip_data[0][0], ip, '<a href="/thread/' + topic_num + '#' + num + '">' + html.escape(name) + ' - ' + html.escape(sub) + '#' + num + '</a>')
+                else:
+                    set_id = topic_num.split('-')
+                    add_alarm(ip_data[0][0], ip, 'BBS <a href="/bbs/w/' + set_id[0] + '/' + set_id[1] + '#' + num + '">' + html.escape(name) + ' - ' + html.escape(sub) + '#' + num + '</a>')
+
+            data = re.sub(call_user_regex, rd_data[0] + '<topic_call>@' + rd_data[1] + '</topic_call>' + rd_data[2], data, 1)
+
+        call_user_count -= 1
+
+    return data
+
 # Func-request
 def send_email(who, title, data):
     curs = conn.cursor()
@@ -2301,12 +2368,15 @@ def do_reload_recent_thread(topic_num, date, name = None, sub = None):
             date
         ])
 
-def add_alarm(who, context):
+def add_alarm(to_user, from_user, context):
     curs = conn.cursor()
 
-    curs.execute(db_change(
-        'insert into alarm (name, data, date) values (?, ?, ?)'
-    ), [who, context, get_time()])
+    if to_user != from_user:
+        context = from_user + ' | ' + context
+
+        curs.execute(db_change(
+            'insert into alarm (name, data, date) values (?, ?, ?)'
+        ), [to_user, context, get_time()])
     
 def add_user(user_name, user_pw, user_email = '', user_encode = ''):
     curs = conn.cursor()

+ 24 - 9
route/tool/func_render.py

@@ -20,21 +20,24 @@ class class_do_render:
         curs = self.conn.cursor()
 
         doc_set = {}
-        if data_in == 'from':
-            data_in = ''
+        if data_type == 'from':
             doc_set['doc_from'] = 'O'
+            data_type = 'view'
         
         data_in = (data_in + '_') if data_in != '' else ''
         doc_set['doc_include'] = data_in
+        rep_data = ''
 
-        curs.execute(db_change("select set_data from data_set where doc_name = ? and set_name = 'document_markup'"), [doc_name])
-        rep_data = curs.fetchall()
-        if rep_data and rep_data[0][0] != '':
-            rep_data = rep_data[0][0]
-        else:
+        if rep_data == '' and doc_name != '':
+            curs.execute(db_change("select set_data from data_set where doc_name = ? and set_name = 'document_markup'"), [doc_name])
+            db_data = curs.fetchall()
+            if db_data and db_data[0][0] != '' and db_data[0][0] != 'normal':
+                rep_data = db_data[0][0]
+
+        if rep_data == '':
             curs.execute(db_change('select data from other where name = "markup"'))
-            rep_data = curs.fetchall()
-            rep_data = rep_data[0][0] if rep_data else 'namumark'
+            db_data = curs.fetchall()
+            rep_data = db_data[0][0] if db_data else 'namumark'
 
         if rep_data == 'namumark' or rep_data == 'namumark_beta':
             data_end = class_do_render_namumark(
@@ -57,6 +60,18 @@ class class_do_render:
                 {}
             ]
 
+        if data_type == 'thread' or data_type == 'api_thread':
+            data_end[0] = re.sub(
+                r'&lt;topic_a&gt;(?P<in>(?:(?!&lt;\/topic_a&gt;).)+)&lt;\/topic_a&gt;',
+                '<a href="\g<in>">\g<in></a>',
+                data_end[0]
+            )
+            data_end[0] = re.sub(
+                r'&lt;topic_call&gt;@(?P<in>(?:(?!&lt;\/topic_call&gt;).)+)&lt;\/topic_call&gt;',
+                '<a href="/w/user:\g<in>">@\g<in></a>',
+                data_end[0]
+            )
+
         if data_type == 'backlink':
             if 'backlink' in data_end[2]:
                 backlink = data_end[2]['backlink']

+ 9 - 50
route/topic.py

@@ -66,54 +66,15 @@ def topic(topic_num = 0, do_type = '', doc_name = 'Test'):
                         y_check = 1
 
                 if y_check == 1:
-                    add_alarm(match, ip + ' | <a href="/thread/' + topic_num + '#' + num + '">' + html.escape(name) + ' | ' + html.escape(sub) + ' | #' + num + '</a>')
-
-            cate_re = re.compile(r'\[\[((?:분류|category):(?:(?:(?!\]\]).)*))\]\]', re.I)
-            data = cate_re.sub('[br]', flask.request.form.get('content', 'Test').replace('\r', ''))
-
-            call_thread_regex = r"( |\n|^)(?:#([0-9]+))( |\n|$)"
-            call_thread_count = len(re.findall(call_thread_regex, data)) * 3
-            while 1:
-                rd_data = re.search(call_thread_regex, data)
-                if call_thread_count < 0:
-                    break
-                elif not rd_data:
-                    break
-                else:
-                    rd_data = rd_data.groups()
-
-                    curs.execute(db_change("select ip from topic where code = ? and id = ?"), [topic_num, rd_data[1]])
-                    ip_data = curs.fetchall()
-                    if ip_data and ip_or_user(ip_data[0][0]) == 0 and ip != ip_data[0][0]:
-                        add_alarm(ip_data[0][0], ip + ' | <a href="/thread/' + topic_num + '#' + num + '">' + html.escape(name) + ' | ' + html.escape(sub) + ' | #' + num + '</a>')
-
-                    data = re.sub(call_thread_regex, rd_data[0] + '<topic_a>#' + rd_data[1] + '</topic_a>' + rd_data[2], data, 1)
-
-                call_thread_count -= 1
-
-            call_user_regex = r"( |\n|^)(?:@([^ ]+))( |\n|$)"
-            call_user_count = len(re.findall(call_user_regex, data)) * 3
-            while 1:
-                rd_data = re.search(call_user_regex, data)
-                if call_user_count < 0:
-                    break
-                elif not rd_data:
-                    break
-                else:
-                    rd_data = rd_data.groups()
-
-                    curs.execute(db_change("select ip from history where ip = ? limit 1"), [rd_data[1]])
-                    ip_data = curs.fetchall()
-                    if not ip_data:
-                        curs.execute(db_change("select ip from topic where ip = ? limit 1"), [rd_data[1]])
-                        ip_data = curs.fetchall()
-
-                    if ip_data and ip_or_user(ip_data[0][0]) == 0 and ip != ip_data[0][0]:
-                        add_alarm(ip_data[0][0], ip + ' | <a href="/thread/' + topic_num + '#' + num + '">' + html.escape(name) + ' | ' + html.escape(sub) + ' | #' + num + '</a>')
-
-                    data = re.sub(call_user_regex, rd_data[0] + '<topic_call>@' + rd_data[1] + '</topic_call>' + rd_data[2], data, 1)
+                    add_alarm(match, ip, '<a href="/thread/' + topic_num + '#' + num + '">' + html.escape(name) + ' - ' + html.escape(sub) + '#' + num + '</a>')
+            
+            curs.execute(db_change("select ip from topic where code = ? and id = '1'"), [topic_num])
+            ip_data = curs.fetchall()
+            if ip_data and ip_or_user(ip_data[0][0]) == 0:
+                add_alarm(ip_data[0][0], ip, '<a href="/thread/' + topic_num + '#' + num + '">' + html.escape(name) + ' - ' + html.escape(sub) + '#' + num + '</a>')
 
-                call_user_count -= 1
+            data = flask.request.form.get('content', 'Test').replace('\r', '')
+            data = get_thread_pre_render(data, num, ip, topic_num, name, sub)
 
             do_add_thread(
                 topic_num,
@@ -162,9 +123,7 @@ def topic(topic_num = 0, do_type = '', doc_name = 'Test'):
                 thread_data = thread_data.replace('\r', '')
 
                 thread_data_preview = render_set(
-                    doc_name = '', 
-                    doc_data = thread_data,
-                    data_in = ''
+                    doc_data = thread_data
                 )
 
             acl_display = 'display: none;' if topic_acl == 1 else ''

+ 4 - 5
route/view_acl.py

@@ -83,7 +83,7 @@ def view_acl(name):
                         data_type = 'backlink'
                     )
 
-            markup_data = markup_data if markup_data != '' else 'default'
+            markup_data = markup_data if markup_data != '' else 'normal'
 
             if user_page == 1:
                 admin_check(5, check_data + ' (' + all_d + ')' + ' (' + markup_data + ')')
@@ -155,18 +155,17 @@ def view_acl(name):
                 <h2>''' + load_lang('markup') + '''</h2>
             '''
 
-
             curs.execute(db_change("select set_data from data_set where doc_name = ? and set_name = 'document_markup'"), [name])
             db_data = curs.fetchall()
             markup_load = db_data[0][0] if db_data and db_data[0][0] != '' else ''
 
-            markup_list = ['default'] + get_init_set_list('markup')['list']
+            markup_list = ['normal'] + get_init_set_list('markup')['list']
             markup_html = ''
             for for_a in markup_list:
                 if markup_load == for_a:
-                    markup_html = '<option value="' + (for_a if for_a != 'default' else '') + '">' + for_a + '</option>' + markup_html
+                    markup_html = '<option value="' + (for_a if for_a != 'normal' else '') + '">' + for_a + '</option>' + markup_html
                 else:
-                    markup_html += '<option value="' + (for_a if for_a != 'default' else '') + '">' + for_a + '</option>'
+                    markup_html += '<option value="' + (for_a if for_a != 'normal' else '') + '">' + for_a + '</option>'
             
             markup_html = '<select name="document_markup" ' + check_ok + '>' + markup_html + '</select>'
 

+ 1 - 1
route/view_read.py

@@ -144,7 +144,7 @@ def view_read(name = 'Test', doc_rev = '', doc_from = '', do_type = ''):
         end_data = render_set(
             doc_name = name,
             doc_data = data[0][0] if data else None,
-            data_in = 'from' if do_type == 'from' else ''
+            data_type = 'from' if do_type == 'from' else 'view'
         )
 
         if end_data == 'HTTP Request 401.3':

+ 1 - 1
version.json

@@ -1,6 +1,6 @@
 {
     "beta" : {
-        "r_ver" : "v3.4.6-RC3-dev195",
+        "r_ver" : "v3.4.6-RC3-dev196",
         "c_ver" : "3500361",
         "s_ver" : "3500111"
     }

+ 15 - 0
views/main_css/js/route/bbs_w_post.js

@@ -0,0 +1,15 @@
+function opennamu_change_comment(get_id) {
+    var _a;
+    var input = document.querySelector('#opennamu_comment_select');
+    if (input != null) {
+        input.value = get_id;
+        (_a = document.getElementById('opennamu_edit_textarea')) === null || _a === void 0 ? void 0 : _a.focus();
+    }
+}
+function opennamu_return_comment() {
+    var _a;
+    var input = document.querySelector('#opennamu_comment_select');
+    if (input != null) {
+        (_a = document.getElementById(input.value)) === null || _a === void 0 ? void 0 : _a.focus();
+    }
+}

+ 14 - 0
views/main_css/js/route/bbs_w_post.ts

@@ -0,0 +1,14 @@
+function opennamu_change_comment(get_id : string): void {
+    const input = document.querySelector('#opennamu_comment_select') as HTMLInputElement | null;
+    if(input != null) {
+        input.value = get_id;
+        document.getElementById('opennamu_edit_textarea')?.focus();
+    }
+}
+
+function opennamu_return_comment(): void {
+    const input = document.querySelector('#opennamu_comment_select') as HTMLInputElement | null;
+    if(input != null) {
+        document.getElementById(input.value)?.focus();
+    }
+}

+ 0 - 0
views/main_css/js/route/thread.ts