V13-FE-006: consolidate approved UI and contract hardening
deploy / deploy (push) Successful in 1m52s
deploy / notify (push) Successful in 1s

This commit is contained in:
2026-08-13 02:41:00 +09:00
parent d79edae546
commit 3f293d8aa8
1278 changed files with 14384 additions and 1664 deletions
@@ -0,0 +1,30 @@
<script setup lang="ts">
import { KbxNotificationCenter, KbxOperationCenter, KbxRuntimeBanner } from '@kbx/ui'
import { ref } from 'vue'
import { useKbxRuntime } from './useKbxRuntime'
const { notice, operations, notifications, unreadCount, refresh, markRead } = useKbxRuntime()
const panel = ref<'notifications' | 'operations' | null>(null)
</script>
<template>
<div class="kbx-runtime-shell">
<KbxRuntimeBanner :notice="notice" @retry="refresh" />
<header class="tools">
<button type="button" @click="panel = panel === 'operations' ? null : 'operations'">작업 {{ operations.filter(x => x.status === 'running' || x.status === 'queued').length }}</button>
<button type="button" @click="panel = panel === 'notifications' ? null : 'notifications'">알림 {{ unreadCount }}</button>
</header>
<aside v-if="panel" class="panel">
<KbxOperationCenter v-if="panel === 'operations'" :operations="operations" />
<KbxNotificationCenter v-else :notifications="notifications" @read="markRead" />
</aside>
<slot />
</div>
</template>
<style scoped>
.kbx-runtime-shell { position:relative; min-height:100%; }
.tools { display:flex; justify-content:flex-end; gap:4px; min-height:32px; align-items:center; }
.tools button { min-height:30px; border:1px solid var(--kbx-color-border); border-radius:var(--kbx-radius-sm); background:var(--kbx-color-surface); }
.panel { position:absolute; z-index:50; right:8px; top:76px; width:min(420px, calc(100vw - 24px)); max-height:70vh; overflow:auto; padding:8px; border:1px solid var(--kbx-color-border); border-radius:var(--kbx-radius-md); background:var(--kbx-color-surface); box-shadow:0 10px 30px rgba(16,24,40,.16); }
</style>
@@ -0,0 +1,17 @@
import type { KbxOperationRun, KbxRuntimeNotice, KbxUserNotification } from '@kbx/contracts'
import { kbxApi } from '../http/generated/kbxApiClient'
export const runtimeApi = {
getNotice() {
return kbxApi.request<KbxRuntimeNotice | null>('common.runtime.notice')
},
getOperations() {
return kbxApi.request<KbxOperationRun[]>('common.runtime.operations', { query: { limit: 20 } })
},
getNotifications() {
return kbxApi.request<KbxUserNotification[]>('common.runtime.notifications', { query: { limit: 30 } })
},
markNotificationRead(id: string) {
return kbxApi.request<void>('common.runtime.notificationRead', { path: { Id: id } })
},
}
@@ -0,0 +1,42 @@
import { computed, onMounted, onUnmounted, ref } from 'vue'
import type { KbxOperationRun, KbxRuntimeNotice, KbxUserNotification } from '@kbx/contracts'
import { runtimeApi } from './runtimeApi'
export function useKbxRuntime() {
const notice = ref<KbxRuntimeNotice | null>(null)
const operations = ref<KbxOperationRun[]>([])
const notifications = ref<KbxUserNotification[]>([])
let timer: ReturnType<typeof setInterval> | undefined
async function refresh() {
const [nextNotice, nextOperations, nextNotifications] = await Promise.all([
runtimeApi.getNotice(),
runtimeApi.getOperations(),
runtimeApi.getNotifications(),
])
notice.value = nextNotice
operations.value = nextOperations
notifications.value = nextNotifications
}
async function markRead(id: string) {
await runtimeApi.markNotificationRead(id)
const item = notifications.value.find(x => x.id === id)
if (item && !item.readAt) item.readAt = new Date().toISOString()
}
onMounted(() => {
void refresh()
timer = setInterval(() => void refresh(), 30_000)
})
onUnmounted(() => { if (timer) clearInterval(timer) })
return {
notice,
operations,
notifications,
unreadCount: computed(() => notifications.value.filter(x => !x.readAt).length),
refresh,
markRead,
}
}