[28]
bbs 게시판 개조 keyboard_arrow_down
▶리스트

▶반응형 리스트(모바일 등)

▶카테코리 클릭 시 말머리로 검색한 결과

▶본문


사도세자님 BBS 바탕으로 개조 bbs_2
- 카테고리 기능 미사용
- 따로 카테고리를 사용하지 않고 쿼리와 제목 검색 기능을 활용해서 구분하는 방식으로 구현
- 선택삭제/복사/이동 가능하도록 체크폼 및 버튼 추가
- 본문 및 댓글에서도 치환자 사용 가능

카테고리 관련

MORE
스킨/bbs_str.lib.php
// 1️⃣ 말머리 리스트와 게시글 개수 가져오기
$sql = "SELECT
    SUBSTRING_INDEX(wr_subject, ')', 1) AS genre,
    COUNT(*) AS count
    FROM avo_write_$bo_table
    WHERE wr_subject LIKE '%)%'
    GROUP BY genre
    ORDER BY genre ASC";

$result = sql_query($sql);

// 2️⃣ 결과를 배열($genre)에 저장
$genre = [];
if ($result->num_rows > 0) {
    while ($row = $result->fetch_assoc()) {
        $genre[$row['genre']] = $row['count']; // 카테고리와 개수를 배열에 저장
    }
}

$genre_option = "";
// 3️⃣ 말머리가 존재할 경우 출력
if (!empty($genre)) {
    $genre_option = '<nav id="navi_category">';
    $genre_option .= '    <ul>';
    $genre_option .= '        <li><a href="?bo_table=' . $bo_table . '">전체</a></li>';

    foreach ($genre as $genre_name => $count) {
        $genre_option .= '        <li><a href="?bo_table=' . $bo_table . '&stx=' . urlencode($genre_name) . '">'
            . htmlspecialchars($genre_name) . ' <span class="cnt_tit">' . $count . '</span></a></li>';
    }

    $genre_option .= '    </ul>';
    $genre_option .= '</nav>';
}

return $genre_option;

// 말머리에 class 부여하기
function replace_subject($str)
{
    // $pattern = "/^(\\S+?)\\)\\s(.*)/";
    // $pattern = "/^(\\S+?)\\)\\s?(.*)/";
    $pattern = "/^([^)]+)\\)\\s?(.*)/";
    $replacement = '<span class="genre">$1</span>$2';
    return preg_replace($pattern, $replacement, $str);
}


스킨/list.skin.php
<? include($board_skin_path . "/bbs_str.lib.php") ?>

<? echo replace_subject($list[$i]['subject']) ?>

치환자 관련

MORE
스킨/replacement_str.php
<?
if (!defined("_GNUBOARD_")) exit; // 개별 페이지 접근 불가

