Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions src/app/[locale]/plans/aip-enterprise/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { notFound } from "next/navigation";
import type { Metadata } from "next";
import PlansPage from "@/components/pages/plans/PlansPage";
import { getLocalePath, isLocale, type Locale } from "@/constants/i18n";
import { getPlansPageCopy } from "@/copy/contentPages";
import { withDynamicOgImage } from "@/features/seo/metadata";

type AipEnterprisePlansRouteProps = {
params: Promise<{ locale: string }>;
};

export async function generateMetadata({ params }: AipEnterprisePlansRouteProps): Promise<Metadata> {
const { locale } = await params;

if (!isLocale(locale)) return {};

const { metadataDescription, metadataTitle } = getPlansPageCopy(locale);

return withDynamicOgImage({
title: metadataTitle,
description: metadataDescription,
alternates: {
canonical: getLocalePath(locale, "/plans/aip-enterprise"),
},
}, { locale, title: metadataTitle, description: metadataDescription });
}

export default async function AipEnterprisePlansRoute({ params }: AipEnterprisePlansRouteProps) {
const { locale } = await params;

if (!isLocale(locale)) notFound();

return <PlansPage enterpriseOnly locale={locale as Locale} productKey="aip" />;
}
124 changes: 113 additions & 11 deletions src/components/pages/plans/PlansPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { pageSectionGapClassName, pageXPaddingClassName } from "@/constants/layout";
import { useMemo } from "react";
import { useRouter } from "next/navigation";
import { useRouter, useSearchParams } from "next/navigation";
import Button from "@/components/ui/Button";
import Tab from "@/components/ui/Tab";
import TabGroup from "@/components/ui/TabGroup";
Expand All @@ -12,6 +12,7 @@ import { getLocalePath, type Locale } from "@/constants/i18n";
import { getPlansPageCopy } from "@/copy/contentPages";

type PlansPageProps = {
enterpriseOnly?: boolean;
productKey?: "aip" | "acp";
locale: Locale;
};
Expand Down Expand Up @@ -110,6 +111,73 @@ function PlanSummaryCard({
);
}

function EnterpriseSummaryCard({
billingLabel,
ctaLabel,
description,
features,
href,
name,
priceLabel,
tone = "secondary",
}: PlanCard) {
return (
<article className="grid w-full gap-8 rounded-box bg-bg-content p-[30px] md:grid-cols-[minmax(0,0.8fr)_minmax(0,1.2fr)] md:items-start">
<div className="flex flex-col gap-8">
<div className="flex flex-col gap-1">
<h2 className={cx("m-0 type-h2", tone === "primary" ? "text-brand" : "text-fg")}>{name}</h2>
<p className="m-0 type-body-md text-mute">{description}</p>
</div>

<div className="flex flex-col gap-1">
<p className="m-0 type-h2 text-fg">{priceLabel}</p>
{billingLabel ? <p className="m-0 type-body-lg text-fg">{billingLabel}</p> : null}
</div>

<a className="inline-flex" href={href} {...getExternalLinkProps(href)}>
<Button
arrow
className="min-w-[126px]"
style="full"
variant="primary"
>
{ctaLabel}
</Button>
</a>
</div>

<ul className="m-0 grid list-none gap-x-8 gap-y-2 p-0 sm:grid-cols-2">
{features.map((feature, index) => {
if (isPlanFeatureDivider(feature)) {
return (
<li
aria-hidden="true"
className="col-span-full my-2 h-px w-full bg-border"
key={`divider-${index}`}
/>
);
}

return (
<li
key={`${typeof feature === "string" ? feature : feature.value}-${index}`}
className="flex items-start gap-1.5 type-body-md text-fg"
>
<span className={cx(
"inline-flex w-4 shrink-0 justify-center",
typeof feature === "string" || feature.tone !== "danger" ? "text-success" : "text-destructive",
)}>
{typeof feature === "string" || feature.tone !== "danger" ? "✓" : "✕"}
</span>
<span>{typeof feature === "string" ? feature : feature.value}</span>
</li>
);
})}
</ul>
</article>
);
}

