Skip to content

portal-foundation Ant Design 전환 인수인계서 ​

1. 문서 목적 ​

이 문서는 hohyun94/portal-foundation 공통 UI 영역의 Ant Design 전환 작업을 다른 담당자에게 인수인계하기 위한 문서다. 현재까지의 의사결정, 구현 범위, public API 보존 기준, 브랜치와 커밋, 테스트 결과, 알려진 warning, 잔여 작업, 재개 절차를 하나의 기준으로 정리한다.

인수인계 기준점은 feat/antd-phase-2-components 브랜치다. 이후 작업자는 이 브랜치를 기준으로 상태를 확인하고, 변경 전후에 동일한 contract test와 typecheck를 실행해야 한다.

2. 프로젝트와 작업 범위 ​

항목내용
저장소hohyun94/portal-foundation
작업 브랜치feat/antd-phase-2-components
기본 브랜치main
모노레포pnpm workspace
주요 패키지packages/ui-react
관련 패키지packages/design-tokens, packages/theme-client, packages/directory-sync
UI 라이브러리Ant Design 6.x
아이콘@ant-design/icons
ReactReact 18 이상
테스트Vitest, React static render 기반 contract test
타입 검사TypeScript
기준 언어한국어 중심, ko/en locale 지원

이 작업의 기본 원칙은 Ant Design을 전면적으로 사용하되, Ant Design에 없는 포털 도메인 정책은 별도 adapter와 composite로 유지하는 것이다. 권한, 테마 정책, 메뉴 구조, directory adapter, callback payload는 UI primitive와 분리한다.

3. 핵심 보존 원칙 ​

3.1 Public API ​

기존 소비 포털이 사용하는 root export, props, callback 이름은 변경하지 않는다. 내부 구현을 파일로 분리하더라도 packages/ui-react/src/index.tsx에서 compatibility re-export를 제공해야 한다.

3.2 Callback payload ​

Controlled component는 내부 persistence를 소유하지 않는다. 변경이 발생하면 기존 callback에 새 배열 또는 새 정책 객체를 전달한다. callback에는 UI event object를 직접 전달하지 않는다.

3.3 도메인 정책 ​

Ant Design은 표현과 상호작용을 담당한다. 다음 정책은 portal-foundation 자체 코드가 계속 소유한다.

  • 메뉴 visibility와 direct view access
  • scope, role, group 기반 권한
  • systemReserved 메뉴 보호
  • theme catalog 상태와 selectable theme 정책
  • directory adapter 및 mutation capability
  • locale fallback
  • 메뉴 sort order와 audit history

3.4 CSS와 token ​

기존 pf-* class contract와 design token 이름은 유지한다. Ant Design component token은 PortalConfigProvider를 통해 적용한다. 전역 CSS는 wrapper layout, spacing, responsive behavior, legacy contract를 담당하고 Ant Design primitive의 세부 스타일을 중복 구현하지 않는다.

팝업 레이어(PortalDialog)의 형태와 규칙은 foundation이 소유한다. 컨테이너·헤더·제목·본문·닫기 버튼은 Ant Design 시맨틱 슬롯(classNames)에 pf-dialog__container/header/title/content/close를 부여한다. 제목 바 배경은 Ant Design component token(Modal.headerBg = var(--pf-accent-soft), antdTheme.ts)으로 지정해 메뉴 상단바와 같은 accent 계열 토큰을 따르고, styles.css가 제목 바(하단 구분선·--pf-type-display-size 20px 제목·12px 설명)와 본문 인셋(20/24px)을 정의한다. 제목·설명은 제목 바(header/title 슬롯) 안에 두며, 포털 앱은 팝업 CSS를 다시 오버라이드하지 않는다.

3.5 접근성 ​

다음 의미 계약을 유지한다.

  • dialog는 accessible name과 aria-modal을 가진다.
  • validation error는 role="alert" 또는 적절한 live region을 사용한다.
  • 상태 안내는 role="status" 또는 aria-live를 사용한다.
  • Form control은 visible label 또는 accessible label을 가진다.
  • keyboard focus와 disabled 상태를 보존한다.
  • semantic button과 a는 의미가 명확하면 유지할 수 있다.

4. 완료된 Phase 요약 ​

Phase완료 범위주요 결과
Phase 0Ant Design 도입 기준선dependency, 초기 설치, baseline 확인
Phase 1provider와 공통 primitivePortalConfigProvider, theme mapping, Button/Badge/Empty/Breadcrumbs 전환
Phase 2.0–2.1AuthCardCard, Button, Alert, Typography 기반 구현, legacy class와 semantic contract 유지
Phase 2.2ThemePickertheme-client와 연결된 Select, Segmented, Switch 전환
Phase 2.3BrandMark/icon facadeAnt Design icon facade와 안정적인 portal icon key 연결
Phase 3shell, admin, directory, menu adapterLayout, navigation adapter, DirectoryManagement, admin shell 전환
Phase 4data/content/assetsdata shell 상태, Dialog, Tree, field/tabs/table primitive, attachment list, query adapter 전환
Phase 5 진행menu management, explore dialog상세 Form, Quick Link, validation, search Modal, result/answer/citation 전환

5. Phase 5에서 완료된 구현 ​

5.1 menuManagement.tsx ​

파일: packages/ui-react/src/menuManagement.tsx

전환된 영역은 다음과 같다.

  • 메뉴 생성 입력
  • 메뉴명 KO/EN
  • 메뉴 유형
  • 상위 그룹
  • 경로
  • visibility
  • icon picker wrapper
  • 새 창 열기
  • hero 및 badge field
  • view/edit access field
  • 공개 시작/종료
  • 상세 영역 Collapse
  • validation Alert
  • Quick Link List
  • Quick Link label, URL, icon, enabled, delete action
  • audit history List

