여러 플러그인 디렉토리 추가


39

작업

register_theme_directory()WP 설치에 사용하여 추가 테마 디렉토리를 등록 할 수 있습니다 . 안타깝게도 코어는 플러그인에 대해 동일한 기능을 제공하지 않습니다. 우리는 이미 MU-Plugin, Drop-Ins, Plugins and Themes를 가지고 있습니다. 그러나 더 나은 파일 구성을 위해서는 더 많은 것이 필요합니다.

달성해야 할 작업 목록은 다음과 같습니다.

  • 추가 플러그인 디렉토리 추가
  • 각 플러그인 디렉토리에 대해 다음과 같이 새로운 "탭"이 필요합니다 [1]
  • 추가 디렉토리는 기본 플러그인 디렉토리와 동일한 기능을 수행합니다.

거기에 무엇이 있습니까?

가장 완벽한 답변은 현상금이 수여됩니다.


[1] 새로운 플러그인 폴더 / 디렉토리에 대한 추가 탭


3
디렉토리 구조는 디렉토리 상수와 매우 관련이 있기 때문에 파일 시스템 수준 에서이 작업을 수행하는 것이 실용적입니다 (핵심 채택하지 않음). 관리자의 가상 조직 계층은 확장 수준에서보다 쉽게 ​​달성 할 수 있습니다.
Rarst

@Rarst 어느 보유하지 않아야 :) 의견을 추가하는 백
카이저

이것은 훌륭한 기능이 될 것입니다.
ltfishie

기능이 좋습니다. 코어를 리버스 엔지니어링하고 WP 방식을 수행하는 방법을 알아 낸 다음 Devs에 패치를 제출해야합니다. register_theme_directory ()-search_theme_directories ()-get_raw_theme_root ()-get_theme_roots ()-get_theme ()-get_themes ()
Sterling Hamilton

2
얘들 아 : 제출 무엇 ? 이것은 완전한 코드로 답이 아닌 질문입니다 :) 참고 : 클래스 에 다시 쓰는 trac의 새로운 티켓get_themes() .
카이저

답변:


28

좋아, 나는 이것에 찌를 것이다. 그 과정에서 발생했던 몇 가지 제한 사항 :

  1. WP_List_Table의 서브 클래스에는 필터가 많지 않으며, 필요한 필터는 없습니다.

  2. 필터가 없기 때문에 맨 위에 정확한 플러그인 유형 목록을 유지할 수 없습니다.

  3. 또한 플러그인을 활성화 상태로 표시하려면 멋진 (읽기 : 더티) JavaScript 해킹을 사용해야합니다.

관리 영역 코드를 클래스 안에 래핑하여 함수 이름 앞에 접두사가 붙지 않았습니다. 이 코드는 모두 여기에서 볼 수 있습니다 . 기부 해주세요!

중앙 API

플러그인 배열을 연관 배열에 포함하는 전역 변수를 설정하는 간단한 함수입니다. 는 $key등의 플러그인을 가져 오기 위해 내부적으로 사용 무언가가 될 것입니다 $dir중 하나의 전체 경로 또는 뭔가 상대적인 wp-content디렉토리. $label관리자 영역에 표시 할 예정입니다 (예 : 번역 가능한 문자열).

<?php
function register_plugin_directory( $key, $dir, $label )
{
    global $wp_plugin_directories;
    if( empty( $wp_plugin_directories ) ) $wp_plugin_directories = array();

    if( ! file_exists( $dir ) && file_exists( trailingslashit( WP_CONTENT_DIR ) . $dir ) )
    {
        $dir = trailingslashit( WP_CONTENT_DIR ) . $dir;
    }

    $wp_plugin_directories[$key] = array(
        'label' => $label,
        'dir'   => $dir
    );
}

그런 다음 플러그인을로드해야합니다. plugins_loaded늦게 연결되어 활성화 된 플러그인을 통해 각각로드합니다.

관리 지역

클래스 내에서 기능을 설정합시다.

<?php
class CD_APD_Admin
{

    /**
     * The container for all of our custom plugins
     */
    protected $plugins = array();

    /**
     * What custom actions are we allowed to handle here?
     */
    protected $actions = array();

    /**
     * The original count of the plugins
     */
    protected $all_count = 0;

