발행일

관계도 이웃을 6개씩 펼치기로 했다가 같은 날 폐지했다 — 한 결정이 네 시간 만에 세 번 뒤집힌 기록

관계도 이웃을 6개씩 펼치기로 했다가 같은 날 폐지했다

표준데이터 상세에는 그 자원을 중심으로 한 관계도가 있습니다. 중심 노드 하나에 직접 이웃을 전부 매다는 그림이에요.

메시지 중심이면 그럭저럭 읽힙니다. 문제는 표준을 중심에 뒀을 때였습니다. KS R 1600-3 같은 표준 하나에 이웃이 226개 걸려 있고, 전부 "이 표준이 정의한 데이터"라는 들어오는 관계예요. 첫 화면에서 226개 카드가 한 덩어리로 쏟아지고, 그 안에서 표준·표현·프로파일 같은 다른 관계를 찾아 읽어야 했습니다. 메시지 중심에서도 필드 13개가 고리를 덮어 나머지 관계가 묻혔고요.

검토 의견은 이랬습니다. "표준번호를 누르면 관계된 데이터가 모두 한번에 표출된다. 표준이 다루는 데이터를 1차로 묶고, 2차로 정보 주제, 3차에서 데이터를 보여 주자."

관계 이름만으로는 안 묶였다

첫 초안은 중심과의 관계 이름으로만 묶었습니다. 그런데 표준 중심은 이웃 226개가 전부 들어오는 관계라 그룹이 참조된 자원 226개 하나뿐이었고, 그걸 펴면 226개가 한꺼번에 나와 그룹화 전과 똑같았어요.

그래서 클래스를 더했습니다. 관계 이름 + 클래스가 그룹 키예요.

        //  · 🔄 같은 날 — 묶는 기준에 **클래스**를 더했다(관계 이름 + 클래스). 표준 중심은
        //    이웃 226개가 전부 들어오는 관계라(= `참조된 자원` 하나) 그룹이 하나뿐이었고,
        //    펴면 226개가 한꺼번에 쏟아져 그룹화 전과 같았다.

그룹 노드 id는 <중심 URL>#group=<관계|클래스> 꼴입니다. 인스턴스 URL 정규식에 안 걸리게 해서, 그룹을 인스턴스로 오인해 API를 받으러 가는 길을 막았어요. 우측 패널에서도 그룹은 "이 항목으로 이동"과 "이 노드 중심으로 보기"가 비활성이고 펼치기만 삽니다(isGroup 게이트).

이 과정에서 지운 것이 하나 있습니다. 8월에 만든 mergeNeighbourhood — 제자리 펼치기를 할 때 그 노드의 관계도를 다시 받아 병합하던 함수예요. 이제 펼치는 대상은 그룹뿐이고 그룹의 항목은 중심 응답에 이미 다 있으니 재조회가 필요 없어졌습니다. 8월 24일에 "펼치기는 그 노드의 관계도를 받아서 편다"로 정한 규칙을 2주 만에 이 화면에서 걷어낸 셈입니다. 데이터 맵 카탈로그 탭·위임 관계도는 그 규칙 그대로예요.

6개씩 — 오전에 두 번 뒤집혔다

목업에는 "더블클릭하면 6개씩 펼쳐진다"고 적혀 있었습니다. 그대로 옮겼어요. GROUP_PAGE = 6, 더블클릭마다 다음 6개, 다 나오면 다음 더블클릭이 접힘. 카드 제목이 데이터 엘리먼트 6/104개로 진행을 말합니다.

그런데 이 결정이 오전 안에 두 번 흔들렸습니다. 문서에 그대로 남겨 뒀어요.

① 처음엔 목업의 "더블클릭하면 6개씩 펼쳐진다" 를 그대로 옮겨 `GROUP_PAGE=6` 으로 쪽을 넘겼다.
 `instance/185` 에서 `데이터 엘리먼트 6/104개` 를 본 사용자가 "104개가 떠야 하지 않나" 해서
전부 펴기로 바꿨다.  그렇게 펴 보니 104개 카드가 한 덩어리로 겹쳐 쌓여(캔버스 왼쪽에
부채꼴 선 다발) 힘 배치가 풀리기까지 읽을 수 없었고, 사용자가 다시 "6개씩 나와야 할 듯" 으로
결정했다. **최종은 ①** 이다.

전부 펴기 판은 instance/185에서 8노드가 112노드가 되는 걸 확인하고 되돌렸습니다. 6개씩으로 돌아오면서 감수한 것도 적었어요 — 7개짜리 MessageField 그룹이 6/7개로 열려 하나를 보려면 한 번 더 눌러야 한다는 것.