function getValueToneClass(tone?: ComparisonValue["tone"]) {
if (tone === "success") return "text-success";
if (tone === "danger") return "text-destructive";
Expand Down Expand Up @@ -199,23 +267,44 @@ function ComparisonTable({
}

export default function PlansPage({
enterpriseOnly = false,
productKey = "aip",
locale,
}: PlansPageProps) {
const pricingProducts = pricingProductsByLocale[locale];
const pageCopy = getPlansPageCopy(locale);
const router = useRouter();
const searchParams = useSearchParams();
const activeProductKey: keyof typeof pricingProducts =
productKey in pricingProducts ? productKey : "aip";
const activeProduct = useMemo(
() => pricingProducts[activeProductKey],
[activeProductKey, pricingProducts],
);
const planCards = useMemo(
() => (
enterpriseOnly
? activeProduct.cards.filter((card) => card.name === "Enterprise")
: activeProduct.cards
),
[activeProduct.cards, enterpriseOnly],
);
const activeProductCaption =
activeProductKey === "aip" ? "AI Platform" : "Access Control Platform";

function handleProductChange(nextKey: keyof typeof pricingProducts) {
if (nextKey === activeProductKey) return;

if (enterpriseOnly && nextKey === "acp") {
router.push(getLocalePath(locale, "/plans/acp?returnTo=aip-enterprise"), { scroll: false });
return;
}

if (searchParams.get("returnTo") === "aip-enterprise" && nextKey === "aip") {
router.push(getLocalePath(locale, "/plans/aip-enterprise"), { scroll: false });
return;
}

router.push(getLocalePath(locale, `/plans/${nextKey}`), { scroll: false });
}

Expand Down Expand Up @@ -247,17 +336,30 @@ export default function PlansPage({

{/* 선택된 제품군에 맞는 카드/비교표 렌더링 */}
<div className="flex flex-col items-center gap-[60px] md:gap-[80px]">
<div className="grid w-full gap-5 md:grid-cols-3">
{activeProduct.cards.map((plan, index) => (
<PlanSummaryCard
key={`${activeProductKey}-${plan.name}`}
{...plan}
href={withLocaleHref(locale, plan.href)}
/>
))}
<div className={cx(
"grid w-full gap-5",
!enterpriseOnly && "md:grid-cols-3",
)}>
{planCards.map((plan) => {
const href = withLocaleHref(locale, plan.href);

return enterpriseOnly ? (
<EnterpriseSummaryCard
key={`${activeProductKey}-${plan.name}`}
{...plan}
href={href}
/>
) : (
<PlanSummaryCard
key={`${activeProductKey}-${plan.name}`}
{...plan}
href={href}
/>
);
})}
</div>

{activeProductKey === "aip" ? (
{activeProductKey === "aip" && !enterpriseOnly ? (
<ComparisonTable
comparisonGroups={activeProduct.comparisonGroups}
plans={activeProduct.plans}
Expand All @@ -266,7 +368,7 @@ export default function PlansPage({
</div>
</div>
</section>
<Cta locale={locale} />
{!enterpriseOnly ? <Cta locale={locale} /> : null}
</div>
);
}
21 changes: 21 additions & 0 deletions src/constants/plans.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,10 +102,17 @@ export const pricingProductsByLocale: Record<Locale, PricingProducts> = {
"Unlimited custom AI agents",
"Unlimited RAG knowledge bundles",
{ type: "divider" },
"Agent Scheduling",
"Skills",
"Image Generation",
"My Drive",
{ type: "divider" },
"Audit logs (max 180 days)",
"SSO",
"DLP",
"Login IP ACL",
"MCP Access Control",
"Agent Access Control",
{ type: "divider" },
"Custom Branding",
"Advanced AI Security Features",
Expand Down Expand Up @@ -607,10 +614,17 @@ export const pricingProductsByLocale: Record<Locale, PricingProducts> = {
"커스텀 AI 에이전트 무제한",
"RAG 지식 번들 무제한",
{ type: "divider" },
"에이전트 스케줄링",
"스킬",
"이미지 생성",
"My Drive",
{ type: "divider" },
"감사 로그 (최대 180일)",
"SSO",
"DLP",
"로그인 IP ACL",
"MCP 접근 제어",
"에이전트 접근 제어",
{ type: "divider" },
"커스텀 브랜딩",
"고급 AI 보안 기능",
Expand Down Expand Up @@ -895,10 +909,17 @@ export const pricingProductsByLocale: Record<Locale, PricingProducts> = {
"カスタムAIエージェント\n無制限",
"社内文書学習 (RAG) 無制限",
{ type: "divider" },
"エージェントスケジュール",
"スキル",
"画像生成",
"My Drive",
{ type: "divider" },
"操作履歴の記録\n(最大180日間)",
"シングルサインオン (SSO)",
"データ漏洩防止 (DLP)",
"IPアドレス制限 (ACL)",
"連携ツールのアクセス管理",
"エージェントのアクセス管理",
{ type: "divider" },
"カスタムブランディング",
"高度な AI セキュリティ機能",
Expand Down
Loading