feat: 가격표 2,900원 리뉴얼 + 티어 전환기 + 기능 게이트
가격표: Free/Pro(2,900)/School(학생당 2,900) 3-tier, 비교표 재구성 프로필: 프로토타입용 Free/Pro/School 자유 전환 버튼 기능 게이트: Free 과목 3개, 복습 큐 10개, 마스터리 경로 차단 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -23,7 +23,7 @@ import {
|
|||||||
Max,
|
Max,
|
||||||
Min,
|
Min,
|
||||||
} from 'class-validator';
|
} from 'class-validator';
|
||||||
import { MathUnit, Persona, ReviewIntensity } from '@prisma/client';
|
import { MathUnit, Persona, ReviewIntensity, SubscriptionTier } from '@prisma/client';
|
||||||
import { extname, join } from 'path';
|
import { extname, join } from 'path';
|
||||||
import { mkdirSync } from 'fs';
|
import { mkdirSync } from 'fs';
|
||||||
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
||||||
@@ -84,6 +84,11 @@ class OnboardingDto {
|
|||||||
onboarded?: boolean;
|
onboarded?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class SwitchTierDto {
|
||||||
|
@IsEnum(SubscriptionTier)
|
||||||
|
tier: SubscriptionTier;
|
||||||
|
}
|
||||||
|
|
||||||
class ProfilePatchDto {
|
class ProfilePatchDto {
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
@@ -187,6 +192,11 @@ export class MeController {
|
|||||||
return this.me.updateAvatar(user.id, file);
|
return this.me.updateAvatar(user.id, file);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Patch('subscription-tier')
|
||||||
|
async switchTier(@CurrentUser() user: AuthUser, @Body() dto: SwitchTierDto) {
|
||||||
|
return this.me.switchTier(user.id, dto.tier);
|
||||||
|
}
|
||||||
|
|
||||||
@Delete('avatar')
|
@Delete('avatar')
|
||||||
removeAvatar(@CurrentUser() user: AuthUser) {
|
removeAvatar(@CurrentUser() user: AuthUser) {
|
||||||
return this.me.removeAvatar(user.id);
|
return this.me.removeAvatar(user.id);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import {
|
|||||||
BadRequestException,
|
BadRequestException,
|
||||||
Injectable,
|
Injectable,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { MathUnit, Persona, Prisma, ReviewIntensity } from '@prisma/client';
|
import { MathUnit, Persona, Prisma, ReviewIntensity, SubscriptionTier } from '@prisma/client';
|
||||||
import { unlink } from 'fs/promises';
|
import { unlink } from 'fs/promises';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import { PrismaService } from '../prisma/prisma.service';
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
@@ -173,6 +173,14 @@ export class MeService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async switchTier(userId: number, tier: SubscriptionTier) {
|
||||||
|
const user = await this.prisma.user.update({
|
||||||
|
where: { id: userId },
|
||||||
|
data: { subscriptionTier: tier },
|
||||||
|
});
|
||||||
|
return this.toView(user);
|
||||||
|
}
|
||||||
|
|
||||||
async updateAvatar(
|
async updateAvatar(
|
||||||
userId: number,
|
userId: number,
|
||||||
file?: {
|
file?: {
|
||||||
|
|||||||
@@ -48,7 +48,11 @@ export class ReviewsService {
|
|||||||
take: 100,
|
take: 100,
|
||||||
});
|
});
|
||||||
|
|
||||||
return { items, rescheduledCount };
|
// Free 플랜: 복습 큐 10개 제한
|
||||||
|
const queueUser = await this.prisma.user.findUniqueOrThrow({ where: { id: userId }, select: { subscriptionTier: true } });
|
||||||
|
const limitedItems = queueUser.subscriptionTier === 'free' ? items.slice(0, 10) : items;
|
||||||
|
|
||||||
|
return { items: limitedItems, rescheduledCount };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
import { ForbiddenException, Injectable, NotFoundException } from '@nestjs/common';
|
||||||
import { PrismaService } from '../prisma/prisma.service';
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
import { PersonaForgetService } from '../forget/persona-forget.service';
|
import { PersonaForgetService } from '../forget/persona-forget.service';
|
||||||
|
|
||||||
@@ -71,6 +71,12 @@ export class StatsService {
|
|||||||
* tag becomes mine"** instead of only the decay curve.
|
* tag becomes mine"** instead of only the decay curve.
|
||||||
*/
|
*/
|
||||||
async masteryPath(userId: number, tagId: number, targetP = 0.9) {
|
async masteryPath(userId: number, tagId: number, targetP = 0.9) {
|
||||||
|
// Free 플랜: 마스터리 경로 차단
|
||||||
|
const tierUser = await this.prisma.user.findUniqueOrThrow({ where: { id: userId }, select: { subscriptionTier: true } });
|
||||||
|
if (tierUser.subscriptionTier === 'free') {
|
||||||
|
throw new ForbiddenException('마스터리 경로는 Pro 플랜부터 이용할 수 있습니다.');
|
||||||
|
}
|
||||||
|
|
||||||
const user = await this.prisma.user.findUniqueOrThrow({
|
const user = await this.prisma.user.findUniqueOrThrow({
|
||||||
where: { id: userId },
|
where: { id: userId },
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
import { ForbiddenException, Injectable, NotFoundException } from '@nestjs/common';
|
||||||
import { PrismaService } from '../prisma/prisma.service';
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
import { findPresetUnits } from './curriculum-presets';
|
import { findPresetUnits } from './curriculum-presets';
|
||||||
|
|
||||||
@@ -24,6 +24,15 @@ export class SubjectsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async create(userId: number, name: string, color: string) {
|
async create(userId: number, name: string, color: string) {
|
||||||
|
// Free 플랜: 과목 3개 제한
|
||||||
|
const user = await this.prisma.user.findUniqueOrThrow({ where: { id: userId }, select: { subscriptionTier: true } });
|
||||||
|
if (user.subscriptionTier === 'free') {
|
||||||
|
const count = await this.prisma.subject.count({ where: { userId } });
|
||||||
|
if (count >= 3) {
|
||||||
|
throw new ForbiddenException('Free 플랜은 과목 3개까지만 등록할 수 있습니다. Pro로 업그레이드하세요.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const subject = await this.prisma.subject.create({
|
const subject = await this.prisma.subject.create({
|
||||||
data: { userId, name, color },
|
data: { userId, name, color },
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -50,10 +50,12 @@ const tiers: Tier[] = [
|
|||||||
note: { monthly: '카드 등록 없이 바로 시작', yearly: '언제든 Pro로 전환 가능' },
|
note: { monthly: '카드 등록 없이 바로 시작', yearly: '언제든 Pro로 전환 가능' },
|
||||||
featuresLabel: '바로 사용할 수 있는 기능',
|
featuresLabel: '바로 사용할 수 있는 기능',
|
||||||
features: [
|
features: [
|
||||||
'학습 주제 3개 등록',
|
'학습 과목 3개까지 등록',
|
||||||
'학습 기록 무제한 저장',
|
'학습 기록 무제한 저장',
|
||||||
'매일 50개 적응형 복습 큐',
|
'일일 복습 큐 10개',
|
||||||
'기본 망각 곡선 적용',
|
'PDF 문제집 월 1회 업로드',
|
||||||
|
'샘플 문제집 열람',
|
||||||
|
'기본 복습 캘린더',
|
||||||
'학습 페르소나 4종 선택',
|
'학습 페르소나 4종 선택',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -63,41 +65,41 @@ const tiers: Tier[] = [
|
|||||||
summary: '시험 준비와 장기 학습을 본격화하는 추천 플랜',
|
summary: '시험 준비와 장기 학습을 본격화하는 추천 플랜',
|
||||||
description: '문제 풀이부터 복습 분석까지, 꾸준한 성장을 위한 기능을 모두 엽니다.',
|
description: '문제 풀이부터 복습 분석까지, 꾸준한 성장을 위한 기능을 모두 엽니다.',
|
||||||
cta: 'Pro 시작하기',
|
cta: 'Pro 시작하기',
|
||||||
price: { monthly: '7,900', yearly: '79,000' },
|
price: { monthly: '2,900', yearly: '24,900' },
|
||||||
period: { monthly: '/ 월', yearly: '/ 연' },
|
period: { monthly: '/ 월', yearly: '/ 연' },
|
||||||
note: {
|
note: {
|
||||||
monthly: '월 단위로 유연하게 사용',
|
monthly: '월 단위로 유연하게 사용',
|
||||||
yearly: '연간 결제 시 2개월 무료 혜택',
|
yearly: '연간 결제 시 약 30% 할인',
|
||||||
},
|
},
|
||||||
highlight: true,
|
highlight: true,
|
||||||
featuresLabel: 'Free의 모든 기능 포함, 추가로',
|
featuresLabel: 'Free의 모든 기능 포함, 추가로',
|
||||||
features: [
|
features: [
|
||||||
'학습 주제 무제한 등록',
|
'학습 과목 무제한 등록',
|
||||||
'기출 문제집 연동',
|
'복습 큐 무제한',
|
||||||
'마스터리 경로 뷰와 예상 정답률 예측',
|
'기출 문제집 전체 연동',
|
||||||
'PDF 문제/자료 이미지 업로드',
|
'PDF 문제집 무제한 업로드',
|
||||||
|
'마스터리 경로 뷰 + 예상 정답률',
|
||||||
'단원별 상세 분석 리포트',
|
'단원별 상세 분석 리포트',
|
||||||
'데이터 내보내기와 알림 위젯',
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'School',
|
name: 'School',
|
||||||
summary: '학원, 스터디, 클래스 운영을 위한 플랜',
|
summary: '학원, 학교, 교습소 운영을 위한 플랜',
|
||||||
description: '학습자 관리, 단체 분석, 보고서 생성까지 한 번에 운영할 수 있습니다.',
|
description: '학습자 관리, 과제 배정, 리포트까지 한 번에 운영할 수 있습니다.',
|
||||||
cta: 'School 시작하기',
|
cta: 'School 시작하기',
|
||||||
price: { monthly: '20,000', yearly: '200,000' },
|
price: { monthly: '2,900', yearly: '24,900' },
|
||||||
period: { monthly: '/ 월', yearly: '/ 연' },
|
period: { monthly: '/ 학생·월', yearly: '/ 학생·연' },
|
||||||
note: {
|
note: {
|
||||||
monthly: '기본 30명 시트 포함',
|
monthly: '학생 수에 따라 유연하게',
|
||||||
yearly: '연간 운영 예산에 맞춘 단체 플랜',
|
yearly: '연간 결제 시 약 30% 할인',
|
||||||
},
|
},
|
||||||
featuresLabel: 'Pro의 모든 기능 포함, 추가로',
|
featuresLabel: 'Pro의 모든 기능 포함, 추가로',
|
||||||
features: [
|
features: [
|
||||||
'기본 학습자 30명 시트 제공',
|
'교사 대시보드',
|
||||||
'운영자 대시보드',
|
'과제 관리 (일괄 + 개인 지정)',
|
||||||
'그룹 통계 및 진도 비교',
|
'학생별 풀이 상세 확인',
|
||||||
'학습자별 페르소나/주기 일괄 조정',
|
'학급 리포트 + 성적 비교',
|
||||||
'월간 통합 리포트',
|
'선생님 필기/해설 공유',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -130,7 +132,7 @@ const comparisonSections: Array<{ title: string; rows: ComparisonRow[] }> = [
|
|||||||
title: '학습',
|
title: '학습',
|
||||||
rows: [
|
rows: [
|
||||||
{
|
{
|
||||||
feature: '등록 가능 학습 주제 수',
|
feature: '등록 가능 과목 수',
|
||||||
free: { type: 'text', value: '3개' },
|
free: { type: 'text', value: '3개' },
|
||||||
pro: { type: 'text', value: '무제한' },
|
pro: { type: 'text', value: '무제한' },
|
||||||
school: { type: 'text', value: '무제한' },
|
school: { type: 'text', value: '무제한' },
|
||||||
@@ -142,11 +144,23 @@ const comparisonSections: Array<{ title: string; rows: ComparisonRow[] }> = [
|
|||||||
school: { type: 'text', value: '무제한' },
|
school: { type: 'text', value: '무제한' },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
feature: '일일 적응형 복습 큐',
|
feature: '일일 복습 큐',
|
||||||
free: { type: 'text', value: '50개' },
|
free: { type: 'text', value: '10개' },
|
||||||
pro: { type: 'text', value: '무제한' },
|
pro: { type: 'text', value: '무제한' },
|
||||||
school: { type: 'text', value: '무제한' },
|
school: { type: 'text', value: '무제한' },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
feature: 'PDF 문제집 업로드',
|
||||||
|
free: { type: 'text', value: '월 1회' },
|
||||||
|
pro: { type: 'text', value: '무제한' },
|
||||||
|
school: { type: 'text', value: '무제한' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
feature: '기출 문제집 연동',
|
||||||
|
free: { type: 'text', value: '샘플만' },
|
||||||
|
pro: { type: 'check' },
|
||||||
|
school: { type: 'check' },
|
||||||
|
},
|
||||||
{
|
{
|
||||||
feature: '학습 페르소나 선택',
|
feature: '학습 페르소나 선택',
|
||||||
free: { type: 'text', value: '4종' },
|
free: { type: 'text', value: '4종' },
|
||||||
@@ -158,15 +172,9 @@ const comparisonSections: Array<{ title: string; rows: ComparisonRow[] }> = [
|
|||||||
{
|
{
|
||||||
title: '분석',
|
title: '분석',
|
||||||
rows: [
|
rows: [
|
||||||
{
|
|
||||||
feature: '망각 곡선 모델',
|
|
||||||
free: { type: 'text', value: '기본' },
|
|
||||||
pro: { type: 'text', value: '페르소나 맞춤형' },
|
|
||||||
school: { type: 'text', value: '페르소나 맞춤형' },
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
feature: '마스터리 경로 뷰',
|
feature: '마스터리 경로 뷰',
|
||||||
free: { type: 'dash' },
|
free: { type: 'text', value: '미리보기' },
|
||||||
pro: { type: 'check' },
|
pro: { type: 'check' },
|
||||||
school: { type: 'check' },
|
school: { type: 'check' },
|
||||||
},
|
},
|
||||||
@@ -182,46 +190,34 @@ const comparisonSections: Array<{ title: string; rows: ComparisonRow[] }> = [
|
|||||||
pro: { type: 'dash' },
|
pro: { type: 'dash' },
|
||||||
school: { type: 'check' },
|
school: { type: 'check' },
|
||||||
},
|
},
|
||||||
{
|
|
||||||
feature: '주간 요약과 알림',
|
|
||||||
free: { type: 'dash' },
|
|
||||||
pro: { type: 'check' },
|
|
||||||
school: { type: 'check' },
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '기타',
|
title: '학교/학원',
|
||||||
rows: [
|
rows: [
|
||||||
{
|
{
|
||||||
feature: '기출/문제집 연동',
|
feature: '교사 대시보드',
|
||||||
free: { type: 'dash' },
|
|
||||||
pro: { type: 'check' },
|
|
||||||
school: { type: 'check' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
feature: 'PDF 문제 업로드',
|
|
||||||
free: { type: 'dash' },
|
|
||||||
pro: { type: 'check' },
|
|
||||||
school: { type: 'check' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
feature: '데이터 내보내기',
|
|
||||||
free: { type: 'dash' },
|
|
||||||
pro: { type: 'check' },
|
|
||||||
school: { type: 'check' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
feature: '선생님 관리자 대시보드',
|
|
||||||
free: { type: 'dash' },
|
free: { type: 'dash' },
|
||||||
pro: { type: 'dash' },
|
pro: { type: 'dash' },
|
||||||
school: { type: 'check' },
|
school: { type: 'check' },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
feature: '우선 지원',
|
feature: '과제 관리 (일괄 + 개인)',
|
||||||
free: { type: 'dash' },
|
free: { type: 'dash' },
|
||||||
pro: { type: 'text', value: '일반' },
|
pro: { type: 'dash' },
|
||||||
school: { type: 'text', value: '1영업일 내' },
|
school: { type: 'check' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
feature: '학생 풀이 상세 확인',
|
||||||
|
free: { type: 'dash' },
|
||||||
|
pro: { type: 'dash' },
|
||||||
|
school: { type: 'check' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
feature: '선생님 해설 공유',
|
||||||
|
free: { type: 'dash' },
|
||||||
|
pro: { type: 'dash' },
|
||||||
|
school: { type: 'check' },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -231,7 +227,7 @@ const faqItems = [
|
|||||||
{
|
{
|
||||||
question: '연간 결제로 바꾸면 언제 할인 적용이 되나요?',
|
question: '연간 결제로 바꾸면 언제 할인 적용이 되나요?',
|
||||||
answer:
|
answer:
|
||||||
'즉시 적용됩니다. 연간 플랜으로 전환하는 순간 2개월 무료가 반영된 가격으로 청구되고, 남아 있는 월간 사용 기간은 일할 계산해 차감합니다.',
|
'즉시 적용됩니다. 연간 플랜으로 전환하는 순간 약 30% 할인이 반영된 가격으로 청구되고, 남아 있는 월간 사용 기간은 일할 계산해 차감합니다.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
question: 'Free에서 Pro로 올리면 기존 데이터가 유지되나요?',
|
question: 'Free에서 Pro로 올리면 기존 데이터가 유지되나요?',
|
||||||
@@ -376,8 +372,8 @@ function PricingContent({ appMode, me, meLoading, billingToast }: PricingContent
|
|||||||
나에게 맞는 <HeroGradient>플랜을 선택하세요</HeroGradient>
|
나에게 맞는 <HeroGradient>플랜을 선택하세요</HeroGradient>
|
||||||
</HeroTitle>
|
</HeroTitle>
|
||||||
<HeroSubtitle>
|
<HeroSubtitle>
|
||||||
Free로 가볍게 시작하고, 필요할 때 Pro와 School로 확장할 수 있습니다. 시험
|
Free로 가볍게 시작하고, 필요할 때 Pro와 School로 확장할 수 있습니다. 월
|
||||||
준비, 언어 학습, 취미 공부까지 반복의 핵심은 모든 플랜에 동일하게 포함됩니다.
|
2,900원, 편의점 음료 2개 값으로 무제한 복습과 AI 분석을 시작하세요.
|
||||||
</HeroSubtitle>
|
</HeroSubtitle>
|
||||||
|
|
||||||
<BillingToggle aria-label="결제 주기 선택">
|
<BillingToggle aria-label="결제 주기 선택">
|
||||||
@@ -397,7 +393,7 @@ function PricingContent({ appMode, me, meLoading, billingToast }: PricingContent
|
|||||||
>
|
>
|
||||||
연간
|
연간
|
||||||
<DiscountBadge $visible={isYearly || billing === 'monthly'}>
|
<DiscountBadge $visible={isYearly || billing === 'monthly'}>
|
||||||
2개월 무료
|
약 30% 할인
|
||||||
</DiscountBadge>
|
</DiscountBadge>
|
||||||
</ToggleButton>
|
</ToggleButton>
|
||||||
</BillingToggle>
|
</BillingToggle>
|
||||||
@@ -408,8 +404,7 @@ function PricingContent({ appMode, me, meLoading, billingToast }: PricingContent
|
|||||||
<SectionEyebrow id="pricing-heading">Pricing</SectionEyebrow>
|
<SectionEyebrow id="pricing-heading">Pricing</SectionEyebrow>
|
||||||
<SectionTitle>세 가지 플랜, 하나의 복습 철학</SectionTitle>
|
<SectionTitle>세 가지 플랜, 하나의 복습 철학</SectionTitle>
|
||||||
<SectionBody>
|
<SectionBody>
|
||||||
월간은 유연하게, 연간은 더 경제적으로. 연간 결제 시 Pro와 School은
|
월간은 유연하게, 연간은 더 경제적으로. 연간 결제 시 약 30% 할인이 적용됩니다.
|
||||||
2개월 무료 혜택이 반영됩니다.
|
|
||||||
</SectionBody>
|
</SectionBody>
|
||||||
</SectionHeader>
|
</SectionHeader>
|
||||||
|
|
||||||
|
|||||||
@@ -373,6 +373,22 @@ function ProfileBody() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleTierSwitch(tier: 'free' | 'pro' | 'school') {
|
||||||
|
if (!userRef.current || userRef.current.subscriptionTier === tier) return;
|
||||||
|
setSavingKey('tier');
|
||||||
|
try {
|
||||||
|
const response = await api.patch<MeUser>('/me/subscription-tier', { tier });
|
||||||
|
const nextUser: MeUser = { ...response.data, focusUnits: response.data.focusUnits ?? [] };
|
||||||
|
userRef.current = nextUser;
|
||||||
|
setUser(nextUser);
|
||||||
|
showToast({ message: `${planLabel(tier)}으로 전환했어요`, variant: 'success' });
|
||||||
|
} catch {
|
||||||
|
showToast({ message: '플랜 전환에 실패했습니다', variant: 'danger' });
|
||||||
|
} finally {
|
||||||
|
setSavingKey((current) => (current === 'tier' ? null : current));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function handleLogout() {
|
function handleLogout() {
|
||||||
clearToken();
|
clearToken();
|
||||||
router.replace('/login');
|
router.replace('/login');
|
||||||
@@ -649,17 +665,39 @@ function ProfileBody() {
|
|||||||
<Icon name="crown" size={18} />
|
<Icon name="crown" size={18} />
|
||||||
플랜 관리
|
플랜 관리
|
||||||
</PlanManagementLabel>
|
</PlanManagementLabel>
|
||||||
<PlanManagementTitle>현재 플랜을 확인하고 변경하세요</PlanManagementTitle>
|
<PlanManagementTitle>프로토타입 모드 — 플랜 자유 전환</PlanManagementTitle>
|
||||||
<PlanManagementDesc>
|
<PlanManagementDesc>
|
||||||
현재 이용 중인 요금제와 업그레이드 옵션을 `/pricing`에서 바로 확인할 수
|
프로토타입이므로 결제 없이 Free / Pro / School을 자유롭게 전환할 수 있습니다.
|
||||||
있습니다.
|
|
||||||
</PlanManagementDesc>
|
</PlanManagementDesc>
|
||||||
</PlanManagementMeta>
|
</PlanManagementMeta>
|
||||||
<PlanManagementActions>
|
<PlanManagementActions>
|
||||||
<PlanTierBadge>{planLabel(user.subscriptionTier)}</PlanTierBadge>
|
<TierSwitcher>
|
||||||
<Button as={Link} href="/pricing" $variant="secondary">
|
<TierSwitchBtn
|
||||||
플랜 변경
|
type="button"
|
||||||
</Button>
|
$active={user.subscriptionTier === 'free'}
|
||||||
|
onClick={() => void handleTierSwitch('free')}
|
||||||
|
disabled={savingKey === 'tier'}
|
||||||
|
>
|
||||||
|
Free
|
||||||
|
</TierSwitchBtn>
|
||||||
|
<TierSwitchBtn
|
||||||
|
type="button"
|
||||||
|
$active={user.subscriptionTier === 'pro'}
|
||||||
|
$accent
|
||||||
|
onClick={() => void handleTierSwitch('pro')}
|
||||||
|
disabled={savingKey === 'tier'}
|
||||||
|
>
|
||||||
|
Pro
|
||||||
|
</TierSwitchBtn>
|
||||||
|
<TierSwitchBtn
|
||||||
|
type="button"
|
||||||
|
$active={user.subscriptionTier === 'school'}
|
||||||
|
onClick={() => void handleTierSwitch('school')}
|
||||||
|
disabled={savingKey === 'tier'}
|
||||||
|
>
|
||||||
|
School
|
||||||
|
</TierSwitchBtn>
|
||||||
|
</TierSwitcher>
|
||||||
</PlanManagementActions>
|
</PlanManagementActions>
|
||||||
</PlanManagementBody>
|
</PlanManagementBody>
|
||||||
</PlanManagementCard>
|
</PlanManagementCard>
|
||||||
@@ -1336,3 +1374,37 @@ const AlgoDesc = styled.p`
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const TierSwitcher = styled.div`
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const TierSwitchBtn = styled.button<{ $active: boolean; $accent?: boolean }>`
|
||||||
|
padding: 10px 18px;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid ${({ $active, $accent }) =>
|
||||||
|
$active
|
||||||
|
? $accent ? 'rgba(99, 102, 241, 0.6)' : 'rgba(255, 255, 255, 0.3)'
|
||||||
|
: theme.color.borderSoftAlpha};
|
||||||
|
background: ${({ $active, $accent }) =>
|
||||||
|
$active
|
||||||
|
? $accent ? 'rgba(79, 70, 229, 0.25)' : 'rgba(255, 255, 255, 0.1)'
|
||||||
|
: 'rgba(255, 255, 255, 0.04)'};
|
||||||
|
color: ${({ $active }) => $active ? theme.color.textBright : theme.color.textSub};
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s;
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
background: rgba(79, 70, 229, 0.15);
|
||||||
|
color: ${theme.color.textBright};
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|||||||
Reference in New Issue
Block a user