    /**
     * constructor
     * 
     * @since 0.1
     */
    function __construct()
    {
        add_action( 'load-plugins.php', array( &$this, 'init' ) );
        add_action( 'plugins_loaded', array( &$this, 'setup_actions' ), 1 );

    }

} // end class

우리는 plugins_loaded실제로 초기 에 연결하고 우리가 사용할 허용 된 "동작"을 설정합니다. 내장 함수는 사용자 정의 디렉토리로는 플러그인 활성화 및 비활성화를 처리 할 수 ​​없으므로 플러그인 활성화 및 비활성화를 처리합니다.

function setup_actions()
{
    $tmp = array(
        'custom_activate',
        'custom_deactivate'
    );
    $this->actions = apply_filters( 'custom_plugin_actions', $tmp );
}

그런 다음 함수가 연결되어 load-plugins.php있습니다. 이것은 모든 종류의 재미있는 일을합니다.

function init()
{
    global $wp_plugin_directories;

    $screen = get_current_screen();

    $this->get_plugins();

    $this->handle_actions();

    add_filter( 'views_' . $screen->id, array( &$this, 'views' ) );

    // check to see if we're using one of our custom directories
    if( $this->get_plugin_status() )
    {
        add_filter( 'views_' . $screen->id, array( &$this, 'views_again' ) );
        add_filter( 'all_plugins', array( &$this, 'filter_plugins' ) );
        // TODO: support bulk actions
        add_filter( 'bulk_actions-' . $screen->id, '__return_empty_array' );
        add_filter( 'plugin_action_links', array( &$this, 'action_links' ), 10, 2 );
        add_action( 'admin_enqueue_scripts', array( &$this, 'scripts' ) );
    }
}

한 번에 한 가지만 살펴 보겠습니다. 이 get_plugins메소드는 다른 함수를 감싸는 래퍼입니다. 속성 plugins을 데이터로 채 웁니다 .

function get_plugins()
{
    global $wp_plugin_directories;
    foreach( array_keys( $wp_plugin_directories ) as $key )
    {
       $this->plugins[$key] = cd_apd_get_plugins( $key );
    }
}

cd_apd_get_pluginsget_plugins하드 코딩 WP_CONTENT_DIRplugins비즈니스 없이 내장 된 기능을 사용합니다 . 기본적으로 : $wp_plugin_directories전역 에서 디렉토리를 가져 와서 열고 모든 플러그인 파일을 찾으십시오. 나중에 캐시에 저장하십시오.

<?php
function cd_apd_get_plugins( $dir_key ) 
{
    global $wp_plugin_directories;

    // invalid dir key? bail
    if( ! isset( $wp_plugin_directories[$dir_key] ) )
    {
        return array();
    }
    else
    {
        $plugin_root = $wp_plugin_directories[$dir_key]['dir'];
    }

    if ( ! $cache_plugins = wp_cache_get( 'plugins', 'plugins') )
        $cache_plugins = array();

    if ( isset( $cache_plugins[$dir_key] ) )
        return $cache_plugins[$dir_key];

    $wp_plugins = array();

    $plugins_dir = @ opendir( $plugin_root );
    $plugin_files = array();
    if ( $plugins_dir ) {
        while ( ( $file = readdir( $plugins_dir ) ) !== false ) {
            if ( substr($file, 0, 1) == '.' )
                continue;
            if ( is_dir( $plugin_root.'/'.$file ) ) {
                $plugins_subdir = @ opendir( $plugin_root.'/'.$file );
                if ( $plugins_subdir ) {
                    while (($subfile = readdir( $plugins_subdir ) ) !== false ) {
                        if ( substr($subfile, 0, 1) == '.' )
                            continue;
                        if ( substr($subfile, -4) == '.php' )
                            $plugin_files[] = "$file/$subfile";
                    }
                    closedir( $plugins_subdir );
                }
            } else {
                if ( substr($file, -4) == '.php' )
                    $plugin_files[] = $file;
            }
        }
        closedir( $plugins_dir );
    }

    if ( empty($plugin_files) )
        return $wp_plugins;

    foreach ( $plugin_files as $plugin_file ) {
        if ( !is_readable( "$plugin_root/$plugin_file" ) )
            continue;

        $plugin_data = get_plugin_data( "$plugin_root/$plugin_file", false, false ); //Do not apply markup/translate as it'll be cached.

        if ( empty ( $plugin_data['Name'] ) )
            continue;

        $wp_plugins[trim( $plugin_file )] = $plugin_data;
    }

    uasort( $wp_plugins, '_sort_uname_callback' );

    $cache_plugins[$dir_key] = $wp_plugins;
    wp_cache_set('plugins', $cache_plugins, 'plugins');

    return $wp_plugins;
}

