[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 범위 제한을 없앰)
아마 나는 오래된, 여러 게시판에 흩어져 있던 글들을 사혼의 구슬 조각 모으듯이 한 게시판으로 모아놔서 저런 문제가 나온 거 같긴 한데... 검색 속도의 문제를 생각하면 분명 필요한 코드일 거 같긴 하거든? 근데 일단 나한테는글이 재대로 안 보이는 문제가 더 커서 게시판 보기의 편의를 위해 주석처리해둠
[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
);