캐스케이드 삭제


200

계단식 삭제를 수행하려는 Postgresql 데이터베이스가 있습니다. 그러나 테이블은 ON DELETE CASCADE 규칙으로 설정되지 않습니다. 삭제를 수행하고 Postgresql에 단 한 번만 계단식으로 연결하도록 할 수있는 방법이 있습니까? 이에 상응하는 것

DELETE FROM some_table CASCADE;

이 오래된 질문에 대한 답변은 그러한 해결책이 존재하지 않는 것처럼 보이지만 확실하게하기 위해이 질문을 명시 적으로 요구할 것이라고 생각했습니다.


아래의 내 사용자 정의 기능을 참조하십시오. 특정 제한 사항이있을 수 있습니다.
Joe Love

답변:


175

아니요. 캐스케이드하려는 테이블에 대해 delete 문을 작성하기 만하면됩니다.

DELETE FROM some_child_table WHERE some_fk_field IN (SELECT some_id FROM some_Table);
DELETE FROM some_table;

12
원래 계단식 (재귀)에서 계단식으로 연결된 다른 외래 키가있을 수 있으므로 반드시 작동하지는 않습니다. 테이블 a가 a를 참조하는 b를 가리키는 루프에 들어갈 수도 있습니다. 일반적인 의미에서이 작업을 수행하려면 아래 표를 참조하십시오. 그러나 몇 가지 제한 사항이 있습니다. 간단한 테이블 설정이 있으면 위의 코드를 사용해보십시오.하고있는 것을 이해하는 것이 더 쉽습니다.
Joe Love

2
간단하고 안전합니다. 밀도 삽입이있는 경우 단일 트랜잭션으로 실행해야합니다.
İsmail Yavuz

40

당신이 정말로 원하는 경우 DELETE FROM some_table CASCADE; 의미 " 테이블에서 모든 행을 제거some_table 당신이 사용할 수있는" TRUNCATE대신 DELETE하고 CASCADE항상 지원됩니다. 그러나 where절 과 함께 선택적 삭제를 사용하려면 TRUNCATE충분하지 않습니다.

사용과 CARE는 -이됩니다 모든 테이블의 모든 행 드롭 에 외래 키 제약 조건이 some_table등 해당 테이블에 제약이있는 모든 테이블을

포스트 그레스 지원 CASCADETRUNCATE 명령 :

TRUNCATE some_table CASCADE;

이 기능은 다른 동시 트랜잭션과 완전히 분리되어 있지 않지만 다른 몇 가지주의 사항이 있지만 트랜잭션 방식 (즉, 롤백 가능)입니다. 자세한 내용은 문서를 읽으십시오.


226
"계단식으로 몇 번 삭제"≠ 테이블에서 모든 데이터 삭제…
lensovet

33
그러면 some_table에 외래 키 제약 조건이있는 모든 테이블과 해당 테이블에 제약 조건이있는 모든 테이블의 모든 행이 삭제됩니다. 잠재적으로 매우 위험합니다.
AJP

56
조심하십시오. 이것은 무모한 대답입니다.
Jordan Arseno

4
누군가 가이 답변에 삭제를 표시 했습니다. 아마도 동의하지 않기 때문일 것입니다. 이 경우 올바른 행동 과정은 깃발이 아닌 공감하는 것입니다.
Wai Ha Lee

7
그는 상단에 경고가 있습니다. 당신이 그것을 무시하기로 선택하면 아무도 당신을 도울 수 없습니다. 귀하의 "copyPaste"사용자가 여기에 실제로 위험하다고 생각합니다.
BluE

28