사용하는 Ant Design primitive는 Form, Form.Item, Input, Input.TextArea, Select, Switch, Collapse, Alert, List, Button, Empty, Space, Typography, Tag다.

추가된 순수 adapter는 다음과 같다.

ts
applyPortalMenuItemPatch(items, id, patch, updatedAt?)
applyPortalQuickLinkPatch(items, id, patch, updatedAt?)

adapter는 대상 item만 새 객체로 만들고, 나머지 항목의 identity와 payload를 보존해야 한다. updatedAt은 callback payload에 포함된다.

보존한 정책은 다음과 같다.

  • systemReserved 메뉴는 비활성화·삭제할 수 없다.
  • 메뉴 reorder 후 sortOrder는 10 단위로 다시 계산된다.
  • Quick Link callback은 기존 onQuickLinksChange를 사용한다.
  • scope/role/group 입력은 현재 comma-separated 자유 입력이다.
  • option 목록이 없는 포털에서 자유 문자열 권한이 손실되지 않는다.

5.2 PortalExploreDialog ​

파일: packages/ui-react/src/PortalExploreDialog.tsx

root export는 index.tsx에서 compatibility를 유지한다.

tsx
export { PortalExploreDialog } from "./PortalExploreDialog.js";

Ant Design primitive는 Modal, Input.Search, List, Button, Card, Empty, Spin, Tag, Typography다.

보존한 public props는 다음과 같다.

ts
export type PortalExploreProps = {
  open: boolean;
  query: string;
  results: PortalExploreResult[];
  onOpenChange: (open: boolean) => void;
  onQueryChange: (value: string) => void;
  onSelect?: (result: PortalExploreResult) => void;
  onAsk?: (question: string) => void;
  answer?: PortalExploreAnswer;
  answerLoading?: boolean;
  placeholder?: string;
  loading?: boolean;
  title?: string;
  locale?: PortalLocale;
};

지원 동작은 다음과 같다.

  • open/onOpenChange controlled modal
  • ESC 및 cancel을 통한 close
  • 검색어 입력과 Enter callback
  • allowClear 검색어 초기화
  • locale별 placeholder와 title
  • 결과를 kind별 그룹화
  • 결과 선택 후 close
  • onSelect가 없을 때 href navigation fallback
  • loading, empty, answer, citation 표시
  • answer loading 중 ask action disabled
  • aria-live="polite" 검색 결과 영역
  • SSR static render를 고려한 getContainer={false}

5.3 문서화 ​

작성·반영된 주요 문서는 다음과 같다.

문서목적
docs/ANT_DESIGN_ADOPTION_PLAN.md전체 Ant Design 도입 마스터 계획
docs/AUTHCARD_API_AUDIT.mdAuthCard public API와 style contract audit
docs/PHASE_3_REMAINING_COMPONENT_PLAN.mdPhase 3 잔여 컴포넌트 계획
docs/PHASE_3_TEST_AND_ACCESSIBILITY_CHECKLIST.mdPhase 3 테스트·접근성 checklist
docs/PHASE_4_REFACTORING_PLAN.mdPhase 4 데이터·콘텐츠 계획
docs/PHASE_5_REFACTORING_PLAN.mdPhase 5 최초 계획
docs/PHASE_5_NEXT_REFACTORING_PLAN.mdPhase 5 다음 작업 계획
docs/PHASE_5_COMPLETE_REFACTORING_PLAN.mdPhase 5 전체 잔여 범위와 완료 기준

6. 브랜치와 최근 원격 커밋 ​

현재 확인된 원격 브랜치는 feat/antd-phase-2-components다. GitHub upload 방식으로 파일이 반영되어 각 업로드가 별도 verified commit으로 생성되었다.

Commit의미
9530cf4Phase 5 전체 리팩토링 계획 문서
f5a6ac6PortalExploreDialog Ant Design 구현과 entrypoint/test 변경
dbcba36Phase 5 다음 작업 계획 문서
d4cdd15Phase 4 이후 소스 반영 커밋
6fcf01amenuManagement 상세 field·Quick Link·validation 반영
a9e378c이전 Phase 5 menuManagement 구현 반영
9bd4314Phase 4 attachment/integration 반영
150993e이전 Phase 4 소스 반영

커밋 메시지는 대부분 GitHub upload 기본 메시지인 Add files via upload다. 실제 변경 의미는 파일과 위 표의 작업 맥락으로 확인한다.

main에는 이전 단계의 PR #6이 병합된 상태로 정리되어 있었다. 현재 Phase 2–5 작업 브랜치의 PR 생성·merge 상태는 인수인계 후 GitHub Pull Requests 화면에서 다시 확인해야 한다. 이 세션의 CLI gh 인증은 401을 반환했으므로 CLI 결과를 최종 근거로 사용하지 않는다.

7. 테스트와 검증 기준 ​

마지막으로 확인된 packages/ui-react 기준 결과는 다음과 같다.

text
Test files: 9 passed
Tests: 38 passed
Typecheck: passed
Build: passed

테스트 파일은 다음과 같다.

text
dataShells.test.tsx
 directoryManagement.test.tsx
 index.test.tsx
 menuManagement.test.tsx
 phase3.contract.test.tsx
 phase4.contract.test.tsx
 phase4.integration.test.tsx
 portalComponents.test.tsx
 portalShell.test.tsx

