주소기반산업서비스에서 제공하는 건물DB를 PostgreSQL 환경에 적재하는 방법을 알아보겠습니다.
postgresql18.3 로컬 주소DB 구축 (3) - 테이블 생성 글에서 설명드린 다운로드 주소파일과 테이블 생성여부 확인이 필요합니다.
postgresql18.3 로컬 주소DB 구축 (3) - 테이블 생성
DB 적재 주소데이터 다운로드테이블을 생성하기 전에 사용될 주소 데이터를 다운로드 받습니다.주소기반산업지원서비스에서 제공하는 건물DB, 위치정보요약DB의 전체 월 데이터입니다.건물정보
codingdrive.tistory.com
작업 환경 및 준비사항
- DB 버전: PostgreSQL 18.3
- OS: Windows Server (데이터 경로: C:\juso\download)
- 데이터 소스: 주소기반산업서비스 건물DB (텍스트 파일, | 구분자, EUC-KR 인코딩)
- 주요 도구: DBeaver
고성능 적재를 위한 전략
- UNLOGGED 테이블 활용: 스테이징(임시) 테이블 생성 시 UNLOGGED 옵션을 사용하여 WAL(Write Ahead Log) 기록을 생략합니다. 이를 통해 쓰기 속도를 대폭 향상시킬 수 있습니다.
- MERGE 구문 도입: PostgreSQL 15부터 지원되는 MERGE 문을 사용하여 신규(31), 변경(34), 폐지(63) 데이터를 하나의 쿼리로 처리합니다.
- 서버 측 COPY: 네트워크 부하를 줄이기 위해 DB 서버가 로컬 파일을 직접 읽는 COPY 명령을 사용합니다.
임시 테이블 생성
-- DROP TABLE juso.tmp_juso_master;
-- DROP TABLE juso.tmp_juso_related_jibun;
-- DROP TABLE juso.tmp_road_name_code;
-- 임시테이블 생성 (COPY → 임시 → merge into)
CREATE UNLOGGED TABLE IF NOT EXISTS juso.tmp_juso_master (LIKE juso.juso_master INCLUDING DEFAULTS);
CREATE UNLOGGED TABLE IF NOT EXISTS juso.tmp_juso_related_jibun (LIKE juso.juso_related_jibun INCLUDING DEFAULTS);
CREATE UNLOGGED TABLE IF NOT EXISTS juso.tmp_road_name_code (LIKE juso.road_name_code INCLUDING DEFAULTS);
-- 임시 테이블에서 불필요한 PK 제약 제거 (COPY 속도를 위해) : INCLUDING ALL로 부모 pk 만들어진 경우
ALTER TABLE IF EXISTS juso.tmp_juso_master DROP CONSTRAINT IF EXISTS pk_juso_master;
ALTER TABLE IF EXISTS juso.tmp_juso_related_jibun DROP CONSTRAINT IF EXISTS pk_juso_related_jibun;
ALTER TABLE IF EXISTS juso.tmp_road_name_code DROP CONSTRAINT IF EXISTS pk_road_name_code;
전체분 적재 프로시저
CREATE OR REPLACE PROCEDURE juso.prc_load_build_full_data()
LANGUAGE plpgsql
AS $procedure$
DECLARE
v_start TIMESTAMP := clock_timestamp();
v_cnt_m BIGINT;
v_cnt_j BIGINT;
v_cnt_r BIGINT;
v_sido_files TEXT[] := ARRAY[
'busan', 'chungbuk', 'chungnam', 'daegu', 'daejeon', 'gangwon', 'gwangju',
'gyeongbuk', 'gyeongnam', 'gyunggi', 'incheon', 'jeju', 'jeonbuk', 'jeonnam',
'sejong', 'seoul', 'ulsan'
];
v_sido TEXT;
v_master_path TEXT;
v_jibun_path TEXT;
v_road_path TEXT := '/juso/download/202602_build_db_full_data/road_code_total.txt';
BEGIN
RAISE NOTICE '[전체분] 적재 시작: %', v_start;
-- 임시 테이블 초기화
TRUNCATE juso.tmp_juso_master;
TRUNCATE juso.tmp_juso_related_jibun;
TRUNCATE juso.tmp_road_name_code;
-- --------------------------------------------------------
-- 1-1. 도로명코드 전체분 COPY
-- 파일: road_code_total.txt (단일 전국 파일)
-- --------------------------------------------------------
RAISE NOTICE '[도로명코드] COPY 시작: %', v_road_path;
EXECUTE format(
$sql$
COPY juso.tmp_road_name_code (
sigungu_code,
road_name_no,
road_name,
road_name_en,
emd_serial_no,
sido_name,
sigungu_name,
emd_type,
emd_code,
emd_name,
upper_road_name_no,
upper_road_name,
is_used,
history_reason_code,
history_info,
sido_name_en,
sigungu_name_en,
emd_name_en,
announcement_date,
expiration_date
)
FROM %L
WITH (
FORMAT TEXT,
DELIMITER '|',
ENCODING 'UHC',
NULL ''
)
$sql$,
v_road_path
);
-- --------------------------------------------------------
-- 1-2. 시도별 건물/관련지번 전체분 COPY
-- 파일: build_seoul.txt, build_busan.txt, ...
-- --------------------------------------------------------
FOREACH v_sido IN ARRAY v_sido_files LOOP
v_master_path := format('/juso/download/202602_build_db_full_data/build_%s.txt', v_sido);
v_jibun_path := format('/juso/download/202602_build_db_full_data/jibun_%s.txt', v_sido);
RAISE NOTICE '[주소 전체분] % COPY 시작', v_sido;
EXECUTE format(
$sql$
COPY juso.tmp_juso_master (
legal_emd_code,
sido_name,
sigungu_name,
legal_emd_name,
legal_ri_name,
is_mountain,
jibun_main_no,
jibun_sub_no,
road_name_code,
road_name,
is_basement,
building_main_no,
building_sub_no,
building_name_bld,
building_name_detail,
manage_no,
emd_serial_no,
admin_emd_code,
admin_emd_name,
zip_code,
zip_serial_no,
bulk_delivery_name,
move_reason_code,
announcement_date,
prev_road_address,
sigungu_building_name,
is_apartment,
basic_area_no,
is_detail_address,
remarks1,
remarks2
)
FROM %L
WITH (
FORMAT TEXT,
DELIMITER '|',
ENCODING 'UHC',
NULL ''
)
$sql$,
v_master_path
);
RAISE NOTICE '[지번 전체분] % COPY 시작', v_sido;
EXECUTE format(
$sql$
COPY juso.tmp_juso_related_jibun (
legal_emd_code,
sido_name,
sigungu_name,
legal_emd_name,
legal_ri_name,
is_mountain,
jibun_main_no,
jibun_sub_no,
road_name_code,
is_basement,
building_main_no,
building_sub_no,
jibun_serial_no,
move_reason_code
)
FROM %L
WITH (
FORMAT TEXT,
DELIMITER '|',
ENCODING 'UHC',
NULL ''
)
$sql$,
v_jibun_path
);
END LOOP;
-- --------------------------------------------------------
-- 1-3. 임시 → 본 테이블 UPSERT (전체분이므로 TRUNCATE+INSERT 방식)
-- --------------------------------------------------------
RAISE NOTICE '[전체분] 본 테이블 적재 시작';
TRUNCATE juso.juso_master;
TRUNCATE juso.juso_related_jibun;
TRUNCATE juso.road_name_code;
INSERT INTO juso.road_name_code
(
sigungu_code,
road_name_no,
road_name,
road_name_en,
emd_serial_no,
sido_name,
sigungu_name,
emd_type,
emd_code,
emd_name,
upper_road_name_no,
upper_road_name,
is_used,
history_reason_code,
history_info,
sido_name_en,
sigungu_name_en,
emd_name_en,
announcement_date,
expiration_date
)
SELECT sigungu_code,
road_name_no,
road_name,
road_name_en,
emd_serial_no,
sido_name,
sigungu_name,
emd_type,
emd_code,
emd_name,
upper_road_name_no,
upper_road_name,
is_used,
history_reason_code,
history_info,
sido_name_en,
sigungu_name_en,
emd_name_en,
announcement_date,
expiration_date
FROM juso.tmp_road_name_code;
GET DIAGNOSTICS v_cnt_r = ROW_COUNT;
INSERT INTO juso.juso_master
(
legal_emd_code,
sido_name,
sigungu_name,
legal_emd_name,
legal_ri_name,
is_mountain,
jibun_main_no,
jibun_sub_no,
road_name_code,
road_name,
is_basement,
building_main_no,
building_sub_no,
building_name_bld,
building_name_detail,
manage_no,
emd_serial_no,
admin_emd_code,
admin_emd_name,
zip_code,
zip_serial_no,
bulk_delivery_name,
move_reason_code,
announcement_date,
prev_road_address,
sigungu_building_name,
is_apartment,
basic_area_no,
is_detail_address,
remarks1,
remarks2
)
SELECT legal_emd_code,
sido_name,
sigungu_name,
legal_emd_name,
legal_ri_name,
is_mountain,
jibun_main_no,
jibun_sub_no,
road_name_code,
road_name,
is_basement,
building_main_no,
building_sub_no,
building_name_bld,
building_name_detail,
manage_no,
emd_serial_no,
admin_emd_code,
admin_emd_name,
zip_code,
zip_serial_no,
bulk_delivery_name,
move_reason_code,
announcement_date,
prev_road_address,
sigungu_building_name,
is_apartment,
basic_area_no,
is_detail_address,
remarks1,
remarks2
FROM juso.tmp_juso_master;
GET DIAGNOSTICS v_cnt_m = ROW_COUNT;
INSERT INTO juso.juso_related_jibun
(
legal_emd_code,
sido_name,
sigungu_name,
legal_emd_name,
legal_ri_name,
is_mountain,
jibun_main_no,
jibun_sub_no,
road_name_code,
is_basement,
building_main_no,
building_sub_no,
jibun_serial_no,
move_reason_code
)
SELECT legal_emd_code,
sido_name,
sigungu_name,
legal_emd_name,
legal_ri_name,
is_mountain,
jibun_main_no,
jibun_sub_no,
road_name_code,
is_basement,
building_main_no,
building_sub_no,
jibun_serial_no,
move_reason_code
FROM juso.tmp_juso_related_jibun;
GET DIAGNOSTICS v_cnt_j = ROW_COUNT;
-- --------------------------------------------------------
-- 1-4. 인덱스 갱신 및 통계
-- --------------------------------------------------------
ANALYZE juso.juso_master;
ANALYZE juso.juso_related_jibun;
ANALYZE juso.road_name_code;
RAISE NOTICE '=====================================================';
RAISE NOTICE '[전체분] 적재 완료';
RAISE NOTICE ' juso_master : % 건', v_cnt_m;
RAISE NOTICE ' juso_related_jibun : % 건', v_cnt_j;
RAISE NOTICE ' road_name_code : % 건', v_cnt_r;
RAISE NOTICE ' 소요시간: %', clock_timestamp() - v_start;
RAISE NOTICE '=====================================================';
EXCEPTION WHEN OTHERS THEN
RAISE EXCEPTION '[전체분] 오류 발생: % / %', SQLERRM, SQLSTATE;
END;
$procedure$
;
--COMMENT ON PROCEDURE juso.prc_load_build_full_data() IS
--'건물DB 전체분 적재. /juso/download/202602_build_db_full_data/경로의 시도별 txt 파일을 읽어 juso_master, juso_related_jibun, road_name_code에 적재';
변경분 적재 프로시저
CREATE OR REPLACE PROCEDURE juso.prc_load_build_delta_data()
LANGUAGE plpgsql
AS $procedure$
DECLARE
v_start TIMESTAMP := clock_timestamp();
v_cnt_m BIGINT;
v_cnt_j BIGINT;
v_cnt_r BIGINT;
v_master_path TEXT := '/juso/download/202602_build_db_delta_data/build_mod.txt';
v_jibun_path TEXT := '/juso/download/202602_build_db_delta_data/jibun_mod.txt';
v_road_path TEXT := '/juso/download/202602_build_db_delta_data/road_code_mod.txt';
BEGIN
RAISE NOTICE '[변동분] 적재 시작: %', v_start;
-- 임시 테이블 초기화
TRUNCATE juso.tmp_juso_master;
TRUNCATE juso.tmp_juso_related_jibun;
TRUNCATE juso.tmp_road_name_code;
-- --------------------------------------------------------
-- 2-1. 도로명코드 변동분 COPY
-- --------------------------------------------------------
RAISE NOTICE '[도로명코드 변동분] COPY 시작';
EXECUTE format(
$sql$
COPY juso.tmp_road_name_code (
sigungu_code,
road_name_no,
road_name,
road_name_en,
emd_serial_no,
sido_name,
sigungu_name,
emd_type,
emd_code,
emd_name,
upper_road_name_no,
upper_road_name,
is_used,
history_reason_code,
history_info,
sido_name_en,
sigungu_name_en,
emd_name_en,
announcement_date,
expiration_date
)
FROM %L
WITH (
FORMAT TEXT,
DELIMITER '|',
ENCODING 'UHC',
NULL ''
)
$sql$,
v_road_path
);
-- --------------------------------------------------------
-- 2-2. road_name_code 변동분 처리
-- 신규(31) : INSERT
-- 변경(34) : UPDATE
-- 폐지(63) : DELETE
-- --------------------------------------------------------
MERGE INTO juso.road_name_code t
USING juso.tmp_road_name_code s
ON (
t.sigungu_code = s.sigungu_code
AND t.road_name_no = s.road_name_no
AND t.emd_serial_no = s.emd_serial_no)
WHEN MATCHED THEN -- 변경
UPDATE SET
sigungu_code = s.sigungu_code,
road_name_no = s.road_name_no,
road_name = s.road_name,
road_name_en = s.road_name_en,
emd_serial_no = s.emd_serial_no,
sido_name = s.sido_name,
sigungu_name = s.sigungu_name,
emd_type = s.emd_type,
emd_code = s.emd_code,
emd_name = s.emd_name,
upper_road_name_no = s.upper_road_name_no,
upper_road_name = s.upper_road_name,
is_used = s.is_used,
history_reason_code = s.history_reason_code,
history_info = s.history_info,
sido_name_en = s.sido_name_en,
sigungu_name_en = s.sigungu_name_en,
emd_name_en = s.emd_name_en,
announcement_date = s.announcement_date,
expiration_date = s.expiration_date,
updated_at = NOW()
WHEN NOT MATCHED THEN -- 신규
INSERT (
sigungu_code,
road_name_no,
road_name,
road_name_en,
emd_serial_no,
sido_name,
sigungu_name,
emd_type,
emd_code,
emd_name,
upper_road_name_no,
upper_road_name,
is_used,
history_reason_code,
history_info,
sido_name_en,
sigungu_name_en,
emd_name_en,
announcement_date,
expiration_date
)
VALUES (
s.sigungu_code,
s.road_name_no,
s.road_name,
s.road_name_en,
s.emd_serial_no,
s.sido_name,
s.sigungu_name,
s.emd_type,
s.emd_code,
s.emd_name,
s.upper_road_name_no,
s.upper_road_name,
s.is_used,
s.history_reason_code,
s.history_info,
s.sido_name_en,
s.sigungu_name_en,
s.emd_name_en,
s.announcement_date,
s.expiration_date);
GET DIAGNOSTICS v_cnt_r = ROW_COUNT;
-- --------------------------------------------------------
-- 2-3. 시도별 주소(건물) 변동분 COPY
-- --------------------------------------------------------
RAISE NOTICE '[주소 변동분] COPY 시작';
EXECUTE format(
$sql$
COPY juso.tmp_juso_master (
legal_emd_code,
sido_name,
sigungu_name,
legal_emd_name,
legal_ri_name,
is_mountain,
jibun_main_no,
jibun_sub_no,
road_name_code,
road_name,
is_basement,
building_main_no,
building_sub_no,
building_name_bld,
building_name_detail,
manage_no,
emd_serial_no,
admin_emd_code,
admin_emd_name,
zip_code,
zip_serial_no,
bulk_delivery_name,
move_reason_code,
announcement_date,
prev_road_address,
sigungu_building_name,
is_apartment,
basic_area_no,
is_detail_address,
remarks1,
remarks2
)
FROM %L
WITH (
FORMAT TEXT,
DELIMITER '|',
ENCODING 'UHC',
NULL ''
)
$sql$,
v_master_path
);
RAISE NOTICE '[지번 변동분] COPY 시작';
EXECUTE format(
$sql$
COPY juso.tmp_juso_related_jibun (
legal_emd_code,
sido_name,
sigungu_name,
legal_emd_name,
legal_ri_name,
is_mountain,
jibun_main_no,
jibun_sub_no,
road_name_code,
is_basement,
building_main_no,
building_sub_no,
jibun_serial_no,
move_reason_code
)
FROM %L
WITH (
FORMAT TEXT,
DELIMITER '|',
ENCODING 'UHC',
NULL ''
)
$sql$,
v_jibun_path
);
-- --------------------------------------------------------
-- 2-4. juso_master 변동분 처리
-- 신규(31) : INSERT
-- 변경(34) : UPDATE
-- 폐지(63) : DELETE
-- --------------------------------------------------------
MERGE INTO juso.juso_master t
USING (
SELECT
legal_emd_code,
sido_name,
sigungu_name,
legal_emd_name,
legal_ri_name,
is_mountain,
jibun_main_no,
jibun_sub_no,
road_name_code,
road_name,
is_basement,
building_main_no,
building_sub_no,
building_name_bld,
building_name_detail,
manage_no,
emd_serial_no,
admin_emd_code,
admin_emd_name,
zip_code,
zip_serial_no,
bulk_delivery_name,
move_reason_code,
announcement_date,
prev_road_address,
sigungu_building_name,
is_apartment,
basic_area_no,
is_detail_address,
remarks1,
remarks2
FROM juso.tmp_juso_master
WHERE move_reason_code IN ('31', '34', '63')
) AS s
ON (t.manage_no = s.manage_no)
WHEN MATCHED AND s.move_reason_code = '34' THEN -- 변경
UPDATE SET
legal_emd_code = s.legal_emd_code,
sido_name = s.sido_name,
sigungu_name = s.sigungu_name,
legal_emd_name = s.legal_emd_name,
legal_ri_name = s.legal_ri_name,
is_mountain = s.is_mountain,
jibun_main_no = s.jibun_main_no,
jibun_sub_no = s.jibun_sub_no,
road_name_code = s.road_name_code,
road_name = s.road_name,
is_basement = s.is_basement,
building_main_no = s.building_main_no,
building_sub_no = s.building_sub_no,
building_name_bld = s.building_name_bld,
building_name_detail = s.building_name_detail,
emd_serial_no = s.emd_serial_no,
admin_emd_code = s.admin_emd_code,
admin_emd_name = s.admin_emd_name,
zip_code = s.zip_code,
zip_serial_no = s.zip_serial_no,
bulk_delivery_name = s.bulk_delivery_name,
move_reason_code = s.move_reason_code,
announcement_date = s.announcement_date,
prev_road_address = s.prev_road_address,
sigungu_building_name = s.sigungu_building_name,
is_apartment = s.is_apartment,
basic_area_no = s.basic_area_no,
is_detail_address = s.is_detail_address,
remarks1 = s.remarks1,
remarks2 = s.remarks2,
updated_at = NOW()
WHEN MATCHED AND s.move_reason_code = '63' THEN -- 폐지
DELETE
WHEN NOT MATCHED AND s.move_reason_code = '31' THEN -- 신규
INSERT (
legal_emd_code,
sido_name,
sigungu_name,
legal_emd_name,
legal_ri_name,
is_mountain,
jibun_main_no,
jibun_sub_no,
road_name_code,
road_name,
is_basement,
building_main_no,
building_sub_no,
building_name_bld,
building_name_detail,
manage_no,
emd_serial_no,
admin_emd_code,
admin_emd_name,
zip_code,
zip_serial_no,
bulk_delivery_name,
move_reason_code,
announcement_date,
prev_road_address,
sigungu_building_name,
is_apartment,
basic_area_no,
is_detail_address,
remarks1,
remarks2
)
VALUES (
s.legal_emd_code,
s.sido_name,
s.sigungu_name,
s.legal_emd_name,
s.legal_ri_name,
s.is_mountain,
s.jibun_main_no,
s.jibun_sub_no,
s.road_name_code,
s.road_name,
s.is_basement,
s.building_main_no,
s.building_sub_no,
s.building_name_bld,
s.building_name_detail,
s.manage_no,
s.emd_serial_no,
s.admin_emd_code,
s.admin_emd_name,
s.zip_code,
s.zip_serial_no,
s.bulk_delivery_name,
s.move_reason_code,
s.announcement_date,
s.prev_road_address,
s.sigungu_building_name,
s.is_apartment,
s.basic_area_no,
s.is_detail_address,
s.remarks1,
s.remarks2);
GET DIAGNOSTICS v_cnt_m = ROW_COUNT;
-- --------------------------------------------------------
-- 2-4. juso_related_jibun 변동분 처리
-- --------------------------------------------------------
MERGE INTO juso.juso_related_jibun t
USING (
SELECT
legal_emd_code,
sido_name,
sigungu_name,
legal_emd_name,
legal_ri_name,
is_mountain,
jibun_main_no,
jibun_sub_no,
road_name_code,
is_basement,
building_main_no,
building_sub_no,
jibun_serial_no,
move_reason_code
FROM juso.tmp_juso_related_jibun
WHERE move_reason_code IN ('31', '34', '63')
) AS s
ON (
t.road_name_code = s.road_name_code
AND t.is_basement = s.is_basement
AND t.building_main_no = s.building_main_no
AND t.building_sub_no = s.building_sub_no
AND t.jibun_serial_no = s.jibun_serial_no)
WHEN MATCHED AND s.move_reason_code = '34' THEN -- 변경
UPDATE SET
legal_emd_code = s.legal_emd_code,
sido_name = s.sido_name,
sigungu_name = s.sigungu_name,
legal_emd_name = s.legal_emd_name,
legal_ri_name = s.legal_ri_name,
is_mountain = s.is_mountain,
jibun_main_no = s.jibun_main_no,
jibun_sub_no = s.jibun_sub_no,
move_reason_code = s.move_reason_code,
updated_at = NOW()
WHEN MATCHED AND s.move_reason_code = '63' THEN -- 폐지
DELETE
WHEN NOT MATCHED AND s.move_reason_code = '31' THEN -- 신규
INSERT (
legal_emd_code,
sido_name,
sigungu_name,
legal_emd_name,
legal_ri_name,
is_mountain,
jibun_main_no,
jibun_sub_no,
road_name_code,
is_basement,
building_main_no,
building_sub_no,
jibun_serial_no,
move_reason_code
)
VALUES (
s.legal_emd_code,
s.sido_name,
s.sigungu_name,
s.legal_emd_name,
s.legal_ri_name,
s.is_mountain,
s.jibun_main_no,
s.jibun_sub_no,
s.road_name_code,
s.is_basement,
s.building_main_no,
s.building_sub_no,
s.jibun_serial_no,
s.move_reason_code);
GET DIAGNOSTICS v_cnt_j = ROW_COUNT;
-- --------------------------------------------------------
-- 2-5. 통계 갱신
-- --------------------------------------------------------
ANALYZE juso.juso_master;
ANALYZE juso.juso_related_jibun;
ANALYZE juso.road_name_code;
RAISE NOTICE '=====================================================';
RAISE NOTICE '[변동분] 적재 완료';
RAISE NOTICE ' juso_master : % 건', v_cnt_m;
RAISE NOTICE ' juso_related_jibun : % 건', v_cnt_j;
RAISE NOTICE ' road_name_code : % 건', v_cnt_r;
RAISE NOTICE ' 소요시간: %', clock_timestamp() - v_start;
RAISE NOTICE '=====================================================';
EXCEPTION WHEN OTHERS THEN
RAISE EXCEPTION '[변동분] 오류 발생: % / %', SQLERRM, SQLSTATE;
END;
$procedure$
;
--COMMENT ON PROCEDURE juso.prc_load_build_delta_data() IS
--'건물DB 변동분 적재. /juso/download/202602_build_db_delta_data\ 변경분 txt 파일을 읽어 juso_master, juso_related_jibun, road_name_code를 MERGE INTO';
'운영체제(OS) > Docker' 카테고리의 다른 글
| postgresql18.3 로컬 주소DB 구축 (7) - 위치정보 테이블 적재 (0) | 2026.04.08 |
|---|---|
| postgresql18.3 로컬 주소DB 구축 (6) - WSL Docker에서 윈도우 로컬 파일을 인식하지 못할 때 해결법 (볼륨 마운트) (0) | 2026.04.07 |
| postgresql18.3 로컬 주소DB 구축 (4) - PostGIS 확장기능 설치 (0) | 2026.03.31 |
| postgresql18.3 로컬 주소DB 구축 (3) - 테이블 생성 (1) | 2026.03.31 |
| postgresql18.3 로컬 주소DB 구축 (2) - 계정 생성 (0) | 2026.03.27 |
댓글