기본 키를 기반으로 행을 삭제하는 (재귀) 함수를 작성했습니다. 캐스케이드를 삭제할 때 제약 조건을 만들고 싶지 않기 때문에 이것을 썼습니다. DBA로 복잡한 데이터 집합을 삭제하고 싶었지만 프로그래머가 모든 영향을 고려하지 않고 삭제를 계단식으로 만들 수는 없었습니다. 나는 여전히이 기능을 테스트 중이므로 버그가있을 수 있지만 DB에 다중 열 기본 (및 외래) 키가있는 경우 시도하지 마십시오. 또한 키는 모두 문자열 형식으로 표현할 수 있어야하지만 그러한 제한이없는 방식으로 작성 될 수 있습니다. 어쨌든이 기능을 아주 조금만 사용합니다. 모든 것에 대해 계단식 제약 조건을 사용하기에 너무 많은 데이터를 중요하게 생각합니다. 기본적으로이 함수는 스키마, 테이블 이름 및 기본 값 (문자열 형식)으로 전달됩니다. 그리고 해당 테이블에서 외래 키를 찾아 시작하여 데이터가 존재하지 않는지 확인합니다. 존재하는 경우 발견 된 데이터를 재귀 적으로 호출합니다. 무한 루프를 방지하기 위해 이미 삭제 표시된 데이터 배열을 사용합니다. 그것을 테스트하고 그것이 당신을 위해 어떻게 작동하는지 알려주십시오. 참고 : 조금 느립니다. 나는 그렇게 그렇게 부른다. select delete_cascade('public','my_table','1');

create or replace function delete_cascade(p_schema varchar, p_table varchar, p_key varchar, p_recursion varchar[] default null)
 returns integer as $$
declare
    rx record;
    rd record;
    v_sql varchar;
    v_recursion_key varchar;
    recnum integer;
    v_primary_key varchar;
    v_rows integer;
begin
    recnum := 0;
    select ccu.column_name into v_primary_key
        from
        information_schema.table_constraints  tc
        join information_schema.constraint_column_usage AS ccu ON ccu.constraint_name = tc.constraint_name and ccu.constraint_schema=tc.constraint_schema
        and tc.constraint_type='PRIMARY KEY'
        and tc.table_name=p_table
        and tc.table_schema=p_schema;

    for rx in (
        select kcu.table_name as foreign_table_name, 
        kcu.column_name as foreign_column_name, 
        kcu.table_schema foreign_table_schema,
        kcu2.column_name as foreign_table_primary_key
        from information_schema.constraint_column_usage ccu
        join information_schema.table_constraints tc on tc.constraint_name=ccu.constraint_name and tc.constraint_catalog=ccu.constraint_catalog and ccu.constraint_schema=ccu.constraint_schema 
        join information_schema.key_column_usage kcu on kcu.constraint_name=ccu.constraint_name and kcu.constraint_catalog=ccu.constraint_catalog and kcu.constraint_schema=ccu.constraint_schema
        join information_schema.table_constraints tc2 on tc2.table_name=kcu.table_name and tc2.table_schema=kcu.table_schema
        join information_schema.key_column_usage kcu2 on kcu2.constraint_name=tc2.constraint_name and kcu2.constraint_catalog=tc2.constraint_catalog and kcu2.constraint_schema=tc2.constraint_schema
        where ccu.table_name=p_table  and ccu.table_schema=p_schema
        and TC.CONSTRAINT_TYPE='FOREIGN KEY'
        and tc2.constraint_type='PRIMARY KEY'
)
    loop
        v_sql := 'select '||rx.foreign_table_primary_key||' as key from '||rx.foreign_table_schema||'.'||rx.foreign_table_name||'
            where '||rx.foreign_column_name||'='||quote_literal(p_key)||' for update';
        --raise notice '%',v_sql;
        --found a foreign key, now find the primary keys for any data that exists in any of those tables.
        for rd in execute v_sql
        loop
            v_recursion_key=rx.foreign_table_schema||'.'||rx.foreign_table_name||'.'||rx.foreign_column_name||'='||rd.key;
            if (v_recursion_key = any (p_recursion)) then
                --raise notice 'Avoiding infinite loop';
            else
                --raise notice 'Recursing to %,%',rx.foreign_table_name, rd.key;
                recnum:= recnum +delete_cascade(rx.foreign_table_schema::varchar, rx.foreign_table_name::varchar, rd.key::varchar, p_recursion||v_recursion_key);
            end if;
        end loop;
    end loop;
    begin
    --actually delete original record.
    v_sql := 'delete from '||p_schema||'.'||p_table||' where '||v_primary_key||'='||quote_literal(p_key);
    execute v_sql;
    get diagnostics v_rows= row_count;
    --raise notice 'Deleting %.% %=%',p_schema,p_table,v_primary_key,p_key;
    recnum:= recnum +v_rows;
    exception when others then recnum=0;
    end;

    return recnum;
end;
$$
language PLPGSQL;