주요 테스트 내용은 다음과 같다.

  • AuthCard Ant Design primitive와 ARIA contract
  • ThemePicker theme-client state contract
  • menu seed, navigation, permission, validation
  • icon facade와 stable icon key
  • menuManagement controlled patch와 accessibility
  • Quick Link patch payload
  • data shell loading/empty/error/success
  • Dialog/Tree/field/tabs/table adapter
  • asset attachment empty/action
  • PortalExploreDialog Modal/Search/List/answer/citation contract

권장 재검증 명령은 다음과 같다.

bash
pnpm --filter @hohyun94/portal-foundation test
pnpm --filter @hohyun94/portal-foundation check
pnpm --filter @hohyun94/portal-foundation... build
pnpm -r test
pnpm -r check
pnpm -r build

작업 디렉터리가 로컬 clone이 아닌 경우 먼저 clone한다.

bash
gh repo clone hohyun94/portal-foundation /tmp/portal-foundation-handover
cd /tmp/portal-foundation-handover
git checkout feat/antd-phase-2-components
pnpm install

8. 알려진 warning과 해석 ​

Ant Design List deprecation ​

Ant Design 6에서 일부 테스트가 다음 warning을 출력한다.

text
The `List` component is deprecated ... please use `Listy` instead.

현재 기능 실패는 아니며 Phase 5 P2의 명시적 의사결정 대상으로 남겨 두었다. Listy의 실제 타입과 renderItem, dataSource, rowKey 호환성을 확인한 뒤 교체하거나, 현재 버전을 유지하면서 migration note를 남긴다.

SSR Modal portal warning ​

Static render 테스트에서 다음 warning이 발생할 수 있다.

text
Portal only work in client side. Please call 'useEffect' to show Portal instead default render in SSR.

PortalExploreDialog에는 getContainer={false}를 적용했지만, Phase 4 PortalDialog 및 다른 Modal 사용처에 warning이 남을 수 있다. client-like test container 또는 SSR 전략을 별도 결정한다.

9. 남은 작업의 우선순위 ​

P0: PortalThemePolicySettings 상태 계약 강화 ​

현재 파일은 Ant Design Card, Radio, Checkbox.Group, Select, Button을 사용한다. 그러나 local state 기반 저장 흐름에 다음이 필요하다.

  • Form/Form.Item ownership
  • approved/published theme validation
  • default theme과 selectable theme 일관성 validation
  • server error를 field error로 변환하는 adapter
  • saving 중 중복 submit 차단
  • 실패 시 입력값 보존
  • reset/cancel과 dirty state
  • aria-describedby와 error association

권장 adapter:

ts
mapPortalThemePolicyErrors(
  errors: Record<string, string | string[]>
): Record<string, string[]>

기존 onSave(policy) payload는 변경하지 않는다.

P1: menu access adapter ​

scope, role, group ID 선택지가 실제로 제공되는 경우에만 Select mode="multiple"을 사용한다. 선택지가 비어 있으면 기존 자유 입력을 유지한다.

ts
portalAccessRuleToSelectValues(rule, field)
selectValuesToPortalAccessRule(rule, field, values)

검증해야 할 edge case는 자기참조 parent, reserved menu 삭제, reorder sort order, validation focus, callback immutability다.

P1: ExploreDialog 상호작용 검증 ​

구현은 완료되었지만 browser-level interaction test가 필요하다.

  • query typing
  • Enter search
  • clear
  • result keyboard selection
  • ask loading disabled
  • citation keyboard activation
  • focus return
  • close callback
  • href fallback navigation

P1: admin composite 정리 ​

DirectorySyncAdminPanel, AdminSection, AdminFeedback, AdminEmptyState, OperationalMetricGrid의 status semantics와 async action 처리를 표준화한다. public props는 유지한다.

P1: root export 분리 ​

index.tsx를 다음 feature 영역으로 분리할 수 있다.

text
src/auth/AuthCard.tsx
src/branding/BrandMark.tsx
src/explore/PortalExploreDialog.tsx
src/admin/DirectorySyncAdminPanel.tsx
src/admin/AdminSection.tsx
src/theme/ThemePicker.tsx
src/navigation/menuManagement.tsx
src/index.tsx

분리 후 dist/index.d.ts, root export 이름, legacy class, props type을 비교한다.

P2: List/Listy, Modal, CSS ​

Ant Design version strategy를 정하고, styles.css에서 primitive 중복 규칙을 제거한다. pf-* wrapper, layout, responsive, token variable은 보존한다.

10. Phase 5 완료 기준 ​

Phase 5는 아래 조건을 모두 충족할 때 종료한다.

  • menuManagement 주요 input/list/validation surface가 Ant Design 기반이다.
  • PortalExploreDialog가 Ant Design Modal/Search/List 기반이다.
  • PortalThemePolicySettings가 명시적인 Form validation과 save error 계약을 갖는다.
  • admin feedback status semantics가 통일된다.
  • 기존 public props, exports, callbacks, permissions가 유지된다.
  • loading, empty, error, success, disabled, submitting 상태가 표준화된다.
  • dialog, form, live region, focus, keyboard interaction이 검증된다.
  • provider token과 legacy CSS 책임이 분리된다.
  • List/Listy와 SSR Modal warning 처리 방침이 문서화된다.
  • workspace build/check/test가 통과한다.
  • 대표 소비 포털 smoke test가 통과한다.

11. 다음 담당자 작업 절차 ​

첫 번째 작업 ​

  1. 저장소를 clone한다.
  2. feat/antd-phase-2-components를 checkout한다.
  3. pnpm install을 실행한다.
  4. ui-react test, check, build를 실행해 baseline을 기록한다.
  5. docs/PHASE_5_COMPLETE_REFACTORING_PLAN.md와 이 문서를 읽는다.

