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 |
| React | React 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 0 | Ant Design 도입 기준선 | dependency, 초기 설치, baseline 확인 |
| Phase 1 | provider와 공통 primitive | PortalConfigProvider, theme mapping, Button/Badge/Empty/Breadcrumbs 전환 |
| Phase 2.0–2.1 | AuthCard | Card, Button, Alert, Typography 기반 구현, legacy class와 semantic contract 유지 |
| Phase 2.2 | ThemePicker | theme-client와 연결된 Select, Segmented, Switch 전환 |
| Phase 2.3 | BrandMark/icon facade | Ant Design icon facade와 안정적인 portal icon key 연결 |
| Phase 3 | shell, admin, directory, menu adapter | Layout, navigation adapter, DirectoryManagement, admin shell 전환 |
| Phase 4 | data/content/assets | data 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는 다음과 같다.
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를 유지한다.
export { PortalExploreDialog } from "./PortalExploreDialog.js";Ant Design primitive는 Modal, Input.Search, List, Button, Card, Empty, Spin, Tag, Typography다.
보존한 public props는 다음과 같다.
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/onOpenChangecontrolled 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.md | AuthCard public API와 style contract audit |
docs/PHASE_3_REMAINING_COMPONENT_PLAN.md | Phase 3 잔여 컴포넌트 계획 |
docs/PHASE_3_TEST_AND_ACCESSIBILITY_CHECKLIST.md | Phase 3 테스트·접근성 checklist |
docs/PHASE_4_REFACTORING_PLAN.md | Phase 4 데이터·콘텐츠 계획 |
docs/PHASE_5_REFACTORING_PLAN.md | Phase 5 최초 계획 |
docs/PHASE_5_NEXT_REFACTORING_PLAN.md | Phase 5 다음 작업 계획 |
docs/PHASE_5_COMPLETE_REFACTORING_PLAN.md | Phase 5 전체 잔여 범위와 완료 기준 |
6. 브랜치와 최근 원격 커밋
현재 확인된 원격 브랜치는 feat/antd-phase-2-components다. GitHub upload 방식으로 파일이 반영되어 각 업로드가 별도 verified commit으로 생성되었다.
| Commit | 의미 |
|---|---|
9530cf4 | Phase 5 전체 리팩토링 계획 문서 |
f5a6ac6 | PortalExploreDialog Ant Design 구현과 entrypoint/test 변경 |
dbcba36 | Phase 5 다음 작업 계획 문서 |
d4cdd15 | Phase 4 이후 소스 반영 커밋 |
6fcf01a | menuManagement 상세 field·Quick Link·validation 반영 |
a9e378c | 이전 Phase 5 menuManagement 구현 반영 |
9bd4314 | Phase 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 기준 결과는 다음과 같다.
Test files: 9 passed
Tests: 38 passed
Typecheck: passed
Build: passed테스트 파일은 다음과 같다.
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
권장 재검증 명령은 다음과 같다.
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한다.
gh repo clone hohyun94/portal-foundation /tmp/portal-foundation-handover
cd /tmp/portal-foundation-handover
git checkout feat/antd-phase-2-components
pnpm install8. 알려진 warning과 해석
Ant Design List deprecation
Ant Design 6에서 일부 테스트가 다음 warning을 출력한다.
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이 발생할 수 있다.
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.Itemownership- 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:
mapPortalThemePolicyErrors(
errors: Record<string, string | string[]>
): Record<string, string[]>기존 onSave(policy) payload는 변경하지 않는다.
P1: menu access adapter
scope, role, group ID 선택지가 실제로 제공되는 경우에만 Select mode="multiple"을 사용한다. 선택지가 비어 있으면 기존 자유 입력을 유지한다.
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
hreffallback navigation
P1: admin composite 정리
DirectorySyncAdminPanel, AdminSection, AdminFeedback, AdminEmptyState, OperationalMetricGrid의 status semantics와 async action 처리를 표준화한다. public props는 유지한다.
P1: root export 분리
index.tsx를 다음 feature 영역으로 분리할 수 있다.
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. 다음 담당자 작업 절차
첫 번째 작업
- 저장소를 clone한다.
feat/antd-phase-2-components를 checkout한다.pnpm install을 실행한다.- ui-react test, check, build를 실행해 baseline을 기록한다.
docs/PHASE_5_COMPLETE_REFACTORING_PLAN.md와 이 문서를 읽는다.
두 번째 작업
PortalThemePolicySettings.tsx의 public props와theme-clienttype을 확인한다.- 기존 사용처를
rg -n "PortalThemePolicySettings|onSave" packages docs로 조사한다. - error adapter의 순수 함수 테스트를 먼저 작성한다.
- Form 전환을 적용한다.
- callback payload equality와 accessibility 테스트를 추가한다.
- test/check/build를 통과시킨다.
세 번째 작업
- menu access adapter를 설계한다.
- option이 없는 상태의 자유 입력 fallback을 보존한다.
- reserved/parent/reorder edge case를 테스트한다.
- 실제 소비 포털에서 callback payload를 확인한다.
작업 종료 전
- 원격 branch에 파일이 실제로 반영되었는지 확인한다.
- commit SHA와 변경 파일을 기록한다.
- 테스트 결과와 남은 warning을 기록한다.
- 다음 담당자가 즉시 실행할 명령을 문서에 남긴다.
- PR 및 merge 상태를 GitHub에서 최종 확인한다.
12. 파일 위치 빠른 참조
| 목적 | 경로 |
|---|---|
| root exports와 legacy composite | packages/ui-react/src/index.tsx |
| Explore dialog | packages/ui-react/src/PortalExploreDialog.tsx |
| Theme policy | packages/ui-react/src/PortalThemePolicySettings.tsx |
| Menu management | packages/ui-react/src/menuManagement.tsx |
| Menu tests | packages/ui-react/src/menuManagement.test.tsx |
| Shared root tests | packages/ui-react/src/index.test.tsx |
| Shell | packages/ui-react/src/PortalShell.tsx |
| Admin shell | packages/ui-react/src/PortalAdminShell.tsx |
| Directory | packages/ui-react/src/DirectoryManagement.tsx |
| Data shells | packages/ui-react/src/dataShells.tsx |
| Content primitive | packages/ui-react/src/contentComponents.tsx |
| Attachment | packages/ui-react/src/assetAttachments.tsx |
| Query adapter | packages/ui-react/src/dataAdapters.ts |
| Ant Design provider | packages/ui-react/src/providers/PortalConfigProvider.tsx |
| Provider exports | packages/ui-react/src/providers/index.ts |
| Design tokens | packages/design-tokens/src |
| Theme client | packages/theme-client/src |
| UI styles | packages/ui-react/styles/styles.css |
References
12. 공통 알림함(PortalNotificationCenter)
추가 버전:
@hohyun94/portal-operations0.2.0,@hohyun94/portal-foundation0.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 소비 포털 사용 예
// 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-ai0.1.0,@hohyun94/portal-foundation0.6.154개정:
@hohyun94/portal-ai0.2.0 — 용어 추출·컨텐츠 링크 계약(§13.5) 추가.개정:
@hohyun94/portal-ai0.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 소비 포털 사용 예
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 추출·검증 재시도·요약 정규화·캐시 재사용·마크업 정리·길이 제한·필드 diffpackages/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-operations0.3.0 —createExploreGatewayClient(ExploreGatewayError·ExploreGatewayResult·ExploreGatewaySearchResponse)@hohyun94/portal-foundation0.6.155 —PortalExploreSearch컴포넌트(라벨 ko/en,testId,onSelect/onOpen)
사용 방법
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· 400invalid-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).
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-foundation0.6.168
추가 위치
packages/ui-react/src/termTooltip.tsx—PortalTerm(정의 툴팁이 붙은 용어 칩) ·PortalTermList(용어 목록) ·PortalTermTooltip(툴팁 내용) ·termSourceLabel·type PortalTermDescriptorpackages/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).
소비 포털 사용 예
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 |
| 연결 push | POST /api/directory/v1/content-term-links |
| 연결 해제 | DELETE /api/directory/v1/content-term-links?contentKey=…&termId=… (또는 portalId 로 좁힘) |
| 후보 등록(LLM) | POST /api/directory/v1/glossary/term-proposals |
| 답변 grounding | GET /api/directory/v1/ontology/pack |
포털 API Key 에 glossary:read(조회)·glossary:write(push·후보)가 있어야 한다.
16-2. 포털 쪽 — 6단계
- outbox 테이블:
term_extraction_jobs(MySQL0033·PG0024와 같은 스키마).content_key/portal_id/entity_type/entity_id/source_text/status/attempts/last_error/next_attempt_at/processed_at. - 중앙 클라이언트:
directoryFetch(PortalConsolePORTAL_CONSOLE_ORIGIN+ API Key)로 2-1 표의 6개 호출을 모은다.contentKey는portalId:entityType:entityId(buildContentKey). - 파이프라인 서비스:
enqueue…(같은 contentKey 대기 작업 병합) +flush…(사전 캐시 →extractTermsByDictionary→planTermLinkChanges→ push/unlink → 후보 등록 → backoff). 규칙은 계약이 갖고 있다(planTermLinkChanges— 사람이 고른manual·LLM 후보llm연결은 불변). - 저장 훅: 컨텐츠 저장·삭제 뒤
enqueue…(실패는 로그만 — 저장을 되돌리지 않는다). 삭제는 본문 없음으로 적재하면 다음 flush 가 자동 링크를 해제한다. - 스케줄러: 디렉터리 동기화와 같은 조건(자격증명·identityMode)에서 주기 실행(
setInterval+unref).NODE_ENV=test·DISABLE_TERM_LINK_SYNC=true는 비활성. - 화면 표시: 상세 화면에서
GET /content-term-links?includeTerm=true결과를PortalTermList로 그린다(정의는 원장이 정본). 연결이 없으면 섹션을 만들지 않고, 못 읽으면 오류 상태만 보여 준다.
16-3. 포털별 확인 지점
| 포털 | 컨텐츠 키 | 중앙 클라이언트 | 마이그레이션 관례 |
|---|---|---|---|
| Process Center | process-center:process_node:<id> | server/directorySyncService.ts(directoryFetch) | drizzle/*.sql + postgres-migrations/*.sql + 저널 수동 등록 |
| Guide Center | guide-center:manual_page:<id> (제안) | server/directorySync.ts 는 자체 fetch + ENV.directoryBaseUrl/directoryPortalApiKey — 재사용하거나 directoryFetch 를 새로 만든다 | drizzle/*.sql + drizzle/meta 저널 |
| Document Library | document-library:document:<id> (제안) | server/directory-api.ts | server/database-migrations.ts(코드 기반) |
- 본문은 각 포털의 저장 필드를 잇는다(PC 는 이름·설명·상세·매뉴얼, GC 는 페이지 본문/번역본).
- 검색 확장: 용어 검색은 승인 용어 사전(
matching-terms)과 연결을 조인해 표기·별칭·약어로 찾는다(PCserver/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/manualsPOST·PUT·DELETE,/api/editor/pagesPOST,/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— PFPortalTermList와 같은 계약(표기=접근성 이름, 정의·개념·출처 툴팁, 빈 목록이면 미렌더)의 로컬 구현(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 는 PG0001·MySQL0011두 저널에 등록하고 실제 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도 함께 실행됨).