Skip to content

엑셀 임포트 ​

기존 엑셀 용어집을 옮기는 경로다. dry-run이 기본이고 실제 반영은 명시해야 한다.

XLSX 없이 JSON으로 일괄 검토·등록·수정하려면 용어 API의 JSON 일괄 검토·반영을 사용한다.

관리자용 전체 스냅샷 ​

/sheet의 내보내기는 현재 화면 또는 선택한 행을 CSV로 저장하는 기능이다. 서버에 있는 전체 용어를 내려받으려면 관리자 패널 → 데이터에서 다음 API를 사용한다.

http
GET /api/v1/admin/exports/terms

이 엔드포인트는 관리자 세션만 허용하고, 현재 용어·추가 표기·분류·의미 관계를 glossary-snapshot-YYYY-MM-DD.json으로 내려준다. 사용자 비밀번호, API 키, AI·RAG 연결 비밀값은 포함하지 않는다. 응답에는 format: "geniuskey.glossary.snapshot", version: 1, readOnly: true 표식이 있다.

이 파일은 일반 POST /api/v1/import의 xlsx 데이터와 다른 백업·검토용 형식이다. 파일 이름을 바꾸거나 dryRun=false를 보내도 서버가 표식을 확인해 validation_failed로 거부하므로 자동으로 기존 용어를 덮어쓰지 않는다. 복원이 필요하면 별도의 관리자 복원 절차에서 차이와 적용 대상을 검토한 뒤 진행해야 한다. 다른 서버로 데이터를 옮기려면 아래 동기화 번들을 쓴다.

서버 간 동기화 번들 ​

네트워크로 연결되지 않은 서버 사이에서 용어·표기·옛 주소·도메인·업무 분류·승인된 관계·위키·본문 이미지를 한 방향으로 옮긴다. 모든 엔드포인트는 관리자 세션만 허용한다. 운영 절차와 CLI는 운영 안내서에 있다.

http
GET /api/v1/admin/sync/export?mode=full
GET /api/v1/admin/sync/export?mode=incremental&base={bundleId}

application/gzip 번들(<label>-<시각>-<mode>.glossary-sync.json.gz)을 내려준다. 번들에는 format: "geniuskey.glossary.sync", version: 1, 출처 서버 식별자, 전체 항목의 내용 해시 목록 (manifest)이 들어 있다. incremental은 기준 번들(base, 생략하면 직전 내보내기) 이후 해시가 달라진 항목만 싣고, manifest와 분류 체계는 항상 전부 싣는다. 이전 내보내기 기록이 없으면 400이다. 사용자 계정, 담당자, AI 설정, 승인되지 않은 관계는 포함하지 않는다.

http
POST /api/v1/admin/sync/import?dryRun=true&localEdits=source
Content-Type: application/gzip

<번들 파일 바이트>

번들을 multipart가 아니라 본문 그대로 보낸다(최대 512MB). 기본은 dryRun=true로, 전체를 트랜잭션 안에서 계산한 뒤 되돌려 결과만 알려 준다. dryRun=false면 반영한다. 응답의 report는 항목별 created·updated·deleted·unchanged 수와 다음 목록을 담는다.

목록뜻
conflicts주소가 이 서버의 다른 항목과 겹치거나 연결 용어가 없어 건너뜀
overwritten양쪽이 모두 바뀌어 출처 내용으로 덮음(이력에 남음)
keptlocalEdits=keep이라 이 서버에서 고친 내용을 남김
stalemanifest와 어긋남 — 번들을 빠뜨렸으니 전체 번들로 다시 맞춰야 함

자기 서버가 만든 번들이나 이미 반영한 것보다 오래된 번들은 409 operation_conflict, 형식이 다른 파일은 400 validation_failed다.

http
GET /api/v1/admin/sync
DELETE /api/v1/admin/sync?source={instanceId}

GET은 이 서버의 식별자, 최근 내보내기 10건, 출처 서버별 마지막 번들과 적용 결과를 돌려준다. DELETE는 출처 서버의 추적 기록만 지운다. 이미 들어온 데이터는 남고 이후로는 이 서버의 데이터로 취급한다.

영문·한글 두 열 가져오기 ​