두 번째 작업 ​

  1. PortalThemePolicySettings.tsx의 public props와 theme-client type을 확인한다.
  2. 기존 사용처를 rg -n "PortalThemePolicySettings|onSave" packages docs로 조사한다.
  3. error adapter의 순수 함수 테스트를 먼저 작성한다.
  4. Form 전환을 적용한다.
  5. callback payload equality와 accessibility 테스트를 추가한다.
  6. test/check/build를 통과시킨다.

세 번째 작업 ​

  1. menu access adapter를 설계한다.
  2. option이 없는 상태의 자유 입력 fallback을 보존한다.
  3. reserved/parent/reorder edge case를 테스트한다.
  4. 실제 소비 포털에서 callback payload를 확인한다.

작업 종료 전 ​

  1. 원격 branch에 파일이 실제로 반영되었는지 확인한다.
  2. commit SHA와 변경 파일을 기록한다.
  3. 테스트 결과와 남은 warning을 기록한다.
  4. 다음 담당자가 즉시 실행할 명령을 문서에 남긴다.
  5. PR 및 merge 상태를 GitHub에서 최종 확인한다.

12. 파일 위치 빠른 참조 ​

목적경로
root exports와 legacy compositepackages/ui-react/src/index.tsx
Explore dialogpackages/ui-react/src/PortalExploreDialog.tsx
Theme policypackages/ui-react/src/PortalThemePolicySettings.tsx
Menu managementpackages/ui-react/src/menuManagement.tsx
Menu testspackages/ui-react/src/menuManagement.test.tsx
Shared root testspackages/ui-react/src/index.test.tsx
Shellpackages/ui-react/src/PortalShell.tsx
Admin shellpackages/ui-react/src/PortalAdminShell.tsx
Directorypackages/ui-react/src/DirectoryManagement.tsx
Data shellspackages/ui-react/src/dataShells.tsx
Content primitivepackages/ui-react/src/contentComponents.tsx
Attachmentpackages/ui-react/src/assetAttachments.tsx
Query adapterpackages/ui-react/src/dataAdapters.ts
Ant Design providerpackages/ui-react/src/providers/PortalConfigProvider.tsx
Provider exportspackages/ui-react/src/providers/index.ts
Design tokenspackages/design-tokens/src
Theme clientpackages/theme-client/src
UI stylespackages/ui-react/styles/styles.css

References ​

12. 공통 알림함(PortalNotificationCenter) ​

추가 버전: @hohyun94/portal-operations 0.2.0, @hohyun94/portal-foundation 0.6.153

12.1 구성 ​

계층제공물책임
계약·도메인 (@hohyun94/portal-operations)PortalNotification(readAt·targetType·targetId 포함), NotificationRepository, NotificationChannel, PortalNotificationInboxStore, dispatchNotification/dispatchNotificationTo(중복 차단), portalNotificationRecipientIds·matchesPortalNotificationRecipient·visibleNotifications·countUnreadNotifications, createInMemoryNotificationStore수신자 판정·중복 차단·읽음 상태 규칙
표현 (@hohyun94/portal-foundation)PortalNotificationCenter, DEFAULT_PORTAL_NOTIFICATION_LABELS, PortalShell의 notificationCenter 슬롯(상단바 end 다음, 사용자 프로필 앞)표현·접근성만 담당(상태는 소비 포털이 소유)

12.2 수신자(사용자 정보) 규칙 ​

알림 수신자 식별자와 소비 포털의 로그인 사용자 정보는 같은 규칙을 쓴다: 이메일 → loginId → openId → 이름 → id(문자열). portalNotificationRecipientIds(user)가 후보를 만들고 matchesPortalNotificationRecipient(recipientId, user)가 대소문자·공백을 무시하고 대조한다. 저장소 조회는 후보 전체를 inArray 조건으로 넘겨 한 번에 조회한다.

12.3 소비 포털 사용 예 ​

tsx
// 1) 저장소 구현(포털 DB) — findByDedupeKey · save · list · countUnread · markRead
const store: PortalNotificationInboxStore = createMyStore(db);

// 2) 발송(중복 차단 포함)
await dispatchNotificationTo(store, inAppChannel, {
  portalId: "process-center",
  recipientIds: accountableUserIds,
  kind: "process.review_requested",
  title: "L6-000001 검토 요청",
  body: "“운영 관리” 항목의 게시 검토가 요청되었습니다.",
  priority: "HIGH",
  dedupeKey: `process:19:review_requested:${submittedAt.toISOString()}`,
  targetType: "process",
  targetId: String(nodeId),
});

// 3) 화면 — 셸의 사용자 정보 영역에 배치
<PortalShell
  /* ... */
  notificationCenter={
    <PortalNotificationCenter
      notifications={notifications}
      pending={pending}
      unreadCount={unreadCount}
      locale={locale}
      onSelect={item => item.targetType === "process" && selectProcess(Number(item.targetId))}
      onMarkRead={id => markRead([id])}
      onMarkAllRead={() => markRead(undefined, { all: true })}
    />
  }
/>

12.4 소비 포털이 구현해야 하는 것 ​

  • PortalNotificationInboxStore: findByDedupeKey · save · list · countUnread · markRead (알림 테이블 스키마는 포털 소유)
  • 조회·읽음 처리 시 항상 수신자 후보(portalNotificationRecipientIds(user))를 조건에 넣어 다른 사용자의 알림을 건드리지 않는다.
  • 메일 등 추가 채널은 NotificationChannel 구현을 하나 더 넘겨 처리한다(저장·중복 차단 로직은 그대로 재사용).

