master-project
CHAPTER 21 / 50
읽기 약 2분
FUNCTION
설정 페이지: 프로필/알림/결제
핵심 개념
Tabs 기반 설정 — 프로필 변경·이메일 인증·알림 toggle·구독 관리·계정 삭제.
본문
설정 페이지 구조
// src/app/(app)/settings/page.tsx
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { ProfileTab } from './profile-tab'
import { NotificationTab } from './notification-tab'
import { BillingTab } from './billing-tab'
import { DangerTab } from './danger-tab'
export default function SettingsPage() {
return (
<div className="max-w-3xl">
<h1 className="text-2xl font-bold mb-6">설정</h1>
<Tabs defaultValue="profile">
<TabsList>
<TabsTrigger value="profile">프로필</TabsTrigger>
<TabsTrigger value="notifications">알림</TabsTrigger>
<TabsTrigger value="billing">결제</TabsTrigger>
<TabsTrigger value="danger">계정</TabsTrigger>
</TabsList>
<TabsContent value="profile"><ProfileTab /></TabsContent>
<TabsContent value="notifications"><NotificationTab /></TabsContent>
<TabsContent value="billing"><BillingTab /></TabsContent>
<TabsContent value="danger"><DangerTab /></TabsContent>
</Tabs>
</div>
)
}프로필 탭
// src/app/(app)/settings/profile-tab.tsx
'use client'
import { useFormState } from 'react-dom'
import { updateProfile } from './actions'
import { toast } from 'sonner'
export function ProfileTab() {
const [state, action] = useFormState(updateProfile, null)
return (
<form action={action} className="space-y-4 max-w-md">
<div>
<label className="block text-sm mb-1">이메일</label>
<input disabled className="w-full px-3 py-2 border rounded bg-muted" />
<p className="text-xs text-muted-foreground mt-1">이메일 변경은 지원팀 문의</p>
</div>
<div>
<label className="block text-sm mb-1">이름</label>
<input name="name" className="w-full px-3 py-2 border rounded" />
</div>
<div>
<label className="block text-sm mb-1">아바타</label>
<input name="avatar" type="file" accept="image/*" />
</div>
<button className="bg-primary text-primary-foreground px-4 py-2 rounded">
저장
</button>
{state?.success && toast.success('저장됨')}
{state?.error && toast.error(state.error)}
</form>
)
}알림 탭 (Toggle)
'use client'
import { Switch } from '@/components/ui/switch'
export function NotificationTab() {
const [prefs, setPrefs] = useState({
email_marketing: true,
email_product: true,
push_enabled: false,
})
const update = async (key: string, value: boolean) => {
setPrefs(p => ({ ...p, [key]: value }))
await updatePreferences({ [key]: value })
}
return (
<ul className="space-y-4 max-w-md">
<li className="flex items-center justify-between border-b pb-3">
<div>
<p className="font-medium">마케팅 이메일</p>
<p className="text-xs text-muted-foreground">신기능·할인 안내</p>
</div>
<Switch checked={prefs.email_marketing} onCheckedChange={v => update('email_marketing', v)} />
</li>
<li className="flex items-center justify-between border-b pb-3">
<div>
<p className="font-medium">제품 알림</p>
<p className="text-xs text-muted-foreground">결제·구독 변경 시</p>
</div>
<Switch checked={prefs.email_product} onCheckedChange={v => update('email_product', v)} />
</li>
</ul>
)
}결제 탭
// src/app/(app)/settings/billing-tab.tsx
import { createClient } from '@/lib/supabase/server'
import { CreatePortalButton } from './create-portal-button'
export async function BillingTab() {
const supabase = await createClient()
const { data: sub } = await supabase
.from('subscriptions')
.select('*')
.single()
return (
<div className="space-y-6 max-w-md">
<div className="border rounded-lg p-4">
<p className="text-sm text-muted-foreground">현재 플랜</p>
<p className="text-2xl font-bold capitalize">{sub?.plan ?? 'Free'}</p>
{sub?.current_period_end && (
<p className="text-sm">갱신: {new Date(sub.current_period_end).toLocaleDateString('ko')}</p>
)}
</div>
{sub?.plan === 'free' ? (
<a href="/pricing" className="block bg-primary text-primary-foreground text-center py-2 rounded">
업그레이드
</a>
) : (
<CreatePortalButton /> {/* CH.44 Stripe Customer Portal */}
)}
</div>
)
}계정 삭제 (Danger Zone)
'use client'
import { useState } from 'react'
import { Dialog, DialogContent, DialogTrigger } from '@/components/ui/dialog'
export function DangerTab() {
const [confirm, setConfirm] = useState('')
return (
<div className="border border-red-500/30 rounded-lg p-6 max-w-md">
<h3 className="font-bold text-red-500 mb-2">계정 삭제</h3>
<p className="text-sm text-muted-foreground mb-4">
모든 데이터가 영구 삭제됩니다. 되돌릴 수 없습니다.
</p>
<Dialog>
<DialogTrigger className="bg-red-500 text-white px-3 py-1.5 rounded text-sm">
계정 삭제
</DialogTrigger>
<DialogContent>
<h3 className="font-semibold mb-3">정말 삭제하시겠습니까?</h3>
<p className="text-sm mb-3">
확인을 위해 <strong>DELETE</strong>를 입력하세요:
</p>
<input value={confirm} onChange={e => setConfirm(e.target.value)} className="w-full px-3 py-2 border rounded" />
<button
disabled={confirm !== 'DELETE'}
onClick={deleteAccount}
className="mt-3 bg-red-500 text-white px-3 py-1.5 rounded disabled:opacity-50"
>
영구 삭제
</button>
</DialogContent>
</Dialog>
</div>
)
}다음 챕터
CH.22 "다크 모드 + 반응형 완성".
AI 프롬프트
🤖 AI에게 잘 물어보는 법 — 모델·전략별 프롬프트
Claude
무료: Sonnet 4.6 / Pro $20/mo: Opus 4.6
내 마스터 프로젝트의 설정 페이지 부분을 분석해서 실전 적용 + 개선 우선순위 3가지를 알려줘.
ChatGPT
무료: GPT-5.5 / Plus $20/mo: GPT-5.5 Pro
설정 페이지 관련 모범 사례·안티패턴 5개를 비교 분석해서 실전 적용를 위한 추천 방안을 알려줘.
Gemini
무료: 2.5 Flash / Pro $19.99/mo: 3.1 Pro
내 프로젝트 전체에서 설정 페이지 최적화 가능 위치와 리스크를 보고해줘.
Grok
무료: Grok 4.1 / SuperGrok $30/mo
2026년 한국 1인 개발자 시장의 설정 페이지 트렌드와 차별화 포인트를 정리해줘.
⭐ 이것만 기억하세요
설정 페이지: 프로필/알림/결제는 이 3가지만 확실히 잡으세요
1.Tabs로 4개 섹션 분리 = 일반적 설정 패턴
2.Danger Zone에서 명시적 확인 (DELETE 타이핑)
3.다음 챕터에서 다크모드·반응형 마감
공유하기
진행도 21 / 50