{"id":"68ea332a1648b763ce66af58","created_date":"2025-10-11T10:36:26.991000","updated_date":"2026-03-23T09:43:01.832000","organization_id":"6899bc225cbc135eb3f9c034","app_type":"user_app","name":"[청취자분들리스트]","user_description":"","logo_url":"https://qtrypzzcjebvfcihiynt.supabase.co/storage/v1/object/public/base44-prod/public/68e8bfe54c5ec0e12ee25bfb/74f533a2f_KakaoTalk_20251010_124241349.jpg","avatar_index":null,"pages":{"Gallery":"\nimport React, { useState, useEffect, useCallback, useMemo } from \"react\";\nimport { UserGift, User, SelectedSticker } from \"@/entities/all\";\nimport { Card, CardContent } from \"@/components/ui/card\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Dialog, DialogContent, DialogHeader, DialogTitle } from \"@/components/ui/dialog\";\nimport { Input } from \"@/components/ui/input\";\nimport { Search, Lock, Sparkles, Trophy, Gift as GiftIcon, Settings, Pencil, RefreshCw, Trash2, User as UserIcon, Users as UsersIcon } from \"lucide-react\";\nimport { motion, AnimatePresence } from \"framer-motion\";\nimport { Button } from \"@/components/ui/button\";\nimport { Link } from \"react-router-dom\";\nimport { createPageUrl } from \"@/utils\";\nimport { getAdminGalleryData } from \"@/functions/getAdminGalleryData\";\nimport { syncExternalGifts } from \"@/functions/syncExternalGifts\";\nimport { checkFileChanges } from \"@/functions/checkFileChanges\";\nimport { updateGiftSender } from \"@/functions/updateGiftSender\";\nimport { deleteGift } from \"@/functions/deleteGift\";\nimport { removeStickerFromGallery } from \"@/functions/removeStickerFromGallery\"; // 스티커 제거 함수 import\n\nexport default function Gallery() {\n  const [userGifts, setUserGifts] = useState([]);\n  const [adminGifts, setAdminGifts] = useState([]);\n  const [selectedStickers, setSelectedStickers] = useState([]);\n  const [selectedGift, setSelectedGift] = useState(null);\n  const [currentUser, setCurrentUser] = useState(null);\n  const [isSendGiftOpen, setIsSendGiftOpen] = useState(false);\n  const [isLoading, setIsLoading] = useState(true);\n  const [galleryTitle, setGalleryTitle] = useState(\"🖤SOMI🖤 님의 스티커 갤러리\");\n  const [galleryDescription, setGalleryDescription] = useState(\"\");\n  const [isSyncing, setIsSyncing] = useState(false);\n  const [lastFileSize, setLastFileSize] = useState(null);\n  const [isCheckingFile, setIsCheckingFile] = useState(false);\n  const [error, setError] = useState(null);\n  const [isCollecting, setIsCollecting] = useState(false); // 자동 수집 진행 상태\n\n  const loadData = useCallback(async () => {\n    try {\n      setIsLoading(true);\n      setError(null);\n\n      let user = null;\n      try {\n        user = await User.me();\n        setCurrentUser(user);\n        console.log(\"로그인 사용자:\", user);\n        \n        // 최고 관리자가 아닌 모든 사용자는 청취자 관리 페이지로 즉시 리다이렉트\n        if (!user || user.email !== '102810aa@gmail.com') {\n          window.location.replace(createPageUrl('ListenerManagement'));\n          return;\n        }\n      } catch (error) {\n        console.log(\"로그인하지 않은 사용자 - 청취자 관리로 이동\");\n        window.location.replace(createPageUrl('ListenerManagement'));\n        return;\n      }\n\n      try {\n        console.log(\"관리자 갤러리 데이터 로딩 시작...\");\n        const response = await getAdminGalleryData();\n        console.log(\"관리자 갤러리 응답:\", response);\n\n        if (response?.data?.error) {\n          throw new Error(`관리자 데이터 로딩 실패: ${response.data.error}`);\n        }\n        \n        if (!response || !response.data) {\n          throw new Error('서버로부터 응답을 받지 못했습니다. 잠시 후 다시 시도해주세요.');\n        }\n\n        const adminData = response?.data?.data || response?.data;\n\n        if (adminData) {\n          console.log(\"올바르게 추출된 관리자 갤러리 데이터:\", adminData);\n          setGalleryTitle(adminData.galleryTitle || \"🖤SOMI🖤 님의 스티커 갤러리\");\n          setGalleryDescription(adminData.galleryDescription || \"\");\n          setAdminGifts(adminData.adminGifts || []);\n          console.log(\"관리자 선물 개수:\", adminData.adminGifts?.length || 0);\n        }\n      } catch (error) {\n        console.error(\"관리자 데이터 로딩 실패:\", error);\n        setError(error.message);\n      }\n\n      const stickers = await SelectedSticker.list('-display_order');\n      setSelectedStickers(stickers);\n      console.log(\"선택된 스티커 개수:\", stickers.length);\n\n      if (user) {\n        try {\n          const myGifts = await UserGift.filter({ created_by: user.email });\n          setUserGifts(myGifts || []);\n          console.log(\"사용자 선물 개수:\", myGifts?.length || 0);\n        } catch (error) {\n          console.error(\"선물 데이터 로딩 실패:\", error);\n          setUserGifts([]);\n        }\n      }\n\n    } catch (error) {\n      console.error(\"데이터 로딩 실패:\", error);\n      setError(error.message);\n      setSelectedStickers([]);\n    } finally {\n      setIsLoading(false);\n    }\n  }, []);\n\n  const refreshAdminGifts = useCallback(async () => {\n    try {\n      const adminDataResponse = await getAdminGalleryData();\n      if (adminDataResponse && adminDataResponse.data) {\n        const adminData = adminDataResponse?.data?.data || adminDataResponse?.data;\n        \n        if (adminData) {\n          setAdminGifts(currentGifts => {\n            const newGifts = adminData.adminGifts || [];\n            if (JSON.stringify(currentGifts) !== JSON.stringify(newGifts)) {\n              console.log(\"새로운 선물 데이터 발견! 갤러리를 업데이트합니다.\");\n              return newGifts;\n            }\n            return currentGifts;\n          });\n          // 성공 시 에러 메시지 초기화\n          if (error) setError(null);\n        }\n      }\n    } catch (error) {\n      console.error(\"실시간 동기화 중 오류 발생:\", error);\n      // 주기적인 백그라운드 작업이므로, 기존 에러가 없을 때만 에러 설정\n      if (!error) {\n          setError('실시간 동기화 중 오류가 발생했습니다. 잠시 후 다시 시도됩니다.');\n      }\n    }\n  }, [error]);\n\n  const checkForFileChanges = useCallback(async () => {\n    if (currentUser?.role !== 'admin' || isCheckingFile) return;\n\n    try {\n      setIsCheckingFile(true);\n      const result = await checkFileChanges();\n\n      if (result.data && result.data.success && result.data.fileInfo) {\n        const newFileSize = result.data.fileInfo.size;\n        if (lastFileSize !== null && newFileSize !== lastFileSize) {\n          console.log(`파일 크기 변화 감지! 자동 동기화를 실행합니다...`);\n          const syncResult = await syncExternalGifts();\n          if (syncResult.data && syncResult.data.success) {\n            console.log(`자동 동기화 완료: ${syncResult.data.syncedCount}개 선물 추가`);\n            await loadData();\n          }\n        }\n        setLastFileSize(newFileSize);\n        // 성공 시 에러 메시지 초기화\n        if (error) setError(null);\n      }\n    } catch (error) {\n      console.error(\"파일 변화 확인 실패:\", error);\n       if (!error) {\n          setError('외부 파일 변경 확인 중 오류가 발생했습니다.');\n       }\n    } finally {\n      setIsCheckingFile(false);\n    }\n  }, [currentUser?.role, lastFileSize, isCheckingFile, loadData, error]);\n\n  useEffect(() => {\n    loadData();\n    const intervalId = setInterval(refreshAdminGifts, 5000);\n    return () => clearInterval(intervalId);\n  }, [loadData, refreshAdminGifts]);\n\n  useEffect(() => {\n    let fileCheckInterval;\n    if (currentUser?.role === 'admin') {\n      const timeoutId = setTimeout(() => checkForFileChanges(), 5000);\n      fileCheckInterval = setInterval(checkForFileChanges, 10000);\n      return () => {\n        clearTimeout(timeoutId);\n        clearInterval(fileCheckInterval);\n      };\n    }\n  }, [currentUser?.role, checkForFileChanges]);\n\n  useEffect(() => {\n    const handleFocus = () => {\n      refreshAdminGifts();\n      if (currentUser?.role === 'admin') {\n        checkForFileChanges();\n      }\n    };\n    window.addEventListener('focus', handleFocus);\n    return () => window.removeEventListener('focus', handleFocus);\n  }, [refreshAdminGifts, checkForFileChanges, currentUser?.role]);\n\n  // '내가 받은 스티커' 자동 수집 로직\n  useEffect(() => {\n    const autoCollectStickers = async () => {\n      // 로그인한 사용자이고, 필요한 데이터가 모두 로드되었을 때만 실행\n      if (!currentUser || adminGifts.length === 0 || selectedStickers.length === 0 || isCollecting) {\n        return;\n      }\n\n      setIsCollecting(true); // 수집 시작 (잠금)\n\n      try {\n        // 1. 갤러리에서 활성화된 *고유한* 스티커 ID 목록을 만듭니다.\n        const unlockedStickerIds = [...new Set(adminGifts.map(g => g.gift_id))];\n        // 2. 내가 이미 가지고 있는 *고유한* 스티커 ID 목록을 만듭니다.\n        const myStickerIds = new Set(userGifts.map(g => g.gift_id));\n\n        // 3. 내가 아직 가지고 있지 않은, 새로 수집해야 할 스티커만 필터링합니다.\n        const newStickersToCollectData = unlockedStickerIds\n          .filter(stickerId => !myStickerIds.has(stickerId))\n          .map(stickerId => {\n              const originalSticker = selectedStickers.find(s => s.sticker_id === stickerId);\n              // adminGifts에서 해당 stickerId를 가진 첫 번째 선물만 사용 (received_from, received_at 등의 정보를 위해)\n              const unlockingGift = adminGifts.find(g => g.gift_id === stickerId);\n              if (originalSticker && unlockingGift) {\n                  return {\n                      gift_id: stickerId,\n                      received_from: unlockingGift.received_from,\n                      received_at: unlockingGift.received_at, // 원본 선물 시간 기록\n                      message: \"갤러리에서 자동 수집됨\",\n                      created_by: currentUser.email, // 내 이메일로 생성\n                      title: originalSticker.title,\n                      image_thumbnail: originalSticker.image_thumbnail,\n                      price: originalSticker.price,\n                  };\n              }\n              return null;\n          })\n          .filter(Boolean); // null 값 제거\n\n        // 4. 새로 수집할 스티커가 있을 경우에만 데이터베이스에 생성합니다.\n        if (newStickersToCollectData.length > 0) {\n          console.log(`${newStickersToCollectData.length}개의 새로운 스티커를 '내가 받은 스티커'에 추가합니다.`);\n          try {\n            // 여러 개를 동시에 생성 (Bulk Create)\n            const createdGifts = await UserGift.bulkCreate(newStickersToCollectData);\n            // 5. UI 상태를 즉시 업데이트합니다.\n            setUserGifts(prev => [...prev, ...createdGifts]);\n          } catch (error) {\n            console.error(\"자동 스티커 수집 중 오류 발생:\", error);\n          }\n        }\n      } finally {\n        setIsCollecting(false); // 수집 종료 (잠금 해제)\n      }\n    };\n\n    autoCollectStickers();\n  }, [adminGifts, currentUser, userGifts, selectedStickers, isCollecting]);\n\n\n  const handleGiftSent = (newGift) => {\n    if (newGift) {\n      setAdminGifts(prev => [...prev, newGift]);\n    }\n  };\n\n  const handleManualSync = async () => {\n    if (isSyncing) return;\n    setIsSyncing(true);\n    try {\n      const result = await syncExternalGifts();\n      if (result.data && result.data.success) {\n        alert(`동기화 완료: ${result.data.syncedCount}개의 새로운 선물이 추가되었습니다.`);\n        await loadData();\n      } else {\n        alert(`동기화 실패: ${result.data?.message || '알 수 없는 오류가 발생했습니다.'}`);\n      }\n    } catch (error) {\n      console.error(\"수동 동기화 실패:\", error);\n      alert(\"동기화 중 오류가 발생했습니다.\");\n    } finally {\n      setIsSyncing(false);\n    }\n  };\n\n  const handleDeleteGift = async (giftIdToDelete) => {\n    if (!isAdmin) return;\n\n    if (window.confirm(\"이 선물 기록을 정말 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.\")) {\n        try {\n            const result = await deleteGift({ giftId: giftIdToDelete });\n            if (result.data && result.data.success) {\n                setAdminGifts(prevGifts => prevGifts.filter(g => g.id !== giftIdToDelete));\n                setSelectedGift(null); \n            } else {\n                alert(`선물 삭제에 실패했습니다: ${result.data?.message || '알 수 없는 오류'}`);\n            }\n        } catch (error) {\n            console.error(\"선물 삭제 중 오류:\", error);\n            alert(\"선물 삭제 중 오류가 발생했습니다.\");\n        }\n    }\n  };\n  \n  const handleRemoveSticker = async (stickerToRemove) => {\n    if (!isAdmin || !stickerToRemove) return;\n\n    if (window.confirm(`'${stickerToRemove.title}' 스티커를 갤러리에서 완전히 제거하시겠습니까?\\n모든 선물 기록이 함께 삭제되며, 이 작업은 되돌릴 수 없습니다.`)) {\n        try {\n            const result = await removeStickerFromGallery({ stickerEntityId: stickerToRemove.id });\n\n            if (result.data && result.data.success) {\n                const { removedOriginalStickerId } = result.data;\n                setSelectedStickers(prev => prev.filter(s => s.id !== stickerToRemove.id));\n                setAdminGifts(prev => prev.filter(g => g.gift_id !== removedOriginalStickerId));\n                setSelectedGift(null); \n            } else {\n                alert(`스티커 제거에 실패했습니다: ${result.data?.message || '알 수 없는 오류'}`);\n            }\n        } catch (error) {\n            console.error(\"스티커 제거 중 오류:\", error);\n            alert(\"스티커 제거 중 오류가 발생했습니다.\");\n        }\n    }\n  };\n\n\n  const handleUpdateGiftSender = async (giftId, newSender) => {\n    if (!currentUser || currentUser.role !== 'admin') return;\n    try {\n      const result = await updateGiftSender({ giftId, newSender });\n      if (result.data && result.data.success) {\n        setAdminGifts(prevGifts => \n          prevGifts.map(gift => \n            gift.id === giftId ? { ...gift, received_from: newSender } : gift\n          )\n        );\n      } else {\n        alert(result.data?.message || '선물 닉네임 수정에 실패했습니다.');\n      }\n    } catch (error) {\n      alert('닉네임 수정 중 오류가 발생했습니다.');\n    }\n  };\n\n  const isGiftUnlocked = useCallback((stickerId) => {\n    return adminGifts.some(ug => ug.gift_id === String(stickerId));\n  }, [adminGifts]);\n\n  const unlockedCount = useMemo(() => {\n    return selectedStickers.filter(sticker => isGiftUnlocked(sticker.sticker_id)).length;\n  }, [selectedStickers, isGiftUnlocked]);\n\n  const totalGifts = selectedStickers.length;\n  const isLoggedIn = !!currentUser;\n  const isAdmin = currentUser?.role === 'admin';\n  // 최고 관리자 확인\n  const isSuperAdmin = currentUser?.email === '102810aa@gmail.com';\n\n  // 최고 관리자가 아니면 아무것도 렌더링하지 않음 (깜빡임 방지)\n  if (!isSuperAdmin && !isLoading) {\n    return null;\n  }\n\n  if (isLoading) {\n    return (\n      <div className=\"min-h-screen bg-gradient-to-br from-gray-900 via-black to-gray-900 text-white flex items-center justify-center\">\n        <div className=\"text-center\">\n          <div className=\"animate-spin rounded-full h-12 w-12 border-b-2 border-amber-500 mx-auto mb-4\"></div>\n          <p className=\"text-gray-400 mb-2\">갤러리를 불러오는 중...</p>\n        </div>\n      </div>\n    );\n  }\n\n  return (\n    <div className=\"min-h-screen bg-gradient-to-br from-gray-900 via-black to-gray-900 text-white\">\n      <div className=\"relative overflow-hidden\">\n        <div className=\"absolute inset-0 bg-gradient-to-r from-amber-500/20 to-orange-600/20 blur-3xl\" />\n        <div className=\"relative px-6 py-8\">\n          <div className=\"max-w-6xl mx-auto\">\n            <div className=\"flex items-center justify-between mb-6 flex-wrap gap-4\">\n              <div className=\"flex items-center gap-4\">\n                <div>\n                  <h1 className=\"text-3xl font-bold text-white\">\n                    {galleryTitle}\n                  </h1>\n                  {galleryDescription && (\n                    <p className=\"text-gray-400 mt-1\">\n                      {galleryDescription}\n                    </p>\n                  )}\n                  <div className=\"flex items-center gap-4 mt-2\">\n                    {isAdmin && (\n                      <Badge variant=\"outline\" className=\"border-red-500 text-red-400\">\n                        <Trophy className=\"w-4 h-4 mr-1\" />\n                        관리자\n                      </Badge>\n                    )}\n                    {!isLoggedIn && (\n                      <Badge variant=\"outline\" className=\"border-blue-500 text-blue-400\">\n                        방문자\n                      </Badge>\n                    )}\n                    <span className=\"text-gray-400\">\n                      {unlockedCount}/{totalGifts}\n                    </span>\n                    <span className=\"text-xs text-green-400\">\n                      (관리자 선물: {adminGifts.length})\n                    </span>\n                  </div>\n                </div>\n              </div>\n\n              <div className=\"flex flex-wrap items-center gap-2\">\n                {isLoggedIn && (\n                  <Link to={createPageUrl('MyStickers')}>\n                    <Button className=\"bg-purple-600 hover:bg-purple-700\">\n                      <UserIcon className=\"w-4 h-4 mr-2\" />\n                      내가 받은 스티커\n                    </Button>\n                  </Link>\n                )}\n\n                {isAdmin && (\n                  <>\n                    <Button onClick={handleManualSync} disabled={isSyncing} className=\"bg-green-600 hover:bg-green-700\">\n                      {isSyncing ? (\n                        <div className=\"animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2\"></div>\n                      ) : (\n                        <RefreshCw className=\"w-4 h-4 mr-2\" />\n                      )}\n                      {isSyncing ? '동기화 중...' : '강제 동기화'}\n                    </Button>\n                    <Link to={createPageUrl('ListenerManagement')}>\n                      <Button className=\"bg-indigo-600 hover:bg-indigo-700\">\n                        <UsersIcon className=\"w-4 h-4 mr-2\" />\n                        청취자 관리\n                      </Button>\n                    </Link>\n                    {isSuperAdmin && ( // 슈퍼 관리자만 접근 가능\n                      <Link to={createPageUrl('AdminSettings')}>\n                        <Button className=\"bg-blue-600 hover:bg-blue-700\">\n                          <Settings className=\"w-4 h-4 mr-2\" />\n                          갤러리 설정\n                        </Button>\n                      </Link>\n                    )}\n                    {isSuperAdmin && ( // 슈퍼 관리자만 접근 가능\n                      <Link to={createPageUrl('AdminPanel')}>\n                        <Button className=\"bg-blue-600 hover:bg-blue-700\">\n                          <Settings className=\"w-4 h-4 mr-2\" />\n                          스티커 관리\n                        </Button>\n                      </Link>\n                    )}\n                    <Button onClick={() => setIsSendGiftOpen(true)} className=\"bg-gradient-to-r from-amber-500 to-orange-600 text-white hover:opacity-90\">\n                      <GiftIcon className=\"w-4 h-4 mr-2\" />\n                      스티커 선물하기\n                    </Button>\n                  </>\n                )}\n\n                {!isLoggedIn && (\n                  <Button\n                    onClick={() => User.login()}\n                    className=\"bg-amber-600 hover:bg-amber-700\"\n                  >\n                    로그인\n                  </Button>\n                )}\n              </div>\n            </div>\n\n            {error && (\n              <div className=\"mt-4 p-4 bg-red-900/50 border border-red-500/50 rounded-lg text-red-300\">\n                <p className=\"font-bold\">오류가 발생했습니다</p>\n                <p className=\"text-xs mt-1\">{error}</p>\n              </div>\n            )}\n            \n            <div className=\"bg-gray-800 rounded-full h-2 my-6\">\n              <div\n                className=\"bg-gradient-to-r from-amber-500 to-orange-500 h-2 rounded-full transition-all duration-1000\"\n                style={{ width: `${totalGifts > 0 ? (unlockedCount / totalGifts) * 100 : 0}%` }}\n              />\n            </div>\n          </div>\n        </div>\n      </div>\n\n      <div className=\"max-w-6xl mx-auto px-6 py-8\">\n        <motion.div\n          layout\n          className=\"grid grid-cols-3 md:grid-cols-5 gap-4\"\n        >\n          <AnimatePresence>\n            {selectedStickers.map((sticker, index) => {\n              const adminGift = adminGifts.find(ug => ug.gift_id === String(sticker.sticker_id));\n              const personalGift = userGifts.find(ug => ug.gift_id === String(sticker.sticker_id));\n              \n              const displayGift = personalGift || adminGift;\n              \n              return (\n                <StickerCard\n                  key={sticker.sticker_id}\n                  sticker={sticker}\n                  isUnlocked={isGiftUnlocked(sticker.sticker_id)}\n                  userGift={displayGift}\n                  onClick={() => setSelectedGift(sticker)}\n                  index={index}\n                  isLoggedIn={isLoggedIn}\n                  isAdmin={isAdmin}\n                  onRemove={(e) => {\n                    e.stopPropagation();\n                    handleRemoveSticker(sticker);\n                  }}\n                />\n              );\n            })}\n          </AnimatePresence>\n        </motion.div>\n\n        {selectedStickers.length === 0 && (\n          <div className=\"text-center py-16\">\n            <div className=\"w-24 h-24 bg-gray-800 rounded-full flex items-center justify-center mx-auto mb-4\">\n              <Search className=\"w-12 h-12 text-gray-600\" />\n            </div>\n            <p className=\"text-gray-400 text-lg\">선택된 스티커가 없습니다</p>\n            {isAdmin && (\n              <Link to={createPageUrl('AdminPanel')}>\n                <Button className=\"mt-4 bg-blue-600 hover:bg-blue-700\">\n                  스티커 추가하러 가기\n                </Button>\n              </Link>\n            )}\n          </div>\n        )}\n      </div>\n\n      <StickerModal\n        sticker={selectedGift}\n        senders={\n          selectedGift\n            ? adminGifts\n                .filter(ug => ug.gift_id === String(selectedGift.sticker_id))\n                .sort((a, b) => new Date(a.received_at) - new Date(b.received_at))\n            : []\n        }\n        isUnlocked={selectedGift ? isGiftUnlocked(selectedGift.sticker_id) : false}\n        onClose={() => setSelectedGift(null)}\n        isLoggedIn={isLoggedIn}\n        isAdmin={isAdmin}\n        onUpdateSender={handleUpdateGiftSender}\n        onDeleteGift={handleDeleteGift}\n        onRemoveSticker={handleRemoveSticker}\n      />\n\n      {isAdmin && (\n        <SendStickerDialog\n          isOpen={isSendGiftOpen}\n          onClose={() => setIsSendGiftOpen(false)}\n          onGiftSent={handleGiftSent}\n          currentUser={currentUser}\n          selectedStickers={selectedStickers}\n        />\n      )}\n    </div>\n  );\n}\n\nconst StickerCard = React.memo(function StickerCard({ sticker, isUnlocked, userGift, onClick, index, isLoggedIn, isAdmin, onRemove }) {\n  \n  return (\n    <motion.div\n      layout\n      initial={{ opacity: 0, y: 20 }}\n      animate={{ opacity: 1, y: 0 }}\n      exit={{ opacity: 0, y: -20 }}\n      transition={{ delay: Math.min(index * 0.01, 0.3) }}\n    >\n      <Card\n        className={`cursor-pointer transition-all duration-200 hover:scale-105 border-2 aspect-[4/5] relative group\n          ${isUnlocked\n            ? 'border-amber-500 bg-gray-800/90 shadow-xl hover:shadow-2xl hover:border-amber-400'\n            : 'border-gray-700 bg-gray-800 opacity-60 hover:border-gray-600'\n          }`}\n        onClick={onClick}\n      >\n        {isAdmin && (\n          <Button\n            size=\"icon\"\n            variant=\"destructive\"\n            className=\"absolute top-1 right-1 z-10 w-6 h-6 rounded-full opacity-0 group-hover:opacity-100 transition-opacity\"\n            onClick={onRemove}\n            title=\"갤러리에서 스티커 제거\"\n          >\n            <Trash2 className=\"w-3 h-3\" />\n          </Button>\n        )}\n        <CardContent className=\"p-2 h-full\">\n          <div className=\"relative w-full h-full rounded-lg bg-gray-700 flex items-center justify-center overflow-hidden\">\n            {sticker.image_thumbnail ? (\n              <img\n                src={sticker.image_thumbnail}\n                alt={sticker.title}\n                className={`w-full h-full object-contain object-top ${!isUnlocked ? 'grayscale' : ''}`}\n                loading=\"lazy\"\n                onError={(e) => { e.target.style.display = 'none'; }}\n              />\n            ) : (\n              <div className=\"w-full h-full bg-gray-700 flex items-center justify-center\">\n                <span className={`text-xs ${isUnlocked ? 'text-gray-300' : 'text-gray-500'}`}>\n                  이미지 없음\n                </span>\n              </div>\n            )}\n\n            {!isUnlocked && (\n              <div className=\"absolute inset-0 bg-black/60 flex items-center justify-center\">\n                <Lock className=\"w-6 h-6 text-gray-400\" />\n              </div>\n            )}\n\n            {isUnlocked && (\n              <div className=\"absolute bottom-0 left-0 right-0 bg-black/70 backdrop-blur-sm\">\n                <p className=\"text-xs text-white font-medium text-center py-2 px-2 truncate\">\n                  {userGift?.received_from || \"관리자\"}\n                </p>\n              </div>\n            )}\n\n            {!isUnlocked && (\n              <div className=\"absolute bottom-0 left-0 right-0 bg-black/70 backdrop-blur-sm flex items-center justify-center\">\n                <Lock className=\"w-3 h-3 text-gray-400 mr-1\" />\n                <span className=\"text-xs text-gray-400 py-2\">미수집</span>\n              </div>\n            )}\n          </div>\n        </CardContent>\n      </Card>\n    </motion.div>\n  );\n});\n\nfunction StickerModal({ sticker, senders, isUnlocked, onClose, isLoggedIn, isAdmin, onUpdateSender, onDeleteGift, onRemoveSticker }) {\n  const [editingGiftId, setEditingGiftId] = useState(null);\n  const [editingValue, setEditingValue] = useState('');\n\n  if (!sticker) return null;\n  \n  const firstGift = senders && senders.length > 0 ? senders[0] : null;\n\n  const handleStartEdit = (gift) => {\n    setEditingGiftId(gift.id);\n    setEditingValue(gift.received_from);\n  };\n\n  const handleSaveEdit = async () => {\n    if (!editingValue.trim()) return;\n    \n    await onUpdateSender(editingGiftId, editingValue.trim());\n    setEditingGiftId(null);\n    setEditingValue('');\n  };\n\n  const handleCancelEdit = () => {\n    setEditingGiftId(null);\n    setEditingValue('');\n  };\n\n  return (\n    <Dialog open={!!sticker} onOpenChange={onClose}>\n      <DialogContent className=\"max-w-md bg-gray-900 border-gray-700 text-white\">\n        <DialogHeader>\n          <DialogTitle className=\"text-center text-xl font-bold bg-gradient-to-r from-amber-400 to-orange-400 bg-clip-text text-transparent\">\n            스티커 상세 정보\n          </DialogTitle>\n        </DialogHeader>\n\n        <div className=\"space-y-6\">\n          <div className=\"relative\">\n            <div className={`w-full h-48 rounded-xl bg-gray-700 flex items-center justify-center overflow-hidden`}>\n              {sticker.image_thumbnail ? (\n                <img\n                  src={sticker.image_thumbnail}\n                  alt={sticker.title}\n                  className={`w-full h-full object-contain ${!isUnlocked ? 'grayscale' : ''}`}\n                />\n              ) : (\n                <div className=\"w-full h-48 bg-gray-700 flex items-center justify-center\">\n                  <span className=\"text-gray-400\">이미지 없음</span>\n                </div>\n              )}\n\n              {!isUnlocked && (\n                <div className=\"absolute inset-0 bg-black/60 flex items-center justify-center\">\n                  <Lock className=\"w-12 h-12 text-gray-400\" />\n                </div>\n              )}\n            </div>\n          </div>\n\n          <div className=\"text-center space-y-2\">\n            <h2 className=\"text-2xl font-bold text-white\">{sticker.title}</h2>\n\n            <div className=\"flex items-center justify-center gap-2\">\n              <span className=\"text-3xl\">🥄</span>\n              <span className=\"text-xl font-bold text-amber-400\">\n                {sticker.price?.toLocaleString() || 0}\n              </span>\n            </div>\n          </div>\n\n          {isUnlocked ? (\n            <div className=\"bg-gray-800 rounded-xl p-4 space-y-3\">\n              <h3 className=\"font-semibold text-amber-400 flex items-center gap-2\">\n                <Sparkles className=\"w-5 h-5\" />\n                선물한 사람들\n              </h3>\n\n              <div className=\"space-y-2\">\n                {senders.slice(0, 3).map((gift, index) => (\n                  <div key={gift.id || index} className=\"bg-gray-700/50 rounded-lg p-3\">\n                    <div className=\"flex items-baseline justify-between\">\n                      <div className=\"flex items-baseline gap-2 flex-1\">\n                        <span className={`text-lg font-bold ${index === 0 ? 'text-yellow-400' : 'text-gray-300'}`}>\n                          {index + 1}.\n                        </span>\n                        \n                        {isAdmin && editingGiftId === gift.id ? (\n                          <div className=\"flex items-center gap-2 flex-1\">\n                            <Input\n                              value={editingValue}\n                              onChange={(e) => setEditingValue(e.target.value)}\n                              className=\"bg-gray-600 border-gray-500 text-white text-sm h-8\"\n                              onKeyDown={(e) => {\n                                if (e.key === 'Enter') handleSaveEdit();\n                                if (e.key === 'Escape') handleCancelEdit();\n                              }}\n                              autoFocus\n                            />\n                            <Button\n                              onClick={handleSaveEdit}\n                              size=\"sm\"\n                              className=\"bg-green-600 hover:bg-green-700 h-8 px-2\"\n                            >\n                              저장\n                            </Button>\n                            <Button\n                              onClick={handleCancelEdit}\n                              size=\"sm\"\n                              variant=\"outline\"\n                              className=\"border-gray-500 text-gray-300 hover:bg-gray-700 h-8 px-2\"\n                            >\n                              취소\n                            </Button>\n                          </div>\n                        ) : (\n                          <div className=\"flex items-center gap-2 flex-1 justify-between\">\n                            <div className=\"flex items-center gap-2\">\n                                <p className={`text-white font-medium text-lg ${index === 0 ? 'text-yellow-400' : ''}`}>\n                                  {gift.received_from}\n                                </p>\n                                {isAdmin && (\n                                  <Button\n                                    onClick={() => handleStartEdit(gift)}\n                                    size=\"sm\"\n                                    variant=\"ghost\"\n                                    className=\"text-gray-400 hover:text-white h-6 w-6 p-0\"\n                                  >\n                                    <Pencil className=\"w-3 h-3\" />\n                                  </Button>\n                                )}\n                            </div>\n                            {isAdmin && (\n                                <Button\n                                    onClick={() => onDeleteGift(gift.id)}\n                                    size=\"sm\"\n                                    variant=\"ghost\"\n                                    className=\"text-red-500 hover:text-red-400 h-6 w-6 p-0\"\n                                    title=\"선물 기록 삭제\"\n                                >\n                                    <Trash2 className=\"w-4 h-4\" />\n                                </Button>\n                            )}\n                          </div>\n                        )}\n                      </div>\n                    </div>\n                    {gift.received_at && (\n                       <p className=\"text-gray-500 text-xs mt-1 ml-6\">\n                         {new Date(gift.received_at).toLocaleString('ko-KR')}\n                       </p>\n                    )}\n                  </div>\n                ))}\n                {senders.length > 3 && (\n                  <p className=\"text-xs text-center text-gray-400\">\n                    외 {senders.length - 3}명이 더 선물했습니다.\n                  </p>\n                )}\n              </div>\n\n              {firstGift?.message && firstGift.message !== \"외부 동기화 선물\" && (\n                <div className=\"mt-3 pt-3 border-t border-gray-700\">\n                  <p className=\"text-gray-300 text-sm mb-2\">첫 번째 메시지:</p>\n                  <p className=\"text-white bg-gray-700 rounded-lg p-3\">\n                    \"{firstGift.message}\"\n                  </p>\n                </div>\n              )}\n            </div>\n          ) : (\n            <div className=\"bg-gray-800 rounded-xl p-4 text-center\">\n              <Lock className=\"w-8 h-8 text-gray-500 mx-auto mb-2\" />\n              <p className=\"text-gray-400 mb-2\">\n                아직 받지 않은 스티커입니다\n              </p>\n              {!isLoggedIn && (\n                <Button\n                  onClick={() => User.login()}\n                  className=\"bg-amber-600 hover:bg-amber-700 mt-2\"\n                >\n                  로그인\n                </Button>\n              )}\n            </div>\n          )}\n          \n          {isAdmin && (\n            <Button\n              variant=\"destructive\"\n              onClick={() => onRemoveSticker(sticker)}\n              className=\"w-full mt-4\"\n            >\n              <Trash2 className=\"w-4 h-4 mr-2\" />\n              갤러리에서 스티커 제거\n            </Button>\n          )}\n\n          <Button\n            onClick={onClose}\n            className=\"w-full bg-gradient-to-r from-amber-600 to-orange-600 hover:from-amber-700 hover:to-orange-700\"\n          >\n            닫기\n          </Button>\n        </div>\n      </DialogContent>\n    </Dialog>\n  );\n}\n\nfunction SendStickerDialog({ isOpen, onClose, onGiftSent, currentUser, selectedStickers }) {\n  const [selectedSticker, setSelectedSticker] = useState(null);\n  const [isSending, setIsSending] = useState(false);\n\n  const handleSendSticker = async () => {\n    if (!selectedSticker || !currentUser) return;\n    setIsSending(true);\n\n    try {\n      const newGift = {\n        gift_id: String(selectedSticker.sticker_id),\n        received_from: currentUser.full_name || \"나 자신\",\n        message: \"나에게 보내는 스티커!\",\n        received_at: new Date().toISOString(),\n        created_by: currentUser.email,\n        // 스냅샷 정보 추가\n        title: selectedSticker.title,\n        image_thumbnail: selectedSticker.image_thumbnail,\n        price: selectedSticker.price\n      };\n\n      const createdGift = await UserGift.create(newGift);\n\n      onGiftSent(createdGift);\n\n      setSelectedSticker(null);\n      onClose();\n\n    } catch (error) {\n      console.error(\"스티커 보내기 실패:\", error);\n    } finally {\n      setIsSending(false);\n    }\n  };\n\n  const handleOpenChange = (open) => {\n    if (!open) {\n      setSelectedSticker(null);\n    }\n    onClose();\n  };\n\n  return (\n    <Dialog open={isOpen} onOpenChange={handleOpenChange}>\n      <DialogContent className=\"max-w-2xl max-h-[80vh] flex flex-col bg-gray-900 border-gray-700 text-white\">\n        <DialogHeader className=\"bg-amber-600 text-white p-4\">\n          <DialogTitle className=\"text-xl\">스티커 선택</DialogTitle>\n        </DialogHeader>\n\n        <div className=\"flex-grow overflow-y-auto pr-2 p-4\">\n          <div className=\"grid grid-cols-3 sm:grid-cols-4 md:grid-cols-5 gap-3\">\n            <AnimatePresence>\n              {selectedStickers.map((sticker, index) => (\n                <motion.div\n                  key={sticker.sticker_id}\n                  layout\n                  initial={{ opacity: 0, scale: 0.8 }}\n                  animate={{ opacity: 1, scale: 1 }}\n                  exit={{ opacity: 0, scale: 0.8 }}\n                  transition={{ delay: index * 0.01 }}\n                >\n                  <Card\n                    className={`cursor-pointer transition-all duration-200 hover:shadow-lg h-[120px] flex flex-col items-center justify-center ${\n                      selectedSticker?.sticker_id === sticker.sticker_id\n                        ? 'ring-2 ring-amber-500 bg-amber-50/10 border-amber-300'\n                        : 'hover:border-amber-200 bg-gray-800 border-gray-700'\n                    }`}\n                    onClick={() => setSelectedSticker(selectedSticker?.sticker_id === sticker.sticker_id ? null : sticker)}\n                  >\n                    <CardContent className=\"p-3 flex flex-col items-center justify-center h-full\">\n                      <div className=\"h-[70px] flex items-center justify-center mb-1\">\n                        {sticker.image_thumbnail ? (\n                          <img\n                            src={sticker.image_thumbnail}\n                            alt={sticker.title}\n                            className=\"max-h-[70px] max-w-full object-contain\"\n                          />\n                        ) : (\n                          <div className=\"h-[70px] w-full bg-gray-700 flex items-center justify-center rounded\">\n                            <span className=\"text-gray-500 text-xs text-center\">이미지<br/>없음</span>\n                          </div>\n                        )}\n                      </div>\n                      <p className=\"text-xs text-center font-medium truncate mb-1 w-full px-1 text-white\">\n                        {sticker.title}\n                      </p>\n                      <Badge\n                        variant=\"secondary\"\n                        className=\"text-xs bg-amber-600 text-white flex items-center gap-1\"\n                      >\n                        🥄{sticker.price?.toLocaleString() || 0}\n                      </Badge>\n                    </CardContent>\n                  </Card>\n                </motion.div>\n              ))}\n            </AnimatePresence>\n          </div>\n        </div>\n\n        <div className=\"flex justify-end gap-3 p-4 border-t border-gray-700\">\n          <Button\n            variant=\"outline\"\n            onClick={onClose}\n            className=\"border-gray-600 text-gray-300 hover:bg-gray-800\"\n            disabled={isSending}\n          >\n            취소\n          </Button>\n          <Button\n            onClick={handleSendSticker}\n            disabled={!selectedSticker || isSending}\n            className=\"bg-amber-600 hover:bg-amber-700 min-w-[120px]\"\n          >\n            {isSending ? (\n              <div className=\"flex items-center gap-2\">\n                <div className=\"animate-spin rounded-full h-4 w-4 border-b-2 border-white\"></div>\n                <span>보내는 중...</span>\n              </div>\n            ) : (\n              selectedSticker ? `${selectedSticker.title} 선택 완료` : '선택 완료'\n            )}\n          </Button>\n        </div>\n      </DialogContent>\n    </Dialog>\n  );\n}\n","AdminPanel":"\nimport React, { useState, useEffect, useCallback, useMemo } from \"react\";\nimport { User, SelectedSticker, UserGift } from \"@/entities/all\";\nimport { Card, CardContent } from \"@/components/ui/card\";\nimport { Input } from \"@/components/ui/input\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Tabs, TabsList, TabsTrigger } from \"@/components/ui/tabs\";\nimport { Search, Plus, Trash2, Eye, Settings, Lock } from \"lucide-react\";\nimport { motion, AnimatePresence } from \"framer-motion\";\nimport { Button } from \"@/components/ui/button\";\nimport { getStickerData } from \"@/functions/getStickerData\";\nimport { Link } from \"react-router-dom\";\nimport { createPageUrl } from \"@/utils\";\n\n// 안전한 toLowerCase 함수\nconst safeToLowerCase = (value) => {\n  try {\n    return String(value || '').toLowerCase();\n  } catch (e) {\n    return '';\n  }\n};\n\nexport default function AdminPanel() {\n  const [allStickerData, setAllStickerData] = useState([]);\n  const [selectedStickers, setSelectedStickers] = useState([]);\n  const [searchQuery, setSearchQuery] = useState(\"\");\n  const [activeCategory, setActiveCategory] = useState(\"전체\");\n  const [currentUser, setCurrentUser] = useState(null);\n  const [isLoading, setIsLoading] = useState(true);\n  const [isUnauthorized, setIsUnauthorized] = useState(false);\n  const [debugInfo, setDebugInfo] = useState('');\n  const [isDeselecting, setIsDeselecting] = useState(false); // 모두 해제 로딩 상태\n  const [error, setError] = useState(null); // 오류 상태 추가\n\n  const loadData = useCallback(async () => {\n    try {\n      setIsLoading(true);\n      setError(null); // 오류 초기화\n      setDebugInfo('사용자 정보를 가져오는 중...');\n      \n      const user = await User.me();\n      setCurrentUser(user);\n      \n      // 최고 관리자만 접근 가능\n      if (!user || user.email !== '102810aa@gmail.com') {\n        setIsUnauthorized(true);\n        setIsLoading(false);\n        return;\n      }\n      \n      setDebugInfo('스티커 데이터 API 호출 중...');\n      console.log('스티커 데이터 로딩 시작...');\n      \n      const response = await getStickerData();\n      console.log('getStickerData 응답:', response);\n      \n      const responseBody = response.data;\n      setDebugInfo(`API 응답 수신: ${responseBody ? '성공' : '실패'}`);\n\n      if (!responseBody || responseBody.success === false) {\n        const errorMessage = responseBody?.error || '백엔드 함수에서 오류가 발생했습니다.';\n        setError(errorMessage);\n        throw new Error(errorMessage);\n      }\n      \n      let stickerCategories = responseBody.data;\n      console.log('원본 스티커 데이터:', stickerCategories);\n\n      // 데이터 형태를 유연하게 처리\n      if (!stickerCategories) {\n        stickerCategories = [];\n      } else if (typeof stickerCategories === 'object' && !Array.isArray(stickerCategories)) {\n        if (stickerCategories.categories && Array.isArray(stickerCategories.categories)) {\n          stickerCategories = stickerCategories.categories;\n        } else {\n          const values = Object.values(stickerCategories);\n          if (values.length > 0 && typeof values[0] === 'object') {\n            stickerCategories = values;\n          } else {\n            stickerCategories = [];\n          }\n        }\n      }\n\n      const allStickers = [];\n      \n      if (Array.isArray(stickerCategories)) {\n        stickerCategories.forEach((category, index) => {\n          let categoryName = 'Unknown';\n          let stickers = [];\n          \n          if (typeof category === 'object') {\n            categoryName = category.name || category.title || category.category || `카테고리 ${index + 1}`;\n            stickers = category.stickers || category.items || [];\n          }\n\n          if (Array.isArray(stickers)) {\n            stickers.forEach((sticker, stickerIndex) => {\n              if (typeof sticker === 'object') {\n                allStickers.push({\n                  id: sticker.id || `${categoryName}-${stickerIndex}`,\n                  title: sticker.title || sticker.name || `스티커 ${stickerIndex + 1}`,\n                  image_thumbnail: sticker.image_thumbnail || sticker.image || sticker.thumbnail || null,\n                  price: typeof sticker.price === 'number' ? sticker.price : 0,\n                  category: categoryName\n                });\n              }\n            });\n          }\n        });\n      }\n      \n      console.log(`총 ${allStickers.length}개의 스티커 로드 완료`);\n      setAllStickerData(allStickers);\n      \n      // 이미 선택된 스티커들 로드\n      const selected = await SelectedSticker.list('-display_order');\n      setSelectedStickers(updatedSelectedStickers => {\n        // 기존 선택된 스티커 목록이 비어있으면 새로 로드된 목록으로 설정\n        // 아니면 새로운 목록으로 업데이트\n        return updatedSelectedStickers.length === 0 ? selected : selected;\n      });\n      \n      setDebugInfo(`로딩 완료: ${allStickers.length}개 스티커, ${selected.length}개 선택됨`);\n      \n    } catch (error) {\n      console.error(\"데이터 로딩 실패:\", error);\n      setError(error.message); // 오류 상태가 설정되지 않은 경우에만 설정\n      setDebugInfo(`오류: ${error.message}`);\n      setAllStickerData([]);\n    } finally {\n      setIsLoading(false);\n    }\n  }, []);\n\n  useEffect(() => {\n    loadData();\n  }, [loadData]);\n\n  // 스티커가 이미 선택되었는지 확인\n  const isSelected = useCallback((stickerId) => {\n    return selectedStickers.some(s => s.sticker_id === String(stickerId));\n  }, [selectedStickers]);\n\n  // 스티커를 갤러리에서 제거 (선물 기록도 함께 제거)\n  const removeFromGallery = async (stickerId) => {\n    try {\n      const stringStickerId = String(stickerId);\n      \n      // 1. 갤러리에서 선택된 스티커를 찾아서 삭제합니다.\n      const targetSticker = selectedStickers.find(s => s.sticker_id === stringStickerId);\n      if (targetSticker && targetSticker.id) {\n        try {\n          await SelectedSticker.delete(targetSticker.id);\n        } catch (e) {\n            console.warn(`SelectedSticker ${targetSticker.id} 삭제 중 경고 발생 (이미 삭제되었을 수 있음): ${e.message}`);\n        }\n      }\n\n      // 2. 해당 스티커와 관련된 모든 선물 기록(UserGift)을 찾습니다.\n      const giftsToDelete = await UserGift.filter({ gift_id: stringStickerId });\n\n      // 3. 찾은 모든 선물 기록을 삭제합니다.\n      if (giftsToDelete.length > 0) {\n        await Promise.all(giftsToDelete.map(gift => UserGift.delete(gift.id)));\n      }\n\n      // 4. UI를 업데이트합니다.\n      setSelectedStickers(prev => prev.filter(s => s.sticker_id !== stringStickerId));\n      console.log(`스티커 ${stickerId} 및 관련 선물 ${giftsToDelete.length}개가 모두 삭제되었습니다.`);\n\n    } catch (error) {\n      console.error(\"스티커 및 선물 기록 제거 실패:\", error);\n    }\n  };\n\n  // 스티커를 갤러리에 추가 (기존 선물 기록 모두 삭제 후 추가)\n  const addToGallery = async (sticker) => {\n    try {\n      // 이미 선택된 스티커인지 다시 확인 (optimistic UI update 전에)\n      if (isSelected(sticker.id)) {\n        return; // 이미 추가된 스티커라면 추가하지 않음\n      }\n\n      const stringStickerId = String(sticker.id);\n\n      // 기존 선물 기록이 있다면 모두 삭제 (초기화)\n      const existingGifts = await UserGift.filter({ gift_id: stringStickerId });\n      if (existingGifts.length > 0) {\n        await Promise.all(existingGifts.map(gift => UserGift.delete(gift.id)));\n        console.log(`스티커 ${sticker.id}의 기존 선물 ${existingGifts.length}개를 초기화했습니다.`);\n      }\n\n      const newSelectedSticker = await SelectedSticker.create({\n        sticker_id: stringStickerId,\n        title: sticker.title,\n        image_thumbnail: sticker.image_thumbnail,\n        price: sticker.price,\n        category: sticker.category,\n        display_order: selectedStickers.length // 마지막 순서로 추가\n      });\n      \n      setSelectedStickers(prev => [...prev, newSelectedSticker]);\n      console.log(`스티커 ${sticker.id}가 갤러리에 추가되었습니다 (선물 기록 초기화됨).`);\n\n    } catch (error) {\n      console.error(\"스티커 추가 실패:\", error);\n      // 오류 발생 시 UI 롤백 또는 오류 메시지 표시\n    }\n  };\n\n  // 한 번에 모든 스티커를 갤러리에서 제거하는 함수\n  const handleDeselectAll = async () => {\n    if (selectedStickers.length === 0) return;\n\n    const confirmed = window.confirm(\n      `정말 갤러리에 추가된 ${selectedStickers.length}개의 스티커를 모두 제거하시겠습니까?\\n이 작업은 모든 선물 기록도 함께 영구적으로 삭제하며, 되돌릴 수 없습니다.`\n    );\n\n    if (confirmed) {\n      setIsDeselecting(true);\n      try {\n        // 1. 현재 선택된 스티커와 연관된 모든 선물 기록만 삭제합니다. (병렬 처리)\n        const stickerIdsToDelete = selectedStickers.map(s => s.sticker_id);\n        const giftsToDelete = await UserGift.filter({ gift_id: { $in: stickerIdsToDelete } });\n        \n        if (giftsToDelete.length > 0) {\n          await Promise.all(giftsToDelete.map(gift => UserGift.delete(gift.id).catch(e => console.warn(`UserGift ${gift.id} 삭제 오류 무시: ${e.message}`))));\n        }\n\n        // 2. 모든 SelectedSticker 기록 삭제 (병렬 처리)\n        // sticker.id가 null이나 undefined가 아닌 경우에만 삭제 시도\n        const validSelectedStickers = selectedStickers.filter(sticker => sticker.id);\n        await Promise.all(validSelectedStickers.map(sticker => {\n          return SelectedSticker.delete(sticker.id).catch(e => {\n            // 404 (Not Found) 오류는 무시하고, 다른 오류만 콘솔에 경고로 표시\n            if (e.response && e.response.status === 404) {\n              console.log(`Sticker ${sticker.id}는 이미 삭제되었습니다. 건너뜀.`);\n            } else {\n              console.warn(`SelectedSticker ${sticker.id} 삭제 중 오류 (무시): ${e.message}`);\n            }\n          });\n        }));\n\n        // 3. UI 업데이트\n        setSelectedStickers([]);\n        alert(`${selectedStickers.length}개의 스티커와 관련 선물 기록이 갤러리에서 완전히 제거되었습니다.`);\n      } catch (error) {\n        console.error(\"모든 스티커 제거 실패:\", error);\n        alert(\"스티커를 제거하는 중 오류가 발생했습니다.\");\n      } finally {\n        setIsDeselecting(false);\n      }\n    }\n  };\n\n\n  // 필터링된 결과\n  const filteredStickers = useMemo(() => {\n    if (!allStickerData.length) return [];\n    \n    return allStickerData.filter(sticker => {\n      const stickerTitle = safeToLowerCase(sticker.title || '');\n      const query = safeToLowerCase(searchQuery || '');\n      \n      const matchesSearch = !query || stickerTitle.includes(query);\n      const matchesCategory = activeCategory === \"전체\" || sticker.category === activeCategory;\n      \n      return matchesSearch && matchesCategory;\n    });\n  }, [allStickerData, searchQuery, activeCategory]);\n\n  // 카테고리 목록\n  const allCategories = useMemo(() => {\n    const categories = allStickerData.map(s => s.category).filter(Boolean);\n    return [\"전체\", ...new Set(categories)];\n  }, [allStickerData]);\n\n  if (isLoading) {\n    return (\n      <div className=\"min-h-screen bg-gradient-to-br from-gray-900 via-black to-gray-900 text-white flex items-center justify-center\">\n        <div className=\"text-center\">\n          <div className=\"animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500 mx-auto mb-4\"></div>\n          <p className=\"text-gray-400 mb-2\">스티커 데이터를 불러오는 중...</p>\n          <p className=\"text-xs text-gray-500\">{debugInfo}</p>\n        </div>\n      </div>\n    );\n  }\n\n  // 권한이 없는 경우\n  if (isUnauthorized) {\n    return (\n      <div className=\"min-h-screen bg-gradient-to-br from-gray-900 via-black to-gray-900 text-white flex items-center justify-center\">\n        <div className=\"text-center max-w-md\">\n          <div className=\"w-24 h-24 bg-red-800 rounded-full flex items-center justify-center mx-auto mb-4\">\n            <Lock className=\"w-12 h-12 text-red-400\" />\n          </div>\n          <h2 className=\"text-xl font-bold text-white mb-2\">접근 권한이 없습니다</h2>\n          <p className=\"text-gray-400 mb-4\">이 페이지는 특정 관리자만 접근할 수 있습니다.</p> {/* Updated message */}\n          <Button \n            onClick={() => User.logout()}\n            className=\"bg-red-600 hover:bg-red-700\"\n          >\n            로그아웃\n          </Button>\n        </div>\n      </div>\n    );\n  }\n\n  return (\n    <div className=\"min-h-screen bg-gradient-to-br from-gray-900 via-black to-gray-900 text-white\">\n      {/* 헤더 */}\n      <div className=\"relative overflow-hidden\">\n        <div className=\"absolute inset-0 bg-gradient-to-r from-blue-500/20 to-purple-600/20 blur-3xl\" />\n        <div className=\"relative px-6 py-8\">\n          <div className=\"max-w-6xl mx-auto\">\n            <div className=\"flex items-center justify-between mb-6 flex-wrap gap-4\">\n              <div className=\"flex items-center gap-4\">\n                <div className=\"w-16 h-16 bg-gradient-to-r from-blue-500 to-purple-500 rounded-2xl flex items-center justify-center shadow-xl\">\n                  <Settings className=\"w-8 h-8 text-white\" />\n                </div>\n                <div>\n                  <h1 className=\"text-3xl font-bold text-white\">\n                    스티커 관리 패널\n                  </h1>\n                  <div className=\"flex items-center gap-4 mt-2\">\n                    <Badge variant=\"outline\" className=\"border-blue-500 text-blue-400\">\n                      관리자\n                    </Badge>\n                    <span className=\"text-gray-400\">\n                      {selectedStickers.length}개 스티커 선택됨\n                    </span>\n                  </div>\n                </div>\n              </div>\n              <div className=\"flex gap-2\">\n                <Button\n                  variant=\"destructive\"\n                  onClick={handleDeselectAll}\n                  disabled={selectedStickers.length === 0 || isDeselecting}\n                >\n                  {isDeselecting ? (\n                    <div className=\"animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2\"></div>\n                  ) : (\n                    <Trash2 className=\"w-4 h-4 mr-2\" />\n                  )}\n                  {isDeselecting ? '삭제 중...' : '선택 모두 해제'}\n                </Button>\n                <Link to={createPageUrl('Gallery')}>\n                  <Button className=\"bg-amber-600 hover:bg-amber-700\">\n                    <Eye className=\"w-4 h-4 mr-2\" />\n                    갤러리 보기\n                  </Button>\n                </Link>\n              </div>\n            </div>\n\n            {/* 오류 메시지 표시 */}\n            {error && (\n              <div className=\"mt-4 p-4 bg-red-900/50 border border-red-500/50 rounded-lg text-red-300\">\n                <p className=\"font-bold\">스티커 데이터 로딩 실패</p>\n                <p className=\"text-xs mt-1\">{error}</p>\n                <p className=\"text-xs mt-2\">외부 스티커 서버에 문제가 있을 수 있습니다. 잠시 후 다시 시도해주세요.</p>\n              </div>\n            )}\n            \n            <div className=\"flex items-center gap-4 p-4 mt-6 bg-gray-800/50 rounded-xl border border-gray-700\">\n              <span className=\"text-gray-300\">전체 스티커</span>\n              <span className=\"text-blue-400\">총 {allStickerData.length}개의 스푼캐스트 스티커</span>\n              <span className=\"text-2xl\">⚙️</span>\n            </div>\n          </div>\n        </div>\n      </div>\n\n      <div className=\"max-w-6xl mx-auto px-6 py-8\">\n        {/* 검색 및 필터 */}\n        <div className=\"mb-8\">\n          <div className=\"relative mb-6\">\n            <Search className=\"absolute left-4 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400\" />\n            <Input\n              placeholder=\"스티커 검색...\"\n              value={searchQuery}\n              onChange={(e) => setSearchQuery(e.target.value || '')}\n              className=\"pl-12 bg-gray-800 border-gray-700 text-white placeholder:text-gray-400 focus:border-blue-500\"\n            />\n          </div>\n\n          <Tabs value={activeCategory} onValueChange={setActiveCategory}>\n            <TabsList className=\"bg-gray-800 border-gray-700 flex-wrap h-auto\">\n              {allCategories.map(category => (\n                <TabsTrigger \n                  key={category} \n                  value={category}\n                  className=\"data-[state=active]:bg-blue-600 data-[state=active]:text-white\"\n                >\n                  {category}\n                </TabsTrigger>\n              ))}\n            </TabsList>\n          </Tabs>\n        </div>\n\n        {/* 스티커 그리드 */}\n        <motion.div \n          layout\n          className=\"grid grid-cols-2 sm:grid-cols-3 md:grid-cols-5 gap-4\"\n        >\n          <AnimatePresence>\n            {filteredStickers.map((sticker, index) => (\n              <AdminStickerCard\n                key={sticker.id}\n                sticker={sticker}\n                isSelected={isSelected(sticker.id)}\n                onAdd={() => addToGallery(sticker)}\n                onRemove={() => removeFromGallery(sticker.id)}\n                index={index}\n              />\n            ))}\n          </AnimatePresence>\n        </motion.div>\n\n        {filteredStickers.length === 0 && (\n          <div className=\"text-center py-16\">\n            <div className=\"w-24 h-24 bg-gray-800 rounded-full flex items-center justify-center mx-auto mb-4\">\n              <Search className=\"w-12 h-12 text-gray-600\" />\n            </div>\n            <p className=\"text-gray-400 text-lg\">검색 결과가 없습니다</p>\n          </div>\n        )}\n      </div>\n    </div>\n  );\n}\n\n// 관리자용 스티커 카드 컴포넌트\nconst AdminStickerCard = React.memo(function AdminStickerCard({ sticker, isSelected, onAdd, onRemove, index }) {\n  \n  // 스티커 카드 클릭 시 토글 기능\n  const handleToggle = () => {\n    if (isSelected) {\n      onRemove(); // 이미 선택된 경우 제거\n    } else {\n      onAdd(); // 선택되지 않은 경우 추가\n    }\n  };\n\n  return (\n    <motion.div\n      layout\n      initial={{ opacity: 0, y: 20 }}\n      animate={{ opacity: 1, y: 0 }}\n      exit={{ opacity: 0, y: -20 }}\n      transition={{ delay: Math.min(index * 0.01, 0.3) }}\n    >\n      <Card \n        className={`cursor-pointer transition-all duration-200 hover:scale-105 border-2 h-[280px] flex flex-col\n          ${isSelected \n            ? 'border-green-500 bg-green-900/20 shadow-xl hover:border-green-400' \n            : 'border-gray-700 bg-gray-800 hover:border-blue-400'\n          }`}\n        onClick={handleToggle}  // 카드 클릭 시 토글\n      >\n        <CardContent className=\"p-3 flex flex-col h-full\">\n          {/* 이미지 영역 - 고정 높이 */}\n          <div className=\"relative mb-3 h-32 flex-shrink-0\">\n            <div className=\"w-full h-full rounded-lg bg-gray-700 flex items-center justify-center overflow-hidden\">\n              {sticker.image_thumbnail ? (\n                <img \n                  src={sticker.image_thumbnail} \n                  alt={sticker.title}\n                  className=\"w-full h-full object-contain\"\n                  loading=\"lazy\"\n                  onError={(e) => { e.target.style.display = 'none'; }}\n                />\n              ) : (\n                <div className=\"w-full h-full bg-gray-700 flex items-center justify-center\">\n                  <span className=\"text-xs text-gray-300\">이미지 없음</span>\n                </div>\n              )}\n              \n              {isSelected && (\n                <div className=\"absolute top-2 right-2\">\n                  <div className=\"w-6 h-6 bg-green-500 rounded-full flex items-center justify-center\">\n                    <span className=\"text-white text-xs font-bold\">✓</span>\n                  </div>\n                </div>\n              )}\n            </div>\n          </div>\n\n          {/* 텍스트 정보 영역 - 유연한 높이 */}\n          <div className=\"text-center space-y-2 flex-grow flex flex-col justify-between\">\n            <div className=\"space-y-1\">\n              <h3 className=\"font-semibold text-xs text-white line-clamp-2\" title={sticker.title}>\n                {sticker.title}\n              </h3>\n              \n              <div className=\"flex items-center justify-center gap-1\">\n                <span className=\"text-sm\">🥄</span>\n                <span className=\"text-xs font-medium text-amber-400\">\n                  {sticker.price?.toLocaleString()}\n                </span>\n              </div>\n\n              {sticker.category && (\n                <Badge variant=\"outline\" className=\"text-xs border-blue-500 text-blue-400\">\n                  {sticker.category}\n                </Badge>\n              )}\n            </div>\n\n            {/* 상태 표시 영역 - 하단 고정 */}\n            <div className=\"mt-3\">\n              <div className={`text-xs px-3 py-2 rounded-lg font-medium ${\n                isSelected \n                  ? 'bg-green-600/20 text-green-400 border border-green-500/30'\n                  : 'bg-blue-600/20 text-blue-400 border border-blue-500/30'\n              }`}>\n                {isSelected ? '갤러리에 추가됨' : '클릭하여 추가'}\n              </div>\n            </div>\n          </div>\n        </CardContent>\n      </Card>\n    </motion.div>\n  );\n});\n","AdminSettings":"\nimport React, { useState, useEffect } from \"react\";\nimport { User } from \"@/entities/all\";\nimport { Card, CardContent, CardHeader, CardTitle } from \"@/components/ui/card\";\nimport { Input } from \"@/components/ui/input\";\nimport { Textarea } from \"@/components/ui/textarea\";\nimport { Button } from \"@/components/ui/button\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Settings, Save, Lock, ArrowLeft } from \"lucide-react\";\nimport { Link } from \"react-router-dom\";\nimport { createPageUrl } from \"@/utils\";\nimport { SelectedSticker } from \"@/entities/selected-sticker\";\n\nexport default function AdminSettings() {\n  const [currentUser, setCurrentUser] = useState(null);\n  const [galleryTitle, setGalleryTitle] = useState(\"\");\n  const [galleryDescription, setGalleryDescription] = useState(\"\");\n  const [syncFilename, setSyncFilename] = useState(\"sum\"); // .json 제거\n  const [selectedStickers, setSelectedStickers] = useState([]);\n  const [isLoading, setIsLoading] = useState(true);\n  const [isUnauthorized, setIsUnauthorized] = useState(false);\n  const [isSaving, setIsSaving] = useState(false);\n  const [saveMessage, setSaveMessage] = useState(\"\");\n\n  useEffect(() => {\n    loadUserData();\n  }, []);\n\n  const loadUserData = async () => {\n    try {\n      setIsLoading(true);\n      const user = await User.me();\n      setCurrentUser(user);\n      \n      // 최고 관리자만 접근 가능\n      if (!user || user.email !== '102810aa@gmail.com') {\n        setIsUnauthorized(true);\n        setIsLoading(false);\n        return;\n      }\n      \n      // 저장된 갤러리 설정 로드\n      setGalleryTitle(user.gallery_title || \"🖤SOMI🖤 님의 스티커 갤러리\");\n      setGalleryDescription(user.gallery_description || \"\");\n      \n      // 파일 이름에서 .json 확장자 제거하여 표시\n      const storedFilename = user.external_sync_filename || \"sum.json\";\n      const filenameWithoutExtension = storedFilename.replace(/\\.json$/, '');\n      setSyncFilename(filenameWithoutExtension);\n      \n      // 선택된 스티커들 로드\n      const stickers = await SelectedSticker.list('-display_order');\n      setSelectedStickers(stickers);\n      \n    } catch (error) {\n      console.error(\"사용자 데이터 로딩 실패:\", error);\n    } finally {\n      setIsLoading(false);\n    }\n  };\n\n  const handleSave = async () => {\n    if (!currentUser) return;\n    \n    try {\n      setIsSaving(true);\n      \n      // 파일명에 자동으로 .json 확장자 추가\n      const fullFilename = syncFilename.endsWith('.json') ? syncFilename : `${syncFilename}.json`;\n      \n      await User.updateMyUserData({\n        gallery_title: galleryTitle,\n        gallery_description: galleryDescription,\n        external_sync_filename: fullFilename\n      });\n      \n      setSaveMessage(\"설정이 저장되었습니다!\");\n      setTimeout(() => setSaveMessage(\"\"), 3000);\n      \n    } catch (error) {\n      console.error(\"설정 저장 실패:\", error);\n      setSaveMessage(\"저장 중 오류가 발생했습니다.\");\n    } finally {\n      setIsSaving(false);\n    }\n  };\n\n  if (isLoading) {\n    return (\n      <div className=\"min-h-screen bg-gradient-to-br from-gray-900 via-black to-gray-900 text-white flex items-center justify-center\">\n        <div className=\"text-center\">\n          <div className=\"animate-spin rounded-full h-12 w-12 border-b-2 border-purple-500 mx-auto mb-4\"></div>\n          <p className=\"text-gray-400\">설정을 불러오는 중...</p>\n        </div>\n      </div>\n    );\n  }\n\n  // 권한이 없는 경우\n  if (isUnauthorized) {\n    return (\n      <div className=\"min-h-screen bg-gradient-to-br from-gray-900 via-black to-gray-900 text-white flex items-center justify-center\">\n        <div className=\"text-center max-w-md\">\n          <div className=\"w-24 h-24 bg-red-800 rounded-full flex items-center justify-center mx-auto mb-4\">\n            <Lock className=\"w-12 h-12 text-red-400\" />\n          </div>\n          <h2 className=\"text-xl font-bold text-white mb-2\">접근 권한이 없습니다</h2>\n          <p className=\"text-gray-400 mb-4\">이 페이지는 최고 관리자만 접근할 수 있습니다.</p>\n          <Button \n            onClick={() => User.logout()}\n            className=\"bg-red-600 hover:bg-red-700\"\n          >\n            로그아웃\n          </Button>\n        </div>\n      </div>\n    );\n  }\n\n  return (\n    <div className=\"min-h-screen bg-gradient-to-br from-gray-900 via-black to-gray-900 text-white\">\n      {/* 헤더 */}\n      <div className=\"relative overflow-hidden\">\n        <div className=\"absolute inset-0 bg-gradient-to-r from-purple-500/20 to-pink-600/20 blur-3xl\" />\n        <div className=\"relative px-6 py-8\">\n          <div className=\"max-w-4xl mx-auto\">\n            <div className=\"flex items-center justify-between mb-6 flex-wrap gap-4\">\n              <div className=\"flex items-center gap-4\">\n                <Link to={createPageUrl('Gallery')}>\n                  <Button variant=\"outline\" className=\"border-gray-600 text-gray-300 hover:bg-gray-800\">\n                    <ArrowLeft className=\"w-4 h-4 mr-2\" />\n                    갤러리로 돌아가기\n                  </Button>\n                </Link>\n                <div className=\"w-16 h-16 bg-gradient-to-r from-purple-500 to-pink-500 rounded-2xl flex items-center justify-center shadow-xl\">\n                  <Settings className=\"w-8 h-8 text-white\" />\n                </div>\n                <div>\n                  <h1 className=\"text-3xl font-bold bg-gradient-to-r from-purple-400 to-pink-400 bg-clip-text text-transparent\">\n                    갤러리 설정\n                  </h1>\n                  <div className=\"flex items-center gap-4 mt-2\">\n                    <Badge variant=\"outline\" className=\"border-purple-500 text-purple-400\">\n                      최고 관리자\n                    </Badge>\n                    <span className=\"text-gray-400\">\n                      {currentUser?.full_name}\n                    </span>\n                  </div>\n                </div>\n              </div>\n            </div>\n          </div>\n        </div>\n      </div>\n\n      <div className=\"max-w-4xl mx-auto px-6 py-8\">\n        <Card className=\"bg-gray-800/50 border-gray-700\">\n          <CardHeader>\n            <CardTitle className=\"text-xl text-white flex items-center gap-2\">\n              <Settings className=\"w-5 h-5\" />\n              갤러리 표시 설정\n            </CardTitle>\n          </CardHeader>\n          <CardContent className=\"space-y-6\">\n            {/* 갤러리 제목 설정 */}\n            <div className=\"space-y-2\">\n              <label className=\"text-sm font-medium text-gray-300\">\n                갤러리 제목\n              </label>\n              <Input\n                value={galleryTitle}\n                onChange={(e) => setGalleryTitle(e.target.value)}\n                placeholder=\"갤러리 제목을 입력하세요\"\n                className=\"bg-gray-700 border-gray-600 text-white placeholder:text-gray-400\"\n              />\n              <p className=\"text-xs text-gray-400\">\n                갤러리 페이지 상단에 표시되는 제목입니다.\n              </p>\n            </div>\n\n            {/* 갤러리 설명 설정 */}\n            <div className=\"space-y-2\">\n              <label className=\"text-sm font-medium text-gray-300\">\n                갤러리 설명 (선택사항)\n              </label>\n              <Textarea\n                value={galleryDescription}\n                onChange={(e) => setGalleryDescription(e.target.value)}\n                placeholder=\"갤러리에 대한 설명을 입력하세요\"\n                className=\"bg-gray-700 border-gray-600 text-white placeholder:text-gray-400 h-24\"\n              />\n              <p className=\"text-xs text-gray-400\">\n                갤러리 제목 아래에 표시되는 설명입니다.\n              </p>\n            </div>\n\n            {/* 외부 동기화 파일 설정 */}\n            <div className=\"space-y-2\">\n              <label className=\"text-sm font-medium text-gray-300\">\n                디제이 고유닉 설정\n              </label>\n              <div className=\"relative\">\n                <Input\n                  value={syncFilename}\n                  onChange={(e) => setSyncFilename(e.target.value.replace(/\\.json$/, ''))} // .json 입력 시 자동 제거\n                  placeholder=\"예: shoe85\"\n                  className=\"bg-gray-700 border-gray-600 text-white placeholder:text-gray-400 pr-16\"\n                />\n                <div className=\"absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 text-sm pointer-events-none\">\n                  .json\n                </div>\n              </div>\n              <p className=\"text-xs text-gray-400\">\n                @를 제외한 디제이 고유 닉네임을 입력하세요.\n              </p>\n            </div>\n\n            {/* 미리보기 */}\n            <div className=\"space-y-2\">\n              <label className=\"text-sm font-medium text-gray-300\">\n                미리보기\n              </label>\n              <div className=\"bg-gray-700 rounded-lg p-4 border border-gray-600\">\n                <h2 className=\"text-2xl font-bold bg-gradient-to-r from-amber-400 to-orange-400 bg-clip-text text-transparent mb-2\">\n                  {galleryTitle || \"갤러리 제목\"}\n                </h2>\n                {galleryDescription && (\n                  <p className=\"text-gray-300 text-sm\">\n                    {galleryDescription}\n                  </p>\n                )}\n              </div>\n            </div>\n\n            {/* 저장 버튼 */}\n            <div className=\"flex items-center gap-4 pt-4\">\n              <Button\n                onClick={handleSave}\n                disabled={isSaving}\n                className=\"bg-purple-600 hover:bg-purple-700\"\n              >\n                {isSaving ? (\n                  <div className=\"animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2\"></div>\n                ) : (\n                  <Save className=\"w-4 h-4 mr-2\" />\n                )}\n                설정 저장\n              </Button>\n              \n              {saveMessage && (\n                <span className={`text-sm ${saveMessage.includes('오류') ? 'text-red-400' : 'text-green-400'}`}>\n                  {saveMessage}\n                </span>\n              )}\n            </div>\n          </CardContent>\n        </Card>\n\n        {/* 등록된 스티커 목록 카드 */}\n        <Card className=\"bg-gray-800/50 border-gray-700 mt-6\">\n          <CardHeader>\n            <CardTitle className=\"text-xl text-white flex items-center gap-2\">\n              <Badge className=\"bg-amber-600 text-white\">\n                {selectedStickers.length}\n              </Badge>\n              등록된 스티커 목록\n            </CardTitle>\n          </CardHeader>\n          <CardContent>\n            {selectedStickers.length > 0 ? (\n              <div className=\"space-y-4\">\n                <p className=\"text-gray-400 text-sm mb-4\">\n                  현재 갤러리에 등록된 스티커들입니다. 외부 동기화 시 이 목록의 스티커들과 매칭됩니다.\n                </p>\n                \n                <div className=\"bg-gray-700/50 rounded-lg p-4 max-h-80 overflow-y-auto\">\n                  <div className=\"grid gap-2\">\n                    {selectedStickers.map((sticker, index) => (\n                      <div key={sticker.id} className=\"flex items-center gap-3 p-2 bg-gray-600/30 rounded border border-gray-600\">\n                        <span className=\"text-gray-400 text-sm w-8\">\n                          {index + 1}.\n                        </span>\n                        \n                        {sticker.image_thumbnail && (\n                          <img \n                            src={sticker.image_thumbnail} \n                            alt={sticker.title}\n                            className=\"w-8 h-8 object-contain rounded\"\n                            onError={(e) => { e.target.style.display = 'none'; }}\n                          />\n                        )}\n                        \n                        <div className=\"flex-1\">\n                          <div className=\"text-white font-medium text-sm\">\n                            {sticker.title}\n                          </div>\n                          <div className=\"text-xs text-gray-400 font-mono\">\n                            ID: {sticker.sticker_id}\n                          </div>\n                        </div>\n                        \n                        <div className=\"flex items-center gap-2\">\n                          <Badge variant=\"outline\" className=\"border-blue-500 text-blue-400 text-xs\">\n                            {sticker.category}\n                          </Badge>\n                          <span className=\"text-xs text-amber-400 flex items-center gap-1\">\n                            🥄{sticker.price?.toLocaleString() || 0}\n                          </span>\n                        </div>\n                      </div>\n                    ))}\n                  </div>\n                </div>\n                \n                <div className=\"bg-blue-900/30 border border-blue-500/30 rounded-lg p-3\">\n                  <p className=\"text-blue-300 text-sm\">\n                    💡 <strong>외부 동기화 사용법:</strong> \n                    <code className=\"bg-gray-700 px-1 rounded\">{syncFilename || 'sum'}.json</code> 파일에서 위 목록의 <code className=\"bg-gray-700 px-1 rounded\">ID</code>와 \n                    일치하는 <code className=\"bg-gray-700 px-1 rounded\">name</code>이 있으면 자동으로 활성화됩니다.\n                  </p>\n                </div>\n              </div>\n            ) : (\n              <div className=\"text-center py-8\">\n                <div className=\"w-16 h-16 bg-gray-700 rounded-full flex items-center justify-center mx-auto mb-4\">\n                  <Settings className=\"w-8 h-8 text-gray-500\" />\n                </div>\n                <p className=\"text-gray-400 mb-4\">\n                  아직 등록된 스티커가 없습니다.\n                </p>\n                <Link to={createPageUrl('AdminPanel')}>\n                  <Button className=\"bg-blue-600 hover:bg-blue-700\">\n                    스티커 추가하러 가기\n                  </Button>\n                </Link>\n              </div>\n            )}\n          </CardContent>\n        </Card>\n\n        {/* 추가 설정 카드들 */}\n        <div className=\"grid gap-6 mt-8\">\n          <Card className=\"bg-gray-800/50 border-gray-700\">\n            <CardHeader>\n              <CardTitle className=\"text-lg text-white\">\n                빠른 액션\n              </CardTitle>\n            </CardHeader>\n            <CardContent>\n              <div className=\"flex gap-4 flex-wrap\">\n                <Link to={createPageUrl('AdminPanel')}>\n                  <Button className=\"bg-blue-600 hover:bg-blue-700\">\n                    스티커 관리\n                  </Button>\n                </Link>\n                <Link to={createPageUrl('ListenerManagement')}>\n                  <Button className=\"bg-indigo-600 hover:bg-indigo-700\">\n                    <Settings className=\"w-4 h-4 mr-2\" />\n                    청취자 관리\n                  </Button>\n                </Link>\n                {/* 최고 관리자에게만 '사용자 관리' 버튼이 보입니다 */}\n                {currentUser?.email === '102810aa@gmail.com' && (\n                  <Link to={createPageUrl('UserManagement')}>\n                    <Button className=\"bg-green-600 hover:bg-green-700\">\n                      <Settings className=\"w-4 h-4 mr-2\" />\n                      사용자 관리\n                    </Button>\n                  </Link>\n                )}\n                <Link to={createPageUrl('Gallery')}>\n                  <Button variant=\"outline\" className=\"border-gray-600 text-gray-300 hover:bg-gray-700\">\n                    갤러리 보기\n                  </Button>\n                </Link>\n              </div>\n            </CardContent>\n          </Card>\n        </div>\n      </div>\n    </div>\n  );\n}\n","UserManagement":"\nimport React, { useState, useEffect } from \"react\";\nimport { User } from \"@/entities/all\";\nimport { Card, CardContent, CardHeader, CardTitle } from \"@/components/ui/card\";\nimport { Button } from \"@/components/ui/button\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Users, Shield, Crown, ArrowLeft, Lock, UserCog, Pencil } from \"lucide-react\";\nimport { Link } from \"react-router-dom\";\nimport { createPageUrl } from \"@/utils\";\nimport { updateUserRole } from \"@/functions/updateUserRole\";\nimport { updateUserProfile } from \"@/functions/updateUserProfile\"; // New import\nimport { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogClose } from \"@/components/ui/dialog\"; // New import\nimport { Input } from \"@/components/ui/input\"; // New import\nimport { Textarea } from \"@/components/ui/textarea\"; // New import\nimport { Label } from \"@/components/ui/label\"; // New import\n\nexport default function UserManagement() {\n  const [currentUser, setCurrentUser] = useState(null);\n  const [allUsers, setAllUsers] = useState([]);\n  const [isLoading, setIsLoading] = useState(true);\n  const [isUnauthorized, setIsUnauthorized] = useState(false);\n  const [updatingUserId, setUpdatingUserId] = useState(null);\n  const [editingUser, setEditingUser] = useState(null); // New state\n  const [isEditModalOpen, setIsEditModalOpen] = useState(false); // New state\n\n  useEffect(() => {\n    loadData();\n  }, []);\n\n  const loadData = async () => {\n    try {\n      setIsLoading(true);\n      const user = await User.me();\n      setCurrentUser(user);\n      \n      // 최고 관리자 권한 체크\n      // 이 페이지는 'admin' 역할을 가지고 있으며 특정 이메일 주소(최고 관리자)를 가진 사용자만 접근할 수 있습니다.\n      if (user.role !== 'admin' || user.email !== '102810aa@gmail.com') {\n        setIsUnauthorized(true);\n        setIsLoading(false);\n        return;\n      }\n      \n      // 모든 사용자 목록 로드 (최고 관리자만 가능)\n      const users = await User.list('-created_date');\n      setAllUsers(users);\n      \n    } catch (error) {\n      console.error(\"사용자 데이터 로딩 실패:\", error);\n      setAllUsers([]);\n    } finally {\n      setIsLoading(false);\n    }\n  };\n\n  // 역할 변경 기능을 제거하고 안내 메시지만 표시\n  const handleRoleChangeNotice = () => {\n    alert(\"사용자 역할 변경은 Base44 대시보드에서만 가능합니다.\\n\\nBase44 대시보드 → 데이터(Data) → User 엔티티에서 직접 role 필드를 수정해주세요.\");\n  };\n\n  // New function to open the edit modal\n  const handleOpenEditModal = (user) => {\n    setEditingUser(user);\n    setIsEditModalOpen(true);\n  };\n\n  // New function to handle profile update\n  const handleProfileUpdate = async (updatedData) => {\n    if (!editingUser) return;\n    setUpdatingUserId(editingUser.id);\n    try {\n        await updateUserProfile({ userId: editingUser.id, profileData: updatedData });\n        \n        // UI 즉시 업데이트\n        setAllUsers(prevUsers => prevUsers.map(u => \n            u.id === editingUser.id ? { ...u, ...updatedData } : u\n        ));\n        \n        setIsEditModalOpen(false);\n        setEditingUser(null);\n    } catch(error) {\n        console.error(\"프로필 업데이트 실패:\", error);\n        alert(\"프로필 업데이트에 실패했습니다.\");\n    } finally {\n        setUpdatingUserId(null);\n    }\n  };\n\n\n  if (isLoading) {\n    return (\n      <div className=\"min-h-screen bg-gradient-to-br from-gray-900 via-black to-gray-900 text-white flex items-center justify-center\">\n        <div className=\"text-center\">\n          <div className=\"animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500 mx-auto mb-4\"></div>\n          <p className=\"text-gray-400\">사용자 데이터를 불러오는 중...</p>\n        </div>\n      </div>\n    );\n  }\n\n  // 권한이 없는 경우\n  if (isUnauthorized) {\n    return (\n      <div className=\"min-h-screen bg-gradient-to-br from-gray-900 via-black to-gray-900 text-white flex items-center justify-center\">\n        <div className=\"text-center max-w-md\">\n          <div className=\"w-24 h-24 bg-red-800 rounded-full flex items-center justify-center mx-auto mb-4\">\n            <Lock className=\"w-12 h-12 text-red-400\" />\n          </div>\n          <h2 className=\"text-xl font-bold text-white mb-2\">접근 권한이 없습니다</h2>\n          <p className=\"text-gray-400 mb-4\">이 페이지는 최고 관리자만 접근할 수 있습니다.</p>\n          <Button \n            onClick={() => User.logout()}\n            className=\"bg-red-600 hover:bg-red-700\"\n          >\n            로그아웃\n          </Button>\n        </div>\n      </div>\n    );\n  }\n\n  const adminUsers = allUsers.filter(user => user.role === 'admin');\n  const regularUsers = allUsers.filter(user => user.role !== 'admin');\n\n  return (\n    <div className=\"min-h-screen bg-gradient-to-br from-gray-900 via-black to-gray-900 text-white\">\n      {/* 헤더 */}\n      <div className=\"relative overflow-hidden\">\n        <div className=\"absolute inset-0 bg-gradient-to-r from-blue-500/20 to-green-600/20 blur-3xl\" />\n        <div className=\"relative px-6 py-8\">\n          <div className=\"max-w-6xl mx-auto\">\n            <div className=\"flex items-center justify-between mb-6 flex-wrap gap-4\">\n              <div className=\"flex items-center gap-4\">\n                <Link to={createPageUrl('AdminSettings')}>\n                  <Button variant=\"outline\" className=\"border-gray-600 text-gray-300 hover:bg-gray-800\">\n                    <ArrowLeft className=\"w-4 h-4 mr-2\" />\n                    설정으로 돌아가기\n                  </Button>\n                </Link>\n                <div className=\"w-16 h-16 bg-gradient-to-r from-blue-500 to-green-500 rounded-2xl flex items-center justify-center shadow-xl\">\n                  <Users className=\"w-8 h-8 text-white\" />\n                </div>\n                <div>\n                  <h1 className=\"text-3xl font-bold bg-gradient-to-r from-blue-400 to-green-400 bg-clip-text text-transparent\">\n                    사용자 관리\n                  </h1>\n                  <div className=\"flex items-center gap-4 mt-2\">\n                    <Badge variant=\"outline\" className=\"border-blue-500 text-blue-400\">\n                      <Crown className=\"w-4 h-4 mr-1\" />\n                      최고 관리자\n                    </Badge>\n                    <span className=\"text-gray-400\">\n                      총 {allUsers.length}명의 사용자\n                    </span>\n                  </div>\n                </div>\n              </div>\n            </div>\n          </div>\n        </div>\n      </div>\n\n      <div className=\"max-w-6xl mx-auto px-6 py-8\">\n        <div className=\"grid gap-8\">\n          {/* 관리자 섹션 */}\n          <Card className=\"bg-gray-800/50 border-gray-700\">\n            <CardHeader>\n              <CardTitle className=\"text-xl text-white flex items-center gap-2\">\n                <Crown className=\"w-5 h-5 text-yellow-400\" />\n                관리자 ({adminUsers.length}명)\n              </CardTitle>\n            </CardHeader>\n            <CardContent>\n              <div className=\"grid gap-4\">\n                {adminUsers.map((user) => (\n                  <UserCard \n                    key={user.id}\n                    user={user}\n                    currentUser={currentUser}\n                    onRoleChange={handleRoleChangeNotice}\n                    onEditProfile={handleOpenEditModal} // New prop\n                    isUpdating={updatingUserId === user.id}\n                  />\n                ))}\n                {adminUsers.length === 0 && (\n                  <p className=\"text-gray-400 text-center py-4\">관리자가 없습니다.</p>\n                )}\n              </div>\n            </CardContent>\n          </Card>\n\n          {/* 일반 사용자 섹션 */}\n          <Card className=\"bg-gray-800/50 border-gray-700\">\n            <CardHeader>\n              <CardTitle className=\"text-xl text-white flex items-center gap-2\">\n                <Users className=\"w-5 h-5 text-blue-400\" />\n                일반 사용자 ({regularUsers.length}명)\n              </CardTitle>\n            </CardHeader>\n            <CardContent>\n              <div className=\"grid gap-4\">\n                {regularUsers.map((user) => (\n                  <UserCard \n                    key={user.id}\n                    user={user}\n                    currentUser={currentUser}\n                    onRoleChange={handleRoleChangeNotice}\n                    onEditProfile={handleOpenEditModal} // New prop\n                    isUpdating={updatingUserId === user.id}\n                  />\n                ))}\n                {regularUsers.length === 0 && (\n                  <p className=\"text-gray-400 text-center py-4\">일반 사용자가 없습니다.</p>\n                )}\n              </div>\n            </CardContent>\n          </Card>\n        </div>\n      </div>\n      \n      {/* Edit Profile Modal */}\n      {isEditModalOpen && (\n        <EditProfileModal\n          user={editingUser}\n          isOpen={isEditModalOpen}\n          onClose={() => setIsEditModalOpen(false)}\n          onSave={handleProfileUpdate}\n          isSaving={!!updatingUserId}\n        />\n      )}\n    </div>\n  );\n}\n\n// 사용자 카드 컴포넌트\nfunction UserCard({ user, currentUser, onRoleChange, onEditProfile, isUpdating }) { // Added onEditProfile prop\n  const isCurrentUser = user.id === currentUser?.id;\n  const isAdmin = user.role === 'admin';\n  const isSuperAdmin = currentUser?.email === '102810aa@gmail.com'; // 현재 로그인한 사용자가 최고 관리자인지 여부\n  \n  return (\n    <div className=\"flex items-center justify-between p-4 bg-gray-700/50 rounded-lg border border-gray-600\">\n      <div className=\"flex items-center gap-4\">\n        <div className={`w-12 h-12 rounded-full flex items-center justify-center font-bold text-white ${\n          isAdmin ? 'bg-gradient-to-r from-yellow-500 to-orange-500' : 'bg-gradient-to-r from-blue-500 to-purple-500'\n        }`}>\n          {user.full_name ? user.full_name[0].toUpperCase() : 'U'}\n        </div>\n        <div>\n          <div className=\"flex items-center gap-2\">\n            <h3 className=\"text-white font-medium\">\n              {user.full_name || '이름 없음'}\n            </h3>\n            {isCurrentUser && (\n              <Badge variant=\"outline\" className=\"border-green-500 text-green-400 text-xs\">\n                본인\n              </Badge>\n            )}\n            {/* 타겟 사용자가 최고 관리자인 경우 (email로 확인) */}\n            {user.email === '102810aa@gmail.com' && (\n                <Badge variant=\"outline\" className=\"border-red-500 text-red-400 text-xs\">\n                    최고 관리자\n                </Badge>\n            )}\n          </div>\n          <p className=\"text-gray-400 text-sm\">{user.email}</p>\n          <div className=\"flex items-center gap-2 mt-1\">\n            <Badge className={`text-xs ${\n              isAdmin \n                ? 'bg-yellow-600/20 text-yellow-400 border border-yellow-500/30' \n                : 'bg-blue-600/20 text-blue-400 border border-blue-500/30'\n            }`}>\n              {isAdmin ? '관리자' : '일반 사용자'}\n            </Badge>\n            <span className=\"text-xs text-gray-500\">\n              가입일: {new Date(user.created_date).toLocaleDateString('ko-KR')}\n            </span>\n          </div>\n        </div>\n      </div>\n\n      <div className=\"flex gap-2 items-center\">\n        {/* 최고 관리자는 본인 계정 포함 모든 계정 수정 가능 */}\n        {isSuperAdmin && (\n          <Button\n            onClick={() => onEditProfile(user)}\n            disabled={isUpdating}\n            size=\"sm\"\n            variant=\"ghost\"\n            className=\"text-gray-400 hover:text-white\"\n          >\n            <Pencil className=\"w-4 h-4 mr-1\" />\n            프로필 수정\n          </Button>\n        )}\n\n        {/* 역할 변경 안내 버튼 - Base44 대시보드 사용 안내 */}\n        {isSuperAdmin && !isCurrentUser && (\n          <Button\n            onClick={onRoleChange}\n            size=\"sm\"\n            variant=\"outline\"\n            className=\"border-gray-600 text-gray-400 hover:bg-gray-700\"\n          >\n            <UserCog className=\"w-4 h-4 mr-1\" />\n            역할 변경 방법\n          </Button>\n        )}\n\n        {isCurrentUser && (\n          <Badge className=\"bg-green-600/20 text-green-400 border border-green-500/30\">\n            본인 계정\n          </Badge>\n        )}\n      </div>\n    </div>\n  );\n}\n\n// 프로필 수정 모달 컴포넌트\nfunction EditProfileModal({ user, isOpen, onClose, onSave, isSaving }) {\n  const [formData, setFormData] = useState({\n    full_name: user?.full_name || \"\",\n    gallery_title: user?.gallery_title || \"\",\n    gallery_description: user?.gallery_description || \"\",\n  });\n\n  useEffect(() => {\n    if (user) {\n      setFormData({\n        full_name: user.full_name || \"\",\n        // Provide a default for gallery_title if user.full_name exists, otherwise a generic one\n        gallery_title: user.gallery_title || (user.full_name ? `🖤${user.full_name}🖤 님의 스티커 갤러리` : \"사용자님의 스티커 갤러리\"),\n        gallery_description: user.gallery_description || \"\",\n      });\n    }\n  }, [user]);\n\n  const handleChange = (e) => {\n    const { name, value } = e.target;\n    setFormData((prev) => ({ ...prev, [name]: value }));\n  };\n\n  const handleSaveClick = () => {\n    onSave(formData);\n  };\n\n  return (\n    <Dialog open={isOpen} onOpenChange={onClose}>\n      <DialogContent className=\"bg-gray-900 border-gray-700 text-white\">\n        <DialogHeader>\n          <DialogTitle className=\"text-xl\">\n            '{user?.full_name}'님 프로필 수정\n          </DialogTitle>\n        </DialogHeader>\n        <div className=\"grid gap-4 py-4\">\n          <div className=\"grid grid-cols-4 items-center gap-4\">\n            <Label htmlFor=\"full_name\" className=\"text-right text-gray-400\">\n              이름\n            </Label>\n            <Input\n              id=\"full_name\"\n              name=\"full_name\"\n              value={formData.full_name}\n              onChange={handleChange}\n              className=\"col-span-3 bg-gray-800 border-gray-600\"\n            />\n          </div>\n          <div className=\"grid grid-cols-4 items-center gap-4\">\n            <Label htmlFor=\"gallery_title\" className=\"text-right text-gray-400\">\n              갤러리 제목\n            </Label>\n            <Input\n              id=\"gallery_title\"\n              name=\"gallery_title\"\n              value={formData.gallery_title}\n              onChange={handleChange}\n              className=\"col-span-3 bg-gray-800 border-gray-600\"\n            />\n          </div>\n          <div className=\"grid grid-cols-4 items-center gap-4\">\n            <Label htmlFor=\"gallery_description\" className=\"text-right text-gray-400\">\n              갤러리 설명\n            </Label>\n            <Textarea\n              id=\"gallery_description\"\n              name=\"gallery_description\"\n              value={formData.gallery_description}\n              onChange={handleChange}\n              className=\"col-span-3 bg-gray-800 border-gray-600\"\n            />\n          </div>\n        </div>\n        <DialogFooter>\n          <Button\n            type=\"button\"\n            variant=\"outline\"\n            onClick={onClose}\n            className=\"border-gray-600 text-gray-300 hover:bg-gray-800\"\n          >\n            취소\n          </Button>\n          <Button onClick={handleSaveClick} disabled={isSaving} className=\"bg-blue-600 hover:bg-blue-700\">\n            {isSaving ? \"저장 중...\" : \"저장\"}\n          </Button>\n        </DialogFooter>\n      </DialogContent>\n    </Dialog>\n  );\n}\n","MyStickers":"\nimport React, { useState, useEffect, useCallback, useMemo } from \"react\";\nimport { UserGift, User } from \"@/entities/all\";\nimport { Card, CardContent, CardHeader, CardTitle } from \"@/components/ui/card\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Dialog, DialogContent, DialogHeader, DialogTitle } from \"@/components/ui/dialog\";\nimport { Sparkles, ArrowLeft, Gift, Lock, Trash2, CheckCircle2, Edit, HelpCircle } from \"lucide-react\";\nimport { motion, AnimatePresence } from \"framer-motion\";\nimport { Button } from \"@/components/ui/button\";\nimport { Link } from \"react-router-dom\";\nimport { createPageUrl } from \"@/utils\";\n\nexport default function MyStickers() {\n  const [myGifts, setMyGifts] = useState([]);\n  const [selectedGift, setSelectedGift] = useState(null);\n  const [isLoading, setIsLoading] = useState(true);\n  const [currentUser, setCurrentUser] = useState(null);\n  // 선택 삭제 기능 상태\n  const [isSelectionMode, setIsSelectionMode] = useState(false);\n  const [selectedGiftIds, setSelectedGiftIds] = new Set();\n  const [isDeleting, setIsDeleting] = useState(false);\n\n  const loadMyGifts = useCallback(async () => {\n    try {\n      setIsLoading(true);\n      const user = await User.me();\n      setCurrentUser(user);\n\n      // 최고 관리자만 접근 가능\n      if (!user || user.email !== '102810aa@gmail.com') {\n        setCurrentUser(null);\n        setIsLoading(false);\n        return;\n      }\n\n      if (user) {\n        const gifts = await UserGift.filter({ created_by: user.email }, '-received_at');\n        setMyGifts(gifts);\n      }\n    } catch (error) {\n      console.error(\"내 선물 목록 로딩 실패:\", error);\n      setCurrentUser(null); // 로그아웃 상태로 간주\n    } finally {\n      setIsLoading(false);\n    }\n  }, []);\n\n  useEffect(() => {\n    loadMyGifts();\n  }, [loadMyGifts]);\n  \n  // 중복이 제거된 선물 목록\n  const uniqueGifts = useMemo(() => {\n    const seen = new Set();\n    // 최신 선물을 기준으로 중복을 제거합니다 (received_at 기준 내림차순 정렬 후)\n    return myGifts\n      .sort((a, b) => new Date(b.received_at).getTime() - new Date(a.received_at).getTime())\n      .filter(gift => {\n        if (seen.has(gift.gift_id)) {\n          return false;\n        } else {\n          seen.add(gift.gift_id);\n          return true;\n        }\n      });\n  }, [myGifts]);\n\n  // 선택 모드 토글\n  const toggleSelectionMode = () => {\n    setIsSelectionMode(prev => !prev);\n    setSelectedGiftIds(new Set());\n  };\n  \n  // 스티커 클릭 핸들러\n  const handleGiftClick = (gift) => {\n    if (isSelectionMode) {\n      setSelectedGiftIds(prev => {\n        const newSet = new Set(prev);\n        if (newSet.has(gift.id)) {\n          newSet.delete(gift.id);\n        } else {\n          newSet.add(gift.id);\n        }\n        return newSet;\n      });\n    } else {\n      setSelectedGift(gift);\n    }\n  };\n\n  // 선택된 스티커 삭제\n  const handleDeleteSelected = async () => {\n    if (selectedGiftIds.size === 0) return;\n\n    if (window.confirm(`${selectedGiftIds.size}개의 스티커를 정말 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.`)) {\n      setIsDeleting(true);\n      try {\n        const deletePromises = Array.from(selectedGiftIds).map(id => UserGift.delete(id));\n        await Promise.all(deletePromises);\n\n        setMyGifts(prev => prev.filter(gift => !selectedGiftIds.has(gift.id)));\n        toggleSelectionMode(); // 선택 모드 종료\n      } catch (error) {\n        console.error(\"선택한 스티커 삭제 실패:\", error);\n        alert(\"스티커 삭제 중 오류가 발생했습니다.\");\n      } finally {\n        setIsDeleting(false);\n      }\n    }\n  };\n\n  if (isLoading) {\n    return (\n      <div className=\"min-h-screen bg-gradient-to-br from-gray-900 via-black to-gray-900 text-white flex items-center justify-center\">\n        <div className=\"text-center\">\n          <div className=\"animate-spin rounded-full h-12 w-12 border-b-2 border-purple-500 mx-auto mb-4\"></div>\n          <p className=\"text-gray-400\">내가 받은 스티커를 불러오는 중...</p>\n        </div>\n      </div>\n    );\n  }\n\n  if (!currentUser) {\n    return (\n      <div className=\"min-h-screen bg-gradient-to-br from-gray-900 via-black to-gray-900 text-white flex items-center justify-center text-center\">\n        <div>\n          <Lock className=\"w-16 h-16 text-gray-500 mx-auto mb-4\" />\n          <h2 className=\"text-xl font-bold mb-2\">로그인이 필요합니다</h2>\n          <p className=\"text-gray-400 mb-6\">로그인하여 내가 받은 스티커를 확인하세요.</p>\n          <Button onClick={() => User.login()} className=\"bg-purple-600 hover:bg-purple-700\">\n            로그인\n          </Button>\n        </div>\n      </div>\n    );\n  }\n  \n  return (\n    <div className=\"min-h-screen bg-gradient-to-br from-gray-900 via-black to-gray-900 text-white\">\n      <div className=\"relative overflow-hidden\">\n        <div className=\"absolute inset-0 bg-gradient-to-r from-purple-500/20 to-pink-600/20 blur-3xl\" />\n        <div className=\"relative px-6 py-8\">\n          <div className=\"max-w-6xl mx-auto\">\n            <div className=\"flex items-center justify-between mb-6 flex-wrap gap-4\">\n              <div className=\"flex items-center gap-4\">\n                <div className=\"w-16 h-16 bg-gradient-to-r from-purple-500 to-pink-500 rounded-2xl flex items-center justify-center shadow-xl\">\n                  <Gift className=\"w-8 h-8 text-white\" />\n                </div>\n                <div>\n                  <h1 className=\"text-3xl font-bold text-white\">\n                    내가 받은 스티커\n                  </h1>\n                  <p className=\"text-gray-400 mt-1\">총 {uniqueGifts.length}개의 스티커를 선물받았습니다.</p>\n                </div>\n              </div>\n              <div className=\"flex items-center gap-2\">\n                {isSelectionMode ? (\n                  <>\n                    <Button\n                      variant=\"destructive\"\n                      onClick={handleDeleteSelected}\n                      disabled={selectedGiftIds.size === 0 || isDeleting}\n                    >\n                      <Trash2 className=\"w-4 h-4 mr-2\" />\n                      {isDeleting ? '삭제 중...' : `${selectedGiftIds.size}개 삭제`}\n                    </Button>\n                    <Button variant=\"outline\" onClick={toggleSelectionMode} className=\"border-gray-600 text-gray-300 hover:bg-gray-700\">\n                      취소\n                    </Button>\n                  </>\n                ) : (\n                  uniqueGifts.length > 0 && (\n                    <Button variant=\"outline\" onClick={toggleSelectionMode} className=\"border-gray-600 text-gray-300 hover:bg-gray-700\">\n                      <Edit className=\"w-4 h-4 mr-2\" />\n                      선택\n                    </Button>\n                  )\n                )}\n                <Link to={createPageUrl('Gallery')}>\n                  <Button variant=\"outline\" className=\"border-gray-600 text-gray-300 hover:bg-gray-700\">\n                    <ArrowLeft className=\"w-4 h-4 mr-2\" />\n                    전체 갤러리로 돌아가기\n                  </Button>\n                </Link>\n              </div>\n            </div>\n          </div>\n        </div>\n      </div>\n      \n      <div className=\"max-w-6xl mx-auto px-6 py-8\">\n        {uniqueGifts.length > 0 ? (\n          <motion.div layout className=\"grid grid-cols-3 md:grid-cols-5 gap-4\">\n            <AnimatePresence>\n              {uniqueGifts.map((gift, index) => {\n                const isSelected = selectedGiftIds.has(gift.id);\n                return (\n                  <motion.div\n                    key={gift.id}\n                    layout\n                    initial={{ opacity: 0, y: 20 }}\n                    animate={{ opacity: 1, y: 0 }}\n                    exit={{ opacity: 0, y: -20 }}\n                    transition={{ delay: Math.min(index * 0.01, 0.3) }}\n                    onClick={() => handleGiftClick(gift)}\n                  >\n                    <Card className={`cursor-pointer transition-all duration-200 aspect-[4/5] relative group border-2 ${\n                        isSelectionMode \n                          ? 'hover:scale-105'\n                          : 'hover:scale-105 shadow-xl hover:shadow-2xl'\n                      } ${\n                        isSelected \n                          ? 'border-green-500 bg-green-900/30' \n                          : 'border-purple-500 bg-gray-800/90'\n                      }`}>\n                      <CardContent className=\"p-2 h-full\">\n                        <div className=\"relative w-full h-full rounded-lg bg-gray-700 flex items-center justify-center overflow-hidden\">\n                          {gift.image_thumbnail ? (\n                            <img\n                              src={gift.image_thumbnail}\n                              alt={gift.title}\n                              className={`w-full h-full object-contain object-center transition-opacity ${isSelected ? 'opacity-50' : ''}`}\n                              loading=\"lazy\"\n                            />\n                          ) : (\n                            <div className=\"w-full h-full bg-gray-700 flex items-center justify-center\">\n                              <span className=\"text-xs text-gray-300\">이미지 없음</span>\n                            </div>\n                          )}\n                          {!isSelectionMode && (\n                            <div className=\"absolute bottom-0 left-0 right-0 bg-black/70 backdrop-blur-sm\">\n                              <p className=\"text-xs text-white font-medium text-center py-2 px-2 truncate\">\n                                {gift.received_from}\n                              </p>\n                            </div>\n                          )}\n                          {isSelected && (\n                              <div className=\"absolute inset-0 flex items-center justify-center bg-black/50\">\n                                  <CheckCircle2 className=\"w-10 h-10 text-green-400\"/>\n                              </div>\n                          )}\n                        </div>\n                      </CardContent>\n                    </Card>\n                  </motion.div>\n                )\n            })}\n            </AnimatePresence>\n          </motion.div>\n        ) : (\n          <div className=\"text-center py-16\">\n            <div className=\"w-24 h-24 bg-gray-800 rounded-full flex items-center justify-center mx-auto mb-4\">\n              <Gift className=\"w-12 h-12 text-gray-600\" />\n            </div>\n            <p className=\"text-gray-400 text-lg\">아직 받은 스티커가 없습니다.</p>\n            <p className=\"text-gray-500 text-sm mt-2\">다른 사람에게 스티커 선물을 요청해보세요!</p>\n          </div>\n        )}\n      </div>\n\n      {!isSelectionMode && selectedGift && (\n        <Dialog open={!!selectedGift} onOpenChange={() => setSelectedGift(null)}>\n          <DialogContent className=\"max-w-md bg-gray-900 border-gray-700 text-white\">\n            <DialogHeader>\n              <DialogTitle className=\"text-center text-xl font-bold bg-gradient-to-r from-purple-400 to-pink-400 bg-clip-text text-transparent\">\n                선물 상세 정보\n              </DialogTitle>\n            </DialogHeader>\n            <div className=\"space-y-6\">\n              <div className=\"w-full h-48 rounded-xl bg-gray-700 flex items-center justify-center overflow-hidden\">\n                {selectedGift.image_thumbnail ? (\n                  <img src={selectedGift.image_thumbnail} alt={selectedGift.title} className=\"w-full h-full object-contain\" />\n                ) : <span className=\"text-gray-400\">이미지 없음</span>}\n              </div>\n              <div className=\"text-center space-y-2\">\n                <h2 className=\"text-2xl font-bold text-white\">{selectedGift.title}</h2>\n                <div className=\"flex items-center justify-center gap-2\">\n                  <span className=\"text-3xl\">🥄</span>\n                  <span className=\"text-xl font-bold text-amber-400\">{selectedGift.price?.toLocaleString() || 0}</span>\n                </div>\n              </div>\n              <div className=\"bg-gray-800 rounded-xl p-4 space-y-3\">\n                <h3 className=\"font-semibold text-purple-400 flex items-center gap-2\">\n                  <Sparkles className=\"w-5 h-5\" />\n                  선물 정보\n                </h3>\n                <div className=\"text-sm\">\n                  <p><span className=\"text-gray-400\">보낸 사람:</span> <span className=\"font-medium text-white\">{selectedGift.received_from}</span></p>\n                  <p><span className=\"text-gray-400\">받은 날짜:</span> <span className=\"font-medium text-white\">{new Date(selectedGift.received_at).toLocaleString('ko-KR')}</span></p>\n                </div>\n                {selectedGift.message && (\n                  <div className=\"pt-3 border-t border-gray-700\">\n                    <p className=\"text-gray-300 text-sm mb-2\">함께 온 메시지:</p>\n                    <p className=\"text-white bg-gray-700 rounded-lg p-3\">\"{selectedGift.message}\"</p>\n                  </div>\n                )}\n              </div>\n              <Button onClick={() => setSelectedGift(null)} className=\"w-full bg-purple-600 hover:bg-purple-700\">닫기</Button>\n            </div>\n          </DialogContent>\n        </Dialog>\n      )}\n\n      {/* 페이지 설명서 */}\n      <div className=\"max-w-6xl mx-auto px-6 pb-12\">\n        <Card className=\"bg-gray-800/50 border-gray-700\">\n            <CardHeader>\n                <CardTitle className=\"text-lg text-white flex items-center gap-2\">\n                    <HelpCircle className=\"w-5 h-5 text-purple-400\" />\n                    페이지 안내\n                </CardTitle>\n            </CardHeader>\n            <CardContent className=\"text-sm text-gray-300 space-y-4\">\n                <div>\n                    <h3 className=\"font-semibold text-purple-300 mb-1\">여기는 어떤 곳인가요?</h3>\n                    <p>\n                        이곳은 <strong>'나만의 스티커 보관함'</strong>입니다. 전체 갤러리에서 활성화된 스티커들이 자동으로 수집되며, 내가 직접 받은 선물 기록도 모두 여기에 저장됩니다.\n                    </p>\n                </div>\n                <div>\n                    <h3 className=\"font-semibold text-purple-300 mb-1\">스티커가 사라지지 않아요!</h3>\n                    <p>\n                        전체 갤러리에서 특정 스티커가 제거되더라도, 내가 한 번이라도 획득했던 스티커는 이 페이지에 <strong>영구적으로 보관</strong>됩니다. 소중한 스티커를 잃어버릴 걱정 없이 언제든 다시 볼 수 있습니다.\n                    </p>\n                </div>\n                <div>\n                    <h3 className=\"font-semibold text-purple-300 mb-1\">스티커를 정리하고 싶어요</h3>\n                    <p>\n                        우측 상단의 <Button variant=\"outline\" size=\"sm\" className=\"h-auto px-2 py-1 text-xs pointer-events-none\"><Edit className=\"w-3 h-3 mr-1\" />선택</Button> 버튼을 누르면 여러 스티커를 선택하여 한 번에 삭제할 수 있습니다.\n                    </p>\n                </div>\n            </CardContent>\n        </Card>\n      </div>\n    </div>\n  );\n}\n","ListenerManagement":"\nimport React, { useState, useEffect, useCallback, useMemo } from \"react\";\nimport { User } from \"@/entities/all\";\nimport { Card, CardContent, CardHeader, CardTitle } from \"@/components/ui/card\";\nimport { Input } from \"@/components/ui/input\";\nimport { Textarea } from \"@/components/ui/textarea\";\nimport { Button } from \"@/components/ui/button\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from \"@/components/ui/dialog\";\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from \"@/components/ui/select\";\nimport { Label } from \"@/components/ui/label\";\nimport { Users, Plus, Pencil, Trash2, Lock, Search, ArrowLeft, Calendar, Briefcase, MapPin, User as UserIcon } from \"lucide-react\";\nimport { Link } from \"react-router-dom\";\nimport { createPageUrl } from \"@/utils\";\nimport { motion, AnimatePresence } from \"framer-motion\";\nimport { base44 } from \"@/api/base44Client\";\n\nconst MBTI_TYPES = [\"ISTJ\", \"ISFJ\", \"INFJ\", \"INTJ\", \"ISTP\", \"ISFP\", \"INFP\", \"INTP\", \"ESTP\", \"ESFP\", \"ENFP\", \"ENTP\", \"ESTJ\", \"ESFJ\", \"ENFJ\", \"ENTJ\"];\n\nexport default function ListenerManagement() {\n  const [currentUser, setCurrentUser] = useState(null);\n  const [listeners, setListeners] = useState([]);\n  const [isLoading, setIsLoading] = useState(true);\n  const [searchQuery, setSearchQuery] = useState(\"\");\n  const [isDialogOpen, setIsDialogOpen] = useState(false);\n  const [editingListener, setEditingListener] = useState(null);\n  const [isSaving, setIsSaving] = useState(false);\n  const [selectedListener, setSelectedListener] = useState(null);\n  const [isDetailOpen, setIsDetailOpen] = useState(false);\n\n  const [formData, setFormData] = useState({\n    spoon_nickname: \"\",\n    chzzk_nickname: \"\",\n    youtube_nickname: \"\",\n    region: \"\",\n    job: \"\",\n    birthday: \"\",\n    mbti: \"\",\n    memo: \"\"\n  });\n\n  const loadData = useCallback(async () => {\n    try {\n      setIsLoading(true);\n      \n      // 로그인 여부 확인 (선택사항)\n      try {\n        const user = await base44.auth.me();\n        setCurrentUser(user);\n      } catch (error) {\n        console.log(\"비로그인 사용자\", error);\n        setCurrentUser(null);\n      }\n      \n      // 모든 사용자가 청취자 목록을 볼 수 있도록 변경\n      const listenerList = await base44.entities.Listener.list('-created_date');\n      setListeners(listenerList);\n      \n    } catch (error) {\n      console.error(\"데이터 로딩 실패:\", error);\n      setListeners([]);\n    } finally {\n      setIsLoading(false);\n    }\n  }, []);\n\n  useEffect(() => {\n    loadData();\n  }, [loadData]);\n\n  const handleOpenDialog = (listener = null) => {\n    // 관리자만 추가/수정 가능\n    if (!currentUser || currentUser.role !== 'admin') {\n      alert(\"청취자 정보를 추가/수정하려면 관리자 권한이 필요합니다.\");\n      return;\n    }\n    \n    if (listener) {\n      setEditingListener(listener);\n      setFormData({\n        spoon_nickname: listener.spoon_nickname || \"\",\n        chzzk_nickname: listener.chzzk_nickname || \"\",\n        youtube_nickname: listener.youtube_nickname || \"\",\n        region: listener.region || \"\",\n        job: listener.job || \"\",\n        birthday: listener.birthday || \"\",\n        mbti: listener.mbti || \"\",\n        memo: listener.memo || \"\"\n      });\n    } else {\n      setEditingListener(null);\n      setFormData({\n        spoon_nickname: \"\",\n        chzzk_nickname: \"\",\n        youtube_nickname: \"\",\n        region: \"\",\n        job: \"\",\n        birthday: \"\",\n        mbti: \"\",\n        memo: \"\"\n      });\n    }\n    setIsDialogOpen(true);\n  };\n\n  const handleCloseDialog = () => {\n    setIsDialogOpen(false);\n    setEditingListener(null);\n  };\n\n  const handleSave = async () => {\n    if (!formData.spoon_nickname.trim()) {\n      alert(\"스푼 닉네임은 필수 항목입니다.\");\n      return;\n    }\n\n    setIsSaving(true);\n    try {\n      if (editingListener) {\n        const updated = await base44.entities.Listener.update(editingListener.id, formData);\n        setListeners(prev => prev.map(l => l.id === editingListener.id ? updated : l));\n      } else {\n        const newListener = await base44.entities.Listener.create(formData);\n        setListeners(prev => [newListener, ...prev]);\n      }\n      handleCloseDialog();\n    } catch (error) {\n      console.error(\"저장 실패:\", error);\n      alert(\"저장 중 오류가 발생했습니다.\");\n    } finally {\n      setIsSaving(false);\n    }\n  };\n\n  const handleDelete = async (listenerId) => {\n    // 관리자만 삭제 가능\n    if (!currentUser || currentUser.role !== 'admin') {\n      alert(\"청취자 정보를 삭제하려면 관리자 권한이 필요합니다.\");\n      return;\n    }\n    \n    if (window.confirm(\"정말 이 청취자 정보를 삭제하시겠습니까?\")) {\n      try {\n        await base44.entities.Listener.delete(listenerId);\n        setListeners(prev => prev.filter(l => l.id !== listenerId));\n        if (selectedListener?.id === listenerId) {\n          setIsDetailOpen(false);\n          setSelectedListener(null);\n        }\n      } catch (error) {\n        console.error(\"삭제 실패:\", error);\n        alert(\"삭제 중 오류가 발생했습니다.\");\n      }\n    }\n  };\n\n  const handleViewDetail = (listener) => {\n    setSelectedListener(listener);\n    setIsDetailOpen(true);\n  };\n\n  const filteredListeners = useMemo(() => {\n    if (!searchQuery.trim()) return listeners;\n    \n    const query = searchQuery.toLowerCase();\n    return listeners.filter(l => \n      (l.spoon_nickname?.toLowerCase() || \"\").includes(query) ||\n      (l.chzzk_nickname?.toLowerCase() || \"\").includes(query) ||\n      (l.youtube_nickname?.toLowerCase() || \"\").includes(query) ||\n      (l.region?.toLowerCase() || \"\").includes(query) ||\n      (l.job?.toLowerCase() || \"\").includes(query) ||\n      (l.mbti?.toLowerCase() || \"\").includes(query)\n    );\n  }, [listeners, searchQuery]);\n\n  const isSuperAdmin = currentUser?.email === '102810aa@gmail.com';\n  const isAdmin = currentUser?.role === 'admin';\n\n  if (isLoading) {\n    return (\n      <div className=\"min-h-screen bg-gradient-to-br from-blue-50 via-white to-purple-50 flex items-center justify-center\">\n        <div className=\"text-center\">\n          <div className=\"animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4\"></div>\n          <p className=\"text-gray-600\">청취자 목록을 불러오는 중...</p>\n        </div>\n      </div>\n    );\n  }\n\n  return (\n    <div className=\"min-h-screen bg-gradient-to-br from-blue-50 via-white to-purple-50\">\n      {/* 헤더 */}\n      <div className=\"relative overflow-hidden bg-gradient-to-r from-blue-600 to-purple-600\">\n        <div className=\"relative px-6 py-8\">\n          <div className=\"max-w-7xl mx-auto\">\n            <div className=\"flex items-center justify-between mb-6 flex-wrap gap-4\">\n              <div className=\"flex items-center gap-4\">\n                {isSuperAdmin && (\n                  <Link to={createPageUrl('Gallery')}>\n                    <Button variant=\"outline\" className=\"border-white/30 text-white hover:bg-white/20 backdrop-blur-sm\">\n                      <ArrowLeft className=\"w-4 h-4 mr-2\" />\n                      갤러리로 돌아가기\n                    </Button>\n                  </Link>\n                )}\n                <div className=\"w-16 h-16 bg-white rounded-2xl flex items-center justify-center shadow-xl\">\n                  <Users className=\"w-8 h-8 text-blue-600\" />\n                </div>\n                <div>\n                  <h1 className=\"text-3xl font-bold text-white\">\n                    청취자 관리\n                  </h1>\n                  <div className=\"flex items-center gap-4 mt-2\">\n                    {isAdmin && (\n                      <Badge className=\"bg-white/20 text-white border-white/30 backdrop-blur-sm\">\n                        관리자\n                      </Badge>\n                    )}\n                    <span className=\"text-white/90 font-medium\">\n                      총 {listeners.length}명\n                    </span>\n                  </div>\n                </div>\n              </div>\n              <div className=\"flex items-center gap-2\">\n                {!currentUser ? (\n                  <Button \n                    onClick={() => base44.auth.redirectToLogin()}\n                    className=\"bg-white text-blue-600 hover:bg-blue-50\"\n                  >\n                    <UserIcon className=\"w-4 h-4 mr-2\" />\n                    로그인\n                  </Button>\n                ) : isAdmin ? (\n                  <Button onClick={() => handleOpenDialog()} className=\"bg-white text-blue-600 hover:bg-blue-50\">\n                    <Plus className=\"w-4 h-4 mr-2\" />\n                    청취자 추가\n                  </Button>\n                ) : null}\n              </div>\n            </div>\n\n            {/* 검색 */}\n            <div className=\"relative\">\n              <Search className=\"absolute left-4 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400\" />\n              <Input\n                placeholder=\"닉네임, 지역, 직업, MBTI로 검색...\"\n                value={searchQuery}\n                onChange={(e) => setSearchQuery(e.target.value)}\n                className=\"pl-12 bg-white/90 backdrop-blur-sm border-white/50 text-gray-900 placeholder:text-gray-500 shadow-lg\"\n              />\n            </div>\n          </div>\n        </div>\n      </div>\n\n      {/* 청취자 목록 */}\n      <div className=\"max-w-7xl mx-auto px-6 py-8\">\n        {filteredListeners.length > 0 ? (\n          <motion.div layout className=\"grid gap-4\">\n            <AnimatePresence>\n              {filteredListeners.map((listener, index) => (\n                <ListenerCard\n                  key={listener.id}\n                  listener={listener}\n                  index={index}\n                  onEdit={() => handleOpenDialog(listener)}\n                  onDelete={() => handleDelete(listener.id)}\n                  onView={() => handleViewDetail(listener)}\n                  isAdmin={isAdmin}\n                />\n              ))}\n            </AnimatePresence>\n          </motion.div>\n        ) : (\n          <div className=\"text-center py-16\">\n            <div className=\"w-24 h-24 bg-blue-100 rounded-full flex items-center justify-center mx-auto mb-4\">\n              <Users className=\"w-12 h-12 text-blue-600\" />\n            </div>\n            <p className=\"text-gray-600 text-lg\">\n              {searchQuery ? \"검색 결과가 없습니다\" : \"등록된 청취자가 없습니다\"}\n            </p>\n          </div>\n        )}\n      </div>\n\n      {/* 추가/수정 다이얼로그 */}\n      <Dialog open={isDialogOpen} onOpenChange={handleCloseDialog}>\n        <DialogContent className=\"max-w-2xl max-h-[90vh] overflow-y-auto bg-white text-gray-900\">\n          <DialogHeader>\n            <DialogTitle className=\"text-xl text-gray-900\">\n              {editingListener ? \"청취자 정보 수정\" : \"새 청취자 추가\"}\n            </DialogTitle>\n          </DialogHeader>\n          <div className=\"grid gap-4 py-4\">\n            <div className=\"grid grid-cols-4 items-center gap-4\">\n              <Label htmlFor=\"spoon_nickname\" className=\"text-right text-gray-700\">\n                스푼 닉네임 *\n              </Label>\n              <Input\n                id=\"spoon_nickname\"\n                value={formData.spoon_nickname}\n                onChange={(e) => setFormData({...formData, spoon_nickname: e.target.value})}\n                className=\"col-span-3 bg-gray-50 border-gray-300\"\n                placeholder=\"필수 항목\"\n              />\n            </div>\n            <div className=\"grid grid-cols-4 items-center gap-4\">\n              <Label htmlFor=\"chzzk_nickname\" className=\"text-right text-gray-700\">\n                치지직 닉네임\n              </Label>\n              <Input\n                id=\"chzzk_nickname\"\n                value={formData.chzzk_nickname}\n                onChange={(e) => setFormData({...formData, chzzk_nickname: e.target.value})}\n                className=\"col-span-3 bg-gray-50 border-gray-300\"\n              />\n            </div>\n            <div className=\"grid grid-cols-4 items-center gap-4\">\n              <Label htmlFor=\"youtube_nickname\" className=\"text-right text-gray-700\">\n                유튜브 닉네임\n              </Label>\n              <Input\n                id=\"youtube_nickname\"\n                value={formData.youtube_nickname}\n                onChange={(e) => setFormData({...formData, youtube_nickname: e.target.value})}\n                className=\"col-span-3 bg-gray-50 border-gray-300\"\n              />\n            </div>\n            <div className=\"grid grid-cols-4 items-center gap-4\">\n              <Label htmlFor=\"region\" className=\"text-right text-gray-700\">\n                지역\n              </Label>\n              <Input\n                id=\"region\"\n                value={formData.region}\n                onChange={(e) => setFormData({...formData, region: e.target.value})}\n                className=\"col-span-3 bg-gray-50 border-gray-300\"\n                placeholder=\"예: 서울\"\n              />\n            </div>\n            <div className=\"grid grid-cols-4 items-center gap-4\">\n              <Label htmlFor=\"job\" className=\"text-right text-gray-700\">\n                직업\n              </Label>\n              <Input\n                id=\"job\"\n                value={formData.job}\n                onChange={(e) => setFormData({...formData, job: e.target.value})}\n                className=\"col-span-3 bg-gray-50 border-gray-300\"\n              />\n            </div>\n            <div className=\"grid grid-cols-4 items-center gap-4\">\n              <Label htmlFor=\"birthday\" className=\"text-right text-gray-700\">\n                생일\n              </Label>\n              <Input\n                id=\"birthday\"\n                type=\"date\"\n                value={formData.birthday}\n                onChange={(e) => setFormData({...formData, birthday: e.target.value})}\n                className=\"col-span-3 bg-gray-50 border-gray-300\"\n              />\n            </div>\n            <div className=\"grid grid-cols-4 items-center gap-4\">\n              <Label htmlFor=\"mbti\" className=\"text-right text-gray-700\">\n                MBTI\n              </Label>\n              <Select value={formData.mbti} onValueChange={(value) => setFormData({...formData, mbti: value})}>\n                <SelectTrigger className=\"col-span-3 bg-gray-50 border-gray-300\">\n                  <SelectValue placeholder=\"MBTI 선택\" />\n                </SelectTrigger>\n                <SelectContent className=\"bg-white\">\n                  {MBTI_TYPES.map(type => (\n                    <SelectItem key={type} value={type}>{type}</SelectItem>\n                  ))}\n                </SelectContent>\n              </Select>\n            </div>\n            <div className=\"grid grid-cols-4 items-start gap-4\">\n              <Label htmlFor=\"memo\" className=\"text-right text-gray-700 pt-2\">\n                메모\n              </Label>\n              <Textarea\n                id=\"memo\"\n                value={formData.memo}\n                onChange={(e) => setFormData({...formData, memo: e.target.value})}\n                className=\"col-span-3 bg-gray-50 border-gray-300 h-24\"\n                placeholder=\"특이사항, 기타 정보 등\"\n              />\n            </div>\n          </div>\n          <DialogFooter>\n            <Button variant=\"outline\" onClick={handleCloseDialog} className=\"border-gray-300 text-gray-700 hover:bg-gray-100\">\n              취소\n            </Button>\n            <Button onClick={handleSave} disabled={isSaving} className=\"bg-blue-600 hover:bg-blue-700 text-white\">\n              {isSaving ? \"저장 중...\" : \"저장\"}\n            </Button>\n          </DialogFooter>\n        </DialogContent>\n      </Dialog>\n\n      {/* 상세보기 다이얼로그 */}\n      <Dialog open={isDetailOpen} onOpenChange={setIsDetailOpen}>\n        <DialogContent className=\"max-w-md bg-white text-gray-900\">\n          <DialogHeader>\n            <DialogTitle className=\"text-xl bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent\">\n              청취자 상세 정보\n            </DialogTitle>\n          </DialogHeader>\n          {selectedListener && (\n            <div className=\"space-y-4 py-4\">\n              <div className=\"text-center pb-4 border-b border-gray-200\">\n                <div className=\"w-20 h-20 bg-gradient-to-r from-blue-500 to-purple-500 rounded-full flex items-center justify-center mx-auto mb-3 shadow-lg\">\n                  <UserIcon className=\"w-10 h-10 text-white\" />\n                </div>\n                <h3 className=\"text-2xl font-bold text-gray-900\">{selectedListener.spoon_nickname}</h3>\n              </div>\n              \n              <InfoRow icon={<UserIcon className=\"w-4 h-4 text-blue-600\" />} label=\"치지직\" value={selectedListener.chzzk_nickname} />\n              <InfoRow icon={<UserIcon className=\"w-4 h-4 text-blue-600\" />} label=\"유튜브\" value={selectedListener.youtube_nickname} />\n              <InfoRow icon={<MapPin className=\"w-4 h-4 text-blue-600\" />} label=\"지역\" value={selectedListener.region} />\n              <InfoRow icon={<Briefcase className=\"w-4 h-4 text-blue-600\" />} label=\"직업\" value={selectedListener.job} />\n              <InfoRow icon={<Calendar className=\"w-4 h-4 text-blue-600\" />} label=\"생일\" value={selectedListener.birthday ? new Date(selectedListener.birthday).toLocaleDateString('ko-KR') : \"\"} />\n              {selectedListener.mbti && (\n                <div className=\"flex items-center gap-2\">\n                  <span className=\"text-gray-700 text-sm w-20 font-medium\">MBTI:</span>\n                  <Badge className=\"bg-purple-100 text-purple-700 border border-purple-300\">\n                    {selectedListener.mbti}\n                  </Badge>\n                </div>\n              )}\n              {selectedListener.memo && (\n                <div className=\"pt-3 border-t border-gray-200\">\n                  <p className=\"text-gray-700 text-sm mb-2 font-medium\">메모:</p>\n                  <p className=\"text-gray-900 bg-gray-50 rounded-lg p-3 text-sm whitespace-pre-wrap border border-gray-200\">\n                    {selectedListener.memo}\n                  </p>\n                </div>\n              )}\n              \n              {isAdmin && (\n                <div className=\"flex gap-2 pt-4\">\n                  <Button onClick={() => { handleOpenDialog(selectedListener); setIsDetailOpen(false); }} className=\"flex-1 bg-blue-600 hover:bg-blue-700 text-white\">\n                    <Pencil className=\"w-4 h-4 mr-2\" />\n                    수정\n                  </Button>\n                  <Button onClick={() => { handleDelete(selectedListener.id); }} variant=\"destructive\" className=\"flex-1\">\n                    <Trash2 className=\"w-4 h-4 mr-2\" />\n                    삭제\n                  </Button>\n                </div>\n              )}\n            </div>\n          )}\n        </DialogContent>\n      </Dialog>\n    </div>\n  );\n}\n\nfunction ListenerCard({ listener, index, onEdit, onDelete, onView, isAdmin }) {\n  return (\n    <motion.div\n      layout\n      initial={{ opacity: 0, y: 20 }}\n      animate={{ opacity: 1, y: 0 }}\n      exit={{ opacity: 0, y: -20 }}\n      transition={{ delay: Math.min(index * 0.02, 0.3) }}\n    >\n      <Card className=\"bg-white border-gray-200 hover:border-blue-400 hover:shadow-xl transition-all cursor-pointer\" onClick={onView}>\n        <CardContent className=\"p-6\">\n          <div className=\"flex items-center justify-between\">\n            <div className=\"flex items-center gap-4 flex-1\">\n              <div className=\"w-14 h-14 bg-gradient-to-r from-blue-500 to-purple-500 rounded-full flex items-center justify-center shadow-lg\">\n                <UserIcon className=\"w-7 h-7 text-white\" />\n              </div>\n              <div className=\"flex-1\">\n                <div className=\"flex items-center gap-2 mb-2 flex-wrap\">\n                  {listener.mbti && (\n                    <Badge className=\"bg-purple-100 text-purple-700 border border-purple-300\">\n                      {listener.mbti}\n                    </Badge>\n                  )}\n                </div>\n                <div className=\"flex flex-wrap items-center gap-2 text-sm text-gray-600 mb-2\">\n                  {listener.spoon_nickname && (\n                    <span className=\"flex items-center gap-1.5 bg-amber-50 px-3 py-1.5 rounded-lg border border-amber-200\">\n                      <span className=\"text-xs font-bold text-amber-700\">스푼:</span> \n                      <span className=\"font-medium text-amber-800\">{listener.spoon_nickname}</span>\n                    </span>\n                  )}\n                  {listener.chzzk_nickname && (\n                    <span className=\"flex items-center gap-1.5 bg-blue-50 px-3 py-1.5 rounded-lg border border-blue-200\">\n                      <span className=\"text-xs font-bold text-blue-700\">치지직:</span> \n                      <span className=\"font-medium text-blue-800\">{listener.chzzk_nickname}</span>\n                    </span>\n                  )}\n                  {listener.youtube_nickname && (\n                    <span className=\"flex items-center gap-1.5 bg-red-50 px-3 py-1.5 rounded-lg border border-red-200\">\n                      <span className=\"text-xs font-bold text-red-700\">유튜브:</span> \n                      <span className=\"font-medium text-red-800\">{listener.youtube_nickname}</span>\n                    </span>\n                  )}\n                </div>\n                <div className=\"flex flex-wrap items-center gap-3 text-sm text-gray-600\">\n                  {listener.region && (\n                    <span className=\"flex items-center gap-1.5 bg-green-50 px-2 py-1 rounded\">\n                      <MapPin className=\"w-3.5 h-3.5 text-green-700\" /> \n                      <span className=\"text-green-700\">{listener.region}</span>\n                    </span>\n                  )}\n                  {listener.job && (\n                    <span className=\"flex items-center gap-1.5 bg-orange-50 px-2 py-1 rounded\">\n                      <Briefcase className=\"w-3.5 h-3.5 text-orange-700\" /> \n                      <span className=\"text-orange-700\">{listener.job}</span>\n                    </span>\n                  )}\n                  {listener.birthday && (\n                    <span className=\"flex items-center gap-1.5 bg-pink-50 px-2 py-1 rounded\">\n                      <Calendar className=\"w-3.5 h-3.5 text-pink-700\" /> \n                      <span className=\"text-pink-700\">{new Date(listener.birthday).toLocaleDateString('ko-KR', { month: 'long', day: 'numeric' })}</span>\n                    </span>\n                  )}\n                </div>\n                {listener.memo && (\n                  <div className=\"mt-3 pt-3 border-t border-gray-200\">\n                    <p className=\"text-xs text-gray-500 mb-1 font-medium\">메모</p>\n                    <p className=\"text-sm text-gray-700 line-clamp-2 bg-gray-50 p-2 rounded border border-gray-200\">\n                      {listener.memo}\n                    </p>\n                  </div>\n                )}\n              </div>\n            </div>\n            {isAdmin && (\n              <div className=\"flex gap-2\" onClick={(e) => e.stopPropagation()}>\n                <Button size=\"sm\" variant=\"ghost\" onClick={onEdit} className=\"text-blue-600 hover:text-blue-700 hover:bg-blue-50\">\n                  <Pencil className=\"w-4 h-4\" />\n                </Button>\n                <Button size=\"sm\" variant=\"ghost\" onClick={onDelete} className=\"text-red-600 hover:text-red-700 hover:bg-red-50\">\n                  <Trash2 className=\"w-4 h-4\" />\n                </Button>\n              </div>\n            )}\n          </div>\n        </CardContent>\n      </Card>\n    </motion.div>\n  );\n}\n\nfunction InfoRow({ icon, label, value }) {\n  if (!value) return null;\n  return (\n    <div className=\"flex items-center gap-2\">\n      <span className=\"text-gray-700 text-sm w-20 flex items-center gap-1.5 font-medium\">\n        {icon}\n        {label}:\n      </span>\n      <span className=\"text-gray-900 font-medium\">{value}</span>\n    </div>\n  );\n}\n"},"page_names":["Gallery","AdminPanel","AdminSettings","UserManagement","MyStickers","ListenerManagement"],"discovered_routes":null,"components":{},"layout":null,"globals_css":null,"entities":{"Gift":{"name":"Gift","type":"object","properties":{"title":{"type":"string","description":"선물 제목"},"image_thumbnail":{"type":"string","description":"선물 썸네일 이미지 URL"},"price":{"type":"number","description":"선물 가격 (스푼)"},"category":{"type":"string","description":"선물 카테고리"},"description":{"type":"string","description":"선물 설명"},"is_unlocked":{"type":"boolean","default":false,"description":"잠금 해제 여부"}},"required":["title","category","price"]},"UserGift":{"name":"UserGift","type":"object","properties":{"gift_id":{"type":"string","description":"원본 스티커 ID"},"received_from":{"type":"string","description":"선물한 사람"},"received_at":{"type":"string","format":"date-time","description":"받은 날짜"},"message":{"type":"string","description":"받은 메시지"},"title":{"type":"string","description":"선물받은 스티커 제목 (스냅샷)"},"image_thumbnail":{"type":"string","description":"선물받은 스티커 이미지 URL (스냅샷)"},"price":{"type":"number","description":"선물받은 스티커 가격 (스냅샷)"}},"required":["gift_id","received_from","title"]},"GiftCategory":{"name":"GiftCategory","type":"object","properties":{"name":{"type":"string","description":"카테고리 이름"},"order":{"type":"number","description":"정렬 순서"}},"required":["name","order"]},"SelectedSticker":{"name":"SelectedSticker","type":"object","properties":{"sticker_id":{"type":"string","description":"원본 스티커 ID"},"title":{"type":"string","description":"스티커 제목"},"image_thumbnail":{"type":"string","description":"스티커 이미지 URL"},"price":{"type":"number","description":"스티커 가격"},"category":{"type":"string","description":"스티커 카테고리"},"display_order":{"type":"number","default":0,"description":"표시 순서"}},"required":["sticker_id","title"]},"Listener":{"name":"Listener","type":"object","properties":{"spoon_nickname":{"type":"string","description":"스푼캐스트 닉네임"},"chzzk_nickname":{"type":"string","description":"치지직 닉네임"},"youtube_nickname":{"type":"string","description":"유튜브 닉네임"},"region":{"type":"string","description":"지역"},"job":{"type":"string","description":"직업"},"birthday":{"type":"string","format":"date","description":"생일"},"mbti":{"type":"string","enum":["ISTJ","ISFJ","INFJ","INTJ","ISTP","ISFP","INFP","INTP","ESTP","ESFP","ENFP","ENTP","ESTJ","ESFJ","ENFJ","ENTJ"],"description":"MBTI"},"memo":{"type":"string","description":"메모"}},"required":["spoon_nickname"]}},"additional_user_data_schema":{"name":"User","type":"object","properties":{"gallery_title":{"type":"string","default":"🖤SOMI🖤 님의 스티커 갤러리","description":"갤러리 제목"},"gallery_description":{"type":"string","description":"갤러리 설명"},"external_sync_filename":{"type":"string","default":"sum.json","description":"외부 동기화에 사용할 JSON 파일 이름"}},"required":["gallery_description"]},"agents":{},"agents_enabled":false,"public_settings":"public_without_login","custom_auth_enrolled":false,"main_page":"Gallery","auth_config":{"enable_username_password":false,"enable_google_login":true,"enable_microsoft_login":false,"enable_facebook_login":false,"enable_apple_login":false,"sso_provider_name":null,"enable_sso_login":false,"google_oauth_mode":"default","google_oauth_client_id":null,"use_workspace_sso":false},"enable_username_password":false,"hide_entity_created_by":false,"installable_integrations":{},"installed_integration_context_items":[],"payment_integrations":null,"using_sandbox":true,"captured_from":null,"figma_source_url":null,"last_git_commit_hash":"1070ba20da4d9e785b016f60b51b3a264eb3c69f","git_remote_source":"s3","has_unchained_ai":false,"tools_permission_config":{"auto_approved_operations":[],"connector_guards":{}},"security_headers_enabled":false,"platform_version":1,"github_repo_url":null,"is_remixable":true,"remixed_from_app_id":"68e8bfe54c5ec0e12ee25bfb","purchase_from_id":null,"purchased_at":null,"slug":"copy-ce66af58","is_blocked":false,"is_unpublished":false,"last_deployed_at":"2026-03-23T07:43:05.012054+00:00","last_deployed_checkpoint_id":"690a2ce1c60a5a69c0992382","screenshot_url":null,"screenshot_updated_at":null,"created_by_id":null,"owner_id":"6899bc225cbc135eb3f9c035","wix_metasite_id":null,"app_stage":"ready","is_managed_source_code":true,"has_non_prod_entities":false,"is_deleted":false,"deleted_date":null,"data_region":null,"function_names":["getStickerData","getAdminGalleryData","sendGiftToUser","updateUserRole","syncExternalGifts","checkFileChanges","updateUserProfile","updateGiftSender","updateStickerTitle","deleteGift","removeStickerFromGallery","getGalleryDataByUser"],"user_entity":{"type":"object","name":"User","title":"User","properties":{"role":{"type":"string","description":"The role of the user in the app","enum":["admin","user"]},"email":{"type":"string","description":"The email of the user"},"full_name":{"type":"string","description":"The full name of the user"},"gallery_title":{"type":"string","default":"🖤SOMI🖤 님의 스티커 갤러리","description":"갤러리 제목"},"gallery_description":{"type":"string","description":"갤러리 설명"},"external_sync_filename":{"type":"string","default":"sum.json","description":"외부 동기화에 사용할 JSON 파일 이름"}},"required":["email","role","gallery_description","full_name"]},"has_backend_functions_enabled":true,"is_private":false,"backend_deployment_ids":{"getStickerData":"nprfvrztgtvk","getAdminGalleryData":"5vky2yfqrxj9","sendGiftToUser":"9q8x12vhz8p7","updateUserRole":"nkgj4tpnk8zg","syncExternalGifts":"gk49brp3wkds","checkFileChanges":"2k4ss1eaps6v","updateUserProfile":"5ymz2xy27rmf","updateGiftSender":"y8cz7f4takcz","updateStickerTitle":"pk2pecvf02ra","deleteGift":"vp390kn9yrep","removeStickerFromGallery":"geka57184pht","getGalleryDataByUser":"cmbkfbd4c426"},"backend_deno_app_slugs":{},"snapshot_id":"690a27dbfe2410d2ff91cd92"}