12.5 검증 ​

  • packages/operations-core/src/notifications.test.ts: 수신자 식별자 생성·대조, 최신순 필터, 다중 수신자 중복 차단, 메모리 저장소 읽음 처리, 업무 대상 보존
  • packages/ui-react/src/notifications.contract.test.tsx: 배지 합산(처리 대기 + 미읽음), 목록·선택 콜백, 읽음 처리·모두 읽음, 영문 라벨·빈 상태

13. AI 게시본 차이 분석(@hohyun94/portal-ai) ​

추가 버전: @hohyun94/portal-ai 0.1.0, @hohyun94/portal-foundation 0.6.154

개정: @hohyun94/portal-ai 0.2.0 — 용어 추출·컨텐츠 링크 계약(§13.5) 추가.

개정: @hohyun94/portal-ai 0.3.0 — LLM 용어 후보 추출(suggestTermCandidates) 추가(§13.5).

13.1 구성 ​

계층제공물책임
계약·로직 (@hohyun94/portal-ai)PortalAiProvider·PortalAiMessage·PortalAiCompletion, createOpenAiCompatibleProvider, createDisabledAiProvider, completeWithRetry, completeStructured, extractJsonPayload, createAiResponseCache, summarizeContentChange, stripMarkup/prepareContentForAnalysis/buildContentFieldDiff, PortalAiError모델 호출·JSON 검증·재시도·캐시·요약 규칙
컨텐츠↔용어 (@hohyun94/portal-ai 0.2.0)ContentKeyParts, buildContentKey/parseContentKey/isContentKey, GlossaryTermLite·approvedGlossaryTerms, extractTermsByDictionary, TermMatch·toContentTermLinks·ContentTermLink컨텐츠 참조 키 형식(portalId:entityType:entityId)과 사전 매칭 규칙(긴 표기 우선·ASCII 단어 경계·겹침 방지). LLM 없이 결정적으로 찾는다
표현 (@hohyun94/portal-foundation)PortalAiSummaryCard요약·불릿·영향 영역·모델/시각·상태(진행/실패/비활성)·다시 분석

13.2 엔드포인트·보안 원칙 ​

  • OpenAI 호환 POST {baseUrl}/v1/chat/completions 하나로 사내 LLM(Ollama·vLLM)과 외부 클라우드(OpenAI·Azure·게이트웨이)를 함께 지원한다. apiKey가 없으면 Authorization 헤더를 보내지 않는다(사내 무키 환경 호환).
  • 키·프롬프트·호출은 서버에서만 수행하고 브라우저에 노출하지 않는다.
  • 온프렘 배포(.env.onprem.example)는 외부 API가 차단될 수 있으므로 provider 설정을 배포 프로필별로 분리하고, 미설정 시 createDisabledAiProvider 폴백으로 기능만 조용히 비활성화한다(게시 절차는 계속 동작해야 한다).
  • 실패는 PortalAiError.retryable로 구분해 5xx·429·타임아웃만 재시도한다.

13.3 소비 포털 사용 예 ​

ts
const provider = createOpenAiCompatibleProvider({ baseUrl: process.env.PRIVATE_LLM_URL ?? "", model: process.env.PRIVATE_LLM_MODEL ?? "local-model", timeoutMs: 20_000 });
const summary = await summarizeContentChange(provider, {
  before: prepareContentForAnalysis(beforeText),
  after: prepareContentForAnalysis(afterText),
  label: "L6-000001 운영 관리",
  cache,
});
// → { summary, bullets[], changedAreas[], riskLevel, model, generatedAt, cached }

AI 요약은 참고용이며 확정(게시·승인)은 사람이 담당한다. 모델·생성 시각·입력 해시를 함께 저장해 재현·감사가 가능해야 한다.

13.4 검증 ​

  • packages/ai-core/src/provider.test.ts: 요청 본문·헤더(키 유무)·재시도 구분(5xx/429 vs 4xx)·빈 응답·미설정 오류
  • packages/ai-core/src/analysis.test.ts: JSON 추출·검증 재시도·요약 정규화·캐시 재사용·마크업 정리·길이 제한·필드 diff
  • packages/ui-react/src/aiSummary.contract.test.tsx: 요약/불릿/영향/모델 표시, 상태별 문구, 다시 분석·영문 라벨

13.5 컨텐츠↔용어 매칭(0.2.0) ​

포털이 컨텐츠 본문에서 승인된 공통 용어를 찾아 PortalConsole 원장에 연결할 때 쓰는 순수 규칙이다.

  • buildContentKey({ portalId, entityType, entityId }) — contentKey 는 portalId:entityType:entityId 이며 온톨로지 참조 노드(process-center:process_node:5678)와 같은 형식이다. parseContentKey/isContentKey 로 검증한다.
  • extractTermsByDictionary(text, terms) — 표기·별칭·약어를 긴 것부터 매칭하고, ASCII 영숫자 경계를 지키며(API 가 RAPID 안에서 걸리지 않는다), 이미 매칭된 구간은 다시 세지 않는다. 마크업은 stripMarkup 으로 걷어낸다.
  • approvedGlossaryTerms(terms) — 매칭 대상은 승인(approved)·활성 용어다. 상태 미지정은 승인으로 본다(간이 사전 호환).
  • toContentTermLinks(contentKey, matches) — 원장에 보낼 링크 모양(detectedBy·surfaces·occurrences).
  • suggestTermCandidates(provider, { text, knownSurfaces, label, maxCandidates }) — 사전 매칭이 못 찾은 낯선 표기를 모델에게 묻는다(0.3.0). completeStructured 로 JSON 을 강제하고, 응답은 normalizeTermCandidates 가 규칙으로 좁힌다(글자 없는 표기·문장부호 양끝·길이 밖·아는 표기 제거, 중복 제거, 상한). 결과는 후보이지 지식이 아니다 — 호출부가 원장에 proposed 로 올리고 사람이 승인한 뒤에야 사전에 들어간다. 포털은 ModelGateway 호출에 x-purpose: term-extraction 을 붙여 목적별 모델 라우팅을 쓴다.
  • 검증: packages/ai-core/src/terms.test.ts — contentKey 왕복, 승인 필터, 표기·별칭·약어 매칭, ASCII 경계, 마크업·겹침, 링크 변환, 후보 정규화(형식 흔들림·필터·상한), 빈 본문에서 모델 미호출.

