- 발행일
날짜 칸을 씌웠더니 옛 값이 지워질 뻔했다 — 용어가 날짜라고 말해 주지 않는 슬롯에 형식을 강제하기
날짜 칸을 씌웠더니 옛 값이 지워질 뻔했다
표준데이터 인스턴스의 구성 속성 중에 생성일자·수정일자·발행일자·표준 폐지일자·바이트 크기가 있습니다. 값이 날짜거나 숫자인 자리인데, 입력칸은 전부 평범한 텍스트칸이었어요. MetaClassInstanceMetadata.value가 TextField고 서버가 형식을 검사하지 않으니 "작년 여름" 같은 값도 그대로 저장됩니다.
지금까지 값이 안 깨져 있던 건 화면이 막아서가 아닙니다. 적재기와 운영 미러링이 채운 값이라서예요. 신청서에서 사람이 직접 쓰는 흐름이 열리면 무너질 자리였습니다. 9월 10일에 이 다섯 칸에 형식을 강제했습니다(bcfb211b).
용어는 이 슬롯이 날짜라고 말해 주지 않는다
가장 먼저 막힌 게 판정이었습니다. 이 시스템의 속성은 온톨로지 용어라서 값 영역(range)을 보면 유형을 알 수 있어야 하는데, 정작 봐야 할 곳이 비어 있었어요.
| 속성 | 값 영역 | 날짜/숫자인가 |
|---|---|---|
dct:created | rdfs:Literal | 날짜지만 range로는 알 수 없음 |
sdo:dateModified | (비어 있음) | 날짜지만 range 자체가 없음 (구성 속성으로 안 쓰여 이번 명단에는 없음) |
dcat:byteSize | rdfs:Literal | 숫자지만 range로는 알 수 없음 |
값 영역을 xsd:date로 고치는 게 정공법입니다. 그쪽이 온톨로지로서는 맞아요. 그런데 그건 용어 마이그레이션과 운영 용어 수정이 같이 따라옵니다. 화면 입력칸 하나 바꾸자고 용어 데이터를 건드릴 수는 없어서, 기존 MULTILINE_TEXT_PROPERTIES 옆에 CURIE 집합을 두는 쪽으로 갔습니다.
DATE_PROPERTIES = {
'dct:created',
'dct:issued',
'dct:modified',
'kadif:retracted',
}
NUMERIC_PROPERTIES = {
'dcat:byteSize',
}
판정 함수는 is_date_property()·is_numeric_property() 한 곳이고, 이미 있던 is_multiline_property()·is_code_block_property()와 같은 모양입니다. 조회를 새로 내지 않고 CURIE 문자열만 봅니다.
명단이라 티가 나는 부채이기도 해요. 그래서 주석에 **"값 영역을 xsd:date로 고치면 이 집합은 없앨 수 있다"**를 적어 뒀습니다. 반대로 값 영역이 진짜 숫자인 속성 15개(time:*·dcat:spatialResolutionInMeters)는 구성 속성으로 안 쓰여서 명단에 넣지 않았습니다. 쓰이면 그때 넣습니다.
날짜칸은 꼴이 다른 값을 조용히 비운다
type="date"만 씌우고 끝낼 수 없는 이유가 있습니다. 브라우저의 네이티브 날짜칸은 YYYY-MM-DD가 아닌 값을 말없이 비워서 보여 줍니다. 그 상태로 저장을 누르면 원래 값이 지워져요. 예컨대 20260509가 든 칸은 빈 칸으로 뜨고, 사용자는 자기가 아무것도 안 지웠다고 생각합니다.
그래서 그리는 쪽이 값을 먼저 확인하고, 꼴이 다르면 텍스트칸으로 남깁니다.
def can_use_date_input(value) -> bool:
"""이 값을 날짜칸에 담아도 값이 안 지워지는지."""
value = (value or "").strip()
return not value or bool(DATE_VALUE_RE.match(value))
빈 값은 날짜칸입니다 — 새로 쓰는 자리야말로 형식을 막아야 할 자리니까요. 꼴이 다른 자리는 테두리를 border-warning으로 바꾸고 title로 "날짜 꼴이 아니라 달력 칸으로 못 바꿉니다. YYYY-MM-DD로 고쳐 주세요"를 답니다. 표시를 안 남기면 평범한 텍스트칸과 구분이 안 돼서, 고쳐야 하는 값인 줄 아무도 모른 채 남습니다.
표시만으로는 부족해서 서버 검증을 붙였다
여기까지 하고 나니 구멍이 하나 남습니다. 달력 칸은 브라우저가 막아 주지만, 꼴이 어긋난 값은 텍스트칸으로 나오므로 브라우저가 안 막습니다. 그 화면에서 저장을 누르면 같은 옛 값이 그대로 다시 들어갑니다.
_DATE_FORMAT_VALIDATOR = RegexValidator(
regex=DATE_VALUE_RE,
message=_("날짜는 YYYY-MM-DD 로 입력해 주세요. (보기: 2026-05-09)"),
)
날짜 슬롯 필드에만 답니다. 빈 값은 Django가 validator를 아예 안 돌리니 선택 슬롯을 비워 두는 기존 동작이 그대로고, 화면을 우회한 직접 POST도 같이 막힙니다.
대신 부작용이 생깁니다. 옛 값이 든 데이터는 날짜를 고치기 전엔 저장이 안 됩니다. 요약 정보만 바꾸려던 사람도 날짜를 함께 고쳐야 해요. 그 값을 고칠 사람이 결국 그 화면을 연 사람뿐이라 여기서 막는 게 맞다고 봤고, 아래 실측대로 지금 걸릴 데이터가 0건이라 그대로 뒀습니다. 걸림돌이 되면 "날짜 칸을 건드렸을 때만 검사"로 좁힐 수 있습니다.
숫자 슬롯(바이트 크기·인덱스)에는 서버 검증을 안 걸었습니다. 값이 정렬에나 쓰이고 틀려도 데이터가 안 깨지거든요. 화면에서만 거릅니다.
자동으로 고쳐 주지 않기로 한 이유
20260509·2026.05.09 같은 값을 읽어서 날짜로 바꿔 주는 건 안 합니다. 값을 대신 바꾸면 다음 저장에서 원래 문자열이 조용히 사라지는데, 그 슬롯은 자유 문자열이라 우리가 뜻을 확정할 근거가 없어요. 20260509가 날짜인지 문서번호인지 서버는 모릅니다.
일괄 변환 명령도 만들지 않았습니다. 건수가 적으면 화면에서 고치는 게 빠르고, 많으면 그때 실제 꼴을 보고 규칙을 정하는 게 맞습니다. 마이그레이션도 없어요. 모델 필드도 값도 안 건드리고, 값을 어떻게 그릴지와 제출값을 받을지만 정합니다.
숫자칸 안내를 title에서 placeholder로
바이트 크기 칸에 글자를 쳤는데 안 들어간다는 제보가 같은 날 있었습니다. 숫자만 받도록 input 이벤트에서 숫자 외 문자를 지우고 있었는데, 안내가 title에만 있었어요. 마우스를 올려야 보이는 안내는, 글자가 안 들어가는 이유를 그 자리에서 알려 주지 못합니다.
input.placeholder = input.title = "숫자만 입력할 수 있습니다.";
type="number"는 여기서도 안 씁니다. 스피너와 휠 스크롤로 값이 조용히 바뀌고, 브라우저마다 빈 문자열 처리가 갈립니다. 예전에 비활성 버튼에 왜 안 되는지를 못 적는 문제를 다뤘는데, 같은 이야기의 반대편이에요. 막는 건 쉽고 막힌 이유를 그 자리에 두는 게 어렵습니다.
그리는 곳이 둘이라 양쪽을 같이 고쳤다
같은 입력칸을 만드는 코드가 두 벌입니다. 신청서 모달은 폼 위젯(MetaClassInstanceForm), 관리자 표는 DOM을 직접 만드는 instanceManage.js의 inputCell()이에요. 후자는 서버가 내려 주는 _component_payload()의 is_date·is_numeric 깃발을 받아 판정합니다.
판정 정규식은 std_data/constants.py의 DATE_VALUE_RE가 정본이고, JS에 같은 식을 한 벌 더 뒀습니다. 주석으로 정본 위치를 적어 뒀지만 같은 정규식이 두 곳에 사는 건 그대로 부채입니다.
검증 — 운영 실측 어긋난 값 0건
셸에 붙을 수 없는 환경이라 관리 화면에 로그인한 브라우저 콘솔로 셌습니다. 화면이 클래스·슬롯 정보를 JSON으로 이미 들고 있어서 날짜 슬롯 키를 거기서 추리고, 클래스마다 목록 엔드포인트를 페이지 넘겨 가며 읽었어요.
⚠️ 이때 한 번 걸렸습니다. instance/list-page/는 limit/offset이 아니라 page를 받고 한 쪽이 20건인데, limit을 넘기면 조용히 무시돼서 20건만 보고 "다 봤다"고 착각합니다. 응답의 num_pages를 보고서야 알았습니다.
| 클래스 | 인스턴스 | 채워진 날짜값 | 어긋남 |
|---|---|---|---|
| kadif:DataFrame | 693 | 683 | 0 |
| kadif:DataElement | 684 | 572 | 0 |
| kadif:Message | 101 | 91 | 0 |
| kadif:Standard | 44 | 93 | 0 |
| kadif:Service | 32 | 0 | 0 |
| dcat:Catalog | 9 | 2 | 0 |
| dcat:CatalogRecord·DataService·Dataset·Distribution | 0 | 0 | 0 |
| 합계 | 1,563 | 1,441 | 0 |
날짜 슬롯을 가진 클래스는 운영에 10개뿐이고, 전체 7,028건 중 그 클래스에 속한 1,563건을 훑어 채워진 날짜값 1,441건이 모두 한 가지 꼴이었습니다. (kadif:Standard는 인스턴스보다 날짜값이 많은데, 한 인스턴스가 날짜 슬롯을 여러 개 가져서예요.)
회귀는 std_data/tests/test_instance_typed_slots.py로 잠갔습니다. 클래스 5개짜리인데, 챙긴 것들:
test_same_name_in_other_namespace_is_not_a_date— 판정이 CURIE라 네임스페이스가 다르면 이름이 같아도 날짜가 아니어야 합니다.test_malformed_value_falls_back_to_text_so_it_is_not_wiped— 이 글의 핵심입니다. 꼴이 다른 값이 텍스트칸으로 나오는지.test_iso_value_and_empty_value_pass— 빈 값이 validator에 안 걸리는지.test_saving_an_instance_that_holds_a_stale_value_is_blocked— 옛 값을 그대로 다시 저장하려 하면 막히는지.test_numeric_slot_gets_the_digit_only_attrs— 안내가placeholder에도 있는지.
남은 것 · 한계
dcat:byteSize는 운영에 값이 0건입니다. 유일한 소유 클래스인dcat:Distribution에 인스턴스가 없어요. 형식을 강제해 놓고 정작 그 값은 아직 하나도 없는 상태입니다.- CURIE 명단이 늘어날 자리입니다.
dcat:startDate·dct:dateSubmitted처럼 날짜인데 아직 구성 속성으로 안 쓰이는 속성이 있어요. 그 속성이 쓰이기 시작하면 누군가 이 집합에 추가해야 하는데, 잊어도 아무 경고가 안 뜹니다. 그냥 텍스트칸으로 조용히 그려질 뿐이에요. - 옛 값이 든 데이터는 다른 칸만 고치려 해도 막힙니다. 지금 0건이라 감수했습니다.
- 정규식이 파이썬과 JS 두 곳에 있습니다. 한쪽만 고치면 그리는 판정과 받는 판정이 갈라집니다.
- 꼴이 어긋난 칸의 경고도
title에만 있습니다. 숫자칸에서는 "마우스를 올려야 보이는 안내"라며placeholder로 옮겼는데, 날짜칸 경고는 같은 방식으로 남겼습니다. 테두리 색이 먼저 눈에 띄긴 하지만 문구는 hover 전용이에요. - 구분자가 다른 옛 값을 고쳐 주지 않는 원칙은
docs/ui-conventions.md와docs/instance-management-implementation.md양쪽에 적어 뒀습니다. 문서에 없으면 다음 사람이 "친절하게" 자동 변환을 넣을 자리라서요.
관련 글: 속성 range, 클래스 참조와 데이터 유형 분리 · 표준데이터, 고정컬럼에서 인스턴스 기반 모델로 전환 · disabled를 쓰면 왜 안 되는지를 말할 수 없다