다음은 실제로 플러그인을 활성화하고 비활성화하는 성가신 사업입니다. 이를 위해이 handle_actions방법 을 사용합니다 . 이것은 다시 핵심 wp-admin/plugins.php파일 의 상단에서 찢어졌습니다 .

function handle_actions()
{
    $action = isset( $_REQUEST['action'] ) ? $_REQUEST['action'] : '';

    // not allowed to handle this action? bail.
    if( ! in_array( $action, $this->actions ) ) return;

    // Get the plugin we're going to activate
    $plugin = isset( $_REQUEST['plugin'] ) ? $_REQUEST['plugin'] : false;
    if( ! $plugin ) return;

    $context = $this->get_plugin_status();

    switch( $action )
    {
        case 'custom_activate':
            if( ! current_user_can('activate_plugins') )
                    wp_die( __('You do not have sufficient permissions to manage plugins for this site.') );

            check_admin_referer( 'custom_activate-' . $plugin );

            $result = cd_apd_activate_plugin( $plugin, $context );
            if ( is_wp_error( $result ) ) 
            {
                if ( 'unexpected_output' == $result->get_error_code() ) 
                {
                    $redirect = add_query_arg( 'plugin_status', $context, self_admin_url( 'plugins.php' ) );
                    wp_redirect( add_query_arg( '_error_nonce', wp_create_nonce( 'plugin-activation-error_' . $plugin ), $redirect ) ) ;
                    exit();
                } 
                else 
                {
                    wp_die( $result );
                }
            }

            wp_redirect( add_query_arg( array( 'plugin_status' => $context, 'activate' => 'true' ), self_admin_url( 'plugins.php' ) ) );
            exit();
            break;
        case 'custom_deactivate':
            if ( ! current_user_can( 'activate_plugins' ) )
                wp_die( __('You do not have sufficient permissions to deactivate plugins for this site.') );

            check_admin_referer('custom_deactivate-' . $plugin);
            cd_apd_deactivate_plugins( $plugin, $context );
            if ( headers_sent() )
                echo "<meta http-equiv='refresh' content='" . esc_attr( "0;url=plugins.php?deactivate=true&plugin_status=$status&paged=$page&s=$s" ) . "' />";
            else
                wp_redirect( self_admin_url("plugins.php?deactivate=true&plugin_status=$context") );
            exit();
            break;
        default:
            do_action( 'custom_plugin_dir_' . $action );
            break;
    }

}

여기에 몇 가지 사용자 정의 기능이 있습니다. cd_apd_activate_plugin(에서 벗겨짐 activate_plugin) 및 cd_apd_deactivate_plugins(에서 벗겨짐 deactivate_plugins). 둘 다 하드 코딩 된 디렉토리가없는 각각의 "부모"기능과 동일합니다.

function cd_apd_activate_plugin( $plugin, $context, $silent = false ) 
{
    $plugin = trim( $plugin );

    $redirect = add_query_arg( 'plugin_status', $context, admin_url( 'plugins.php' ) );
    $redirect = apply_filters( 'custom_plugin_redirect', $redirect );

    $current = get_option( 'active_plugins_' . $context, array() );

    $valid = cd_apd_validate_plugin( $plugin, $context );
    if ( is_wp_error( $valid ) )
        return $valid;

    if ( !in_array($plugin, $current) ) {
        if ( !empty($redirect) )
            wp_redirect(add_query_arg('_error_nonce', wp_create_nonce('plugin-activation-error_' . $plugin), $redirect)); // we'll override this later if the plugin can be included without fatal error
        ob_start();
        include_once( $valid );

        if ( ! $silent ) {
            do_action( 'custom_activate_plugin', $plugin, $context );
            do_action( 'custom_activate_' . $plugin, $context );
        }

        $current[] = $plugin;
        sort( $current );
        update_option( 'active_plugins_' . $context, $current );

        if ( ! $silent ) {
            do_action( 'custom_activated_plugin', $plugin, $context );
        }

        if ( ob_get_length() > 0 ) {
            $output = ob_get_clean();
            return new WP_Error('unexpected_output', __('The plugin generated unexpected output.'), $output);
        }
        ob_end_clean();
    }

    return true;
}