// 변환 패턴 및 치환 정의
function replace_contents($str)
{

    $str = markup_text($str);


    //링크에 라벨붙게 만들기
    $str = preg_replace_callback(
        '`<a href="([^"])"[^>]>(.*?)<\/a>`i',
        function ($match) {
            $url_label = explode(",", $match[2]);
            $url = $match[1];

            // 일반 링크 설정
            $url = preg_replace_callback(
                '`^(?!http)[^@](@.)?$`i',
                function ($match) {
                    return 'http://' . $match[0];
                },
                $url
            );

            // 라벨 값이 없을 경우 LINK로 설정
            $label = isset($url_label[1]) ? $url_label[1] : 'LINK';

            // 주소와 라벨을 구분하여 설정
            if (isset($url_label[0])) {
                $url = trim($url_label[0]);
            }

            return '<a href="' . $url . '" target="_blank" class="other-site-link textggu--etc4">' . $label . '</a>';
        },
        $str
    );

    // 이미지
    $str = preg_replace("/\[\<a\shref\=\"(http|https|ftp)\:\/\/([^[:space:]]+)\.(gif|png|jpg|jpeg|bmp)\"\s[^\>]\>[^\s]\<\/a\>\]/i", "<img src='$1://$2.$3' id='target_resize_image[]' onclick='image_window(this);' border='0'>", $str);

    // 유튜브 임베드
    $str = preg_replace('/\[\s<a\shref="https:\/\/(?:www\.)?(?:youtube\.com\/(?:watch\?v=|shorts\/|live\/)|youtu\.be\/)([a-zA-Z0-9_-]+)(?:\?si=[a-zA-Z0-9_-]+)?[^"]"[^>]>[^<]<\/a>\s\]/i', '<iframe width="100%" height="315" src="https://www.youtube.com/embed/$1" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>', $str);

    // 트위터 임베드
    $str = preg_replace('/\[\s<a\shref="https:\/\/(?:x\.com|twitter\.com)\/([a-zA-Z0-9_]+)\/status\/([0-9]+)"[^>]>[^<]<\/a>\s*\]/i', '<div><blockquote class="twitter-tweet"><a href="https://twitter.com/$1/status/$2"></a></blockquote><script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script></div>', $str);

    // 요약 변환
    $str = preg_replace('`요1\[(?![\s])(.?)(?<![\s])\]-(?![\s])(.?)(?<![\s])-`', '<span class="details1"><details><summary class="textggu--etc6">$2</summary>$1</details></span>', $str);

    // 해시태그 처리
    // $str = preg_replace(
    //    "/(?<!&\#)(?<=\s)#([0-9a-zA-Z가-힣_]+)/", // #단어 형태의 해시태그 처리, ' 같은 엔티티는 제외
    //    '<a href="?bo_table=' . $bo_table . '&hash=%23$1" class="link_hash_tag textggu--etc7">#$1</a>',
    //    $str
    // );


    // 스타일 설정
    $str = preg_replace('`제목1\[(?![\s])(.?)(?<![\s*])\]`', '<span class="textggu--title1">$1</span>', $str);
    $str = preg_replace('`제목2\[(?![\s])(.?)(?<![\s*])\]`', '<span class="textggu--title2">$1</span>', $str);
    $str = preg_replace('`제목3\[(?![\s])(.?)(?<![\s*])\]`', '<span class="textggu--title3">$1</span>', $str);
    $str = preg_replace('`제목4\[(?![\s])(.?)(?<![\s*])\]`', '<span class="textggu--title4">$1</span>', $str);
    $str = preg_replace('`제목5\[(?![\s])(.?)(?<![\s*])\]`', '<span class="textggu--title5">$1</span>', $str);
    $str = preg_replace('`제목6\[(?![\s])(.?)(?<![\s])\]-(?![\s])(.?)(?<![\s])-`', '<span class="textggu--title6" data-text="$2">$1</span>', $str);
    $str = preg_replace('`제목7\[(?![\s])(.?)(?<![\s*])\]`', '<span class="textggu--title7">$1</span>', $str);

    $str = preg_replace('`소제1\[(?![\s])(.?)(?<![\s*])\]`', '<span class="textggu--sub1">$1</span>', $str);
    $str = preg_replace('`소제2\[(?![\s])(.?)(?<![\s*])\]`', '<span class="textggu--sub2">$1</span>', $str);
    $str = preg_replace('`소제3\[(?![\s])(.?)(?<![\s*])\]`', '<span class="textggu--sub3">$1</span>', $str);
    $str = preg_replace('`소제4\[(?![\s])(.?)(?<![\s*])\]`', '<span class="textggu--sub4">$1</span>', $str);
    $str = preg_replace('`소제5\[(?![\s])(.?)(?<![\s*])\]`', '<span class="textggu--sub5">$1</span>', $str);
    $str = preg_replace('`소제6\[(?![\s])(.?)(?<![\s*])\]`', '<span class="textggu--sub6">$1</span>', $str);
    $str = preg_replace('`소제7\[(?![\s])(.?)(?<![\s*])\]`', '<span class="textggu--sub7">$1</span>', $str);

    $str = preg_replace('`기타1\[(?![\s])(.?)(?<![\s*])\]`', '<span class="textggu--etc1">$1</span>', $str);
    $str = preg_replace('`기타2\[(?![\s])(.?)(?<![\s*])\]`', '<span class="textggu--etc2">$1</span>', $str);
    $str = preg_replace('`기타3\[(?![\s])(.?)(?<![\s*])\]`', '<span class="textggu--etc3">$1</span>', $str);
    $str = preg_replace('`기타4\[(?![\s])(.?)(?<![\s*])\]`', '<span class="textggu--etc4">$1</span>', $str);
    $str = preg_replace('`기타5\[(?![\s])(.?)(?<![\s*])\]`', '<span class="textggu--etc5">$1</span>', $str);
    $str = preg_replace('`기타6\[(?![\s])(.?)(?<![\s*])\]`', '<span class="textggu--etc6">$1</span>', $str);
    $str = preg_replace('`기타7\[(?![\s])(.?)(?<![\s*])\]`', '<span class="textggu--etc7">$1</span>', $str);

    echo $str;
};