/import의 기본 화면과 /sheet의 영문·한글 엑셀 가져오기는 공통 검토 화면을 사용한다. 시트에 영문·한글 헤더를 포함해 붙여넣거나 새 행에 두 열을 붙여넣어도 이 화면이 열린다. 기존 셀에 헤더 없이 붙여넣는 동작은 선택 영역 편집으로 유지된다.

  • 파일의 첫 행은 헤더다. 붙여넣기는 영문·한글 헤더를 자동 인식하고, 헤더 없는 두 열은 왼쪽 영문 / 오른쪽 한글로 읽는다.
  • 영문·한글 두 열은 필수이며 한쪽 언어의 값은 비워둘 수 있다. 도메인·한줄 정의·본문은 체크박스로 선택한다. 헤더 없는 입력은 영문 → 한글 → 선택한 도메인 → 선택한 한줄 정의 → 선택한 본문 순서다. 체크 순서는 열 순서를 바꾸지 않는다. 헤더가 있는 파일·표는 열 이름으로 연결하고, 선택하지 않은 열은 가져오지 않는 열로 표시한다. 본문의 줄바꿈과 마크다운은 그대로 저장한다.
  • 쉼표·세미콜론·셀 안 줄바꿈을 선택해 나눈다. 괄호·따옴표 내부 구분자는 보존한다.
  • 언어별 첫 표기를 대표명으로 쓰고 나머지는 검색 가능한 다른 표기(alias)로 저장한다. 약어·확장명이나 비권장·금지 표기는 자동 추정하지 않는다.
  • 쉼표, 잘못 닫힌 괄호·따옴표, 기존 용어 충돌 및 입력 내 중복은 각각 검토해야 한다. 한 행씩 수정·승인하거나 건너뛸 수 있다. 수정 후 재검사가 필요하며, 변경된 판정에는 기존 승인이 적용되지 않는다. 자동 병합은 하지 않는다.
  • 원본 셀과 검토 결과는 검토 화면에서 유지되며 JSON으로 내려받을 수 있다. 원본 파일 자체를 서버에 보관하는 기능은 아니다.
  • 영문·한글이 모두 있는 용어는 대응표로서 완성된 매핑으로 판정한다. 기존 저장 상태 캐시는 용어가 다시 저장될 때 새 기준을 적용한다.

검토 API는 POST /api/v1/import/review이며 multipart 필드는 다음과 같다.

필드설명
file 또는 textxlsx 파일 또는 클립보드 TSV. 각각 최대 10MB, 최대 5,000행
hasHeaders붙여넣기에 별도 상세 헤더가 있으면 true
reviewJSON: { options: { comma, semicolon, newline }, columns: ["domain", "definitionMd", "bodyMd"], decisions: [] }. columns 기본값은 빈 배열이며 필요한 항목만 선택한다
apply명시적으로 true를 보낼 때만 저장. 기본은 검토

응답의 report.rows에는 원본, 분리 결과(en, ko 배열), reasons, errors, fingerprint가 담긴다. 다음 요청의 decisions에는 rowNumber, en, ko, skip을 보내고 행을 승인할 때 해당 fingerprint를 approval에 넣는다. 저장 요청에서도 원본과 DB로 판정을 다시 계산하며 미승인 행이 있으면 쓰기 없이 needsReview: true를 반환한다.

저장 결과는 created, completed(원본 행 번호), failures를 반환한다. 저장이 실패하면 그 행에서 중단하고 이미 저장된 행 번호를 알려준다. 화면은 완료 행을 재등록하지 않는다. 응답 자체를 받지 못한 경우에는 시트에서 결과를 확인한 후 새로 검사해야 한다.

아래 기존 API와 상세 열 형식은 /import의 접힌 상세 열로 가져오기에서 계속 사용할 수 있다.

http
POST /api/v1/import
Content-Type: multipart/form-data
필드필수설명
file✔xlsx 파일. 10MB까지
dryRun보내지 않으면 dry-run이다. 실제 반영은 "false"를 명시해야 한다
force충돌·중복으로 걸린 행 중 그래도 등록할 행 번호를 쉼표로 나열

행은 한 번에 최대 5000개다(파싱 성공 행 + 행 단위 오류 행). 넘으면 400 validation_failed이고 details에 maxRows와 actual이 실린다.

dryRun의 기본값이 dry-run인 이유

dryRun 필드를 잘못 보내거나 빠뜨렸을 때 수백 행이 실수로 들어가는 쪽보다, 아무 일도 일어나지 않는 쪽이 안전하다. dryRun !== "false"가 곧 dry-run이다.

인식하는 헤더 ​

첫 행이 헤더다. 기존 엑셀이 어떤 헤더를 쓰는지 미리 알 수 없어 매핑이 관대하다. 비교 전에 소문자로 바꾸고 공백을 _로 치환하므로 Name EN과 name_en은 같은 열이다.

필드인식하는 헤더
nameEnname_en, english, 영문, 영문명
nameKoname_ko, korean, 한글, 한글명
fullNameEnfull_name_en, 영문 풀네임, 풀네임, 전체명
fullNameKofull_name_ko, 한글 풀네임
domaindomain, 도메인
categorycategory, 분류, 업무 분류
topic (태그 목록)태그, tags, topic, 주제, 세부 주제 — 쉼표·줄바꿈으로 여러 값 입력
statusstatus, 상태
definitionMddefinition, 정의, 설명
canonicalNamescanonical_names, 추가 표준 표기, 추가 표준명
aliasesaliases, 별칭, 약칭
abbreviationsabbreviations, 약어, 약어 표기
discouragedNamesdiscouraged, 비권장 표기, 비권장
forbiddenNamesforbidden, 금지 표기, 금지

domain과 추가 표기 열(canonicalNames, aliases, abbreviations, discouragedNames, forbiddenNames)은 쉼표 또는 셀 안 줄바꿈으로 여러 값을 나눈다. 같은 셀 안에서 똑같은 값을 반복하면 한 번만 가져온다. category에는 관리자 화면에 등록된 업무 분류 key를 쓴다(가져오기 화면에 현재 허용 목록이 표시된다). 약어는 abbreviations 열에 적는다.