비활성화 기능

function cd_apd_deactivate_plugins( $plugins, $context, $silent = false ) {
    $current = get_option( 'active_plugins_' . $context, array() );

    foreach ( (array) $plugins as $plugin ) 
    {
        $plugin = trim( $plugin );
        if ( ! in_array( $plugin, $current ) ) continue;

        if ( ! $silent )
            do_action( 'custom_deactivate_plugin', $plugin, $context );

        $key = array_search( $plugin, $current );
        if ( false !== $key ) {
            array_splice( $current, $key, 1 );
        }

        if ( ! $silent ) {
            do_action( 'custom_deactivate_' . $plugin, $context );
            do_action( 'custom_deactivated_plugin', $plugin, $context );
        }
    }

    update_option( 'active_plugins_' . $context, $current );
}

도 있습니다 cd_apd_validate_plugin물론 떨어져, 떨어져 찢음 인 기능, validate_plugin하드 코딩 정크 않고는.

<?php
function cd_apd_validate_plugin( $plugin, $context ) 
{
    $rv = true;
    if ( validate_file( $plugin ) )
    {
        $rv = new WP_Error('plugin_invalid', __('Invalid plugin path.'));
    }

    global $wp_plugin_directories;
    if( ! isset( $wp_plugin_directories[$context] ) )
    {
        $rv = new WP_Error( 'invalid_context', __( 'The context for this plugin does not exist' ) );
    }

    $dir = $wp_plugin_directories[$context]['dir'];
    if( ! file_exists( $dir . '/' . $plugin) )
    {
        $rv = new WP_Error( 'plugin_not_found', __( 'Plugin file does not exist.' ) );
    }

    $installed_plugins = cd_apd_get_plugins( $context );
    if ( ! isset($installed_plugins[$plugin]) )
    {
        $rv = new WP_Error( 'no_plugin_header', __('The plugin does not have a valid header.') );
    }

    $rv = $dir . '/' . $plugin;
    return $rv;
}

알겠습니다. 우리는 실제로 목록 테이블 디스플레이에 대해 이야기 할 수 있습니다

1 단계 : 테이블 상단의 목록에 뷰를 추가합니다. 이것은 함수 views_{$screen->id}내부 에서 필터링하여 수행됩니다 init.

add_filter( 'views_' . $screen->id, array( &$this, 'views' ) );

그런 다음 실제 후크 기능은을 통해 반복됩니다 $wp_plugin_directories. 새로 등록 된 디렉토리 중 하나에 플러그인이 있으면이를 표시에 포함시킵니다.

function views( $views )
{
    global $wp_plugin_directories;

    // bail if we don't have any extra dirs
    if( empty( $wp_plugin_directories ) ) return $views;

    // Add our directories to the action links
    foreach( $wp_plugin_directories as $key => $info )
    {
        if( ! count( $this->plugins[$key] ) ) continue;
        $class = $this->get_plugin_status() == $key ? ' class="current" ' : '';
        $views[$key] = sprintf( 
            '<a href="%s"' . $class . '>%s <span class="count">(%d)</span></a>',
            add_query_arg( 'plugin_status', $key, 'plugins.php' ),
            esc_html( $info['label'] ),
            count( $this->plugins[$key] )
        );
    }
    return $views;
}

사용자 정의 플러그인 디렉토리 페이지를 볼 때 가장 먼저해야 할 일은 뷰를 다시 필터링하는 것입니다. inactive카운트가 정확하지 않기 때문에 카운트를 제거해야 합니다. 필요한 곳에 필터가없는 결과. 다시 연결 ...

if( $this->get_plugin_status() )
{
    add_filter( 'views_' . $screen->id, array( &$this, 'views_again' ) );
}

그리고 빠른 설정 해제 ...

function views_again( $views )
{
    if( isset( $views['inactive'] ) ) unset( $views['inactive'] );
    return $views;
}

다음으로 목록 테이블에서 보았던 플러그인을 제거하고 사용자 정의 플러그인으로 교체합시다. 에 연결하십시오 all_plugins.

