OPEN HYPER STEP
← 목록으로 (master-project)
MASTER-PROJECT · 21 / 50
master-project
CHAPTER 21 / 50
읽기 약 2
FUNCTION

설정 페이지: 프로필/알림/결제


핵심 개념

Tabs 기반 설정 — 프로필 변경·이메일 인증·알림 toggle·구독 관리·계정 삭제.

본문

설정 페이지 구조

TSX📋 코드 (26줄)
// 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>
  )
}

프로필 탭

TSX📋 코드 (32줄)
// 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)

TSX📋 코드 (34줄)
'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>
  )
}

결제 탭

TSX📋 코드 (31줄)
// 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)

TSX📋 코드 (34줄)
'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