From 1c74e192ec8f13f09257e695b1b97c50c7290ac7 Mon Sep 17 00:00:00 2001 From: GA Pro Date: Mon, 11 May 2026 00:34:47 +0900 Subject: [PATCH] =?UTF-8?q?fix(upload):=20target=5Ftable=20=EC=9E=90?= =?UTF-8?q?=EC=9C=A0=EC=9E=85=EB=A0=A5=20=E2=86=92=20Select=20(whitelist)?= =?UTF-8?q?=20+=20V21=20PROFILE/RESUME=20=EB=8F=84=EB=A9=94=EC=9D=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [버그 fix] - UploadTemplateManager 의 ModalForm 에서 target_table 이 ProFormText 자유입력 이라 사용자가 잘못 입력 시 ("1" 등) 백엔드 whitelist 검증 실패 ("허용되지 않은 target_table: 1") 발생 - ProFormSelect 로 변경 + request 로 /api/upload-templates/target-tables 호출 → 9개 허용 테이블만 옵션 노출 (오타 / 잘못된 입력 원천 차단) [V21__프로필_이력서_도메인.sql] 도메인 15개 추가: PROFILE 카테고리 (8개): - DOM_PROFILE_BIO (자기소개, TEXT) - DOM_PROFILE_IMAGE (프로필 사진, BIGINT 파일ID) - DOM_NICKNAME (닉네임, VARCHAR(50)) - DOM_BIRTH_DATE (생년월일, DATE) - DOM_GENDER (성별, CHAR(1) M/F/U) - DOM_ADDRESS (주소, VARCHAR(300)) - DOM_ZIP_CODE (우편번호, VARCHAR(10)) - DOM_NATIONALITY (국적, ISO 3166) RESUME 카테고리 (7개): - DOM_RESUME_FILE (이력서 파일, BIGINT) - DOM_CAREER_HISTORY (경력 사항, JSONB 배열) - DOM_EDUCATION (학력, JSONB 배열) - DOM_LICENSE_LIST (자격증 목록, JSONB) - DOM_SKILL_TAGS (스킬 태그, VARCHAR(500)) - DOM_INTRODUCTION (소개 영상 URL, VARCHAR(500)) - DOM_HIRE_TYPE (계약 형태, VARCHAR(20)) 데이터 사전 매핑 28개 추가 (V20 누락 보강): - agent: grade_id/bank_code/leave_date/created_at/updated_at - users: 7컬럼 - organization: 6컬럼 - insurance_company: 4컬럼 - product: 5컬럼 검증: - Flyway V21 적용 200ms - /api/data-domains?domainCategory=PROFILE → 8건 - /api/data-domains?domainCategory=RESUME → 7건 - target-tables 9개 정상 응답 - 프론트 tsc -b 통과 Co-Authored-By: Claude Opus 4.7 (1M context) --- .../migration/V21__프로필_이력서_도메인.sql | 127 ++++++++++++++++++ .../pages/system/UploadTemplateManager.tsx | 10 +- 2 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 ga-common/src/main/resources/db/migration/V21__프로필_이력서_도메인.sql diff --git a/ga-common/src/main/resources/db/migration/V21__프로필_이력서_도메인.sql b/ga-common/src/main/resources/db/migration/V21__프로필_이력서_도메인.sql new file mode 100644 index 0000000..13c1e23 --- /dev/null +++ b/ga-common/src/main/resources/db/migration/V21__프로필_이력서_도메인.sql @@ -0,0 +1,127 @@ +-- V21: 프로필 / 이력서 / 인적사항 관련 도메인 추가 (data_domain) +-- + 기존 agent / users 테이블의 누락 컬럼들 데이터 사전 매핑 보강 + +-- ============================================================ +-- 1) 프로필 / 이력서 카테고리 도메인 추가 +-- ============================================================ + +INSERT INTO ga.data_domain +(domain_code, domain_name, domain_category, data_type, length, precision, scale, + format_pattern, validation_regex, example_value, masking_type, encrypted, description, is_active) +VALUES +-- 프로필 (PROFILE) — 사용자/설계사 프로필 정보 +('DOM_PROFILE_BIO', '자기소개', 'PROFILE', 'TEXT', NULL, NULL, NULL, + NULL, NULL, '15년차 라이프 플래너...', 'NONE', 'N', + '설계사 / 사용자의 자기소개 글', 'Y'), + +('DOM_PROFILE_IMAGE', '프로필 사진', 'PROFILE', 'BIGINT', NULL, NULL, NULL, + NULL, NULL, '12345', 'NONE', 'N', + 'file_storage 테이블의 file_id (프로필 이미지)', 'Y'), + +('DOM_NICKNAME', '닉네임', 'PROFILE', 'VARCHAR', 50, NULL, NULL, + NULL, '^[가-힣A-Za-z0-9_]{2,20}$', '베스트설계사', 'NONE', 'N', + '표시명 / 별명', 'Y'), + +('DOM_BIRTH_DATE', '생년월일', 'PROFILE', 'DATE', NULL, NULL, NULL, + 'yyyy-MM-dd', NULL, '1985-03-15', 'NONE', 'N', + '생년월일 (만 나이 계산용)', 'Y'), + +('DOM_GENDER', '성별', 'PROFILE', 'CHAR', 1, NULL, NULL, + NULL, '^[MFU]$', 'M', 'NONE', 'N', + '성별 코드 — M(남) / F(여) / U(미지정)', 'Y'), + +('DOM_ADDRESS', '주소', 'PROFILE', 'VARCHAR', 300, NULL, NULL, + NULL, NULL, '서울특별시 강남구 테헤란로 123', 'NONE', 'N', + '주소 (도로명 또는 지번)', 'Y'), + +('DOM_ZIP_CODE', '우편번호', 'PROFILE', 'VARCHAR', 10, NULL, NULL, + NULL, '^\d{5,6}$', '06234', 'NONE', 'N', + '우편번호', 'Y'), + +('DOM_NATIONALITY', '국적', 'PROFILE', 'VARCHAR', 10, NULL, NULL, + NULL, '^[A-Z]{2,3}$', 'KR', 'NONE', 'N', + 'ISO 3166 국가 코드 (예: KR, US)', 'Y'), + +-- 이력서 (RESUME) +('DOM_RESUME_FILE', '이력서 파일', 'RESUME', 'BIGINT', NULL, NULL, NULL, + NULL, NULL, '67890', 'NONE', 'N', + 'file_storage 의 file_id (PDF/DOCX 이력서)', 'Y'), + +('DOM_CAREER_HISTORY', '경력 사항', 'RESUME', 'JSONB', NULL, NULL, NULL, + NULL, NULL, + '[{"company":"삼성생명","period":"2015-01~2020-12","role":"FC"}]', + 'NONE', 'N', + '경력 이력 JSONB 배열 (회사/기간/역할)', 'Y'), + +('DOM_EDUCATION', '학력', 'RESUME', 'JSONB', NULL, NULL, NULL, + NULL, NULL, + '[{"school":"서울대학교","major":"경영학","year":2010,"degree":"학사"}]', + 'NONE', 'N', + '학력 이력 JSONB 배열 (학교/전공/연도)', 'Y'), + +('DOM_LICENSE_LIST', '자격증 목록', 'RESUME', 'JSONB', NULL, NULL, NULL, + NULL, NULL, + '[{"name":"생명보험설계사","issuedAt":"2015-03-01","number":"L-12345"}]', + 'NONE', 'N', + '자격증 / 인증 JSONB 배열', 'Y'), + +('DOM_SKILL_TAGS', '스킬 태그', 'RESUME', 'VARCHAR', 500, NULL, NULL, + NULL, NULL, '#종신보험,#건강보험,#연금', 'NONE', 'N', + '쉼표 / 해시태그 구분 스킬', 'Y'), + +('DOM_INTRODUCTION', '소개 영상 URL', 'RESUME', 'VARCHAR', 500, NULL, NULL, + NULL, '^https?://.+', 'https://youtu.be/abc123', 'NONE', 'N', + '소개 영상 외부 URL', 'Y'), + +('DOM_HIRE_TYPE', '계약 형태', 'RESUME', 'VARCHAR', 20, NULL, NULL, + NULL, NULL, 'FULL_TIME', 'NONE', 'N', + 'FULL_TIME / PART_TIME / CONTRACT / FREELANCE', 'Y') + +ON CONFLICT (domain_code) DO NOTHING; + + +-- ============================================================ +-- 2) 기존 테이블에 도메인 매핑 보강 (V20 에서 누락된 부분) +-- ============================================================ + +INSERT INTO ga.data_dictionary +(table_name, column_name, domain_code, column_label, is_pk, is_fk, fk_reference, is_required, description) +VALUES +-- agent 추가 컬럼 (V20 에서 10개 매핑됐음 — 누락 보강) +('agent', 'grade_id', 'DOM_ID_BIGINT', '직급ID', 'N', 'Y', 'grade.grade_id', 'Y', '소속 직급'), +('agent', 'bank_code', 'DOM_BANK_CODE', '은행코드', 'N', 'N', NULL, 'N', '계좌의 은행 코드'), +('agent', 'leave_date', 'DOM_DATE', '퇴사일', 'N', 'N', NULL, 'N', '퇴사 시 입력'), +('agent', 'created_at', 'DOM_DATETIME', '등록일시', 'N', 'N', NULL, 'Y', '레코드 생성 시각'), +('agent', 'updated_at', 'DOM_DATETIME', '수정일시', 'N', 'N', NULL, 'N', '레코드 수정 시각'), + +-- users 테이블 (시스템 사용자) — 신규 매핑 +('users', 'user_id', 'DOM_ID_BIGINT', '사용자ID', 'Y', 'N', NULL, 'Y', NULL), +('users', 'login_id', 'DOM_LICENSE_NO', '로그인 ID', 'N', 'N', NULL, 'Y', '로그인 식별자 (UNIQUE)'), +('users', 'password', 'DOM_TEXT_SHORT', '비밀번호', 'N', 'N', NULL, 'Y', 'BCrypt 해시'), +('users', 'agent_id', 'DOM_ID_BIGINT', '연결 설계사', 'N', 'Y', 'agent.agent_id', 'N', NULL), +('users', 'role', 'DOM_STATUS', '역할', 'N', 'N', NULL, 'Y', 'SUPER_ADMIN / ADMIN / MANAGER / AGENT'), +('users', 'is_active', 'DOM_YN_FLAG', '활성여부', 'N', 'N', NULL, 'Y', NULL), +('users', 'last_login_at','DOM_DATETIME', '마지막 로그인일시', 'N', 'N', NULL, 'N', NULL), + +-- organization 테이블 +('organization', 'org_id', 'DOM_ID_BIGINT', '조직ID', 'Y', 'N', NULL, 'Y', NULL), +('organization', 'parent_org_id', 'DOM_ID_BIGINT', '상위조직ID', 'N', 'Y', 'organization.org_id', 'N', NULL), +('organization', 'org_name', 'DOM_NAME', '조직명', 'N', 'N', NULL, 'Y', NULL), +('organization', 'org_type', 'DOM_STATUS', '조직유형', 'N', 'N', NULL, 'Y', 'HQ / BR / TM'), +('organization', 'org_level', 'DOM_ID_BIGINT', '조직레벨', 'N', 'N', NULL, 'Y', NULL), +('organization', 'effective_from', 'DOM_DATE', '적용시작일', 'N', 'N', NULL, 'Y', NULL), + +-- insurance_company 테이블 +('insurance_company', 'company_id', 'DOM_ID_BIGINT', '보험사ID', 'Y', 'N', NULL, 'Y', NULL), +('insurance_company', 'company_code', 'DOM_COMPANY_CODE','보험사코드','N','N', NULL, 'Y', 'UNIQUE'), +('insurance_company', 'company_name', 'DOM_NAME', '회사명', 'N', 'N', NULL, 'Y', NULL), +('insurance_company', 'biz_no', 'DOM_LICENSE_NO', '사업자번호','N','N', NULL, 'N', NULL), + +-- product 테이블 +('product', 'product_id', 'DOM_ID_BIGINT', '상품ID', 'Y', 'N', NULL, 'Y', NULL), +('product', 'company_id', 'DOM_ID_BIGINT', '보험사ID', 'N', 'Y', 'insurance_company.company_id', 'Y', NULL), +('product', 'product_code', 'DOM_PRODUCT_CODE', '상품코드', 'N', 'N', NULL, 'Y', NULL), +('product', 'product_name', 'DOM_NAME', '상품명', 'N', 'N', NULL, 'Y', NULL), +('product', 'insurance_type', 'DOM_INSURANCE_TYPE','보험종류', 'N', 'N', NULL, 'Y', NULL) + +ON CONFLICT (table_name, column_name) DO NOTHING; diff --git a/ga-frontend/src/pages/system/UploadTemplateManager.tsx b/ga-frontend/src/pages/system/UploadTemplateManager.tsx index 6a506eb..f4bc447 100644 --- a/ga-frontend/src/pages/system/UploadTemplateManager.tsx +++ b/ga-frontend/src/pages/system/UploadTemplateManager.tsx @@ -691,7 +691,15 @@ function TemplateFormModal({ editTarget, onSuccess, trigger }: { options={CATEGORIES} /> - + { + const tables = await uploadTemplateApi.targetTables(); + return tables.map((t) => ({ value: t, label: t })); + }} + placeholder="recruit_ledger 등 선택" + />