[작성자:] oneuldo

  • 정적 HTML 사이트를 워드프레스 커스텀 테마로 이전하기 ③ 헤더·푸터 분리와 관리자 수정 기능 만들기

    지난 글에서는 워드프레스에 커스텀 테마의 기본 파일을 만들고, 테마를 활성화해 임시 헤더와 푸터가 정상적으로 출력되는 것까지 확인했다.

    이번에는 정적 HTML로 제작된 메인 페이지를 워드프레스 테마 구조에 맞게 나누고, 자주 변경되는 항목을 관리자에서 수정할 수 있도록 연결했다.

    1. 정적 HTML을 워드프레스 파일로 분리하기

    기존 HTML 파일은 헤더부터 본문, 푸터까지 하나의 파일 안에 모두 들어 있었다.

    워드프레스에서는 공통 영역과 페이지별 영역을 다음과 같이 분리했다.

    custom-theme/
    ├── _astro/
    ├── inc/
    │   └── site-settings.php
    ├── favicon.svg
    ├── front-page.php
    ├── front-page.css
    ├── functions.php
    ├── header.php
    ├── footer.php
    ├── index.php
    └── style.css
    

    각 파일의 역할은 다음과 같다.

    • header.php: 로고와 상단 메뉴
    • footer.php: 하단 정보, 바로가기, 퀵 메뉴
    • front-page.php: 메인 페이지 본문
    • front-page.css: 메인 페이지에 포함돼 있던 인라인 CSS
    • functions.php: 테마 기능, CSS 연결, 메뉴 등록
    • inc/site-settings.php: 관리자용 사이트 설정 화면
    • _astro: 기존 사이트의 CSS와 이미지 파일
    • index.php: 다른 템플릿이 없을 때 사용하는 기본 파일

    이렇게 분리하면 헤더나 푸터를 한 번 수정했을 때 모든 페이지에 동일하게 적용된다.

    2. 워드프레스 기본 함수를 HTML에 추가하기

    기존 HTML을 그대로 복사하는 것만으로는 워드프레스 테마로 정상 작동하지 않는다.

    header.php에는 다음과 같은 워드프레스 기본 함수가 필요하다.

    <?php wp_head(); ?>
    

    또한 <body> 태그에는 워드프레스가 클래스와 추가 코드를 삽입할 수 있도록 다음 내용을 넣었다.

    <body <?php body_class(); ?>>
    <?php wp_body_open(); ?>
    

    footer.php에서는 닫는 </body> 태그 전에 다음 함수를 추가했다.

    <?php wp_footer(); ?>
    

    이 함수들이 빠지면 플러그인의 CSS나 JavaScript가 제대로 불러와지지 않거나, 관리자 툴바가 정상적으로 표시되지 않을 수 있다.

    3. 기존 CSS와 이미지 경로 연결하기

    기존 사이트는 Astro로 생성돼 _astro 폴더 안에 CSS와 이미지가 들어 있었다.

    테마 폴더 안에서도 _astro라는 이름을 그대로 유지했다. 폴더명을 바꿀 수도 있지만, HTML과 CSS에 들어 있는 모든 경로를 함께 수정해야 하기 때문에 우선 원래 이름을 유지하는 편이 안전했다.

    기존 HTML에 들어 있던 다음과 같은 절대경로는 워드프레스 테마 경로로 변경했다.

    /_astro/example.webp
    

    변경 후에는 다음처럼 테마 폴더를 기준으로 파일을 불러온다.

    <?php echo esc_url(get_template_directory_uri()); ?>/_astro/example.webp
    

    CSS 파일은 HTML에 직접 연결하지 않고 functions.php에서 불러오도록 했다.

    wp_enqueue_style(
        'custom-theme-main',
        get_template_directory_uri() . '/_astro/example.css',
        array(),
        filemtime(get_template_directory() . '/_astro/example.css')
    );
    

    filemtime()을 버전값으로 사용하면 CSS 파일이 수정됐을 때 브라우저 캐시 때문에 이전 디자인이 계속 보이는 문제를 줄일 수 있다.

    4. Astro의 속성은 삭제하지 않기

    원본 HTML에는 다음과 같은 속성이 많이 들어 있었다.

    data-astro-cid-xxxxxx
    

    처음에는 불필요한 코드처럼 보이지만, Astro가 생성한 CSS 선택자와 연결되는 값이다.

    이 속성을 삭제하면 HTML 구조는 그대로인데도 일부 색상, 여백, 글자 크기, 반응형 디자인이 깨질 수 있다. 따라서 워드프레스로 옮길 때도 원본 속성을 그대로 유지했다.

    HTML을 정리한다는 이유로 자동 삭제하거나 일괄 치환하지 않는 것이 안전하다.

    5. 메인 페이지 템플릿 만들기

    워드프레스에서 사이트의 첫 화면은 front-page.php가 담당한다.

    기존 index.html에서 헤더와 푸터를 제외한 본문 영역을 front-page.php로 옮기고, 파일 위아래에는 다음 코드를 넣었다.

    <?php get_header(); ?>
    
    <!-- 메인 페이지 내용 -->
    
    <?php get_footer(); ?>
    

    원본 HTML의 <style> 태그 안에 들어 있던 메인 전용 CSS는 front-page.css로 분리했다.

    이후 functions.php에서 메인 페이지일 때만 해당 CSS가 로드되도록 설정했다. 다른 페이지에서 불필요한 메인 CSS까지 불러오지 않도록 하기 위해서다.

    6. 로고와 상단 메뉴를 관리자에 연결하기

    처음에는 원본 HTML에 들어 있던 로고와 메뉴를 그대로 출력했다.

    하지만 운영 중에는 메뉴 이름이나 주소, 순서가 바뀔 수 있다. 수정할 때마다 header.php를 열어야 한다면 워드프레스를 사용하는 장점이 줄어든다.

    그래서 functions.php에 워드프레스 메뉴 위치를 등록했다.

    register_nav_menus(
        array(
            'primary' => '상단 메뉴',
            'footer'  => '푸터 메뉴',
        )
    );
    

    이제 관리자에서 메뉴를 생성하고 상단 메뉴 위치에 지정하면, 기존의 고정 메뉴 대신 관리자에서 만든 메뉴가 출력된다.

    메뉴가 아직 등록되지 않은 경우에는 사이트가 비어 보이지 않도록 원래 HTML 메뉴가 기본값으로 출력되게 했다.

    로고도 워드프레스의 사용자 정의 로고 기능과 연결했다. 관리자가 로고를 등록하면 해당 로고가 표시되고, 등록하지 않으면 기존 이미지가 출력되는 방식이다.

    7. 푸터 바로가기를 관리자 메뉴로 변경하기

    푸터에 있는 바로가기 목록도 나중에 이름, URL, 순서가 변경될 가능성이 있다.

    이 항목들은 별도의 텍스트 입력칸을 만드는 것보다 워드프레스의 메뉴 기능으로 관리하는 것이 적합했다.

    관리자에서 다음 순서로 설정할 수 있다.

    1. 모양 → 메뉴로 이동
    2. 새로운 메뉴 생성
    3. 페이지 또는 사용자 정의 링크 추가
    4. 탐색 레이블 수정
    5. 드래그하여 순서 변경
    6. 메뉴 위치를 푸터 메뉴로 지정
    7. 메뉴 저장

    이제 푸터의 링크 이름이 바뀌거나 새로운 링크가 추가돼도 footer.php를 직접 수정할 필요가 없다.

    메뉴를 지정하지 않았을 때는 원래 푸터 링크가 기본값으로 출력된다.

    8. 자주 변경되는 정보를 관리자 설정으로 만들기

    메뉴 외에도 운영 중 변경될 가능성이 높은 정보가 많았다.

    예를 들면 다음과 같다.

    • 연락처
    • 운영시간
    • 휴무일과 안내문구
    • 주소와 교통정보
    • 상담 버튼 문구
    • 외부 예약 링크
    • 블로그 링크
    • 지도 링크
    • 하단 사업자 정보
    • 푸터 소개문구

    이 내용을 모두 footer.php에 직접 작성하면, 간단한 문구 하나를 바꾸는 데도 FTP로 접속해서 PHP 파일을 수정해야 한다.

    이를 해결하기 위해 inc/site-settings.php 파일을 만들고 관리자 왼쪽 메뉴에 사이트 설정 페이지를 추가했다.

    운영시간처럼 항목의 개수가 달라질 수 있는 정보는 요일별 입력칸을 고정하지 않고 다음과 같은 형식으로 입력하도록 만들었다.

    평일|09:00 - 18:00
    토요일|09:00 - 13:00
    일요일·공휴일|휴무
    

    이 구조를 사용하면 토요일과 일요일을 각각 표시할 수도 있고, 나중에 주말이라는 항목 하나로 합칠 수도 있다.

    평일|09:00 - 18:00
    주말|휴무
    

    각 줄의 | 앞부분은 항목명, 뒷부분은 내용으로 출력된다. 항목의 이름과 개수를 자유롭게 바꿀 수 있어 디자인을 다시 수정하지 않고도 여러 상황에 대응할 수 있다.

    표시 여부가 필요한 버튼에는 체크 옵션도 추가했다. 사용하지 않는 예약이나 외부 링크는 삭제하지 않고 관리자에서 잠시 숨길 수 있다.

    9. 코드에서 관리할 부분과 관리자에서 관리할 부분 구분하기

    이번 작업을 하면서 모든 내용을 관리자에서 수정 가능하게 만드는 것이 반드시 좋은 것은 아니라는 점도 확인했다.

    관리자에서 관리하기 적합한 항목은 다음과 같다.

    • 메뉴 이름과 순서
    • 페이지 연결 주소
    • 연락처
    • 운영시간
    • 안내문구
    • 외부 링크
    • 로고
    • 푸터 정보

    반대로 파일에서 관리하는 것이 나은 항목도 있다.

    • 헤더와 푸터의 전체 레이아웃
    • 반응형 구조
    • 애니메이션
    • CSS 디자인
    • HTML 구성
    • 특수한 JavaScript 기능

    레이아웃까지 관리자에서 자유롭게 수정하도록 만들면 페이지 빌더 수준의 기능이 필요해지고, 오히려 구조가 복잡해질 수 있다.

    따라서 자주 변경되는 텍스트와 링크는 관리자에서 관리하고, 디자인과 구조는 테마 파일에서 관리하는 방식으로 나눴다.

    10. SEO 관련 코드는 아직 옮기지 않았다

    원본 HTML의 <head>에는 다음과 같은 정보가 포함돼 있었다.

    • 페이지 제목
    • 메타 설명
    • canonical 주소
    • Open Graph 정보
    • 구조화 데이터
    • 검색 차단 태그

    이 코드를 그대로 header.php에 복사하면 모든 페이지에 동일한 SEO 정보가 출력되거나, 나중에 SEO 플러그인과 중복될 수 있다.

    특히 개발용 HTML에 들어 있던 noindex 태그까지 복사하면 완성된 사이트가 검색엔진에 노출되지 않을 수 있다.

    따라서 이번 단계에서는 디자인 출력에 필요한 코드만 옮기고, SEO와 AEO 관련 정보는 개별 페이지를 만든 후 별도로 설정하기로 했다.

    11. 작업 결과

    이번 단계까지 완료한 내용은 다음과 같다.

    • 원본 HTML의 헤더·본문·푸터 분리
    • 메인 페이지를 front-page.php로 변환
    • 원본 CSS와 이미지 연결
    • Astro 전용 속성 유지
    • 워드프레스 필수 함수 추가
    • 관리자용 상단 메뉴 등록
    • 관리자용 푸터 메뉴 등록
    • 사용자 정의 로고 연결
    • 연락처·운영시간·외부 링크 설정 화면 추가
    • 푸터와 퀵 메뉴를 관리자 설정에 연결
    • SEO 중복 및 noindex 복사 방지
    • PC와 모바일 화면 출력 확인

    이제 공통으로 사용하는 헤더와 푸터는 완성됐다. 앞으로 개별 페이지를 추가해도 동일한 헤더와 푸터가 자동으로 적용된다.

    다음 단계에서는 워드프레스에 실제 페이지를 생성하고, 기존 정적 HTML의 서브페이지를 page-{slug}.php 형식의 개별 페이지 템플릿으로 변환할 예정이다.

  • 정적 HTML 사이트를 워드프레스 커스텀 테마로 이전하기 ③ 헤더·푸터 분리와 관리자 수정 기능 만들기

    지난 글에서는 워드프레스 커스텀 테마의 기본 파일을 만들고, 워드프레스가 테마를 정상적으로 인식하는 것까지 확인했다.

    이번에는 정적 HTML로 만들어진 메인 페이지를 워드프레스 테마로 옮기면서 다음 작업을 진행했다.

    • 기존 HTML의 헤더·본문·푸터 분리
    • 원본 CSS와 이미지 연결
    • 워드프레스 메뉴 기능 연동
    • 로고와 사이트 정보 관리자 연동
    • 진료시간, 연락처, 바로가기 등 변경 가능성이 높은 정보의 관리자 설정 기능 추가

    1. 기존 HTML 구조부터 나누기

    원본 index.html에는 헤더, 메인 콘텐츠, 푸터가 하나의 파일에 모두 들어 있었다.

    워드프레스에서는 공통 영역을 별도의 파일로 나누는 것이 관리하기 편하다.

    • header.php: 로고, 상단 메뉴, 모바일 메뉴
    • front-page.php: 메인 페이지 콘텐츠
    • footer.php: 하단 정보, 바로가기, 고정 버튼
    • functions.php: CSS 연결, 메뉴 등록, 테마 기능 설정

    페이지가 많아도 공통 헤더와 푸터는 한 번만 만들면 된다. 이후 디자인을 수정할 때도 모든 HTML 파일을 일일이 고칠 필요 없이 header.php 또는 footer.php만 수정하면 전체 페이지에 반영된다.

    2. 워드프레스에 필요한 기본 함수 추가하기

    기존 HTML의 <head> 내용을 그대로 복사하는 대신 워드프레스에 필요한 기본 구조를 넣었다.

    <?php wp_head(); ?>
    

    </body> 태그 앞에는 다음 함수를 넣었다.

    <?php wp_footer(); ?>
    

    이 함수가 있어야 워드프레스와 플러그인이 필요한 CSS, 자바스크립트, 메타 정보를 정상적으로 출력할 수 있다.

    body 태그에도 워드프레스용 함수를 적용했다.

    <body <?php body_class(); ?>>
    <?php wp_body_open(); ?>
    

    3. SEO 태그는 원본에서 바로 복사하지 않았다

    원본 HTML의 <head>에는 다음과 같은 정보가 포함되어 있었다.

    • 페이지 제목
    • 메타 설명
    • canonical URL
    • 구조화 데이터
    • 검색 노출 차단용 noindex

    이 내용을 테마에 그대로 넣으면 이후 SEO 플러그인에서 생성하는 정보와 중복될 수 있다. 특히 제작 중인 원본에 들어 있던 noindex가 그대로 운영 사이트에 반영되면 검색 결과에서 사이트가 제외될 수도 있다.

    따라서 이번 단계에서는 디자인 출력에 필요한 내용만 옮기고, SEO 관련 정보는 개별 페이지 제작 후 SEO 플러그인에서 설정하기로 했다.

    4. _astro 폴더와 속성은 그대로 유지하기

    이번 원본 파일은 Astro로 제작되어 CSS와 이미지가 _astro 폴더 안에 들어 있었다.

    폴더 이름을 보기 편하게 바꿀 수도 있지만, 기존 HTML과 CSS에서 _astro 경로를 여러 번 사용하고 있기 때문에 이름을 바꾸면 관련 경로도 모두 수정해야 한다.

    불필요한 오류를 줄이기 위해 _astro 폴더명은 그대로 유지했다.

    HTML 태그에 붙어 있던 다음과 같은 속성도 삭제하지 않았다.

    data-astro-cid-xxxx
    

    Astro가 생성한 CSS는 이 속성을 기준으로 특정 요소에 스타일을 적용하는 경우가 있다. 코드가 지저분해 보인다고 삭제하면 디자인이 갑자기 깨질 수 있다.

    5. CSS 파일을 워드프레스 방식으로 연결하기

    원본 HTML에서 사용하던 CSS를 functions.php에서 불러오도록 변경했다.

    wp_enqueue_style(
        'custom-theme-main',
        get_template_directory_uri() . '/_astro/index.css',
        array(),
        filemtime(get_template_directory() . '/_astro/index.css')
    );
    

    원본 HTML의 <style> 안에 직접 들어 있던 메인 페이지 전용 CSS는 front-page.css로 분리했다.

    CSS를 연결할 때는 원본 HTML에 적용되어 있던 순서도 유지해야 한다. 여러 CSS 파일에서 같은 요소를 설정하고 있다면 연결 순서에 따라 최종 디자인이 달라질 수 있기 때문이다.

    filemtime()도 함께 사용했다. CSS 파일을 수정하면 파일의 수정 시간을 버전으로 사용해 브라우저 캐시 때문에 변경사항이 늦게 보이는 문제를 줄일 수 있다.

    6. 이미지와 내부 링크 경로 수정하기

    정적 HTML에서는 이미지 경로가 다음처럼 작성되어 있었다.

    <img src="/_astro/logo.svg" alt="">
    

    워드프레스 테마에서는 설치 경로가 달라질 수 있으므로 테마 폴더 주소를 기준으로 불러오도록 변경했다.

    <img src="<?php echo esc_url(
        get_template_directory_uri() . '/_astro/logo.svg'
    ); ?>" alt="">
    

    사이트 내부 페이지 링크도 고정 주소 대신 home_url()을 사용했다.

    <a href="<?php echo esc_url(home_url('/about/')); ?>">
    

    이렇게 하면 테스트 주소에서 실제 도메인으로 사이트를 이전하더라도 링크를 다시 수정할 필요가 줄어든다.

    7. 상단 메뉴를 관리자에서 수정할 수 있게 만들기

    처음에는 원본 HTML의 메뉴를 그대로 넣어 화면부터 확인했다. 하지만 메뉴 이름이나 주소가 바뀔 때마다 header.php를 수정해야 하는 문제가 있었다.

    이를 해결하기 위해 functions.php에 워드프레스 메뉴 위치를 등록했다.

    register_nav_menus(array(
        'primary' => '상단 메뉴',
        'footer'  => '푸터 메뉴',
    ));
    

    이후 관리자 화면의 메뉴 기능에서 다음 작업을 할 수 있게 됐다.

    • 메뉴 이름 변경
    • 메뉴 링크 변경
    • 메뉴 순서 변경
    • 새로운 메뉴 추가
    • 필요 없는 메뉴 삭제
    • 하위 메뉴 구성

    워드프레스 메뉴를 아직 지정하지 않은 경우에는 원본 메뉴가 표시되도록 기본 메뉴도 남겨두었다. 덕분에 설정 도중 메뉴가 갑자기 사라지는 것을 방지할 수 있었다.

    8. 로고도 관리자에서 변경 가능하게 만들기

    로고 역시 파일에 고정하지 않고 워드프레스의 사용자 정의 로고 기능을 등록했다.

    add_theme_support('custom-logo');
    

    관리자에서 로고를 등록하면 해당 이미지가 출력되고, 등록된 로고가 없으면 기존 HTML에서 사용하던 로고가 표시되도록 처리했다.

    클라이언트가 나중에 로고를 교체하더라도 PHP 파일을 직접 수정하지 않아도 된다.

    9. 변경 가능성이 높은 정보를 별도 설정으로 만들기

    사이트를 운영하다 보면 디자인보다 내용이 더 자주 바뀐다.

    예를 들면 다음과 같다.

    • 연락처
    • 운영시간
    • 평일·주말 구분
    • 휴무 안내
    • 주소와 위치 정보
    • 하단 안내 문구
    • 상담 버튼 이름
    • 외부 예약 링크
    • 블로그 링크
    • 지도 링크

    이 내용을 모두 footer.php에 고정하면 수정 요청이 올 때마다 FTP로 파일을 열어야 한다.

    그래서 워드프레스 관리자에 별도의 사이트 설정 메뉴를 만들었다.

    사이트 설정에서는 연락처, 운영시간, 안내 문구, 외부 링크, 버튼 노출 여부 등을 변경할 수 있다.

    반복되는 정보는 다음과 같은 형태로 입력하도록 구성했다.

    평일|09:00 ~ 18:00
    주말|09:00 ~ 13:00
    휴무|공휴일
    

    왼쪽에는 항목명, 오른쪽에는 내용을 입력하고 | 기호로 구분한다.

    이 방식은 토요일과 일요일을 각각 표시하다가 나중에 주말이라는 하나의 항목으로 합치는 경우에도 PHP 파일을 수정할 필요가 없다는 장점이 있다.

    10. 푸터 바로가기는 사이트 설정이 아닌 메뉴에서 관리하기

    푸터의 바로가기 목록도 나중에 이름이나 순서가 바뀔 가능성이 높다.

    처음에는 사이트 설정에 입력 항목을 만들까 고민했지만, 바로가기는 워드프레스의 메뉴 기능을 이용하는 편이 더 적합했다.

    관리자에서 푸터 메뉴를 만들면 다음 내용을 자유롭게 바꿀 수 있다.

    • 표시되는 이름
    • 연결 주소
    • 메뉴 순서
    • 메뉴 추가 및 삭제

    푸터 메뉴를 지정하기 전에는 기존 HTML의 바로가기 목록이 표시되고, 푸터 메뉴를 지정하면 관리자에서 만든 메뉴가 대신 출력되도록 했다.

    11. 관리자가 수정해야 하는 영역 정리하기

    관리자가 워드프레스 관리자 화면에서 직접 수정할 수 있도록 만든 영역은 다음과 같다.

    • 로고 이미지
    • 상단 메뉴 이름과 링크
    • 상단 메뉴 순서
    • 하위 메뉴 구성
    • 푸터 바로가기 이름과 링크
    • 푸터 바로가기 순서
    • 연락처
    • 운영시간
    • 휴무일 안내
    • 주소와 위치 정보
    • 상담 버튼 문구
    • 예약 링크
    • 블로그 링크
    • 지도 링크
    • 하단 안내 문구
    • 외부 링크와 버튼의 표시 여부

    반대로 다음과 같은 항목은 테마 파일에서 관리한다.

    • 헤더와 푸터의 전체 레이아웃
    • 메인 페이지의 HTML 구조
    • 반응형 디자인
    • 색상, 여백, 글꼴 등 CSS 스타일
    • 애니메이션
    • 특수한 JavaScript 기능
    • SEO 플러그인과 연동되는 메타 정보

    자주 변경되는 텍스트와 링크는 관리자에서 수정할 수 있도록 만들고, 사이트의 구조와 디자인은 테마 파일에서 관리하는 방식으로 구분했다. 이렇게 하면 운영자는 PHP 파일을 직접 수정하지 않고도 필요한 내용을 변경할 수 있으며, 개발자는 전체 레이아웃과 디자인을 안정적으로 유지할 수 있다.

  • 정적 HTML 사이트를 워드프레스 커스텀 테마로 이전하기 ② 테마 기본 구조 만들기

    이전 글에서는 워드프레스의 고유주소를 설정하고, 호스팅 설치 과정에서 기본으로 추가된 플러그인을 정리했다.

    이번에는 정적 HTML 파일을 워드프레스에서 사용할 수 있도록 커스텀 테마의 기본 구조를 만들었다.

    GeneratePress 대신 커스텀 테마를 선택한 이유

    워드프레스 사이트를 제작할 때는 GeneratePress 같은 범용 테마를 설치한 뒤 블록 에디터나 페이지 빌더를 이용해 화면을 구성할 수도 있다.

    하지만 이번 작업은 디자인과 퍼블리싱이 완료된 HTML, CSS, JavaScript 파일을 워드프레스로 이전하는 것이 목적이었다.

    GeneratePress를 사용하면 기존 화면을 워드프레스 블록으로 다시 만들어야 하고, 테마에서 기본으로 제공하는 레이아웃과 CSS가 기존 코드에 영향을 줄 수도 있다.

    따라서 기존 HTML 구조와 디자인을 최대한 유지할 수 있도록 별도의 커스텀 테마를 직접 만들기로 했다.

    커스텀 테마도 일반 워드프레스 테마와 동일하게 메뉴, 페이지, 게시글, 플러그인, SEO 설정 등의 기능을 사용할 수 있다.

    SEO와 AEO는 테마 종류와 별개다

    GeneratePress를 사용하지 않는다고 해서 SEO나 AEO 작업을 할 수 없는 것은 아니다.

    테마에서는 다음과 같은 기술적인 요소를 관리한다.

    • 시맨틱 HTML 구조
    • H1, H2, H3 제목 순서
    • 모바일 반응형
    • 페이지 표시 속도
    • 이미지 대체 텍스트
    • CSS 및 JavaScript 로딩 방식

    페이지별 제목, 설명, canonical, 사이트맵, robots 설정 등은 추후 SEO 플러그인을 이용해 관리할 수 있다.

    FAQ, 질문에 대한 직접적인 답변, 구조화 데이터 등의 AEO 요소도 커스텀 테마에서 동일하게 적용할 수 있다.

    커스텀 테마 폴더 만들기

    프로젝트명이나 업종이 드러나지 않도록 테마 폴더명은 단순하게 custom-theme으로 정했다.

    custom-theme/
    ├── style.css
    ├── functions.php
    ├── header.php
    ├── footer.php
    └── index.php
    

    우선 워드프레스가 테마로 인식하고 실행할 수 있도록 다섯 개의 기본 파일을 생성했다.

    style.css 만들기

    style.css는 워드프레스가 테마의 이름과 정보를 확인하는 파일이다.

    파일 상단에 테마 정보를 주석으로 작성해야 관리자 화면의 테마 목록에 표시된다.

    /*
    Theme Name: Custom Theme
    Description: 정적 HTML 사이트를 워드프레스로 이전하기 위한 커스텀 테마
    Version: 1.0.0
    Text Domain: custom-theme
    */

    아직 기존 사이트의 CSS는 연결하지 않고, 테마 인식에 필요한 정보만 작성했다.

    functions.php 만들기

    functions.php는 테마에서 사용할 워드프레스 기능과 CSS, JavaScript 등을 등록하는 파일이다.

    <?php
    
    function custom_theme_setup() {
        add_theme_support('title-tag');
        add_theme_support('post-thumbnails');
    
        add_theme_support('html5', [
            'search-form',
            'comment-form',
            'comment-list',
            'gallery',
            'caption',
            'style',
            'script',
        ]);
    
        register_nav_menus([
            'primary' => '메인 메뉴',
        ]);
    }
    add_action('after_setup_theme', 'custom_theme_setup');
    
    
    function custom_theme_assets() {
        wp_enqueue_style(
            'custom-theme-style',
            get_stylesheet_uri(),
            [],
            wp_get_theme()->get('Version')
        );
    }
    add_action('wp_enqueue_scripts', 'custom_theme_assets');

    현재 코드에는 다음 기능이 들어 있다.

    • 워드프레스가 페이지 제목을 관리하도록 설정
    • 대표 이미지 기능 활성화
    • HTML5 마크업 지원
    • 메인 메뉴 위치 등록
    • 테마의 style.css 연결

    기존 정적 사이트의 CSS와 JavaScript는 이후 작업에서 이 파일을 통해 추가할 예정이다.

    header.php 만들기

    header.php에는 모든 페이지에서 공통으로 사용하는 문서 시작 부분과 헤더 영역을 작성한다.

    <!doctype html>
    <html <?php language_attributes(); ?>>
    
    <head>
        <meta charset="<?php bloginfo('charset'); ?>">
        <meta name="viewport" content="width=device-width, initial-scale=1">
    
        <?php wp_head(); ?>
    </head>
    
    <body <?php body_class(); ?>>
    <?php wp_body_open(); ?>
    
    <header id="site-header">
        <p>임시 헤더</p>
    </header>

    wp_head()는 워드프레스와 플러그인이 CSS, JavaScript, SEO 태그 등을 <head> 영역에 출력할 수 있도록 하는 필수 코드다.

    body_class()는 현재 페이지 상태에 맞는 클래스를 <body>에 자동으로 추가하고, wp_body_open()은 플러그인이 <body> 시작 부분에 필요한 코드를 삽입할 수 있도록 한다.

    실제 헤더 디자인을 옮기기 전이므로 현재는 ‘임시 헤더’ 문구만 넣었다.

    footer.php 만들기

    footer.php에는 모든 페이지에서 공통으로 사용하는 푸터와 HTML 종료 태그를 작성한다.

    <footer id="site-footer">
        <p>임시 푸터</p>
    </footer>
    
    <?php wp_footer(); ?>
    </body>
    </html>

    wp_footer()는 워드프레스와 플러그인이 필요한 JavaScript를 페이지 하단에 출력할 수 있도록 하는 필수 코드다.

    index.php 만들기

    index.php는 워드프레스가 사용할 적절한 템플릿을 찾지 못했을 때 실행하는 기본 템플릿이다.

    <?php get_header(); ?>
    
    <main id="primary" class="site-main">
        <?php if (have_posts()) : ?>
    
            <?php while (have_posts()) : ?>
                <?php the_post(); ?>
    
                <article id="post-<?php the_ID(); ?>">
                    <h1><?php the_title(); ?></h1>
    
                    <?php the_content(); ?>
                </article>
            <?php endwhile; ?>
    
        <?php else : ?>
    
            <p>표시할 콘텐츠가 없습니다.</p>
    
        <?php endif; ?>
    </main>
    
    <?php get_footer(); ?>

    get_header()get_footer()를 사용하면 각각 header.phpfooter.php를 불러올 수 있다.

    워드프레스 루프를 이용해 현재 페이지나 게시글의 제목과 본문을 출력하도록 기본 구조를 만들었다.

    FTP에 커스텀 테마 업로드하기

    파일을 모두 만든 뒤 custom-theme 폴더 전체를 워드프레스의 테마 경로에 업로드했다.

    /wp-content/themes/custom-theme/

    워드프레스 관리자에서 다음 메뉴로 이동하면 새로 만든 테마를 확인할 수 있다.

    모양(구 버전에서는 외모) → 테마 → Custom Theme

    테마를 ‘활성화’한 뒤 홈페이지에서 임시 헤더, 콘텐츠, 임시 푸터가 출력되면 기본 테마가 정상적으로 작동하는 것이다.

    아직 기존 CSS와 HTML을 옮기지 않았기 때문에 디자인 없이 기본 문구만 표시되는 것이 정상이다.

    사용하지 않는 기본 테마 정리하기

    커스텀 테마를 활성화한 뒤 사용하지 않는 워드프레스 기본 테마도 정리했다.

    기본 테마를 모두 삭제하기보다는 커스텀 테마에 오류가 발생했을 때 사용할 수 있도록 최신 기본 테마 하나를 남겨두는 것이 좋다.

    최종적으로 다음 파일과 폴더만 유지했다.

    themes/
    ├── custom-theme/
    ├── twentytwentyfive/
    └── index.php
    • custom-theme: 실제로 사용할 커스텀 테마
    • twentytwentyfive: 오류 발생 시 사용할 비상용 기본 테마
    • index.php: 테마 폴더의 디렉터리 목록 노출을 방지하는 파일

    오래된 twentytwentyfour, twentytwentythree 테마는 삭제했다.

    특히 themes 폴더 안의 index.php는 테마가 아니므로, 불필요한 파일이라고 생각해 삭제하지 않도록 주의해야 한다.

    커스텀 테마 기본 구조 완성

    여기까지 진행하면 워드프레스가 인식할 수 있는 커스텀 테마의 기본 틀이 완성된다.

    현재는 디자인이 없는 상태지만, 다음 작업부터 기존 정적 HTML의 공통 헤더와 푸터를 각각 header.php, footer.php로 옮기고 CSS와 JavaScript 경로를 워드프레스 테마에 맞게 수정할 예정이다.

  • 정적 HTML 사이트를 워드프레스 커스텀 테마로 이전하기 ① 초기 설정과 플러그인 정리

    호스팅 구매

    카페24에서 ‘뉴매니지드 워드프레스 호스팅’을 구매한다.
    비용은 아래와 같고, 우선 ‘빌드업’으로 구매했다.

    관리자 접속 및 플러그인 설정

    호스팅 구입 후 도메인 뒤에 /wp-admin 을 입력하고 들어가면 워드프레스 관리자에 접속 할 수 있다.

    좌측 메뉴 ‘플러그인 – 설치한 플러그인’ 으로 들어가면, 워드프레스에서 기본으로 설치되는 플러그인과 카페24 뉴매니지드 호스팅에서 기본으로 설치되는 플러그인을 확인 할 수 있다.

    보안, 캐시, 스팸 방지와 관련된 플러그인이 기본으로 여러 개 설치되어 있었다. 각 플러그인이 어떤 역할을 하는지 확인해 보자.

    1. Akismet Anti-spam: 스팸 방지

    워드프레스의 댓글이나 문의 폼으로 들어오는 내용을 분석하여 스팸을 자동으로 걸러주는 플러그인이다.

    댓글 기능을 사용하거나 문의 폼에 스팸이 자주 들어오는 사이트라면 도움이 되지만, 댓글을 사용하지 않는 일반 기업·병원 홈페이지에서는 활용도가 낮을 수 있다.

    2. Disable XML-RPC-API: XML-RPC 차단

    워드프레스의 XML-RPC 기능을 비활성화하는 플러그인이다.

    XML-RPC는 외부 프로그램에서 워드프레스에 글을 작성하거나 원격으로 관리할 때 사용하는 기능이지만, 무차별 로그인 공격이나 핑백 스팸에 악용되기도 한다. 외부 앱, 원격 게시, Jetpack 등의 기능을 사용하지 않는다면 차단해도 된다.

    다만 다른 보안 플러그인에서 XML-RPC 보호 기능을 제공한다면 별도로 설치할 필요는 없다.

    3. Hello Dolly: 관리자 화면에 가사 표시

    워드프레스에 기본으로 포함되는 예제 플러그인이다.

    활성화하면 워드프레스 관리자 화면 오른쪽 위에 재즈 음악 ‘Hello, Dolly!’의 가사가 무작위로 표시된다. 사이트 운영이나 보안, 성능에 필요한 기능은 없으며 워드프레스 플러그인이 작동하는 방식을 보여주기 위한 상징적인 예제에 가깝다.

    홈페이지 제작에는 필요하지 않으므로 삭제해도 된다

    4. Login Lockdown: 로그인 시도 제한

    워드프레스 로그인에 여러 번 실패한 IP 주소를 일정 시간 동안 차단하는 플러그인이다.

    관리자 비밀번호를 반복해서 입력하는 무차별 대입 공격을 막는 용도로 사용한다. 로그인 보안 기능이 포함된 다른 보안·방화벽 플러그인을 사용하고 있다면 기능이 중복될 수 있다.

    5. NinjaFirewall (WP Edition): 워드프레스 방화벽

    워드프레스에 들어오는 HTTP·HTTPS 요청을 검사하여 악성 요청을 차단하는 웹 애플리케이션 방화벽(WAF) 플러그인이다.

    일반적인 워드프레스 플러그인보다 앞단에서 요청을 검사하기 때문에 악성 코드 삽입, 해킹 시도, 비정상적인 로그인 요청 등을 워드프레스에 도달하기 전에 차단할 수 있다.

    로그인 공격 방어, XML-RPC 보호, 파일 변경 감지, 방화벽 로그 확인 등의 기능도 제공한다.

    6. Really Simple Security: SSL 및 종합 보안

    이전에는 ‘Really Simple SSL’이라는 이름으로 알려졌던 플러그인이다.

    HTTP 주소를 HTTPS로 리디렉션하고 SSL 적용을 도와주는 기능에서 시작했으며, 현재는 취약점 탐지, 로그인 보호, 워드프레스 보안 강화, 2단계 인증 등의 기능도 제공한다.

    호스팅에서 이미 SSL이 자동 적용되고 있고 다른 방화벽과 2단계 인증 플러그인을 사용한다면 기능이 상당 부분 중복될 수 있다.

    7. Two Factor: 2단계 인증

    워드프레스 관리자 로그인에 2단계 인증을 추가하는 플러그인이다.

    기존 아이디와 비밀번호를 입력한 후 인증 앱의 일회용 코드, 이메일 인증 코드, 백업 코드 등의 추가 인증을 거쳐야 로그인할 수 있도록 한다.

    비밀번호가 외부에 노출되더라도 인증 코드가 없으면 관리자 페이지에 접근하기 어려워지기 때문에 관리자 계정 보안에 효과적이다.

    플러그인을 활성화한 뒤에는 ‘사용자 → 프로필’에서 사용할 인증 방식을 별도로 설정해야 한다.

    8. WP 슈퍼 캐시: 사이트 속도 개선

    워드프레스에서 생성되는 동적 페이지를 정적 HTML 파일로 저장하는 캐시 플러그인이다.

    방문자가 페이지를 열 때마다 PHP와 데이터베이스를 실행하는 대신 미리 만들어 둔 HTML 파일을 보여주기 때문에 서버 부하를 줄이고 사이트의 표시 속도를 개선할 수 있다.

    다만 사이트 제작 중에는 CSS나 HTML을 수정해도 이전 화면이 계속 보일 수 있다. 따라서 제작 중에는 비활성화하고 사이트 공개 전에 다시 활성화하는 편이 작업하기 편하다.

    9. WPForce Logout: 사용자 강제 로그아웃

    워드프레스에 로그인한 사용자를 강제로 로그아웃시키는 플러그인이다.

    여러 명이 로그인하는 회원제 사이트에서 특정 사용자 또는 전체 사용자의 로그인 세션을 종료할 때 사용할 수 있다. 관리자 한두 명만 사용하는 일반 홈페이지에서는 활용할 일이 많지 않다.

    플러그인 정리 결과

    설치된 플러그인을 확인해 보니 보안과 로그인 보호 기능이 여러 플러그인에 중복되어 있었다.

    이번 작업할 홈페이지는 회원제 사이트가 아니고 관리자만 워드프레스에 로그인하므로, 방화벽은 NinjaFirewall 하나로 관리하고 Two Factor로 관리자 2단계 인증을 적용하기로 했다.

    WP 슈퍼 캐시는 홈페이지 제작 중에는 비활성화하고, 작업이 끝난 뒤 사이트 공개 전에 다시 활성화할 예정이다.

    이렇게 3가지 플러그인만 남기고 ‘지금 업데이트’ 버튼을 눌러 업데이트를 한다.
    나머지 플러그인은 삭제, 활성화 되어있는 플러그인은 ‘비활성화’를 시킨 후 삭제하면 된다.


    Disable XML-RPC-API 플러그인만 삭제를 누르면 치명적인 오류가 있다고 나오는데, 플러그인이 오래돼서 삭제 과정에서 오류를 내는 것 같다. ‘비활성화’ 상태기 때문에 사이트 작동에는 영향이 없어서 그대로 놔둬도 되고, FTP로 접속해서 폴더를 지워도 된다.

    FTP 접속 후 , /www/wp-content/plugins 경로로 들어가서 disable-xml-rpc-api 폴더를 지우면 된다.

    이렇게 깔끔하게 정리를 했다. 정리를 하고나서 보니 ‘Two Factor’ 플러그인 명칭이 한글로 ‘이중 인증’ 이라고 나온다. 이유는 모르겠지만 상관 없으니 다음으로 넘어간다.

  • 그누보드 DB 건드리지 않고, 상담 신청 폼 만드는 법2

    지난 시간에는 신청폼에 입력하는 데이터를 게시판에 쌓이도록 했다.
    이번에는 게시판에 쌓이는 데이터를 관리자에서 확인할 수 있도록 해본다.

    관리자 메뉴에서 상담 내역을 표로 조회하는 것부터 시작한다.

    1. /adm/consultation_list.php 생성

    <?php
    $sub_menu = '910100';
    
    include_once('./_common.php');
    
    if ($is_admin !== 'super') {
        alert('최고관리자만 접근할 수 있습니다.');
    }
    
    $g5['title'] = '상담 신청 관리';
    
    include_once(G5_ADMIN_PATH.'/admin.head.php');
    
    $bo_table = 'consultation';
    $write_table = $g5['write_prefix'].$bo_table;
    
    // 검색값
    $search_field = isset($_GET['search_field'])
        ? $_GET['search_field']
        : '';
    
    $search_keyword = isset($_GET['search_keyword'])
        ? trim($_GET['search_keyword'])
        : '';
    
    $consult_part = isset($_GET['consult_part'])
        ? trim($_GET['consult_part'])
        : '';
    
    $where = array();
    
    if ($search_keyword !== '') {
        $keyword = sql_escape_string($search_keyword);
    
        if ($search_field === 'name') {
            $where[] = "wr_name like '%{$keyword}%'";
        } elseif ($search_field === 'phone') {
            $phone_keyword = preg_replace('/[^0-9]/', '', $search_keyword);
            $phone_keyword = sql_escape_string($phone_keyword);
    
            $where[] = "wr_3 like '%{$phone_keyword}%'";
        }
    }
    
    $allowed_parts = array(
        '유륜',
        '외음부',
        '팔꿈치',
        '겨드랑이',
        '기타 부위'
    );
    
    if (in_array($consult_part, $allowed_parts, true)) {
        $part = sql_escape_string($consult_part);
        $where[] = "wr_1 = '{$part}'";
    }
    
    $where_sql = '';
    
    if (!empty($where)) {
        $where_sql = ' where '.implode(' and ', $where);
    }
    
    // 전체 개수
    $row = sql_fetch("
        select count(*) as cnt
        from {$write_table}
        {$where_sql}
    ");
    
    $total_count = (int) $row['cnt'];
    
    // 페이지 설정
    $rows = 20;
    $page = isset($_GET['page']) ? (int) $_GET['page'] : 1;
    
    if ($page < 1) {
        $page = 1;
    }
    
    $total_page = ceil($total_count / $rows);
    
    if ($total_page > 0 && $page > $total_page) {
        $page = $total_page;
    }
    
    $from_record = ($page - 1) * $rows;
    
    // 상담 목록
    $result = sql_query("
        select *
        from {$write_table}
        {$where_sql}
        order by wr_id desc
        limit {$from_record}, {$rows}
    ");
    
    // 검색조건 유지
    $query_params = array(
        'search_field' => $search_field,
        'search_keyword' => $search_keyword,
        'consult_part' => $consult_part
    );
    
    $query_string = http_build_query($query_params);
    ?>
    
    <style>
    .consultation-summary {
        margin: 0 0 15px;
        font-size: 14px;
    }
    
    .consultation-table td {
        vertical-align: middle;
    }
    
    .consultation-table .consult-name {
        font-weight: 700;
    }
    
    .consultation-table .consult-phone {
        white-space: nowrap;
    }
    
    .consultation-empty {
        padding: 60px 10px !important;
        text-align: center;
    }
    
    .consultation-search {
        display: flex;
        flex-wrap: wrap;
        gap: 5px;
        margin-bottom: 15px;
    }
    
    .consultation-search select,
    .consultation-search input {
        height: 35px;
    }
    
    .consultation-search input {
        width: 220px;
        padding: 0 10px;
        border: 1px solid #d5d5d5;
    }
    
    .consultation-status {
        display: inline-block;
        padding: 5px 9px;
        border-radius: 20px;
        background: #f1f3f5;
        font-size: 12px;
    }
    </style>
    
    <div class="local_ov01 local_ov">
        전체 상담 신청
        <strong><?php echo number_format($total_count); ?>건</strong>
    </div>
    
    <form method="get" class="consultation-search">
        <select name="consult_part">
            <option value="">전체 상담 부위</option>
    
            <?php foreach ($allowed_parts as $part) { ?>
                <option
                    value="<?php echo get_text($part); ?>"
                    <?php echo $consult_part === $part ? 'selected' : ''; ?>
                >
                    <?php echo get_text($part); ?>
                </option>
            <?php } ?>
        </select>
    
        <select name="search_field">
            <option
                value="name"
                <?php echo $search_field === 'name' ? 'selected' : ''; ?>
            >
                성함
            </option>
    
            <option
                value="phone"
                <?php echo $search_field === 'phone' ? 'selected' : ''; ?>
            >
                연락처
            </option>
        </select>
    
        <input
            type="text"
            name="search_keyword"
            value="<?php echo get_text($search_keyword); ?>"
            placeholder="검색어를 입력해주세요"
        >
    
        <button type="submit" class="btn btn_02">검색</button>
    
        <a href="./consultation_list.php" class="btn btn_01">
            초기화
        </a>
    </form>
    
    <div class="tbl_head01 tbl_wrap">
        <table class="consultation-table">
            <caption>상담 신청 목록</caption>
    
            <thead>
                <tr>
                    <th scope="col">번호</th>
                    <th scope="col">접수일</th>
                    <th scope="col">성함</th>
                    <th scope="col">연락처</th>
                    <th scope="col">상담 부위</th>
                    <th scope="col">통화 가능 시간</th>
                    <th scope="col">상태</th>
                </tr>
            </thead>
    
            <tbody>
                <?php
                $list_num = $total_count - $from_record;
    
                for ($i = 0; $row = sql_fetch_array($result); $i++) {
                    $status = $row['wr_5'] !== ''
                        ? $row['wr_5']
                        : '상담 전';
    
                    $phone = preg_replace(
                        '/^(01[0-9])([0-9]{3,4})([0-9]{4})$/',
                        '$1-$2-$3',
                        $row['wr_3']
                    );
                ?>
                    <tr>
                        <td class="td_num">
                            <?php echo number_format($list_num); ?>
                        </td>
    
                        <td class="td_datetime">
                            <?php echo get_text($row['wr_datetime']); ?>
                        </td>
    
                        <td class="consult-name">
                            <?php echo get_text($row['wr_name']); ?>
                        </td>
    
                        <td class="consult-phone">
                            <?php echo get_text($phone); ?>
                        </td>
    
                        <td>
                            <?php echo get_text($row['wr_1']); ?>
                        </td>
    
                        <td>
                            <?php echo get_text($row['wr_2']); ?>
                        </td>
    
                        <td>
                            <span class="consultation-status">
                                <?php echo get_text($status); ?>
                            </span>
                        </td>
    
                    </tr>
                <?php
                    $list_num--;
                }
    
                if ($i === 0) {
                ?>
                    <tr>
                        <td colspan="7" class="consultation-empty">
                            등록된 상담 신청이 없습니다.
                        </td>
                    </tr>
                <?php } ?>
            </tbody>
        </table>
    </div>
    
    <?php
    echo get_paging(
        G5_IS_MOBILE ? 5 : 10,
        $page,
        $total_page,
        './consultation_list.php?'.$query_string.'&amp;page='
    );
    
    include_once(G5_ADMIN_PATH.'/admin.tail.php');
    ?>

    2. 관리자 메뉴 파일 생성

    /adm 폴더에 admin.menu910.php가 이미 있는지 먼저 확인한 뒤, 없으면 새로 만들고 아래 내용을 넣는다.

    <?php
    if (!defined('_GNUBOARD_')) exit;
    
    $menu['menu910'] = array(
        array('910000', '상담 관리', G5_ADMIN_URL.'/consultation_list.php', 'consultation'),
        array('910100', '상담 신청 관리', G5_ADMIN_URL.'/consultation_list.php', 'consultation_list')
    );
    ?>

    3. 확인

    파일 2개를 업로드 후 관리자 페이지를 새로고침한다.
    consultation_list.php
    admin.menu910.php

    관리자 페이지에 consultation 게시판의 상담 신청이 표로 나오게 된다.

  • 그누보드 DB 건드리지 않고, 상담 신청 폼 만드는 법

    ‘이벤트’ 게시판 생성 후, 게시판 목록 하단에 상담 신청 영역을 추가해보자.

    우선 ftp에 접속해서 ‘/www/skin/board’ 경로로 간다.
    사용할 게시판 테마를 basic이든, gallery든 하나 정해서 복사한다.
    복사한 폴더 명을 ‘event’로 변경해준다..

    테마 스킨을 사용할 경우 테마 안에 있는 board로 가야한다.
    /www/theme/테마명/skin/board

    동일하게 ‘/www/mobile/skin/board’ 경로로 가서,
    하나의 폴더를 복사 후, ‘event’로 폴더명을 변경해준다.

    그누보드 관리자에 로그인해서 접속한다.
    사용자에게 노출시킬 ‘이벤트’ 게시판을 하나 생성해주자.
    게시판 관리 > 게시판 관리 로 가서 ‘게시판 추가’ 버튼을 누른다.

    게시판 값들을 입력해준다.

    스킨 디렉토리와 모바일 스킨 디렉토리를 새로 만든 ‘event’로 선택해준다.

    모든 설정을 마치고 확인을 눌러서 ‘이벤트’ 게시판을 생성한다.

    게시판을 하나 더 만든다.
    여기에는 상담신청 폼 데이터들이 쌓이게 할 것이기 때문에 메뉴에는 등록하지 않는다.

    설정값들을 설정한 후, 확인을 눌러서 ‘상담신청내역’ 게시판을 생성한다.

    우선 해야할 일은 3가지다.

    1. list.skin.php 파일 수정 (코드 한줄 넣기)
    2. consultation_form.php 새로 만들기
    3. consultation.css 새로 만들기

    ftp로 돌아가서, list.skin.php를 찾아서 연다.
    경로는 이렇다. /skin/board/event/list.skin.php
    파일을 수정할건데 혹시 모르니 백업 파일을 만들어두자.

    마크업이 끝나는 지점에 아래 코드를 넣어주자.

    <?php include_once($board_skin_path.'/consultation_form.php'); ?>

    consultation_form.php 파일을 새로 만들어준다.
    여기에는 마크업을 입력하면 된다.
    코드는 사용할 폼에 알맞게 변경해주면 된다.

    <?php
    if (!defined('_GNUBOARD_')) exit;
    
    add_stylesheet(
        '<link rel="stylesheet" href="'.$board_skin_url.'/consultation.css">',
        0
    );
    ?>
    
    <section class="consultation-wrap">
        <div class="consultation-inner">
    
            <div class="consultation-title">
                <span>CONSULTATION</span>
                <h2>간편 상담 신청</h2>
                <p>상담 신청을 남겨주시면 확인 후 연락드리겠습니다.</p>
            </div>
    
            <form id="consultationForm" autocomplete="off">
                <div class="consultation-fields">
    
                    <div class="consultation-field">
                        <label for="consult_name">성함</label>
                        <input
                            type="text"
                            id="consult_name"
                            name="consult_name"
                            placeholder="성함을 입력해주세요"
                             required
                        >
                    </div>
    
                    <div class="consultation-field">
                        <label for="consult_part">상담 부위</label>
                        <select id="consult_part" name="consult_part" required>
                            <option value="">상담 부위를 선택해주세요</option>
                            <option value="유륜">유륜</option>
                            <option value="외음부">외음부</option>
                            <option value="팔꿈치">팔꿈치</option>
                            <option value="겨드랑이">겨드랑이</option>
                            <option value="기타 부위">기타 부위</option>
                        </select>
                    </div>
    
                    <div class="consultation-field">
                        <label for="consult_time">통화 가능 시간</label>
                        <select id="consult_time" name="consult_time" required>
                            <option value="">통화 가능 시간을 선택해주세요</option>
                            <option value="10:00~11:00">10:00~11:00</option>
                            <option value="11:00~12:00">11:00~12:00</option>
                            <option value="13:00~14:00">13:00~14:00</option>
                            <option value="14:00~15:00">14:00~15:00</option>
                            <option value="15:00~16:00">15:00~16:00</option>
                            <option value="16:00~17:00">16:00~17:00</option>
                            <option value="17:00~18:00">17:00~18:00</option>
                        </select>
                    </div>
    
                    <div class="consultation-field consultation-phone">
                        <label for="consult_phone">연락처</label>
                        <input
                            type="tel"
                            id="consult_phone"
                            name="consult_phone"
                            placeholder="010-0000-0000"
                            maxlength="13"
                            required
                        >
                    </div>
    
                </div>
    
                <div class="consultation-agree">
                    <label>
                        <input
                            type="checkbox"
                            id="consult_privacy"
                            name="consult_privacy"
                            value="1"
                            required
                        >
                        <span>개인정보 수집 및 이용에 동의합니다.</span>
                    </label>
    
                    <button type="button" class="privacy-detail">
                        자세히 보기
                    </button>
                </div>
    
                <button type="submit" class="consultation-submit">
                    상담 신청
                </button>
            </form>
    
        </div>
    </section>

    consultation.css 파일을 새로 만들어준다.
    여기에는 css를 입력하면 된다.

    .consultation-wrap {
        margin-top: 100px;
        padding: 80px 20px;
        background: #f5f7f9;
        box-sizing: border-box;
    }
    
    .consultation-inner {
        width: 100%;
        max-width: 1280px;
        margin: 0 auto;
    }
    
    .consultation-title {
        margin-bottom: 40px;
        text-align: center;
    }
    
    .consultation-title span {
        display: block;
        margin-bottom: 10px;
        color: #2d6388;
        font-size: 14px;
        font-weight: 700;
        letter-spacing: 2px;
    }
    
    .consultation-title h2 {
        margin: 0 0 12px;
        color: #222;
        font-size: 36px;
        line-height: 1.3;
    }
    
    .consultation-title p {
        margin: 0;
        color: #777;
        font-size: 16px;
    }
    
    .consultation-fields {
        display: grid;
        grid-template-columns: repeat(2, 1fr);
        gap: 18px;
    }
    
    .consultation-field label {
        display: block;
        margin-bottom: 9px;
        color: #333;
        font-size: 15px;
        font-weight: 600;
    }
    
    .consultation-field input,
    .consultation-field select {
        width: 100%;
        height: 58px;
        padding: 0 18px;
        border: 1px solid #ddd;
        border-radius: 4px;
        background: #fff;
        color: #333;
        font-size: 15px;
        box-sizing: border-box;
    }
    
    .consultation-field input:focus,
    .consultation-field select:focus {
        border-color: #2d6388;
        outline: none;
    }
    
    .consultation-agree {
        display: flex;
        align-items: center;
        justify-content: center;
        gap: 12px;
        margin-top: 30px;
        color: #555;
        font-size: 14px;
    }
    
    .consultation-agree label {
        display: flex;
        align-items: center;
        gap: 7px;
        cursor: pointer;
    }
    
    .consultation-agree input {
        width: 17px;
        height: 17px;
        margin: 0;
    }
    
    .privacy-detail {
        padding: 0;
        border: 0;
        border-bottom: 1px solid #777;
        background: none;
        color: #777;
        font-size: 13px;
        cursor: pointer;
    }
    
    .consultation-submit {
        display: block;
        width: 260px;
        height: 62px;
        margin: 30px auto 0;
        border: 0;
        border-radius: 4px;
        background: #2d6388;
        color: #fff;
        font-size: 18px;
        font-weight: 700;
        cursor: pointer;
    }
    
    .consultation-submit:hover {
        background: #234f6d;
    }
    
    @media (max-width: 768px) {
        .consultation-wrap {
            margin-top: 60px;
            padding: 55px 20px;
        }
    
        .consultation-title h2 {
            font-size: 28px;
        }
    
        .consultation-fields {
            grid-template-columns: 1fr;
            gap: 15px;
        }
    
        .consultation-submit {
            width: 100%;
        }
    }

    이제 이 3가지 파일을 ‘/skin/board/event’ 경로에 업로드 시켜준다.
    (list.skin.php, consultation_form.php, consultation.css)

    업로드 후, ‘이벤트’ 게시판 화면 새로고침해서 폼이 제대로 보이는지 확인하자. 아래처럼 나오면 잘 된 것이다.

    이제 다음 단계는 상담 신청 버튼을 누르면 consultation 게시판에 저장되도록 연결하는 것이다.

    우선 그누보드 버전을 확인해야한다.
    그누보드 최상단 /www 경로에서 ‘config.php’ 찾아서 열어본다.
    Ctrl+F를 눌러 ‘G5_VERSION’를 검색해보면, 아래처럼 생긴 것이 있을 것이다. 이것이 버전이다.

    define('G5_VERSION', '5.6.10');

    검색이 되지 않는다면, 아래와 같이 설정이 어디에 있는지 적혀있는 줄이 있을 것이다. 현재 홈페이지의 경우 ‘version.php’에 있다고 한다.

    include_once($g5_path['path'].'/version.php');   // 설정 파일

    그누보드 최상단 /www 경로에서 ‘version.php’ 찾아서 열어본다.
    그누보드 5.6.8 이란 것을 확인 할 수 있다.

    define('G5_GNUBOARD_VER', '5.6.8');

    이제 저장 기능을 연결할 것이다.

    1. consultation_form.php 위쪽 수정
    2. <form> 시작 태그 교체
    3. consultation.css 맨 아래 추가
    4. consultation_submit.php 생성

    1. consultation_form.php 위쪽 수정

    기존 상단 PHP 안에 토큰 생성 코드를 추가한다.

    // 상담 신청 보안 토큰
    $consult_token = md5(uniqid((string) mt_rand(), true));
    
    set_session('ss_consult_token', $consult_token);
    set_session('ss_consult_started', G5_SERVER_TIME);

    2. <form> 시작 태그 교체

    ‘<form id=”consultationForm” autocomplete=”off”>’
    위 한 줄을 아래와 같이 바꾼다.

    <form
        id="consultationForm"
        action="<?php echo $board_skin_url; ?>/consultation_submit.php"
        method="post"
        autocomplete="off"
    >
        <input
            type="hidden"
            name="consult_token"
            value="<?php echo $consult_token; ?>"
        >
    
        <!-- 자동등록 프로그램 확인용 -->
        <div class="consultation-trap" aria-hidden="true">
            <label for="consult_website">홈페이지</label>
            <input
                type="text"
                id="consult_website"
                name="consult_website"
                tabindex="-1"
                autocomplete="off"
            >
        </div>

    3. consultation.css 맨 아래 추가

    .consultation-trap {
        position: absolute !important;
        left: -9999px !important;
        width: 1px !important;
        height: 1px !important;
        overflow: hidden !important;
    }

    4. consultation_submit.php 생성

    <?php
    $g5_path['path'] = '../../..';
    include_once($g5_path['path'].'/common.php');
    
    if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
        alert('올바른 방법으로 이용해주세요.');
    }
    
    // 상담 저장 게시판
    $bo_table = 'consultation';
    $write_table = $g5['write_prefix'].$bo_table;
    
    // 상담 게시판 존재 여부 확인
    $board = sql_fetch("
        select *
        from {$g5['board_table']}
        where bo_table = '{$bo_table}'
    ");
    
    if (empty($board['bo_table'])) {
        alert('상담 게시판이 존재하지 않습니다.');
    }
    
    // 보안 토큰 확인
    $session_token = get_session('ss_consult_token');
    $post_token = isset($_POST['consult_token'])
        ? trim($_POST['consult_token'])
        : '';
    
    if (
        !$session_token ||
        !$post_token ||
        !hash_equals($session_token, $post_token)
    ) {
        alert('잘못된 접근입니다. 페이지를 새로고침한 후 다시 신청해주세요.');
    }
    
    // 한 번 사용한 토큰 제거
    set_session('ss_consult_token', '');
    
    // 너무 빠른 자동등록 방지
    $started = (int) get_session('ss_consult_started');
    set_session('ss_consult_started', '');
    
    if (!$started || G5_SERVER_TIME - $started < 2) {
        alert('잠시 후 다시 신청해주세요.');
    }
    
    // 숨겨진 항목에 값이 있으면 자동등록으로 판단
    $website = isset($_POST['consult_website'])
        ? trim($_POST['consult_website'])
        : '';
    
    if ($website !== '') {
        alert('올바른 방법으로 이용해주세요.');
    }
    
    // 입력값 받기
    $consult_name = isset($_POST['consult_name'])
        ? trim(strip_tags($_POST['consult_name']))
        : '';
    
    $consult_part = isset($_POST['consult_part'])
        ? trim(strip_tags($_POST['consult_part']))
        : '';
    
    $consult_time = isset($_POST['consult_time'])
        ? trim(strip_tags($_POST['consult_time']))
        : '';
    
    $consult_phone = isset($_POST['consult_phone'])
        ? preg_replace('/[^0-9]/', '', $_POST['consult_phone'])
        : '';
    
    $consult_privacy = isset($_POST['consult_privacy'])
        ? $_POST['consult_privacy']
        : '';
    
    // 필수 입력값 확인
    if ($consult_name === '') {
        alert('성함을 입력해주세요.');
    }
    
    if (mb_strlen($consult_name, 'UTF-8') > 30) {
        alert('성함을 올바르게 입력해주세요.');
    }
    
    // 허용된 상담 부위
    $allowed_parts = array(
        '유륜',
        '외음부',
        '팔꿈치',
        '겨드랑이',
        '기타 부위'
    );
    
    if (!in_array($consult_part, $allowed_parts, true)) {
        alert('상담 부위를 선택해주세요.');
    }
    
    // 허용된 통화 시간
    $allowed_times = array(
        '10:00~11:00',
        '11:00~12:00',
        '13:00~14:00',
        '14:00~15:00',
        '15:00~16:00',
        '16:00~17:00',
        '17:00~18:00'
    );
    
    if (!in_array($consult_time, $allowed_times, true)) {
        alert('통화 가능 시간을 선택해주세요.');
    }
    
    // 휴대전화 번호 확인
    if (!preg_match('/^01[0-9]{8,9}$/', $consult_phone)) {
        alert('연락처를 정확히 입력해주세요.');
    }
    
    if ($consult_privacy !== '1') {
        alert('개인정보 수집 및 이용에 동의해주세요.');
    }
    
    // 동일 IP의 연속 등록 방지
    $consult_ip = isset($_SERVER['REMOTE_ADDR'])
        ? preg_replace('/[^0-9a-fA-F:\.]/', '', $_SERVER['REMOTE_ADDR'])
        : '';
    
    $recent = sql_fetch("
        select wr_datetime
        from {$write_table}
        where wr_ip = '".addslashes($consult_ip)."'
        order by wr_id desc
        limit 1
    ");
    
    if (
        !empty($recent['wr_datetime']) &&
        G5_SERVER_TIME - strtotime($recent['wr_datetime']) < 30
    ) {
        alert('이미 상담 신청이 접수되었습니다. 잠시 후 다시 시도해주세요.');
    }
    
    // DB 저장용 처리
    $wr_name = addslashes(clean_xss_tags($consult_name));
    $wr_part = addslashes(clean_xss_tags($consult_part));
    $wr_time = addslashes(clean_xss_tags($consult_time));
    $wr_phone = addslashes($consult_phone);
    
    $wr_subject = addslashes(
        '[상담신청] '.$consult_name.' / '.$consult_part
    );
    
    $wr_content = addslashes(
        "성함: {$consult_name}\n".
        "상담 부위: {$consult_part}\n".
        "통화 가능 시간: {$consult_time}\n".
        "연락처: {$consult_phone}\n".
        "개인정보 수집 동의: 동의"
    );
    
    $wr_password = get_encrypt_string(
        uniqid('consult_', true)
    );
    
    // 상담 게시글 저장
    $sql = "
        insert into {$write_table}
        set
            wr_num = (
                select ifnull(min(wr_num) - 1, -1)
                from {$write_table} as sq
            ),
            wr_reply = '',
            wr_comment = 0,
            ca_name = '',
            wr_option = 'secret',
            wr_subject = '{$wr_subject}',
            wr_content = '{$wr_content}',
            wr_seo_title = '',
            wr_link1 = '',
            wr_link2 = '',
            wr_link1_hit = 0,
            wr_link2_hit = 0,
            wr_hit = 0,
            wr_good = 0,
            wr_nogood = 0,
            mb_id = '',
            wr_password = '{$wr_password}',
            wr_name = '{$wr_name}',
            wr_email = '',
            wr_homepage = '',
            wr_datetime = '".G5_TIME_YMDHIS."',
            wr_last = '".G5_TIME_YMDHIS."',
            wr_ip = '".addslashes($consult_ip)."',
            wr_1 = '{$wr_part}',
            wr_2 = '{$wr_time}',
            wr_3 = '{$wr_phone}',
            wr_4 = '동의',
            wr_5 = '',
            wr_6 = '',
            wr_7 = '',
            wr_8 = '',
            wr_9 = '',
            wr_10 = ''
    ";
    
    sql_query($sql);
    
    $wr_id = sql_insert_id();
    
    // 부모 글 번호 설정
    sql_query("
        update {$write_table}
        set wr_parent = '{$wr_id}'
        where wr_id = '{$wr_id}'
    ");
    
    // 최신글 테이블 등록
    sql_query("
        insert into {$g5['board_new_table']}
            (bo_table, wr_id, wr_parent, bn_datetime, mb_id)
        values
            ('{$bo_table}', '{$wr_id}', '{$wr_id}', '".G5_TIME_YMDHIS."', '')
    ");
    
    // 게시판 글 개수 증가
    sql_query("
        update {$g5['board_table']}
        set bo_count_write = bo_count_write + 1
        where bo_table = '{$bo_table}'
    ");
    
    // 원래 이벤트 페이지로 이동
    $return_url = isset($_SERVER['HTTP_REFERER'])
        ? $_SERVER['HTTP_REFERER']
        : G5_URL;
    
    alert(
        '상담 신청이 완료되었습니다. 확인 후 연락드리겠습니다.',
        $return_url
    );
    ?>

    이 3가지 파일을 ‘/skin/board/event’ 경로에 업로드 시켜준다.
    consultation_form.php
    consultation.css
    consultation_submit.php

    이제 상담신청 폼에서 테스트로 정보들을 입력하고,
    상담신청내역 게시판에 잘 들어오는지 확인해본다.