?>


스킨/view.skin.php
<? include($board_skin_path . "/replacement_str.php") ?>

<? replace_contents(get_view_thumbnail($view['content'])) ?>


스킨/view_comment.skin.php
<? replace_contents($comment) ?>
view.skin.php 에 include 했으면 코멘트쪽은 include 없이 그냥 써도 ok인가봄. 거기에 한 번 더 include 했더니 댓글창이 가출함... 에바임... 왜 탈출하는지는 알려주고 튀라고
그리고 함수를 거부하는 상황이.. 있었는데 거긴 그냥 깡으로 소스 박아넣었음,,,
그래 네가 싫다는데 어쩌겠어,,,

지금 변환 적용되어 있는 스킨
- 티키타카 스킨
- 리뷰 스킨
- 브금 스킨
- bbs_2 스킨
- 로드비 기반 메모 스킨

접기 시 내용에 개행이 있으면 변환 안 되는 오류 수정

MORE
$str = preg_replace_callback(
        '`요1\[(.?)\]-(.?)-`s',
        function ($match) {
            return '<span class="details1"><details><summary class="textggu--etc1">▶' . $match[2] . '</summary>' . replace_contents($match[1]) . '</details></span>';
        },
        $str
    );

하단의 스타일 설정 공통 처리 가능하도록 수정

MORE
 $style_patterns = [
        '제목' => 'textggu--title',
        '소제' => 'textggu--sub',
        '기타' => 'textggu--etc',
    ];

    foreach ($style_patterns as $key => $class_prefix) {
        for ($i = 1; $i <= 7; $i++) {
            $str = preg_replace(
                "`{$key}{$i}\[(.*?)\]`",
                "<span class='{$class_prefix}{$i}'>$1</span>",
                $str
            );
        }
    }

    // 제목6 추가 변환 (data-text 속성 포함)
    $str = preg_replace(
        '`제목6\[(.?)\]-(.?)-`',
        '<span class="textggu--title6" data-text="$2">$1</span>',
        $str
    );

요약 시 내부의 트위터 임베딩이 안 되는 오류 수정

MORE
// 요약 변환 (재귀 적용)
$str = preg_replace_callback(
    '`요1\[(.?)\]-(.?)-`s',
    function ($match) {
        return '<span class="details1"><details><summary class="textggu--etc1">▶' . $match[2] . '</summary>' . $match[1] . '</details></span>';
        // return '<span class="details1"><details><summary class="textggu--etc1">▶' . $match[2] . '</summary>' . replace_contents($match[1]) . '</details></span>';
    },
    $str
);

replace_contents($match[1]) 한 번 더 내부에서 적용하는게 오류를 내고 있었음
나쁜자식~!!!!!

DB 내에서 일괄적으로 말머리 붙일 때 sql문

MORE
일괄적으로 원글(댓글이 아닌 글)만 말머리를 붙일 때
UPDATE avo_write_[테이블명]
SET wr_subject = CONCAT('[말머리A]) ', wr_subject)
WHERE wr_is_comment = 0 AND wr_subject NOT LIKE '[말머리A])%';

특정 카테고리의 글에만 말머리를 붙일 때
UPDATE avo_write_[테이블명]
SET wr_subject = CONCAT('[말머리A]) ', wr_subject)
WHERE wr_is_comment = 0
AND ca_name = '[카테고리명]'
AND wr_subject NOT LIKE '[말머리A])%';

보호글 작성 기능 추가

MORE
write.skin.php
코드참고: 데코BBS게시판(지비님)
 
<dl>
  <dt>OPTION</dt>
  <dd>
  <? if ($is_secret != 2 || $is_admin) { ?>
  <select name="set_secret" id="set_secret">
  <option value="">전체공개</option>
  <?= $sec ?>
  </select>
  <? } ?>
  <?php echo $option ?>
  </dd>
 </dl>

 게시글 공개 옵션 코드 아래에 아래 코드를 추가
 