특히 자체 참조 테이블에서 항상 발생합니다. 부서마다 관리 계층이 다른 회사 또는 일반적인 계층 구조 분류법을 고려하십시오. 예, 저는이 기능이 얇게 썬 빵 이후 절대 최고의 것이 아니라는 점에 동의하지만 올바른 상황에서 유용한 도구입니다.
Joe Love

다시 작성하면 ID 배열을 수락하고 IN연산자를 하위 선택과 함께 사용하는 쿼리를 생성 =하므로 (세트 논리 사용 단계) 훨씬 빨라졌습니다.
Hubbitus

2
솔루션 주셔서 감사합니다. 몇 가지 테스트를 작성하고 레코드를 삭제해야했고 해당 삭제를 계단식으로 만드는 데 문제가있었습니다. 당신의 기능은 정말 잘 작동했습니다!
페르난도 카 마르고

1
@JoeLove 당신은 어떤 속도 문제가 있습니까? 그 상황에서 재귀는 내 마음에 하나의 올바른 해결책입니다.
Hubbitus

1
@arthur 아마도 행의 일부 버전-> json-> 텍스트를 사용하여 완료 할 수는 있지만 그다지 멀지 않았습니다. 여러 해 동안 단일 기본 키 (잠재적 인 보조 키 포함)가 좋은 것으로 여러 해 동안 발견되었습니다.
Joe Love

17

올바르게 이해하면 외래 키 제약 조건을 삭제하고 새 캐스케이드 추가 (연쇄), 작업 수행 및 제한 외래 키 제약 조건을 다시 작성하여 원하는 것을 수행 할 수 있어야합니다.

예를 들면 다음과 같습니다.

testing=# create table a (id integer primary key);
NOTICE:  CREATE TABLE / PRIMARY KEY will create implicit index "a_pkey" for table "a"
CREATE TABLE
testing=# create table b (id integer references a);
CREATE TABLE

-- put some data in the table
testing=# insert into a values(1);
INSERT 0 1
testing=# insert into a values(2);
INSERT 0 1
testing=# insert into b values(2);
INSERT 0 1
testing=# insert into b values(1);
INSERT 0 1

-- restricting works
testing=# delete from a where id=1;
ERROR:  update or delete on table "a" violates foreign key constraint "b_id_fkey" on table "b"
DETAIL:  Key (id)=(1) is still referenced from table "b".

-- find the name of the constraint
testing=# \d b;
       Table "public.b"
 Column |  Type   | Modifiers 
--------+---------+-----------
 id     | integer | 
Foreign-key constraints:
    "b_id_fkey" FOREIGN KEY (id) REFERENCES a(id)

-- drop the constraint
testing=# alter table b drop constraint b_a_id_fkey;
ALTER TABLE

-- create a cascading one
testing=# alter table b add FOREIGN KEY (id) references a(id) on delete cascade; 
ALTER TABLE

testing=# delete from a where id=1;
DELETE 1
testing=# select * from a;
 id 
----
  2
(1 row)

testing=# select * from b;
 id 
----
  2
(1 row)

-- it works, do your stuff.
-- [stuff]

-- recreate the previous state
testing=# \d b;
       Table "public.b"
 Column |  Type   | Modifiers 
--------+---------+-----------
 id     | integer | 
Foreign-key constraints:
    "b_id_fkey" FOREIGN KEY (id) REFERENCES a(id) ON DELETE CASCADE

testing=# alter table b drop constraint b_id_fkey;
ALTER TABLE
testing=# alter table b add FOREIGN KEY (id) references a(id) on delete restrict; 
ALTER TABLE

물론 정신 건강을 위해 그런 것들을 절차로 요약해야합니다.


4
외래 키가 데이터베이스의 일관성을 유지하지 못하도록하는 것을 막을 수 있다고 가정 할 때, 이는 처리 방법이 아닙니다. "불쾌한"항목을 지금 삭제할 수 있지만 좀비 조각을 많이 남겨두고 나중에 문제를 일으킬 수 있습니다
Sprinterfreak

1
샤드 란 정확히 무엇을 의미합니까? 캐스케이드를 통해 레코드가 삭제되므로 불일치가 없어야합니다.
페드로 Borges