원장 쪽 계약·구현은 PortalConsole docs/central-directory-integration.md(개정 — 컨텐츠↔용어 연결)와 server/contentTermLinks.ts, 소비 포털의 비동기 파이프라인은 ProcessCenter server/termLinkSync.ts(term_extraction_jobs 큐)를 참고한다.

14. 통합 탐색(PortalExploreSearch · ExploreGateway 클라이언트) ​

추가 위치

  • @hohyun94/portal-operations 0.3.0 — createExploreGatewayClient(ExploreGatewayError·ExploreGatewayResult·ExploreGatewaySearchResponse)
  • @hohyun94/portal-foundation 0.6.155 — PortalExploreSearch 컴포넌트(라벨 ko/en, testId, onSelect/onOpen)

사용 방법

ts
import { createExploreGatewayClient } from "@hohyun94/portal-operations";
import { PortalExploreSearch } from "@hohyun94/portal-foundation";

const client = createExploreGatewayClient({ baseUrl: "http://explore-gateway:8080", getAccessToken: () => sessionToken });
<PortalExploreSearch onSearch={query => client.search({ query, limit: 20 })} />

동작·계약

  • 게이트웨이 POST /api/explore/v1/search(사용자 토큰 Bearer) → results·partial·unavailablePortals·catalogStale
  • 오류 분류: 401/403 unauthenticated · 400 invalid-request · 5xx·타임아웃 unavailable · 형식 오류 unexpected
  • 화면 상태: 검색 중 · 빈 결과 · 오류(+재시도) · 부분 실패 경고(실패 포털 목록)
  • 각 포털은 어댑터(POST /api/explore/v1/search)를 제공하고 원본 권한을 최종 강제한다(게이트웨이는 대체하지 않음)

테스트: packages/operations-core/src/explore.test.ts(6) · packages/ui-react/src/exploreSearch.contract.test.tsx(6)

릴리스: PF 0.6.155 · artifacts/hohyun94-portal-operations-0.3.0.tgz · artifacts/portal-foundation-ui-react-0.6.155.tgz

14-1. 위임 아웃바운드 — 중앙 RAG 답변(§14 answer) ​

포털마다 다시 쓰던 위임 발급 + 게이트웨이 ask 를 공용으로 옮겼다(@hohyun94/portal-operations 0.4.0).

ts
import { createExploreDelegationIssuer, createExploreGateway } from "@hohyun94/portal-operations";

const delegation = createExploreDelegationIssuer({ origin: PORTAL_CONSOLE_ORIGIN, clientId: "guide-center", clientSecret });
const gateway = createExploreGateway({ baseUrl: EXPLORE_GATEWAY_URL, audience: "explore-gateway", delegation });
if (gateway.configured) await gateway.ask({ subject: directoryUserId, question });
  • 계약: PortalConsole/docs/delegation-token-contract.md 경로 B — 위임 토큰은 subject(Directory 사용자 ID)의 사용자 컨텍스트로만 좁혀 발급된다(권한 상승 금지).
  • createExploreDelegationIssuer: 사용자·audience·scope 별 캐시(연속 질문이 원장을 반복 호출하지 않는다), 만료 30초 skew, 설정 누락·발급 실패는 오류로 알린다.
  • createExploreGateway: 반쪽 구성 금지 — 주소만 있고 위임 자격증명이 없으면 configured: false 이고 호출도 하지 않는다. ask() 는 /api/explore/v1/ask 를 위임 토큰 Bearer 로 부르고 answer·partial·unavailablePortals·requestId 를 그대로 돌려준다.
  • 포털이 소유하는 것은 env 이름·프레임워크(express·tRPC)·응답 매핑뿐이다 — 검색은 각 포털 색인(explore.search), 답변만 위임한다.
  • 채택: ProcessCenter(자체 구현 유지·DISABLE_LOCAL_RAG_FALLBACK 스위치 보유) · AgentCenter · DocumentLibrary · GuideCenter.

테스트: packages/operations-core/src/exploreGateway.test.ts(6 — 캐시·기본 수명·오류·게이트웨이 호출·반쪽 구성·질문 길이)

릴리스: artifacts/hohyun94-portal-operations-0.4.0.tgz (AgentCenter · DocumentLibrary · GuideCenter vendor/ 반영)

15. 공통 용어 표시(PortalTerm · PortalTermList) ​

추가 버전: @hohyun94/portal-foundation 0.6.168