<dl id="set_protect" style="display:<?= $w == 'u' && $pro_select ? 'block' : 'none' ?>;">
  <dt><label for="wr_protect">PW</label></dt>
  <dd><input type="text" name="wr_protect" id="wr_protect" value="<?= $write['wr_protect'] ?>" maxlength="20"></dd>
 </dl>

 </script> 바로 위에 아래 코드 추가
 
$('#set_secret').on('change', function() {
  var selection = $(this).val();
  if(selection=='protect') $('#set_protect').css('display','block');
  else {$('#set_protect').css('display','none'); $('#wr_protect').val('');}
 });

각 카테고리 별 게시글이 검색 구간 나뉘어 보기 불편한 문제 일단 수정

MORE
bbs/list.php
   /* 
// 가장 작은 번호를 얻어서 변수에 저장 (하단의 페이징에서 사용)
    $sql = " select MIN(wr_num) as min_wr_num from {$write_table} ";
    $row = sql_fetch($sql);
    $min_spt = (int)$row['min_wr_num'];

    if (!$spt) $spt = $min_spt;

    $sql_search .= " and (wr_num between {$spt} and ({$spt} + {$config['cf_search_part']})) ";
*/

해당 부분을 /* 와 */로 가둬 주석처리 해주어 spt 조건 제거 (검색 시 wr_num 범위 제한을 없앰)
아마 나는 오래된, 여러 게시판에 흩어져 있던 글들을 사혼의 구슬 조각 모으듯이 한 게시판으로 모아놔서 저런 문제가 나온 거 같긴 한데... 검색 속도의 문제를 생각하면 분명 필요한 코드일 거 같긴 하거든? 근데 일단 나한테는글이 재대로 안 보이는 문제가 더 커서 게시판 보기의 편의를 위해 주석처리해둠
[27]
코드 블럭 추가 keyboard_arrow_down

치환자는 (코드)내용(코드)

head.sub.php
<!-- highlight.js CSS -->
<!-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/styles/default.min.css"> -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/styles/a11y-dark.min.css">
<!-- highlight.js JS -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/highlight.min.js"></script>
<script>
    document.addEventListener('DOMContentLoaded', (event) => {
        // 모든 코드 블록에 대해 하이라이팅 적용
        hljs.highlightAll();
    });
</script>

코드블럭의 css 변경 시: 코드블럭_하이라이터_js 에서 원하는 테마를 찾아 highlight.js CSS의 파일명을 변경하면 된다. 띄어쓰기는 -로 대체하되, min은 .min으로 붙일 것.

common.lib.php
function markup_text($str)
{
// 1. 감싸진 부분을 우선 템포러리한 구분자로 변환
$str = preg_replace_callback('/\(코드\)(.*?)\(코드\)/s', function ($matches) {
// 감싸진 텍스트를 임시 변수에 저장
$temp_code = $matches[1];

// 2. 백틱 처리 전에 임시 변수에 저장된 텍스트를 수정 (백틱 처리)
// 백틱을 임시 마커로 변환
$temp_code = preg_replace('/`(.*?)`/s', '`$1`', $temp_code);

// 감싸진 텍스트를 임시 태그로 감싸기
return '<!--START_CODE-->' . $temp_code . '<!--END_CODE-->';
}, $str);

// 2. 기존 마크업 변환 처리 (중략)

// 3. 감쌌던 부분을 다시 <pre><code>로 복원하고, 'language-php' 클래스를 추가
$str = preg_replace_callback('/<!--START_CODE-->(.*?)<!--END_CODE-->/s', function ($matches) {
// 백틱을 임시 마커에서 원래의 백틱으로 복원
$code = str_replace('`', '`', $matches[1]);
// return '<pre><code class="language-php">' . htmlspecialchars($code, ENT_NOQUOTES) . '</code></pre>';
return '<pre><code class="language-php">' . $code . '</code></pre>';
}, $str);

return $str;
}
[26]
작은따옴표가 해시태그로 변환되는 문제 keyboard_arrow_down

단백님의 티키타카 스킨에서 해당 문제 발생

lib/common.lib.php
+
extend/mmb.lib.php
스킨폴더/replacement_str.php

[25]
관리자 > DB관리 링크 오류 keyboard_arrow_down
data/dbconfig.php
define('G5_DB_URL', 'MySQL 관리자 링크');