그리고 6개씩 쪽 넘김의 단점을 메우려고 우측 패널에 항목 전체 목록을 붙였습니다. 목업의 목록은 항목마다 상세 링크뿐이라 그림에 안 나온 7번째를 보려면 다음 6개를 눌러야 했는데, 우리 목록은 이름을 누르면 그 항목이 든 쪽까지 그림을 펴고 그 항목을 고릅니다. 아직 안 그려진 항목은 흐리게.

                if (canPage) {
                    main.type = "button";
                    // 그 항목까지 보이는 최소 쪽 수. 이미 그려져 있으면 고르기만 한다.
                    const need = Math.max(shown, Math.ceil((index + 1) / pageSize) * pageSize);
                    main.addEventListener("click", () => onGroupShown(node, need, member.id));
                }

페이저는 누적입니다. 다음은 6개를 더 붙이고 앞 항목은 안 사라져요. 이전은 마지막 쪽을 걷습니다 — 7개 다 편 상태에서 6을 빼 1개를 남기는 게 아니라 앞 쪽 경계인 6으로 돌아갑니다.

여기까지가 13시 51분 커밋입니다. e2e 관계도 스펙 8개, 29 passed · 3 skipped.

18시 04분 — 쪽 넘김을 통째로 걷어냈다

같은 날 오후에 검토 의견의 둘째 단, 정보 주제가 들어왔습니다. 오전 커밋에서는 이걸 안 세웠어요. dcat:theme 링크가 dev에 37건뿐이고 전부 삭제 예정 계열인 dcat:Dataset이라 2차 단을 세우면 미지정 한 칸에 다 들어갈 거라고 봤거든요. 그 자리에 "결정을 미룬다"는 주석만 남겨 뒀습니다.

사용자가 2차 단을 요구해서 같은 날 세웠습니다. 그리고 주제로 갈라 놓으니 한 카드의 항목 수가 줄었고, 그러자 "6개씩 자르던 거 한번에 보이게"로 결정이 바뀌었어요.

        //  · 펼치기는 **항목 전부 한 번에** — 다시 더블클릭하면 접힌다. 🔄 2026-09-07 세 번
        //    뒤집혔다: 목업의 6개씩 → 전부(104개가 겹쳐 쌓임) → 6개씩 → 주제 카드(아래)로
        //    한 카드의 항목이 줄자 사용자 결정으로 다시 **전부**. 쪽 넘김·`6/104개` 진행 표시·
        //    패널 페이저는 걷어냈다(`groupPageSize` 를 안 넘기면 공용 렌더러가 페이저를 안 그린다).

GROUP_PAGE, shownCount, 카드 제목의 6/104개, 패널 페이저가 다 사라졌습니다. 오전에 만든 memberList·memberPager는 공용 렌더러에 그대로 두고 groupPageSize를 안 넘기는 방식으로 껐어요. 쪽 크기 0이면 전체라 페이저가 안 그려집니다. 다른 화면이 다시 쓸 수 있게 남긴 건데, 지금 쓰는 화면은 없습니다.

세 단은 이렇게 됐습니다.

무엇펼치기
1차관계 + 클래스 그룹 카드 — 데이터 엘리먼트 104개메시지·데이터 프레임·데이터 엘리먼트면 주제 카드가 선다. 그 밖(데이터 집합·필드·표준·카탈로그…)은 바로 항목
2차정보 주제 카드 — AI플랫폼 9개 · 주제 미지정 95개더블클릭 = 그 주제의 항목 전부
3차항목(인스턴스)없음 — 이 노드 중심으로 보기

엘리먼트에는 주제가 없다 — 위에서 물려받는다

주제 슬롯(dcat:theme)은 메시지·데이터 프레임에만 있고 엘리먼트에는 없습니다. 검토 의견의 규칙은 "엘리먼트는 그것을 담은 메시지·프레임의 주제를 따른다"였어요.

이걸 서버 매니저의 theme_names가 맡습니다. 자기 주제, 없으면 직속 상위의 주제, 거기도 없으면 그 위로 깊이 10까지. 상위는 필드 사슬로 찾습니다 — 항목 ←kadif:references─ 필드 ─inMessage/inDataFrame→ 메시지·프레임. 운영 저장 모양에 메시지→프레임 직접 링크는 없어요.