if( $this->get_plugin_status() )
{
    add_filter( 'views_' . $screen->id, array( &$this, 'views_again' ) );
    add_filter( 'all_plugins', array( &$this, 'filter_plugins' ) );
}

플러그인과 데이터를 이미 설정 했으므로 ( setup_plugins위 참조 ) filter_plugins메소드는 (1) 나중에 모든 플러그인의 수를 저장하고 (2) 목록 테이블에서 플러그인을 대체합니다.

function filter_plugins( $plugins )
{
    if( $key = $this->get_plugin_status() )
    {
        $this->all_count = count( $plugins );
        $plugins = $this->plugins[$key];
    }
    return $plugins;
}

이제 대량 작업을 중단합니다. 이것들은 쉽게 지원 될 수 있다고 생각합니다.

if( $this->get_plugin_status() )
{
    add_filter( 'views_' . $screen->id, array( &$this, 'views_again' ) );
    add_filter( 'all_plugins', array( &$this, 'filter_plugins' ) );
    // TODO: support bulk actions
    add_filter( 'bulk_actions-' . $screen->id, '__return_empty_array' );
}

기본 플러그인 작업 링크가 작동하지 않습니다. 대신, 우리는 (커스텀 액션 등으로) 자신 만의 설정을해야합니다. 에서 init기능.

if( $this->get_plugin_status() )
{
    add_filter( 'views_' . $screen->id, array( &$this, 'views_again' ) );
    add_filter( 'all_plugins', array( &$this, 'filter_plugins' ) );
    // TODO: support bulk actions
    add_filter( 'bulk_actions-' . $screen->id, '__return_empty_array' );
    add_filter( 'plugin_action_links', array( &$this, 'action_links' ), 10, 2 );
}

여기서 변경된 유일한 것은 (1) 액션을 변경하고, (2) 플러그인 상태를 유지하고 (3) nonce 이름을 조금 변경하는 것입니다.

function action_links( $links, $plugin_file )
{
    $context = $this->get_plugin_status();

    // let's just start over
    $links = array();
    $links['activate'] = sprintf(
        '<a href="%s" title="Activate this plugin">%s</a>',
        wp_nonce_url( 'plugins.php?action=custom_activate&amp;plugin=' . $plugin_file . '&amp;plugin_status=' . esc_attr( $context ), 'custom_activate-' . $plugin_file ),
        __( 'Activate' )
    );

    $active = get_option( 'active_plugins_' . $context, array() );
    if( in_array( $plugin_file, $active ) )
    {
        $links['deactivate'] = sprintf(
            '<a href="%s" title="Deactivate this plugin" class="cd-apd-deactivate">%s</a>',
            wp_nonce_url( 'plugins.php?action=custom_deactivate&amp;plugin=' . $plugin_file . '&amp;plugin_status=' . esc_attr( $context ), 'custom_deactivate-' . $plugin_file ),
            __( 'Deactivate' )
        );
    }
    return $links;
}

마지막으로 JavaScript를 대기열에 추가하기 만하면됩니다. 에서 init기능 다시 (모두 함께 이번에).

if( $this->get_plugin_status() )
{
    add_filter( 'views_' . $screen->id, array( &$this, 'views_again' ) );
    add_filter( 'all_plugins', array( &$this, 'filter_plugins' ) );
    // TODO: support bulk actions
    add_filter( 'bulk_actions-' . $screen->id, '__return_empty_array' );
    add_filter( 'plugin_action_links', array( &$this, 'action_links' ), 10, 2 );
    add_action( 'admin_enqueue_scripts', array( &$this, 'scripts' ) );
}

JS를 대기열에 넣는 동안 wp_localize_script총 "모든 플러그인"수의 값을 얻는데 도 사용 됩니다.

function scripts()
{
    wp_enqueue_script(
        'cd-apd-js',
        CD_APD_URL . 'js/apd.js',
        array( 'jquery' ),
        null
    );
    wp_localize_script(
        'cd-apd-js',
        'cd_apd',
        array(
            'count' => esc_js( $this->all_count )
        )
    );
}

물론 JS는 목록 테이블 활성 / 비활성 플러그인을 올바르게 표시하기위한 훌륭한 해킹입니다. 또한 모든 플러그인의 정확한 수를 다시 All링크에 연결합니다.