1
"불쾌한 샤드"(계단식 제약 조건이 여전히 일관적임)에 대해 걱정하기보다는 계단식 배열이 충분히 진행되지 않는 것에 대해 더 걱정할 것입니다. 삭제 된 레코드에 추가로 삭제 된 레코드가 필요한 경우 해당 제약 조건을 변경해야합니다. 계단식을 보장합니다. (또는이 시나리오를 피하기 위해 위에서 쓴 기능을 사용하십시오.) ... 어떤 경우에도 마지막으로 권장하는 사항 : 트랜잭션 사용을 사용하여 잘못 될 경우 롤백 할 수 있습니다.
Joe Love

7

Palehorse의 답변에 댓글을 달 수 없으므로 본인의 답변을 추가했습니다. Palehorse의 논리는 괜찮지 만 빅 데이터 세트에서는 효율성이 떨어질 수 있습니다.

DELETE FROM some_child_table sct 
 WHERE exists (SELECT FROM some_Table st 
                WHERE sct.some_fk_fiel=st.some_id);

DELETE FROM some_table;

열에 대한 색인이 있고 데이터 세트가 소수의 레코드보다 큰 경우 더 빠릅니다.


7

예, 다른 사람들이 말했듯이 편리한 'DELETE FROM my_table ... CASCADE'(또는 이에 상응하는)는 없습니다. 계단식이 아닌 외래 키로 보호되는 자식 레코드와 참조 조상을 삭제하려면 다음과 같은 옵션을 사용하십시오.

  • 자식 테이블부터 시작하여 한 번에 한 번의 쿼리로 모든 삭제를 명시 적으로 수행하십시오 (순환 참조가있는 경우에는 그렇지 않습니다). 또는
  • 단일 (잠재적으로 대규모) 쿼리에서 명시 적으로 모든 삭제를 수행하십시오. 또는
  • 계단식이 아닌 외래 키 제약 조건이 'ON DELETE NO ACTION DEFERRABLE'로 생성되었다고 가정하면 단일 트랜잭션에서 모든 삭제를 명시 적으로 수행하십시오. 또는
  • 그래프에서 '조치 없음'및 '제한'외래 키 제약 조건을 일시적으로 삭제하고 CASCADE로 다시 작성하고 문제가되는 조상을 삭제하고 외래 키 제약 조건을 다시 삭제 한 다음 최종적으로 원래대로 다시 작성합니다. 귀하의 데이터); 또는
  • 아마도 똑같이 재미있을 것입니다.

외래 키 제약 조건을 피하는 것이 편리하지 않다는 것이 목적입니다. 하지만 특정 상황에서 왜 그렇게하고 싶은지 이해합니다. 그것이 당신이 어떤 빈도로 할 일이고 DBA의 지혜를 어디에서나 기꺼이 털어 내고 싶다면 절차로 자동화하고 싶을 것입니다.

나는 몇 달 전에 "CASCADE DELETE just once"질문에 대한 답을 찾기 위해 이곳에 왔습니다. 나는 Joe Love의 영리한 솔루션 (및 Thomas CG de Vilhena의 변형)에서 약간의 마일리지를 얻었지만 결국에는 유스 케이스에 다른 요구 사항을 적용 해야하는 특정 요구 사항 (테이블 내 순환 참조 처리)이있었습니다. 이 접근 방식은 궁극적으로 재귀 적 으로 삭제되었습니다 (PG 10.10).

나는 프로덕션에서 recursively_delete를 한동안 사용 해 왔으며 마침내 아이디어를 찾고있는 다른 사람들이 사용할 수있을만큼 자신감을 느꼈습니다. Joe Love의 솔루션과 마찬가지로 데이터베이스의 모든 외래 키 제약 조건이 일시적으로 CASCADE로 설정된 것처럼 전체 데이터 그래프를 삭제할 수 있지만 몇 가지 추가 기능을 제공합니다.

  • 삭제 대상의 ASCII 미리보기와 해당 종속 항목의 그래프를 제공합니다.
  • 재귀 CTE를 사용하여 단일 쿼리에서 삭제를 수행합니다.
  • 테이블 내 및 테이블 간 순환 종속성을 처리합니다.
  • 복합 키를 처리합니다.
  • 'set default'및 'set null'제약 조건을 건너 뜁니다.