이 사슬은 활용 서비스를 찾을 때 이미 타던 길이었습니다. 데이터 프레임·엘리먼트는 자기를 쓰는 메시지의 서비스를 물려받는데, 그 코드가 get_service_names 안에 박혀 있었어요. _trace_parents로 뽑아내서 둘이 같이 씁니다.

    @staticmethod
    def _trace_parents(seeds, max_depth):
        """아래(자식) → 위(자기를 싣는 메시지·데이터 프레임)로 한 단계씩 거슬러 오른다.

        경로는 `references` ← 필드 → `inMessage`|`inDataFrame` 뿐이다(메시지→프레임 직접
        링크는 저장 모양에 없다). 깊이당 두 번의 질의. 반환 `(edges, seen)` — edges 는
        (자식, 상위) 쌍 목록, seen 은 씨앗을 포함해 만난 pk 전부. 활용 서비스(`get_service_names`)
        와 정보 주제(`theme_names`)가 같은 사슬을 탄다.
        """

주제는 화면 JSON에만 실립니다. RDFFactory._attach_themes가 이웃 노드에 theme를 붙이는데, RDF·PNG 다운로드는 이 길을 안 지나요. 주제 링크 자체는 그리지 않습니다 — 이웃의 다음 관계라 "중심의 직접 연결까지만" 규칙 밖이에요.

한 가지 얼버무린 게 있습니다.

        # 같은 깊이에 주제가 여럿이면 pk 가 작은 상위의 것을 쓴다 — 한 항목은 한
        # 주제 카드에만 서야 해서 하나를 골라야 하는데 우선순위 규칙이 아직 없다.

상위가 여럿이고 주제도 여럿이면 pk가 작은 쪽을 씁니다. 규칙이 아니라 결정을 미룬 자리예요.

검증

  • 브라우저(instance/186 KS R 1600-3): 오전 판은 첫 화면 7카드(중심 + 그룹 6) → 데이터 엘리먼트 더블클릭 6/107개·13노드 → 메시지 더블클릭 시 엘리먼트 접히고 메시지 6/8개 → 다시 8/8개·15노드 → 다시 접혀 7노드.
  • 전부 펴기 판(②)은 instance/185 8노드 → 112노드로 확인한 뒤 되돌렸습니다.
  • 오전 커밋 e2e: 관계도 스펙 8개 29 passed · 3 skipped · 0 failed. 스킵 3은 시드 부족.
  • 오후 커밋: theme_names 단위 테스트(자기 주제 → 두 단 위 물려받기 → 없음, 깊이 상한, 관계도 JSON의 theme) 신규. e2e instance-graph-theme-group 신설 — 1차 → 주제 카드 전부(항목 합 = 1차) → 항목 전부 → 주제 접기(1차 유지) → 1차 접기.
  • dev 실측(instance/185 KS R 1600-2, 이웃 266): 주제 있는 이웃 37 — 데이터 집합 26·엘리먼트 9(물려받음)·프레임 1·메시지 1. 시드에 주제가 거의 없어 dev는 주제 미지정 카드가 큽니다.
  • dev의 kadif 네임스페이스가 비표준(normative=False)이라 표준으로 들어오는 링크가 게이트에 걸려 표준 중심이 카탈로그 하나만 그렸습니다. 검토자가 본 "한번에 다 표출"은 운영 그림이었어요. dev DB 값을 True로 바꿔 운영과 같은 조건에서 확인했습니다.

남은 것 · 한계

  • 오전에 만든 페이저 코드가 죽은 채 남아 있습니다. memberList·memberPager는 공용 렌더러에 그대로 있고 groupPageSize를 넘기는 화면이 없어요. "다른 화면이 다시 쓸 수 있게"라고 적었지만 쓸 화면이 정해진 건 아닙니다. 지웠어야 할 수도 있어요.
  • 전부 펴기가 다시 무너질 조건이 남아 있습니다. 주제로 갈라 카드당 항목이 줄었다는 게 전제인데, dev에서 주제 미지정 95개가 이미 그 전제를 깹니다. 오전에 "104개가 겹쳐 쌓여 읽을 수 없었다"고 기록한 상태가 미지정 카드 하나에서 그대로 재현될 수 있어요.
  • 주제 우선순위 규칙이 없습니다. 상위가 여럿이면 pk가 작은 것. 도메인 규칙이 아니라 정렬 순서예요.
  • 주제 물려받기가 화면 JSON에만 있어 RDF 다운로드로 받은 데이터에서는 엘리먼트에 주제가 없습니다. 화면과 내보내기가 다른 말을 해요.
  • 8월 24일 규칙(펼치기는 그 노드의 관계도를 다시 받는다)이 이 화면에서만 폐기되고 데이터 맵·위임 관계도에는 남았습니다. 같은 더블클릭이 화면마다 다른 일을 합니다.

관련 글: 관계도 11개 화면을 렌더러 한 곳에서 바꿨다 · 노드 두 개가 땅콩처럼 붙었다 · 운영 2,732건이 92,948px 캔버스를 만들었다