OPEN HYPER STEP
← 목록으로 (stack-analysis)
STACK-ANALYSIS · 89 / 90
stack-analysis
CHAPTER 89 / 90
읽기 약 2
FUNCTION

프로필/설정/개인정보


핵심 개념

프로필 편집·privacy·GDPR·계정 삭제·데이터 다운로드 — 사용자 컨트롤.

본문

프로필 모델

SQL📋 코드 (13줄)
ALTER TABLE users ADD COLUMN bio TEXT;
ALTER TABLE users ADD COLUMN website VARCHAR;
ALTER TABLE users ADD COLUMN location VARCHAR;
ALTER TABLE users ADD COLUMN birthday DATE;
ALTER TABLE users ADD COLUMN gender VARCHAR;
ALTER TABLE users ADD COLUMN cover_url VARCHAR;
ALTER TABLE users ADD COLUMN privacy_settings JSONB DEFAULT '{
  "profile_visibility": "public",
  "email_visible": false,
  "show_birthday": false,
  "allow_messages": "everyone",
  "indexable": true
}';

프로필 편집

TSX📋 코드 (58줄)
'use client';

const ProfileSchema = z.object({
  name: z.string().min(1).max(50),
  handle: z.string().regex(/^[a-z0-9_]{3,30}$/),
  bio: z.string().max(160).optional(),
  website: z.string().url().optional().or(z.literal('')),
  location: z.string().max(50).optional(),
  birthday: z.string().date().optional(),
});

function ProfileEditPage({ user }: { user: User }) {
  const { register, handleSubmit, formState: { errors, isDirty, isSubmitting } } =
    useForm<z.infer<typeof ProfileSchema>>({
      resolver: zodResolver(ProfileSchema),
      defaultValues: user,
    });

  const onSubmit = async (data: any) => {
    const res = await fetch('/api/me/profile', {
      method: 'PATCH',
      body: JSON.stringify(data),
    });
    if (!res.ok) {
      const err = await res.json();
      if (err.code === 'handle_taken') {
        toast.error('이미 사용 중인 사용자명');
      }
      return;
    }
    toast.success('프로필 업데이트됨');
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <AvatarUpload userId={user.id} currentUrl={user.avatarUrl} />
      <CoverUpload userId={user.id} currentUrl={user.coverUrl} />

      <Field label="이름" error={errors.name?.message}>
        <input {...register('name')} />
      </Field>

      <Field label="사용자명" error={errors.handle?.message}>
        <input {...register('handle')} />
        <span className="text-sm">openhyperstep.com/@{watch('handle')}</span>
      </Field>

      <Field label="소개" error={errors.bio?.message}>
        <textarea {...register('bio')} rows={3} />
        <CharCount value={watch('bio') ?? ''} max={160} />
      </Field>

      <button disabled={!isDirty || isSubmitting}>
        {isSubmitting ? '저장 중...' : '저장'}
      </button>
    </form>
  );
}

Privacy Settings

TSX📋 코드 (48줄)
function PrivacySettings({ user }: { user: User }) {
  const updateSetting = async (key: string, value: any) => {
    await fetch('/api/me/privacy', {
      method: 'PATCH',
      body: JSON.stringify({ [key]: value }),
    });
  };

  return (
    <div className="space-y-6">
      <Setting
        label="프로필 공개 범위"
        value={user.privacySettings.profile_visibility}
        options={[
          { value: 'public', label: '공개' },
          { value: 'followers', label: '팔로워만' },
          { value: 'private', label: '비공개' },
        ]}
        onChange={v => updateSetting('profile_visibility', v)}
      />

      <Toggle
        label="이메일 공개"
        description="다른 사용자가 내 이메일을 볼 수 있습니다"
        value={user.privacySettings.email_visible}
        onChange={v => updateSetting('email_visible', v)}
      />

      <Setting
        label="DM 받기"
        value={user.privacySettings.allow_messages}
        options={[
          { value: 'everyone', label: '누구나' },
          { value: 'following', label: '팔로잉만' },
          { value: 'no_one', label: '받지 않음' },
        ]}
        onChange={v => updateSetting('allow_messages', v)}
      />

      <Toggle
        label="검색엔진 인덱싱 허용"
        description="Google에서 내 프로필이 검색되도록"
        value={user.privacySettings.indexable}
        onChange={v => updateSetting('indexable', v)}
      />
    </div>
  );
}

