diff --git a/portal/app.mjs b/portal/app.mjs index 2a0fb8a..0058a2f 100644 --- a/portal/app.mjs +++ b/portal/app.mjs @@ -29,6 +29,9 @@ export async function buildApp(deps, opts = {}) { const mod = await import(file); if (typeof mod.register !== 'function') continue; await mod.register(app, deps); loaded.push(a); } + // [통합] 영역 사이의 느슨한 연결: 상품정책 조회 이름 맞춤, 주기 정리 작업 등록 + if (!deps.productPolicy && deps.commerce?.appliedProduct) deps.productPolicy = { activeFor: async (groupId) => deps.commerce.appliedProduct(groupId) }; + if (deps.scheduler?.every) for (const [name, fn] of [['sites.sweep', () => deps.sites?.sweep?.()], ['domains.sweep', () => deps.domains?.sweep?.()], ['source.sweep', () => deps.source?.sweep?.()]]) deps.scheduler.every(name, 10 * 60 * 1000, fn); deps.log(`[portal] 영역 등록: ${loaded.join(', ')}`); return app; } diff --git a/portal/commerce/api.mjs b/portal/commerce/api.mjs index eb8283a..9c10f6e 100644 --- a/portal/commerce/api.mjs +++ b/portal/commerce/api.mjs @@ -48,7 +48,7 @@ export async function register(app, deps) { app.get('/api/commerce/orders', (ctx) => { requireGroup(ctx); return { orders: ord.listOrders(store, { groupId: ctx.group.id }).map((o) => orderView(store, o)) }; }); app.get('/api/commerce/orders/:id', (ctx) => { requireGroup(ctx); const o = ord.getOrder(store, ctx.params.id, ctx.group.id); if (!o) throw new HttpError(404, 'not_found', '주문이 없습니다'); return { order: orderView(store, o) }; }); app.post('/api/commerce/orders/:id/cancel', (ctx) => { requireBilling(ctx); return { ok: true, order: orderView(store, ord.cancelOrder(store, ctx.params.id, ctx.group.id, ctx.user.id)) }; }); - app.get('/api/commerce/billing', async (ctx) => { requireGroup(ctx); const s = ord.billingSummary(store, ctx.group.id); let wallet = null; try { wallet = deps.webmaker?.credits ? await deps.webmaker.credits(ctx.user.id, ctx.group.id) : null; } catch (e) { wallet = { error: 'unavailable', message: 'AI Wallet 정보를 지금 확인할 수 없습니다(정보 확인 지연)' }; } + app.get('/api/commerce/billing', async (ctx) => { requireGroup(ctx); const s = ord.billingSummary(store, ctx.group.id); let wallet = null; try { wallet = deps.webmaker?.credits ? await deps.webmaker.credits(deps.webmaker.loginFor ? deps.webmaker.loginFor(ctx.user, ctx.group) : ctx.user.id) /* [통합] Webmaker 계정명은 어댑터의 loginFor 규칙 */ : null; } catch (e) { wallet = { error: 'unavailable', message: 'AI Wallet 정보를 지금 확인할 수 없습니다(정보 확인 지연)' }; } return { ...s, orders: ord.listOrders(store, { groupId: ctx.group.id }).map((o) => orderView(store, o)), hold: ord.openHold(store, ctx.group.id), overage: ord.listOverage(store, ctx.group.id), wallet: wallet || { available: false, message: 'AI Wallet 은 Webmaker 제작 화면에서 확인·충전합니다(호스팅 사용량과 별도)' }, domainRegistration: ord.domainRegistration(store), entitlements: quota.summary(store, ctx.group.id) }; }); app.post('/api/commerce/downsize/preview', async (ctx) => { requireBilling(ctx); const b = await ctx.body(); const pv = cat.getProductVersion(store, b.productVersionId); if (!pv) throw new HttpError(404, 'product_not_found', '상품이 없습니다'); const priced = ord.priceItems(pv, { includeBase: true, items: b.items || [] }); return { ...ord.evaluateDownsize(store, ctx.group.id, pv, priced.items), quote: priced }; }); app.post('/api/commerce/downsize/request', async (ctx) => { requireBilling(ctx); const b = await ctx.body(); const r = ord.requestDownsize(store, { groupId: ctx.group.id, userId: ctx.user.id, userVerified: !!ctx.user.email_verified_at, productVersionId: b.productVersionId, items: b.items || [], acceptTerms: !!b.acceptTerms }); return { ok: true, fits: r.fits, shortages: r.shortages, order: r.order ? orderView(store, r.order) : null, hold: r.hold || null }; }); diff --git a/portal/domains/ui.mjs b/portal/domains/ui.mjs index bde7ad2..53e6635 100644 --- a/portal/domains/ui.mjs +++ b/portal/domains/ui.mjs @@ -1,7 +1,7 @@ /** portal/domains/ui.mjs — 도메인 화면 뼈대(목록·연결·상세). 데이터는 /ui/domains/domains.js 가 채운다. */ import { esc } from '../../lib/http.mjs'; import { t } from '../../lib/i18n.mjs'; -const shell = (ctx, deps, o) => deps.page({ ...o, user: ctx.user, group: ctx.group, groups: ctx.groups || [], unread: ctx.unread || 0, csrf: ctx.session ? ctx.session.csrf : '', scripts: ['/ui/domains/domains.js'] }); +const shell = (ctx, deps, o) => deps.shell ? deps.shell(ctx, o) /* [통합] 인증 영역 공통 틀 우선 */ : deps.page({ ...o, user: ctx.user, group: ctx.group, groups: ctx.groups || [], unread: ctx.unread || 0, csrf: ctx.session ? ctx.session.csrf : '', scripts: ['/ui/domains/domains.js'] }); export const listPage = (ctx, deps) => shell(ctx, deps, { title: t('domains.title'), nav: 'domains', crumbs: [{ label: t('nav.dashboard'), href: '/' }, { label: t('domains.title') }], actions: [{ label: t('domains.connect'), href: '/domains/connect', kind: 'primary' }, { label: '도메인 구매·DNS 자동화(Webmaker)', href: '/sso/webmaker?path=/' }], body: `
도메인 등록·갱신·네임서버·레코드 자동화는 Webmaker 의 도메인 탭에서 기존 기능 그대로 제공됩니다. 도메인 등록 기능이 제공되지 않는 환경에서는 외부 등록기관에서 구매한 도메인을 여기서 연결하세요.
` }); export const connectPage = (ctx, deps, { siteId = '' } = {}) => shell(ctx, deps, { title: t('domains.connect'), nav: 'domains', crumbs: [{ label: t('domains.title'), href: '/domains' }, { label: t('domains.connect') }], body: `