추가 위치

  • packages/ui-react/src/termTooltip.tsx — PortalTerm(정의 툴팁이 붙은 용어 칩) · PortalTermList(용어 목록) · PortalTermTooltip(툴팁 내용) · termSourceLabel · type PortalTermDescriptor
  • packages/ui-react/src/termTooltip.test.tsx — 칩 계약(표기·식별자·키보드 초점) · 툴팁 내용(정의·개념·출처) · 목록(빈 상태)
  • packages/ui-react/styles/styles.css — .pf-term* 규칙

계약

  • PortalTermDescriptor = { termId, label, definition?, conceptId?, detectedBy? } — detectedBy 는 dictionary(사전 매칭)·llm(모델 후보)·manual(사람이 연결)이고 툴팁에 출처로 표시한다.
  • 용어 칩은 tabIndex=0 이라 키보드 초점에서도 툴팁이 열린다(antd Tooltip 의 focus 트리거). 접근성 이름은 표기 자체다.
  • 정의가 없으면(추출 후보) 빈 툴팁 대신 "등록된 정의가 아직 없습니다." 를 보여 준다.
  • PortalTermList 는 비었을 때 emptyLabel 을 보여 주고, 그마저 없으면 아무것도 렌더하지 않는다 — 섹션을 만들지 말지는 호출부가 정한다.
  • 어휘의 정본은 PortalConsole(원장)이다. 포털은 원장이 붙여 준 표기·정의를 그대로 그리며 사전을 복제하지 않는다(GET /content-term-links?...&includeTerm=true).

소비 포털 사용 예

tsx
import { PortalTermList } from "@hohyun94/portal-foundation";
// 원장 연결을 표기·정의와 함께 받아 그대로 그린다(정의는 원장이 정본).
<PortalTermList terms={terms} ariaLabel="공통 용어" emptyLabel="등록된 용어가 없습니다." />

릴리스: PF 0.6.168 · artifacts/hohyun94-portal-foundation-0.6.168.tgz (Process Center vendor/ 반영)

16. 공통 용어 연결 파이프라인 — 타 포털 포팅 절차 ​

Process Center 에서 만든 구성을 다른 포털(Document Library·Guide Center)로 옮기는 절차다. 공용 계약(@hohyun94/portal-ai)과 공용 UI(PortalTerm)는 이미 있으므로, 각 포털에서는 컨텐츠 저장 경로에 맞춰 붙이기만 한다.

16-1. 원장 쪽(이미 완료 — 손대지 않는다) ​

PortalConsole 이 원장이다 — 용어·온톨로지·연결을 소유하고 이 API 를 제공한다(scope 는 모두 glossary:read/glossary:write).

용도호출
매칭용 사전(승인·활성)GET /api/directory/v1/glossary/matching-terms
연결 조회(+표기·정의)GET /api/directory/v1/content-term-links?contentKey=…&includeDeleted=false&includeTerm=true
연결 pushPOST /api/directory/v1/content-term-links
연결 해제DELETE /api/directory/v1/content-term-links?contentKey=…&termId=… (또는 portalId 로 좁힘)
후보 등록(LLM)POST /api/directory/v1/glossary/term-proposals
답변 groundingGET /api/directory/v1/ontology/pack

포털 API Key 에 glossary:read(조회)·glossary:write(push·후보)가 있어야 한다.

16-2. 포털 쪽 — 6단계 ​

  1. outbox 테이블: term_extraction_jobs(MySQL 0033·PG 0024 와 같은 스키마). content_key/portal_id/entity_type/entity_id/source_text/status/attempts/last_error/next_attempt_at/processed_at.
  2. 중앙 클라이언트: directoryFetch(PortalConsole PORTAL_CONSOLE_ORIGIN + API Key)로 2-1 표의 6개 호출을 모은다. contentKey 는 portalId:entityType:entityId(buildContentKey).
  3. 파이프라인 서비스: enqueue…(같은 contentKey 대기 작업 병합) + flush…(사전 캐시 → extractTermsByDictionary → planTermLinkChanges → push/unlink → 후보 등록 → backoff). 규칙은 계약이 갖고 있다(planTermLinkChanges — 사람이 고른 manual·LLM 후보 llm 연결은 불변).
  4. 저장 훅: 컨텐츠 저장·삭제 뒤 enqueue…(실패는 로그만 — 저장을 되돌리지 않는다). 삭제는 본문 없음으로 적재하면 다음 flush 가 자동 링크를 해제한다.
  5. 스케줄러: 디렉터리 동기화와 같은 조건(자격증명·identityMode)에서 주기 실행(setInterval + unref). NODE_ENV=test·DISABLE_TERM_LINK_SYNC=true 는 비활성.
  6. 화면 표시: 상세 화면에서 GET /content-term-links?includeTerm=true 결과를 PortalTermList 로 그린다(정의는 원장이 정본). 연결이 없으면 섹션을 만들지 않고, 못 읽으면 오류 상태만 보여 준다.

16-3. 포털별 확인 지점 ​