데이터 다운로드 (GDPR)

TYPESCRIPT📋 코드 (54줄)
// 사용자가 자신의 모든 데이터 요청
app.post('/api/me/data-export', authenticate, async (req, res) => {
  const userId = req.user!.id;

  // 비동기 처리 — 큐
  await dataExportQueue.add('export', { userId });

  res.json({
    ok: true,
    message: '데이터 준비 중. 24시간 내 이메일로 발송됩니다.',
  });
});


const exportWorker = new Worker('data-export', async (job) => {
  const { userId } = job.data;
  const user = await db.user.findUnique({ where: { id: userId } });

  // 모든 사용자 데이터 수집
  const data = {
    profile: user,
    posts: await db.post.findMany({ where: { userId } }),
    comments: await db.comment.findMany({ where: { userId } }),
    likes: await db.like.findMany({ where: { userId } }),
    messages: await db.message.findMany({ where: { senderId: userId } }),
    follows: await db.follow.findMany({ where: { followerId: userId } }),
    // ...
  };

  // ZIP 생성
  const archive = createZip();
  archive.append(JSON.stringify(data, null, 2), { name: 'data.json' });

  // 미디어 파일도
  for (const post of data.posts) {
    if (post.mediaUrl) {
      const file = await s3.send(new GetObjectCommand({ Bucket, Key: post.mediaUrl }));
      archive.append(file.Body, { name: `media/${post.id}.jpg` });
    }
  }

  // S3 업로드 (24시간 후 자동 삭제)
  const exportKey = `exports/${userId}/${Date.now()}.zip`;
  await s3.send(new PutObjectCommand({
    Bucket, Key: exportKey, Body: archive, Tagging: 'expires=24h',
  }));

  // 이메일 발송 (presigned URL)
  const url = await getSignedUrl(s3, new GetObjectCommand({ Bucket, Key: exportKey }), {
    expiresIn: 24 * 3600,
  });

  await emailQueue.add('data-export-ready', { email: user!.email, url });
});

계정 삭제 (Right to Erasure)

TYPESCRIPT📋 코드 (40줄)
async function deleteAccount(userId: string, reason?: string) {
  // 1. 즉시 비활성화
  await db.user.update({
    where: { id: userId },
    data: {
      status: 'deleted',
      email: `deleted_${userId}@example.com`,
      handle: `deleted_${userId}`,
      name: '탈퇴한 사용자',
      avatarUrl: null,
      bio: null,
    },
  });

  // 2. 비동기로 데이터 정리 (30일 그레이스)
  await deletionQueue.add('purge', { userId, reason }, {
    delay: 30 * 86400 * 1000,
  });
}


const deletionWorker = new Worker('purge', async (job) => {
  const { userId } = job.data;

  // 작성한 콘텐츠 → 익명화 또는 삭제
  await db.post.deleteMany({ where: { userId } });
  await db.comment.updateMany({
    where: { userId },
    data: { content: '[삭제됨]', userId: ANONYMOUS_USER_ID },
  });

  // 미디어 파일 S3 삭제
  const media = await db.media.findMany({ where: { userId } });
  for (const m of media) {
    await s3.send(new DeleteObjectCommand({ Bucket, Key: m.s3Key }));
  }

  // 계정 완전 삭제
  await db.user.delete({ where: { id: userId } });
});

다음 챕터

CH.90 "커뮤니티 종합".


AI 프롬프트
🤖 AI에게 잘 물어보는 법 — 모델·전략별 프롬프트
Claude

무료: Sonnet 4.6 / Pro $20/mo: Opus 4.6

내 프로젝트의 프로필 설정 개인정보 부분을 분석해서
실전 분석 + 개선 우선순위를 알려줘.
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년 한국 풀스택 시장의
프로필 설정 개인정보 트렌드를 솔직히 알려줘.

⭐ 이것만 기억하세요
프로필/설정/개인정보 이 3가지만 확실히 잡으세요
1.프로필 편집은 Zod 검증 + handle uniqueness — 사용자명 충돌 방지
2.Privacy Settings는 JSONB 컬럼 — 유연한 설정 추가
3.GDPR 준수 = 데이터 다운로드 + 30일 그레이스 후 완전 삭제


공유하기
진행도 89 / 90