수출입 신고서를 쓰고, 관세청에 직접 보낸다
관세사 사무실용 통관업무 시스템입니다. 신고서 작성·검증부터 공동인증서 전자서명, 국가관세종합정보망(유니패스) 전자문서 송수신, 통보서 수신과 신고필증 출력까지 한 곳에서 합니다.
관세청이 배포하는 KCSAPI4.dll 은 Windows 전용이라 서버에서 쓸 수 없습니다. 「전자문서 표준연계 지침서」의 규격대로 ebMS 2.0 을 직접 구현했습니다.
- 6종
- 주고받는 신고 서식 수출입 신고 · 정정 · 취하
- 11종
- 해석하는 관세청 통보서 접수 · 수리 · 각하 · 오류
- 941건
- 내장 코드 테이블 29개 그룹
- 1,343건
- 자동 검사 294개 묶음
무엇을 하는 프로그램인가
관세사 사무실이 하루에 하는 일 — 화주가 보낸 서류를 받고, 신고서를 쓰고, 관세청에 보내고, 결과를 받아 필증을 뽑는 일 — 을 한 곳에서 합니다. 왼쪽 메뉴의 「업무」에 매일 여는 다섯 화면이 있고, 나머지는 기준정보와 설정입니다.
| 관세사 | 공급자 (설치·유지보수) | |
|---|---|---|
| 하는 일 | 신고서 작성·전송, 통보서 확인, 받은 메일에서 신고서 만들기 | 관세청 서식 개정 반영, 설치 |
| 빈도 | 매일 | 서식 개정 때만 |
| 보는 화면 | 신고 · 통보서함 · 메일함 · 기준정보 · 유니패스/인증서 설정 | + 연계 진단 · 코드 테이블 · 전자문서 규격 · 전문 미리보기 |
신고서를 쓴다
수출신고서와 수입신고서. 공통 항목과 란을 탭으로 나눠 한 번에 하나만 그립니다. 저장하면 세액 재계산과 검증이 함께 돕니다.
관세청에 보낸다
ebMS 2.0 전자문서로 직접 보냅니다. 보내기 전에 실제로 나갈 전문을 그대로 보여주고, 전자서명을 스스로 검증한 뒤에야 회선에 올립니다.
결과를 받는다
접수·수리·각하·오류 통보서를 받아 신고서 상태를 갱신합니다. 서식 11종마다 요소가 놓인 자리를 못박아 두었습니다.
필증을 뽑는다
브라우저 인쇄로 A4 신고필증을 뽑습니다. 수리 전에는 「신고서」로 표기하고 배경에 「수리 전 · 신고필증 아님」을 찍습니다.
고칠 것이 생기면
수리된 신고서에서 정정·취하 신청서를 새로 만듭니다. 원본은 건드리지 않고, 무엇이 어떻게 달라졌는지 대비표가 자동으로 나옵니다.
세번을 찾는다
품명·재질·용도로 HSK 10단위 후보를 찾고, 고른 세번의 근거를 품목분류의견서로 접습니다. 한 자리를 잘못 치면 세율이 달라지고, 세율이 세액을 정합니다.
성격이 다른 두 사람이 있고 화면이 갈라져 있습니다. 전자문서 규격이나 매핑 상태는 관세사가 조치할 수 없는 정보라 기본으로 감춥니다. 오류 문구도 대상에 따라 다릅니다 — 인증서 만료처럼 관세사가 고칠 수 있는 것은 그대로 알려 주고 그 화면으로 보내지만, 서식·문서코드·서명 규격은 「공급업체에 문의하세요」로 바꿉니다.
메일이 오고, 필증이 나오기까지
여덟 걸음입니다. 각 걸음이 다음 걸음에 무엇을 넘기는지가 이 프로그램의 뼈대입니다.
- 1메일이 온다
화주가 보낸 인보이스·B/L 이 IMAP 으로 들어옵니다. 붙은 서류의 종류를 받을 때 미리 알아봅니다.
- 2서류를 읽는다
첨부에서 신고서 칸을 읽어 제안을 만듭니다. 값과 함께 서류 원문과 쪽 번호를 들고 옵니다.
- 3신고서를 쓴다
제안은 사람이 채택합니다. 손대지 않은 제안이 남아 있으면 전송 전 점검에서 걸립니다.
- 4검증하고 세액을 낸다
저장할 때마다 재계산과 검증이 함께 돕니다. 검증 패널의 오류를 누르면 그 칸으로 뜁니다.
- 5전문을 미리 본다
실제로 나갈 XML·SOAP·HTTP 본문 전체를 먼저 봅니다. 아무것도 전송하지 않습니다.
- 6서명해 보낸다
브라우저에 열어 둔 공동인증서로 신고서와 SOAP 을 각각 서명하고, 스스로 검증한 뒤 보냅니다.
- 7통보서를 받는다
접수·수리·각하·오류 통보서를 받아 신고서 상태를 갱신합니다.
- 8필증을 뽑는다
수리되면 신고필증을 인쇄합니다. 고칠 것이 생기면 정정신청서가 여기서 갈라져 나옵니다.
칸이 많은 서식을, 헤매지 않고 채운다
수입신고서는 공통 항목만 68칸, 수출신고서는 64칸이고 그 아래에 란이 붙습니다. 다 그려 놓으면 다섯 번째 란을 고치려고 공통을 통째로 지나쳐야 합니다.
공통과 란을 탭으로 나눈다
「공통 │ 란 1 │ 란 2 … │ 란 추가」 탭으로 한 번에 하나만 그립니다. 접이식으로 두었더니 접힌 머리글도 자리를 차지해, 란이 일곱이면 화면 절반이 머리글이었습니다.
안 보이는 칸의 오류로도 뛴다
지금 「공통」을 보는 중에 란 3의 오류를 누르면 뛰어갈 칸이 아직 그려지지도 않았습니다. 탭을 먼저 바꾸고 그려진 뒤에 뜁니다. 오류가 남은 탭에는 점을 찍습니다.
칸마다 관세청 기재요령이 붙는다
관세청 항목정의서의 「항목설명」을 그 칸에서 그대로 봅니다 — 수입 81칸, 수출 90칸에 붙어 있습니다. 한 자리를 여럿이 나눠 쓰는 칸은 일부러 비웁니다. 틀린 기재요령은 없는 것보다 나쁘기 때문입니다.
원문 서류는 옆 칸에 붙는다
이 화면이 하라는 일은 원문 대조인데, 서류를 겹쳐 띄우면 대조할 상대인 신고서가 가려집니다. 옆 칸에 붙여 나란히 봅니다. 서류가 여럿이면 파일 탭이 붙습니다.
칸이 전자문서 어디로 가는지 남는다
기재요령 표에 오른 칸은 전자문서의 어느 요소(X-PATH)로 나가는지가 화면 요소에 함께 남습니다 — 수입 85칸, 수출 90칸. 관세청이 어느 항목을 문제 삼았을 때 그 칸을 찾는 데 걸리는 시간이 이걸로 갈립니다.
매핑이 맞는지 기계가 본다
신고서 칸과 전자문서 요소의 대응이 267자리이고, 서식 6종의 매핑이 모두 관세청 항목정의서와 대조를 마친 상태입니다. 필수(M) 항목 중 못 채우는 것은 여섯 서식 모두 없습니다. 당사자 주소가 어긋나 있던 것도 이 대조에서 잡혔습니다.
관세청과 무엇을, 어떤 규격으로 주고받나
전자문서 송수신은 사용자 S/W 방식을 직접 구현했습니다. 구간마다 어느 규격을 따랐고 어느 파일이 그 일을 하는지 적어 둡니다.
| 구간 | 규격 | 구현 |
|---|---|---|
| 전송 프로토콜 | HTTP/1.1 POST · TLS 1.2 | kcs/ebms/client.ts |
| 메시지 | ebXML Message Service 2.0 (SOAP + MIME multipart/related) | kcs/ebms/soap.ts · mime.ts |
| 전자서명 | XMLDSIG · C14N 1.0 · SHA-256 · RSAwithSHA256 | kcs/crypto/xmldsig.ts |
| 접속 인증 | 인증서 난수 R 을 관세청 공개키로 암호화한 Authorization 헤더 | kcs/ebms/client.ts |
| 인증서 관리 | 개인키는 브라우저에만. 서명할 때마다 브라우저에 물어본다 | kcs/crypto/npkiBrowser.ts |
| 암호화 | 사용자 S/W 방식은 TLS 채널암호화로 대체 (지침서 2.2) | — |
payload-1 신고서
payload-1 Document Meta XML payload-2 신고서 payload-3 첨부파일 (base64) payload-4 …
보내기 전에 전문을 통째로 본다
신고서 XML(서명 전·후), SOAP 봉투(서명 전·후), Document Meta XML, HTTP 본문 전체를 각각 복사·내려받을 수 있습니다. 전송과 같은 함수로 만들므로 요소 이름과 순서·문서코드·크기·첨부 다이제스트가 회선에 나가는 것과 같습니다.
미리보기는 언제나 임시 키로 서명한다
개인키가 서버에 없으므로 미리보기는 실제 인증서로 서명하지 못합니다. 실제 전문과 다른 곳은 서명값과 인증서 정보뿐이고, 화면에 그 사실이 적힙니다. 전송 경로는 이 키를 절대 쓰지 않습니다.
전자서명을 스스로 검증한다
관세청은 서명이 틀리면 C420(SOAP)·C421(신고서)만 돌려주고 이유를 알려주지 않습니다. 그래서 보내기 전에 같은 절차를 밟아 봅니다 — 정규화한 SignedInfo 가 인증서 공개키로 검증되는지, 참조의 다이제스트가 실제 내용의 해시와 같은지. 실패하면 전송하지 않습니다.
- eb:Action = KCSMetaAction
첨부는 신고서와 한 전문에 실린다
서류제출 대상으로 지정되면 B/L·Invoice 를 따로 올리는 것이 아니라 같이 보냅니다. Document Meta XML 이 첫 자리를 차지하므로 신고서가 두 번째로 밀립니다.
다이제스트는 원본 바이트에 대고 뜬다
첨부는 MIME 에서 base64 로 실려 나가지만 서명은 디코딩된 원본 바이트에 대해 뜹니다. Content-Transfer-Encoding 은 전송 수단일 뿐 데이터가 아니기 때문입니다.
응답코드를 뜻으로 옮긴다
관세청이 돌려주는 코드 22종을 뜻과 함께 보여주고, 관세사가 조치할 수 있는 것과 없는 것을 갈라 문구를 바꿉니다. 인증서 만료·문서함 상태는 그 화면으로 보내고, 서식·문서코드·서명 규격은 「공급업체에 문의하세요」로 바꿉니다.
크기는 미리 막는다
전문은 40MB 까지이고 첨부는 base64 로 약 1.34배 부풉니다. 다 만들고 나서 「크기 초과」라고 하면 어느 파일을 빼야 할지 알 수 없으므로, 원본 바이트 기준으로 남은 몫을 계속 화면에 보여줍니다.
파일이 바뀌면 멈춘다
첨부는 서버 파일시스템에 두고 DB 에는 경로와 보관 시점의 다이제스트만 남깁니다. 읽을 때 다시 해시해서 다르면 전송하지 않습니다 — 파일이 바뀐 채로 나가면 서명이 맞지 않는데, 그때 원인을 찾기는 매우 어렵습니다.
공동인증서는 서버에 두지 않는다
공동인증서 개인키는 사무실의 법적 신분입니다. 그것으로 관세청에 신고서를 냅니다. 그래서 서버에 두지 않습니다 — 관세사 컴퓨터의 브라우저에만 있습니다.
- 인증서 파일과 비밀번호
- 꺼낼 수 없는 서명 키 (extractable: false)
- 서명 능력 — 신고서와 SOAP 을 각각 서명한다
- 인증서의 공개값
- 난수 R
- 보낼 전문 — 서명은 만들지 못한다
개인키 바이트는 아무 데도 적히지 않는다
브라우저에서 복호화한 PKCS#8 은 WebCrypto 로 들여오는 데 한 번 쓰고 그 자리에서 0 으로 덮습니다. 남는 것은 꺼낼 수 없는(extractable: false) 서명 능력뿐입니다.
비밀번호도 서버에 오지 않는다
인증서 파일을 브라우저에서 열고 거기서 복호화합니다. 서버가 받는 것은 인증서의 공개값과 난수 R 뿐입니다.
한 시간 손을 놓으면 잠긴다
마지막 사용·조작에서 60분. 이 탭에서 키를 누르거나 서명하는 동안 계속 미뤄지고, 시한이 지나면 저절로 닫히고 브라우저 저장소에서 지워집니다. 자리를 비울 때 손으로 닫는 길도 있습니다.
서명이 두 번, 순서가 강제된다
신고서 XML 을 먼저 서명하고, 그 결과를 담은 SOAP 봉투를 다시 서명합니다. 순서를 바꾸면 안쪽 서명이 바깥 서명의 대상에서 빠집니다.
그래서 무인 전송은 안 된다
서버는 서명을 만들 수 없습니다. 사람이 인증서를 연 브라우저가 있어야 나갑니다. 야간 자동 전송을 붙이려면 개인키를 서버에 두어야 하고, 그 편의와 이 선을 맞바꾸지 않았습니다.
실물 인증서로 확인했다
오랫동안 시험용으로 생성한 인증서로만 검증돼 있었습니다. 실제 발급기관에서 나온 법인 인증서로 복호화·서명·검증 경로를 확인했습니다.
그래도 남는 위험은 적어 둡니다 — 인증서가 열려 있는 동안 이 원본에서 도는 스크립트는 이 키로 서명을 시킬 수 있습니다. 가져가지는 못합니다. 브라우저에서 서명하는 이상 이 선이 한계이고, 열려 있는 시간을 짧게 두는 것이 위 시한의 몫입니다.
서류에서 읽고, 서류끼리 대조한다
올려 둔 인보이스·B/L·팩킹리스트에서 신고서 칸을 읽어 제안을 만듭니다. 제안은 값이 아니라 서류 원문과 쪽 번호를 들고 다닙니다 — 관세사가 보는 것은 숫자가 아니라 원문입니다.
- KCS_EXTRACT_ENABLED
기본은 꺼짐이다
모델을 부르는 기능은 셋입니다 — 서류 읽기 · 반려 진단 · 정정 사유 초안. 셋 다 각자의 환경변수로 켜고 전부 기본이 꺼짐입니다. 인보이스에는 화주의 거래처와 단가가 들어 있는데 그건 사무실 것이 아니라 화주의 영업 정보라, 켜는 것을 사무실이 판단할 일로 두었습니다.
서류 파일이 나가는 길은 하나다
셋 중 서류를 그대로 실어 보내는 것은 서류 읽기뿐입니다. 반려 진단과 정정 사유 초안도 모델을 부르지만 글자만 보내고 파일은 싣지 않습니다. 나갔다는 사실은 감사 기록에 남습니다.
두 번째 건부터는 부르지 않는다
사람이 확인을 끝내면 어느 라벨 옆 어느 자리에서 값이 나왔는지를 규칙으로 굳힙니다. 값이 아니라 자리만 남으므로 지난 건의 금액·상호는 남지 않습니다 — 규칙은 사무실 것이지만 값은 화주 것입니다.
서류끼리 대조한다
인보이스·B/L·팩킹리스트가 같은 것을 말하는 칸 — 총중량과 총포장개수 — 을 견줍니다. 모델을 다시 부르지 않습니다. 값은 이미 숫자로 읽혀 있고 남은 것은 산수인데, 여기에 모델을 끼우면 왜 어긋났다고 보는지 설명하지 못하게 됩니다.
무엇이 맞는지는 고르지 않는다
인보이스가 맞을 때도 팩킹리스트가 맞을 때도 있고, 둘 다 맞는데 신고할 값이 따로인 경우도 있습니다. 어긋났다는 것과 각 서류의 원문까지만 내놓습니다.
한 서류 안에서도 검산한다
품목 줄의 금액을 다 더하면 결제금액이 되고, 포장 줄의 개수를 다 더하면 총포장개수가 됩니다. 줄 합계가 총계보다 크면 짚고 작으면 조용합니다 — CIF 는 아래에 운임을 더하고 총중량에는 팔레트가 얹히므로 작은 쪽은 설명이 됩니다. 작은 쪽까지 짚으면 정상인 서류마다 경고가 뜨고, 그러면 아무도 안 봅니다.
어느 길로 갔는지 센다
지난 30일 동안 모델이 읽은 건과 저장해 둔 자리로 읽은 건을 나누어 세고 앞 30일과 견줍니다. 자리가 깨져도 아무 오류가 나지 않기 때문입니다 — 그냥 매번 모델이 읽고, 매번 서류가 밖으로 나가고, 매번 돈이 듭니다. 이 숫자 말고는 알 방법이 없습니다.
읽는 길이 둘이고 성질이 다릅니다. 저장해 둔 자리로 읽을 때는 서류가 나가지 않고, 처음 보는 양식이거나 양식이 바뀌었거나 글자 없는 스캔본일 때만 모델로 갑니다. PDF 에서 글자와 좌표를 꺼내는 일은 사무실 안에서 끝납니다.
서류는 메일로 온다
인보이스·B/L·포장명세서는 거의 전부 메일로 도착합니다. 사무실 대표 메일을 IMAP 으로 읽어 오고, 붙어 온 서류로 신고서를 바로 만듭니다.
아웃룩을 대신하려는 것이 아니다
폴더·스레드·검색·규칙을 여기서 다시 만들면 더 나쁜 아웃룩이 될 뿐입니다. 이길 수 있는 자리는 하나 — 그 메일의 첨부가 신고서가 되는 순간입니다.
붙은 서류의 종류를 미리 알아본다
하루에 오는 메일 중 신고서가 될 것은 몇 통뿐이고, 인보이스가 붙었는지가 그 판단의 거의 전부입니다. 받을 때 판별해 목록에 적어 둡니다. 모델을 부르지 않는 판별이라 받아 두는 데 돈이 들지 않습니다.
읽기만 한다
폴더를 readOnly 로 엽니다. 우리가 받아 갔다고 메일 서버의 읽음 표시를 건드리면 아웃룩에서 아직 안 본 메일이 이미 읽은 것으로 보입니다. 저쪽 함은 사람의 것이고 우리는 들여다볼 뿐입니다.
보내지도, 지우지도 않는다
메일함 코드에 IMAP 쓰기 명령이 한 줄도 없습니다. 첫 판에 빠뜨린 것이 아니라 넣지 않기로 한 것입니다 — 자유 작문은 아웃룩이 낫고, 보내기는 SMTP 가 아니라 도달(SPF·DKIM·반송)이 어려운 일이라 붙이려면 그것부터 답해야 합니다.
한 통이 실패해도 나머지는 들인다
메일 한 통을 저장하다 던지면 그 판이 통째로 멎고 다음 판도 같은 통에서 다시 멎습니다. 새 메일이 영영 안 들어오는데 오류는 한 줄뿐입니다. 건너뛰되 몇 통을 건너뛰었는지 말합니다.
끌어다 놓으면 붙는다
아웃룩에서 첨부를 그대로 끌어오면 붙습니다. 메일 한 통(.eml)을 통째로 놓으면 그 안의 첨부만 꺼내 붙습니다 — 메일 자체는 붙이지 않습니다.
남이 보낸 HTML 을 그대로 그리지 않습니다. 메일 계정 비밀번호와 본문은 암호화해 둡니다.
통보서는 결과를 아는 유일한 통로다
접수됐는지, 수리됐는지, 각하됐는지, 적재기한이 언제인지 전부 여기서 옵니다. 서식마다 요소 구성이 다르고 같은 이름의 요소가 서식마다 다른 뜻을 가지므로, 이름으로 문서를 뒤지지 않고 서식별로 경로를 못박습니다.
| 문서코드 | 서식 | 판정 |
|---|---|---|
| GOVCBR5AF | 수출통관 접수통보 | 접수 |
| GOVCBR5AA | 수출신고수리 통보서 | 수리 |
| GOVCBRRR5 | 수출신고서 처리결과 통보서 (검사변경) | 처리결과 |
| GOVCBR5DT | 수출신고 정정·취하 신청결과 통보서 | 처리결과 |
| GOVCBRR99 | 수입통관 접수통보 | 접수 |
| GOVCBR023 | 수입신고서 처리결과 통보서 | 처리결과 |
| GOVCBRR21 | 수입신고 각하 통보서 | 각하 |
| GOVCBR5FK | 수입(납세)신고 정정 처리결과 통보서 | 처리결과 |
| GOVCBR5BG | 수입신고 취하신청 처리결과 통보서 | 처리결과 |
| GOVCBRR20 | 사전검증 오류통보 | 오류 |
| GOVCBRINF | 시스템 오류통보 | 오류 |
지어낸 이름은 없다
서식 11종에 걸쳐 항목 119개를 정의해 두었고, 그중 110개는 항목정의서의 항목번호를 달고 있습니다. 테스트가 그 번호로 X-PATH 와 항목명을 찾아 원문과 대조합니다 — 깨지면 코드가 아니라 항목정의서를 다시 보라는 뜻입니다. 항목정의서를 구한 것은 11종 중 10종이고, 구하지 못한 하나는 시스템 오류통보입니다.
근거가 있는 것만 상태를 바꾼다
문서코드만으로 결론이 정해지는 통보서와, 결과코드의 뜻을 항목정의서에서 확인한 통보서. 둘 중 하나에 걸릴 때만 신고서 상태를 옮깁니다. 수리취소가 오면 접수로 되돌리고 수리일시를 지웁니다.
코드값은 뜻과 함께 보여준다
코드만 덩그러니 두면 관세사가 항목정의서를 찾아야 합니다. 뜻을 옆에 답니다.
- KCS_NOTICE_POLL_MINUTES
받는 것은 서버가 아니라 화면이다
목록 요청에도 SOAP 전자서명이 필요한데 개인키는 관세사 브라우저에만 있습니다. 그래서 통보서함 화면이 열려 있고 인증서가 열려 있을 때, 정해 둔 주기로 받습니다. 기본은 꺼짐이고, 무엇이 갖춰져야 도는지 화면에 그대로 적습니다.
켜진 것처럼 보이는데 못 받는 상태가 제일 나쁘다
예전에는 서버가 타이머를 걸었습니다. 그런데 서버는 서명을 만들지 못하므로 한 건도 받은 적이 없었습니다 — 「자동 수신 켜짐」을 찍어 놓고 주기마다 「서명이 필요합니다」만 쌓였습니다. 그 타이머를 걷어냈습니다.
모르는 서식이 오면
삼키지 않고 남깁니다. 해석하지 못한 것과 온 적이 없는 것은 다른 말입니다.
열 자리를 손으로 치지 않는다
세번부호는 손으로 열 자리를 쳐 넣던 자리였습니다. 틀리면 세율이 달라지고 세율이 세액을 정하니, 한 자리 잘못 치면 세액이 통째로 틀어집니다. 품명으로 후보를 찾아 고르게 합니다.
| 하는 일 | 무엇을 푸는가 | 비용 |
|---|---|---|
| 검색 | 품명·재질·용도로 후보 40건 | 없음 (로컬 색인) |
| 세번 상세 | 형제 세번 · 세율 두 갈래 · 수입요건 | 없음 (유니패스 조회만 붙는다) |
| 근거 판정 | 통칙 근거 · 버릴 이유 · 가정 · 결정례 | 건당 실측 $0.20~0.36 |
| 첨부 읽기 | 사진·카탈로그에서 물품 정보 초안 | 판정과 같은 상한 |
| 품목분류의견서 | 판정을 Word·텍스트 문서로 접는다 | 없음 (조립이라 모델을 안 태운다) |
검색은 마지막 수단이다
검색하기 전에 이 사무실이 써 온 세번을 먼저 보여줍니다. 세번을 찾는 일의 상당수는 전에 쓰던 것을 다시 쓰는 일인데, 그 답이 이미 우리 DB 에 있는데 매번 관세율표부터 뒤지게 하면 값이 드는 쪽으로 사람을 미는 셈이 됩니다.
첨부는 판정 앞에 선다
사진·카탈로그에서 읽은 초안을 사람이 손볼 자리를 판정 앞에 둡니다. 잘못 읽은 값이 이미 결론에 반영된 뒤에 나오면 고칠 수가 없습니다. 쓰는 사람은 세번이 맞는지는 몰라도 자기 물건이 스테인리스인지는 압니다.
이미 적어 둔 칸은 덮어쓰지 않는다
사용자가 적은 것을 지우는 것이 이 기능이 낼 수 있는 가장 나쁜 실패입니다.
판정 하나를 문서로 접는다
품목분류의견서를 Word(.docx)·텍스트로 내려받습니다. 문장은 이미 판정이 써 놓은 것이라 값이 들지 않고, 파일은 브라우저 안에서 만듭니다. 「관세사의 검토 없이 대외 제출용으로 쓰지 마십시오」가 문서 안에 박힙니다.
고르는 것은 사람이다
후보와 근거를 세워 놓을 뿐, 프로그램이 세번을 정하지 않습니다.
밖으로 열지 않는다
찾는 일은 한국어 형태소 분석기 위에 서 있어 파이썬 서비스로 따로 뜹니다. 운영에서는 포트를 게시하지 않고, 앱만 같은 네트워크에서 부르며 로그인 검사는 앱이 합니다.
세액과 필증, 그리고 고칠 때
세액 계산은 관세법·국고금관리법의 계산 순서와 절사 규칙을 따릅니다. 세율 자체는 품목마다 다르므로 엔진이 판단하지 않고, 화면에서 넣거나 유니패스 관세율 조회로 채웁니다.
계산 순서가 정해져 있다
관세 → (개별소비세 · 교통세 · 주세) → (교육세 · 농특세) → 부가가치세. 과세표준은 원 미만 절사, 세목별 납부세액은 10원 미만 절사입니다.
감면은 뒤 세목까지 따라간다
감면된 세액은 부과되지 않으므로 뒤따르는 세목의 과세표준에도 들어가지 않습니다.
총 과세가격은 1원까지 맞춘다
란별 신고가격 비중으로 안분하며, 1원 단위 잔액까지 맞춥니다.
수리 전 출력물은 필증이 아니다
수리 전에는 「신고서」로 표기하고 배경에 「수리 전 · 신고필증 아님」을 찍습니다. 수리되지 않은 출력물이 필증처럼 유통되면 안 되기 때문입니다. 브라우저 인쇄로 뽑고, 인쇄 창에서 「PDF로 저장」을 고르면 파일이 됩니다.
정정은 원본을 건드리지 않는다
관세청에 이미 접수된 문서이고 정정은 별개의 신청서이기 때문입니다. 원본을 복사한 새 신청서가 만들어지고, 신고서·정정신청서·취하신청서는 서로 다른 서식이라 문서코드도 각각 다릅니다.
자율정정이 안 되는 항목이 있다
수출 정정은 원칙적으로 신고인이 스스로 하지만, 고시가 못박은 14가지는 세관 심사를 거칩니다. 무엇을 고쳤느냐에 따라 필요한 서류도 달라서 12개 항목에 표준증빙 목록을 붙여 둡니다. 둘 다 수출통관 고시 기준이고, 수입 정정은 별도 고시를 따릅니다.
무엇이 달라졌는지 표로 나온다
원본과 정정 본문을 항목 단위로 비교해 「1란 세번부호: 8532240000 → 8541410000」처럼 사람이 읽는 이름으로 보여줍니다. 세율을 고치면 세액이 얼마나 늘고 줄었는지 따로 뽑아, 추가 납부 여부를 바로 판단할 수 있습니다.
엔진이 다시 계산하는 중간값(과세표준·산출세액·안분된 과세가격)은 대비표에서 뺍니다. 사람이 고친 것이 아니라 따라 바뀐 값이라 중복이기 때문입니다.
코드 테이블은 관세청 정본에서 왔다
세관·과, 거래구분, 결제방법 같은 코드값의 정본은 관세법령정보포털(CLIP)의 「통계부호표」입니다. 통관 사무처리 고시는 코드값을 물을 때마다 「통계부호표 참조」로 그쪽에 넘깁니다.
| 조회 창구 | 호스트 | 쓰는 곳 |
|---|---|---|
| 게이트웨이 | apis.data.go.kr/1220000 | 관세환율정보 · 세관장확인대상물품 |
| 직접호출 | unipass…:38010/ext/rest | 화물통관진행 · 수출이행내역 |
- npm run db:seed-codes
941건 · 29개 그룹이 내장돼 있다
받아 둔 원본을 저장소에 남기고 그것으로 시드를 만듭니다. 코드 테이블만 채우는 명령이 따로 있고, 그것이 운영에서 돌려도 되는 유일한 시드입니다.
막다른 길을 다섯 번 걸었다
유니패스 Open API 에 공통코드조회가 없고, 공공데이터포털에 세관부호 데이터셋이 없고, 항목정의서는 「등등」으로 끊겨 있고, 서식자료실은 코드표가 아니고, 고시는 통계부호표로 넘깁니다. 여섯 번째에 찾았습니다. 다시 걷지 않도록 그 다섯 곳을 적어 두었습니다.
표 이름을 모를 때 찍지 않는다
과(課) 부호를 찾느라 이름을 42가지로 지어 넣는 동안 전부 0건이었고, 정답은 그 규칙 밖에 있었습니다. 목록을 먼저 받아 옵니다.
부분 목록은 고정 선택 상자로 두지 않는다
수입 거래구분은 100번대까지 있는데 가진 것이 몇 개뿐이면, 선택 상자로 둘 때 목록에 없는 코드로는 신고를 아예 못 합니다. 고르거나 직접 칠 수 있는 칸으로 냅니다.
자동으로 동기화하지 않는다
공개 조회 화면을 프로그램으로 부르는 것이라 서비스 약관을 확인하지 않았습니다. 한 번 받아 원본으로 남기고, 개정되면 사람이 판단해 다시 돌립니다.
부호 옆에 이름을 단다
보세구역 부호는 여덟 자리 숫자라 그것만 보아서는 어느 창고인지 알 수 없습니다. 관세청 자료에서 받은 이름표 3,422건을 옆에 답니다.
서류에 없는 값은 관세청에서 받는다
화물관리번호·입항일·반입일 같은 칸은 인보이스나 B/L 을 아무리 읽어도 나오지 않습니다. 관세청이 적하목록을 받으면서 붙이는 값이라 서류에 적힐 자리가 없습니다. 조회한 값도 신고서에 바로 쓰지 않고 지금 값과 나란히 놓아 사람이 채택합니다.
신고서 전송은 ebMS 로 하고 환율·화물진행 같은 조회는 별도의 REST API 를 씁니다. 관세청이 직접호출 방식을 폐기하고 게이트웨이로 옮기면서 창구가 갈렸고, 인증키가 서로 다릅니다 — 바꿔 넣으면 인증 오류가 납니다. 그래서 파일도 나눠 두었습니다.
누가 무엇을 할 수 있고, 무엇이 남는가
이 시스템은 관세청에 전자문서를 실제로 내보냅니다. 상호와 통관고유부호가 신고인으로 그대로 실리므로, 계정이 생기는 세 갈래 어디로도 승인 없이 업무 화면이 열리지 않습니다.
- requireUser로그인만승인 대기 화면 · 계정 화면
- requireApprovedUser+ 사무실이 승인됨사무실 사용자 관리
- requireReadyUser+ 비밀번호를 바꿨음업무 화면 전부
| 무엇 | 어떻게 저장하나 | 왜 |
|---|---|---|
| 비밀번호 | scrypt(N=32768) 해시 + 매번 다른 소금 | 평문은 어디에도 남기지 않는다 |
| 세션 | 쿠키에 토큰 원본, DB 에는 SHA-256 만 | DB 가 새어도 남의 세션을 쓸 수 없어야 한다 |
| 당사자 식별번호 | AES-256-GCM | 식별번호구분부호가 01 이면 주민등록번호가 들어온다 — 사업자등록번호와 같은 칸이라 저장 시점에는 가릴 수 없다 |
| 관세청에 보낸 전문 | AES-256-GCM | 거기엔 식별번호가 평문으로 들어 있다 — 그대로 두면 본문을 암호화한 의미가 거기서 샌다 |
확인은 두 겹이다
프록시는 쿠키가 있는지만 보는 낙관적 검사입니다. 가짜 쿠키 한 줄이면 통과합니다. 실제로 막는 것은 데이터에 가장 가까운 데이터 접근 계층이고, 화면과 서버 액션은 전부 거기를 거칩니다.
무차별 대입은 DB 로 센다
계정별로 8회/15분에 5분 잠금. 세는 자리를 프로세스 메모리에 두면 서버가 여러 대일 때 잠금이 대수만큼 갈려 「8회」가 대수 × 8회가 됩니다 — 늘리는 이유가 부하를 견디려는 것이니 하필 두드림이 많을 때 가장 헐거워집니다.
접속지를 모를 때 모두를 한 통에 묶지 않는다
그렇게 하면 아무나 8번 실패시켜 사무실 전체를 로그인 불가로 만들 수 있습니다. 대신 비밀번호 검증의 동시 실행 수를 묶습니다 — 잠그는 대신 줄을 세우므로 정상 사용자는 막히지 않습니다.
어느 쪽이 틀렸는지 알려주지 않는다
없는 계정도 해시를 한 번 계산해 응답 시간을 맞춥니다. 비밀번호를 바꾸면 다른 기기가 모두 로그아웃되고, 로그인·비밀번호 변경 시 세션을 새로 발급합니다.
고치거나 지우는 경로를 두지 않았다
전송(성공·실패)·정정·취하·삭제·거래처 변경·로그인(실패 포함) 등 21가지 행위가 감사 로그에 남습니다. 고칠 수 있는 기록은 근거가 되지 못합니다. 감사 로그는 암호화하지 않으므로 거기에 개인정보를 담지 않습니다.
세션은 두 가지로 끝난다
받은 지 12시간이 지나면 끝나고, 손을 놓은 지 2시간이 지나도 끝납니다. 백업은 24시간마다 돌고 최근 14벌을 남깁니다 — 둘 다 설치할 때 바꿀 수 있습니다.
복구는 되돌릴 수 없다
그래서 네 겹으로 막습니다 — 백업본이 실제로 열리는지 먼저 보고, 줄어드는 표가 있으면 알리고, 사람이 「복구」라고 직접 쳐야 진행하고, 덮기 전에 지금 것을 옮겨 둡니다.
운영에 넣기 전에 볼 것들은 「운영 환경 점검」 화면에 모여 있습니다 — 실행 모드, 세션 쿠키, 프록시 신뢰 홉, 남은 평문, 백업 경과, 초기 비밀번호, 문서함 연결, 조회 API 키 등. 빠져도 프로그램은 멀쩡히 도는 것들이라 따로 봅니다. 어긋난 항목에는 고치는 법을 함께 냅니다.
아직 없는 것
확인하지 못한 것을 확인한 것처럼 적지 않았습니다. 운영에 넣기 전에 남아 있는 것들입니다.
- 첨부 다이제스트 규약 — 이진 첨부를 원본 바이트로 해시하는 것이 맞는지 관세청 응답으로 확정해야 합니다. 송수신 테스트에서 C420·C421 이 나면 여기를 가장 먼저 의심해야 합니다.
- 신고필증 서식을 관세청 고시 별지와 대조해야 합니다. 지금 칸 배치는 통상 배열을 따랐습니다.
- 5DT 통보구분 — 항목정의서의 설명이 「통보구분코드를 나타냄」 한 줄뿐이라 코드표가 없습니다. 수출 정정·취하의 승인 여부를 자동으로 판정하지 못하고, 받은 내용은 다 보여줍니다.
- GOVCBRINF 의 항목정의서·XSD 가 서식자료실에 없습니다. R20 은 항목정의서를 구했으나 X-PATH 칸이 비어 있어 요소 위치를 단정할 수 없습니다.
- 교통세 세종부호는 서식마다 다릅니다. 수입신고서(929)는 실제 전송이 정상처리돼 확정했고, 정정신청서(5FE) 쪽은 아직 보내 보지 못했습니다.
- 이메일 인증이 없습니다 — 메일 기능이 수신 전용이라 인증 링크를 보낼 곳이 없습니다. 대신 승인 단계에서 사람이 봅니다.
- 화물통관진행정보는 서비스·오퍼레이션 이름만 확인했습니다. 실제 화물번호가 없어 본문 필드는 미확인입니다.
관세청 송수신 테스트를 통과해야 운영 문서함이 열립니다. 어느 단계까지 실제로 지나가 봤는지는 공급자용 「연계 진단」 화면이 실제로 오간 전문에서 세어 보여줍니다 — 지나갔다고 적어 두는 것이 아닙니다.
설치
Next.js 16 · React 19 · Prisma 7 · PostgreSQL 17 위에 돕니다. 데이터 모델은 22개이고, 컨테이너 이미지로 배포하며 세번 찾기만 파이썬 서비스로 따로 뜹니다.
npm install cp .env.example .env npm run gen-key # 나온 값을 .env 의 KCS_ENCRYPTION_KEY 에 넣는다 docker compose up -d # PostgreSQL npx prisma migrate deploy --schema=./prisma/schema.prisma npm run db:seed-codes # 코드 테이블 npm run dev
- /setup
첫 계정은 첫 설치 화면에서
사무실과 관리자를 거기서 만듭니다. 계정이 하나라도 생기면 그 화면은 다시 열리지 않습니다. 여기서 만들어진 사람이 이 서비스의 운영자가 됩니다.
- /signup
그 뒤로는 가입 신청
다른 사무실은 가입을 신청하고 운영자가 열어 줍니다. 만들어진 사무실은 승인 전까지 아무것도 못 합니다. 거절되면 대기 화면에서 값을 고쳐 다시 신청합니다.
- KCS_ENCRYPTION_KEY
암호화 키를 비워 두면 멈춘다
식별번호는 늘 암호화해 저장하므로, 키가 없으면 시드와 거래처 저장이 그 자리에서 멈춥니다. 설치할 때 만들어 넣습니다.
운영에서 돌려도 되는 시드는 하나다
코드 테이블을 채우는 것만 운영에서 돌려도 됩니다. 이름이 짧은 쪽(npm run db:seed)은 개발용 가짜 사무소와 가짜 거래처·품목까지 심어, 운영 서버에서 돌리면 가짜 통관고유부호가 그대로 신고서에 실립니다.
코드 테이블은 배포한다고 갱신되지 않습니다 — 코드는 DB 에서 읽으므로, 서식이 개정돼 다시 배포한 뒤에는 코드 시드를 한 번 돌려야 합니다. 안 돌려도 오류는 나지 않습니다.