jQuery(document).ready(function(){
    jQuery('li.all a').removeClass('current').find('span.count').html('(' + cd_apd.count + ')');
    jQuery('.wp-list-table.plugins tr').each(function(){
        var is_active = jQuery(this).find('a.cd-apd-deactivate');
        if(is_active.length) {
            jQuery(this).removeClass('inactive').addClass('active');
            jQuery(this).find('div.plugin-version-author-uri').removeClass('inactive').addClass('active');
        }
    });
});

마무리

추가 플러그인 디렉토리의 실제 로딩은 매우 흥미 롭습니다. 목록 테이블을 올바르게 표시하는 것이 더 어려운 부분입니다. 나는 그것이 어떻게 밝혀 졌는지 여전히 완전히 만족하지는 않지만 누군가가 코드를 향상시킬 수 있습니다.


1
감동적인! 정말 잘 했어. 주말에 코드를 공부하는 데 시간이 좀 걸릴 것입니다. 참고 : 기능이 __return_empty_array()있습니다.
fuxia

감사! 의견은 언제나 환영합니다. __return_empty_array기능을 도입했습니다 !
chrisguitarguy

1
간단한 코어 필터가 별도의 기능을 저장 한 모든 장소의 목록을 수집해야합니다. 그리고 ... Trac 티켓을 제출하십시오.
fuxia

정말 대단합니다. 테마 내의 라이브러리로 이것을 사용할 수 있다면 훨씬 더 시원 할 것입니다 (Github에 대한 의견 : github.com/chrisguitarguy/WP-Plugin-Directories/issues/4 )
julien_c

1
+1이 답변을 놓쳤다는 사실을 믿을 수 없습니다. 주말에 코드를 자세히 살펴볼 것입니다 :). @Julien_c-왜 테마 안에서 이것을 사용 하시겠습니까?
Stephen Harris

2

개인적으로 UI 수정에 관심이 없지만 몇 가지 이유로 더 체계적인 파일 시스템 레이아웃을 원합니다.

이를 위해 또 다른 방법은 심볼릭 링크를 사용하는 것입니다.

wp-content
    |-- plugins
        |-- acme-widgets               -> ../plugins-custom/acme-widgets
        |-- acme-custom-post-types     -> ../plugins-custom/acme-custom-post-types
        |-- acme-business-logic        -> ../plugins-custom/acme-business-logic
        |-- google-authenticator       -> ../plugins-external/google-authenticator
        |-- rest-api                   -> ../plugins-external/rest-api
        |-- quick-navigation-interface -> ../plugins-external/quick-navigation-interface
    |-- plugins-custom
        |-- acme-widgets
        |-- acme-custom-post-types
        |-- acme-business-logic
    |-- plugins-external
        |-- google-authenticator
        |-- rest-api
        |-- quick-navigation-interface

에서 plugins-custom프로젝트의 버전 관리 저장소의 일부가 될 수있는 커스텀 플러그인을 설정할 수 있습니다.

그런 다음 plugins-externalComposer 또는 Git 하위 모듈 또는 원하는 것을 통해 타사 종속성을 설치할 수 있습니다.

그런 다음 추가 디렉토리를 스캔하고 plugins찾은 각 하위 폴더 에 대한 심볼릭 링크를 생성하는 간단한 Bash 스크립트 또는 WP-CLI 명령을 사용할 수 있습니다.

plugins그래도 혼란 스러울 수 있지만 plugins-customand 만 상호 작용하면되므로 중요하지 않습니다 plugins-external.

n추가 디렉토리로 확장 하면 처음 두 디렉토리와 동일한 프로세스가 수행됩니다.


-3

또는 wp-content 폴더를 가리 키도록 설정된 사용자 정의 디렉토리 경로와 함께 COMPOSER를 사용할 수도 있습니다. 질문에 대한 직접적인 대답이 아닌 경우 워드 프레스를 생각하는 새로운 방법이라면, 작곡가가 당신을 먹기 전에 계속하십시오.


오래 전에 Composer로 이동했습니다. 이 질문의 날짜를 찾아보십시오. 그 외에도 : 이것은 실제로 답이 아닙니다. 실제로 이것을 설정하는 방법을 보여줄 수 있습니까?
kaiser
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.