오류가 발생했습니다. 오류 : 배열에 짝수의 요소가 있어야합니다. PL / pgSQL 함수 _recursively_delete (regclass, text [], integer, jsonb, integer, text [], jsonb, jsonb) 15 번 할당 SQL 문에서 "SELECT * FROM _recursively_delete (ARG_table, VAR_pk_col_names)"PL / pgSQL 함수 recursively_delete (regclass, anyelement, boolean) SQL 문에서 73 행
Joe Love

안녕하세요, @JoeLove. 사용해 주셔서 감사합니다. 재생산 단계를 알려주시겠습니까? 그리고 PG 버전은 무엇입니까?
TRL

이것이 도움이 될지 확실하지 않습니다. 하지만 방금 함수를 만든 다음 다음 코드를 실행했습니다. select recursively_delete ( 'dallas.vendor', 1094, false) 디버깅 후 일부가 곧바로 죽는다는 것을 알았습니다. 즉, 첫 번째 호출 인 것 같습니다. 여러 가지 일을 한 후가 아니라 함수에. 참고로 저는 PG 10.8
Joe Love

@JoeLove, 친절하게 브랜치 trl-fix-array_must_have_even_number_of_element ( github.com/trlorenz/PG-recursively_delete/pull/2 )를 사용해보십시오 .
TRL

해당 분기를 시도하고 원래 오류를 수정했습니다. 슬프게도, 그것은 내 원래 버전보다 빠르지 않습니다 (처음에는 이것을 작성하는 데 요점이 아닐 수도 있습니다). 캐스케이드 삭제시 중복 외래 키를 작성하고 원래 레코드를 삭제 한 다음 새로 작성된 모든 외래 키를 삭제하는 다른 시도를하고 있습니다.
Joe Love

3

이를 자동화하는 데 사용할 수 있으며로 외래 키 제약 조건을 정의 할 수 ON DELETE CASCADE있습니다. 외래 키 제약 설명서를
인용하십시오 .

CASCADE 참조 된 행이 삭제 될 때이를 참조하는 행도 자동으로 삭제되도록 지정합니다.


1
이것은 OP를 다루지 않지만 외래 키가있는 행을 삭제해야하는 경우를 계획하는 것이 좋습니다. 벤 프랭클린 (Ben Franklin)은 "온스 예방은 1 파운드의 치료 가치가있다"고 말했다.
Jesuisme

1
앱에서 많은 형제가있는 레코드를 삭제하고 사소한 오류 대신 영구적으로 큰 데이터 세트를 삭제 한 경우이 솔루션이 매우 위험 할 수 있습니다.
Joe Love

2

나는 Joe Love의 대답 을 취하고 기능을 더 빨리 만들기 위해 (허 비투스의 제안에 따라) IN하위 선택을 가진 연산자를 사용하여 다시 작성했습니다 =.

create or replace function delete_cascade(p_schema varchar, p_table varchar, p_keys varchar, p_subquery varchar default null, p_foreign_keys varchar[] default array[]::varchar[])
 returns integer as $$
declare

    rx record;
    rd record;
    v_sql varchar;
    v_subquery varchar;
    v_primary_key varchar;
    v_foreign_key varchar;
    v_rows integer;
    recnum integer;