용어 편집 화면의 추가 표기는 엑셀에서 역할별 열로 나뉜다. 대표 영문·한글 외의 표준명은 추가 표준 표기, 쓰지 않도록 안내할 말은 비권장 표기 또는 금지 표기에 적는다. 영문·한글 풀네임은 언어별 풀네임 열에 하나씩 적는다.

못 알아본 헤더는 조용히 사라지지 않고 응답의 ignoredHeaders에 원문 그대로 실려 온다. 관대한 매핑 때문에 한 컬럼이 통째로 무시된 것을 모르고 넘어가면 안 된다.

dry-run ​

bash
curl -s -X POST -H "Authorization: Bearer $KEY" \
  -F "file=@glossary.xlsx" \
  http://localhost:3000/api/v1/import
json
{
  "dryRun": true,
  "report": {
    "total": 312,
    "ready": 297,
    "conflicts": [
      { "rowNumber": 41, "name": "AE", "conflictingSlugs": ["ae-audio-engine"] }
    ],
    "duplicatesInFile": [
      { "key": "autoexposure", "rowNumbers": [12, 205] }
    ],
    "errors": [
      { "rowNumber": 88, "message": "영문명과 한글명이 모두 비어 있습니다." }
    ],
    "fileErrors": [],
    "ignoredHeaders": ["담당자", "비고"]
  }
}

용어를 정확히 읽는 법.

  • total = 파싱된 행 + 행 단위 오류 행. 파일 단위 실패는 여기 안 들어간다.
  • ready = "파싱된 행 수"가 아니라 **"충돌도 파일 내 중복도 없어서 그대로 반영 가능한 행 수"**다. 화면의 "N개 실제로 등록하기" 버튼 문구가 이 값을 그대로 쓴다.
  • conflicts = 기존 DB의 용어와 정규화 키가 겹치는 행. 한 행이 기존 용어 여러 개와 겹쳐도 행 하나당 항목 하나다. 그래서 conflicts.length가 곧 충돌 행 수다.
  • duplicatesInFile = 파일 안에서 같은 정규화 키가 여러 행에 나온 경우.
  • errors = 행 단위 실패. rowNumber는 워크시트의 실제 행 번호(1-base)다.
  • fileErrors = "시트를 찾을 수 없습니다", "인식 가능한 헤더가 없습니다"처럼 아직 행이라는 개념이 성립하지 않는 파일 단위 실패. total에서 빠져 있다.

반영 ​

bash
curl -s -X POST -H "Authorization: Bearer $KEY" \
  -F "file=@glossary.xlsx" -F "dryRun=false" -F "force=41,205" \
  http://localhost:3000/api/v1/import
json
{
  "dryRun": false,
  "created": 299,
  "skipped": [
    { "rowNumber": 12, "reason": "duplicate_in_file" },
    { "rowNumber": 77, "reason": "conflict" }
  ],
  "parseErrors": [],
  "fileErrors": [],
  "ignoredHeaders": []
}

skipped[].reason은 conflict 또는 duplicate_in_file이다.

반영은 dry-run 결과를 믿지 않는다

반영 요청은 클라이언트가 넘긴 판정을 신뢰하지 않고 매번 스스로 같은 판정을 다시 계산한다. dry-run을 아예 건너뛰고 바로 반영을 호출해도, 두 요청 사이에 DB 상태가 바뀌어도 안전하다. force에 명시적으로 담긴 행만 예외로 그대로 등록한다 — 동음이의어는 합법이므로 강제 경로는 남겨두되 기본값은 항상 "건너뛴다"다.

force 값이 이상하면(정수가 아니거나 0 이하) 조용히 무시한다. 잘못 파싱된 행 번호가 강제 등록 대상에 끼어드는 쪽보다 안전하다.

크기 제한 ​

10MB 상한은 두 겹으로 검사한다.

  1. Content-Length 헤더를 본문을 읽기 전에 확인한다. formData()는 호출하는 순간 본문 전체를 메모리에 올리므로 그 뒤에 검사하면 이미 늦다. xlsx는 압축률이 높아 20만 행이 10MB 안에 들어간다.
  2. 파싱 후 file.size를 다시 확인한다. 헤더가 없거나 클라이언트가 거짓 값을 보낸 경우의 두 번째 방어선이다.

둘 다 413 payload_too_large다. 표준 Fetch API에 스트리밍 멀티파트 파서가 없어 완전한 조기 차단은 이 플랫폼의 구조적 한계다.

화면에서 하기 ​

/import가 같은 흐름을 감싼다. 파일 업로드 → dry-run 리포트 확인 → 충돌 행 중 "그래도 등록"할 것을 고르고 → 반영.

같은 화면이 위 표와 같은 내용을 설명으로 싣고, /import/template에서 채워 넣을 샘플 xlsx를 내려받게 한다. 열 정의는 src/lib/import/format.ts 하나에서 나오므로 파서·샘플 파일·화면 설명이 따로 놀 수 없다.

사내망 온프레미스 배포를 전제로 만든 Apache-2.0 프로젝트입니다.