포털컨텐츠 키중앙 클라이언트마이그레이션 관례
Process Centerprocess-center:process_node:<id>server/directorySyncService.ts(directoryFetch)drizzle/*.sql + postgres-migrations/*.sql + 저널 수동 등록
Guide Centerguide-center:manual_page:<id> (제안)server/directorySync.ts 는 자체 fetch + ENV.directoryBaseUrl/directoryPortalApiKey — 재사용하거나 directoryFetch 를 새로 만든다drizzle/*.sql + drizzle/meta 저널
Document Librarydocument-library:document:<id> (제안)server/directory-api.tsserver/database-migrations.ts(코드 기반)
  • 본문은 각 포털의 저장 필드를 잇는다(PC 는 이름·설명·상세·매뉴얼, GC 는 페이지 본문/번역본).
  • 검색 확장: 용어 검색은 승인 용어 사전(matching-terms)과 연결을 조인해 표기·별칭·약어로 찾는다(PC server/services/terminologyService.ts 가 기준 구현).
  • 검증: 포털마다 (1) 큐 병합·backoff, (2) 추가·해제 계획, (3) 사람 연결 불변, (4) 원장 실패 시 저장이 계속되는지, (5) 화면 표시 계약을 테스트로 고정한다.

16-4. 포털 반영 현황 ​

포털상태컨텐츠 키구현
Process Center완료process-center:process_node:<id>server/termLinkSync.ts · server/services/contentTermService.ts · server/services/terminologyService.ts(검색) · drizzle/0033_term_extraction_jobs.sql
Guide Center완료 (2026-10)guide-center:manual_page:<id>server/termLinkDirectory.ts(중앙 클라이언트) · server/termLinkSync.ts · server/contentTermService.ts · server/termSearchService.ts(검색) · server/aiClient.ts · drizzle/0012_term_extraction_jobs.sql · scripts/ensure-i18n-schema.mjs
Document Library완료 (2026-10)document-library:document:<postId>server/termLinkDirectory.ts(중앙 클라이언트) · server/termLinkSync.ts · server/contentTermService.ts · server/termSearchService.ts(검색) · server/aiClient.ts · drizzle/postgres-migrations/0001_term_extraction_jobs.sql + drizzle/legacy/mysql/migrations/0011_term_extraction_jobs.sql

Guide Center 반영 메모(2026-10)

  • 공용 계약은 @hohyun94/portal-ai@0.3.1(vendor/ 반영). 저장 훅은 /api/manuals POST·PUT·DELETE, /api/editor/pages POST, /api/editor/pages/:id/translations, /api/manuals/publish-package, /api/manuals/:id/rollback 에서 적재한다(삭제는 본문 없이 적재 → 다음 flush 가 자동 해제).
  • 스케줄러는 DIRECTORY_PORTAL_API_KEY 가 있을 때만 주기 실행하고, GC 관례대로 POST /api/scheduled/term-links 로도 flush 할 수 있다(디렉터리 동기화 endpoint 와 동일 조건).
  • 마이그레이션은 drizzle/0012_term_extraction_jobs.sql(+저널)과 운영 반영 스크립트 scripts/ensure-i18n-schema.mjs 양쪽에 둔다 — 배포 플랫폼 빌드가 pnpm db:ensure-i18n 을 쓰기 때문(기존 common_ui_release_state 와 같은 관례).
  • 화면 표시는 서버 계약 manuals.terms(PortalTermList 데이터 모양)와 검색 manuals.searchTerms 까지 반영했다. 클라이언트는 client/src/components/PortalTermList.tsx — PF PortalTermList 와 같은 계약(표기=접근성 이름, 정의·개념·출처 툴팁, 빈 목록이면 미렌더)의 로컬 구현(radix Tooltip) — 을 Home.tsx 에 배선했다.
  • 공용 UI PortalTermList(PF ≥0.6.168) 교체는 PF 셸 마이그레이션이 선행돼야 한다 — 0.6.168 에서 PortalFrame 이 사라지고 PortalShell/PortalMain/PortalSidebar/PortalTopNav 로 바뀌며 antd 를 끌어온다. 마이그레이션 후 로컬 컴포넌트를 PF 것으로 그대로 바꿔 끼운다(데이터 계약 동일).
  • 검증: server/termLinkSync.test.ts(17) · server/contentTermService.test.ts(2) · server/termSearchService.test.ts(6). 마이그레이션 DDL 은 MySQL 8.0 에서 실제 적용·재적용(멱등) 확인.

Document Library 반영 메모(2026-10)

  • DL 은 dual-dialect(MySQL·PostgreSQL) pool(server/db.ts 의 requirePool(), MySQL ? 파라미터를 PG 에서 자동 변환)을 쓰므로 파이프라인 SQL 을 두 방언 공통으로 썼다. outbox 는 PG 0001·MySQL 0011 두 저널에 등록하고 실제 PostgreSQL 16·MySQL 8.0 에 적용 확인.
  • 컨텐츠 엔티티는 archive_posts(게시글)로 정했다 — DL 의 문서 저장은 게시글 생성(server/archive-api-db.ts)과 전체 교체 저장(server/archive-db.ts 의 writeArchiveRecord) 두 경로다. 둘 다 저장 뒤 enqueueDocumentTermExtractionSafely 로 적재한다.
  • 표시는 server/contentTermService.ts(document-library:document:<postId>) + GET /api/archive/posts/:postId/terms(requireAuth)로 노출했다. 스케줄러는 server/index.ts 에서 조건부로 시작한다.
  • 새 환경변수 DIRECTORY_PORTAL_API_KEY(또는 PORTAL_CONSOLE_API_KEY)가 필요하다 — DL 에는 기존 원장 자격증명 배선이 없었다. .env.example 에도 추가했다.
  • 검색 확장은 server/termSearchService.ts + GET /api/archive/search/terms?q=(requireAuth)로 노출하고, 클라이언트에 TermSearchPanel(Home)로 배선했다. 표시는 PostTerms(BoardDetail) — 연결이 없으면 미렌더.
  • 검증: server/termLinkSync.test.ts(15) · server/contentTermService.test.ts(2) · server/termSearchService.test.ts(4). DL 은 기본 테스트 root 가 client/ 라 서버 계약 테스트가 실행되지 않던 문제를 vite.config.ts 의 test.root/include 로 해소(기존 server/*.test.ts 도 함께 실행됨).