begin

    recnum := 0;
    select ccu.column_name into v_primary_key
        from
        information_schema.table_constraints  tc
        join information_schema.constraint_column_usage AS ccu ON ccu.constraint_name = tc.constraint_name and ccu.constraint_schema=tc.constraint_schema
        and tc.constraint_type='PRIMARY KEY'
        and tc.table_name=p_table
        and tc.table_schema=p_schema;

    for rx in (
        select kcu.table_name as foreign_table_name, 
        kcu.column_name as foreign_column_name, 
        kcu.table_schema foreign_table_schema,
        kcu2.column_name as foreign_table_primary_key
        from information_schema.constraint_column_usage ccu
        join information_schema.table_constraints tc on tc.constraint_name=ccu.constraint_name and tc.constraint_catalog=ccu.constraint_catalog and ccu.constraint_schema=ccu.constraint_schema 
        join information_schema.key_column_usage kcu on kcu.constraint_name=ccu.constraint_name and kcu.constraint_catalog=ccu.constraint_catalog and kcu.constraint_schema=ccu.constraint_schema
        join information_schema.table_constraints tc2 on tc2.table_name=kcu.table_name and tc2.table_schema=kcu.table_schema
        join information_schema.key_column_usage kcu2 on kcu2.constraint_name=tc2.constraint_name and kcu2.constraint_catalog=tc2.constraint_catalog and kcu2.constraint_schema=tc2.constraint_schema
        where ccu.table_name=p_table  and ccu.table_schema=p_schema
        and TC.CONSTRAINT_TYPE='FOREIGN KEY'
        and tc2.constraint_type='PRIMARY KEY'
)
    loop
        v_foreign_key := rx.foreign_table_schema||'.'||rx.foreign_table_name||'.'||rx.foreign_column_name;
        v_subquery := 'select "'||rx.foreign_table_primary_key||'" as key from '||rx.foreign_table_schema||'."'||rx.foreign_table_name||'"
             where "'||rx.foreign_column_name||'"in('||coalesce(p_keys, p_subquery)||') for update';
        if p_foreign_keys @> ARRAY[v_foreign_key] then
            --raise notice 'circular recursion detected';
        else
            p_foreign_keys := array_append(p_foreign_keys, v_foreign_key);
            recnum:= recnum + delete_cascade(rx.foreign_table_schema, rx.foreign_table_name, null, v_subquery, p_foreign_keys);
            p_foreign_keys := array_remove(p_foreign_keys, v_foreign_key);
        end if;
    end loop;

    begin
        if (coalesce(p_keys, p_subquery) <> '') then
            v_sql := 'delete from '||p_schema||'."'||p_table||'" where "'||v_primary_key||'"in('||coalesce(p_keys, p_subquery)||')';
            --raise notice '%',v_sql;
            execute v_sql;
            get diagnostics v_rows = row_count;
            recnum := recnum + v_rows;
        end if;
        exception when others then recnum=0;
    end;

    return recnum;

end;
$$
language PLPGSQL;

2
나는 이것을보고 자기 참조 제약 조건 등이 얼마나 잘 작동하는지 볼 것입니다. 나는 비슷한 것을 시도했지만 완전히 작동하지 못하게되었습니다. 귀하의 솔루션이 저에게 효과적이라면 구현할 것입니다. 이것은 패키지로 만들어지고 github 또는 그 위에 올려야하는 많은 dba 도구 중 하나입니다.
Joe Love

다중 테넌트 CMS에 대한 중간 크기 데이터베이스가 있습니다 (클라이언트는 모두 동일한 테이블을 공유 함). 내 버전 ( "in"제외)은 오래된 클라이언트의 모든 흔적을 삭제하는 데 상당히 느리게 실행되는 것 같습니다 ... 속도를 비교하기 위해 일부 모형 데이터로 시도하고 싶습니다. 사용 사례에서 눈에 띄는 속도 차이에 대해 말할 수있는 것이 있습니까?
Joe Love

유스 케이스의 경우 in연산자와 하위 쿼리를 사용할 때 10 배 정도의 속도가 빨라졌습니다 .
Thomas CG de Vilhena

1

계단식 옵션을 사용한 삭제는 외래 키가 정의 된 테이블에만 적용됩니다. 삭제를 수행하고 외래 키 제약 조건을 위반할 수 없기 때문에 캐스케이드로 인해 문제가되는 행을 삭제하게됩니다.

이런 방식으로 연관된 행을 삭제하려면 먼저 외래 키를 정의해야합니다. 또한 트랜잭션을 시작하도록 명시 적으로 지시하거나 기본값을 변경하지 않으면 자동 커밋이 수행되므로 정리하는 데 시간이 많이 걸릴 수 있습니다.


2
Grant의 답변이 부분적으로 잘못되었습니다. Postgresql은 DELETE 쿼리에서 CASCADE를 지원하지 않습니다. postgresql.org/docs/8.4/static/dml-delete.html
Fredrik Wendt

삭제 쿼리에서 지원되지 않는 이유가 무엇입니까?
Teifion

2
그에 따라 설정되지 않은 테이블, 즉 외래 키 제약 조건이 ON DELETE CASCADE로 정의되지 않은 테이블에서 "캐스케이드로 삭제"하는 방법은 없습니다.
lensovet

질문에 대한 답변으로 이것은 완전히 잘못되었습니다. CASCADE는 한 번만 할 수있는 방법이 없습니다.
Jeremy Jeremy
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.