MySQL 관리자 링크 부분에 http://를 포함한 url을 넣어주면 정상적으로 이동됨
나는 http://를 빼고 넣어서 제대로 링크가 되지 않는 거였음
[24]
치환자 관련 정리 keyboard_arrow_down
[적용] 요약글 태그 간략하게 쓰기+(퍼스널) 하이퍼링크에 라벨 쉽게 붙이기 잉님
[적용] 텍꾸 치환자 넣기 지비님
[적용] 블러 굵은글 기울임 박스 치환자 넣기 비제님
[적용] 텍스트서식 팝업창 넣기 사도세자님
[적용] 박스(blockquote) 스타일 적용 코드펜 (출처)

blockquote 적용폰트는 리디바탕 눈누

blockquote에 내가 설정하지 않았는데도 margin이 40px씩 잡혀서 살펴보니 스타일의 출처는 user agent stylesheet 였음. LINK 을 참고하여 따로
blockquote { margin: 30px 5px 5px 5px; }

을 명시해주니 해결됨

[아보카도 에디션] 트위터, 유튜브, 이미지 링크로 임베딩하기 + 접기 기능 넣기 + 링크에 라벨 붙여 단축하기

LINK

포타에 파일 분리해서 정리한 부분 공유해두었다.
잉님이 올린 접기 기능과 링크에 라벨 붙이는 기능을 추가한 버전으로 텍꾸 부분을 제외하고, 치환과 관련된 패턴들이 길고 많아 정신 없는 관계로 관리의 용이함을 위해 파일을 분리했다.

replacement_str.php → 이 파일을 공용으로 여러 게시판에서 사용하는게 제일 베스트겠지만,,, 일단 이 파일 하나 수정해서 사용하는 게시판에 다 올리기만 하면 되니 수정은 훨씬 용이해질듯. 최종적으로는 이거 하나만 수정하면 모든 적용된 게시판에 바로 적용되도록 하는게 목표.

해시태그 처리 시 '와 같은 엔티티는 제외하도록 정규식 수정

$str = preg_replace(
    "/(?<!&\#)\\#([0-9a-zA-Z가-힣_])([0-9a-zA-Z가-힣_]*)/", // #단어 형태의 해시태그 처리 ('와 같은 엔티티는 제외)
    '<a href="?bo_table=' . $bo_table . '&hash=%23$1$2" class="link_hash_tag textggu--etc7">#$1$2</a>',
    $str
);
[23]
회지 일람용 갤러리 스킨 커스텀 2 keyboard_arrow_down
장르명 및 수위 여부 리스트에 노출, 표지에 마우스 호버 시 제목과 함께 CP명 출력


책과 관련된 내용 입력할 수 있는 폼 추가

wr_link1~wr_link3(3은 따로 추가함) 및 wr_5~wr_10 여분 필드 활용

참고: 링크 주소 입력 개수 늘리는 방법 LINK
wr_link3 및 wr_link3_hit 필드 추가시 사용한 SQL문
ALTER TABLE `avo_write_[테이블명]` ADD `wr_link3` TEXT CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL AFTER `wr_link2` ;
ALTER TABLE `avo_write_[테이블명]` ADD `wr_link3_hit` INT( 11 ) NOT NULL DEFAULT '0' AFTER `wr_link2_hit` ;

게시글 본문에 표 형식으로 입력한 내용 출력, 미입력시 미출력

[22]
스크롤바 숨김처리 keyboard_arrow_down
css/style.css 에 추가
참고: https://www.postype.com/@terophy/post/14695738

[21]
오토셋(Autoset) 8 keyboard_arrow_down
오토셋(Autoset)을 이용한 윈도우에 서버를 직접 설치해서 그누보드 연습하기 LINK
갠홈 뜯어고칠 때마다 업로드해서 적용해보는게 귀찮아서 로컬에 설치해서 쓰려고 찾아보니 이게 제일 심플하고 간편해서 채택.
*참고: 내 계정은 php 5.5 사용중이라 오토셋8 설치했음
아보카도도 별 무리 없이 로컬에 설치 완료. 우선 순정버전 퍼스널로 설치해봤고 잘 돌아감!

▶ autoset.github.io LINK
    ▷ 다운로드 링크
        - autoset_4_3_2 LINK
        - autoset_6_3_1 LINK
        - autoset_8_0_0 LINK
        - autoset_9_0_0 LINK
        - autoset_10_7_0 LINK