import React, { useState, useEffect, useRef } from 'react'; import { initializeApp } from "firebase/app"; import { getAuth, signInAnonymously, onAuthStateChanged, signInWithCustomToken } from "firebase/auth"; import { getFirestore, doc, setDoc, onSnapshot } from "firebase/firestore"; // Firebase configuration: Use environment variables or placeholders. const firebaseConfig = typeof __firebase_config !== 'undefined' ? JSON.parse(__firebase_config) : { apiKey: "YOUR_API_KEY", authDomain: "YOUR_AUTH_DOMAIN", projectId: "YOUR_PROJECT_ID", storageBucket: "YOUR_STORAGE_BUCKET", messagingSenderId: "YOUR_MESSAGING_SENDER_ID", appId: "YOUR_APP_ID" }; // Initialize Firebase app, auth, and Firestore const app = initializeApp(firebaseConfig); const auth = getAuth(app); const db = getFirestore(app); const appId = typeof __app_id !== 'undefined' ? __app_id : 'default-emotion-workbook'; // Helper: Reusable Modal Component const Modal = ({ show, onClose, title, children, size = 'lg' }) => { if (!show) return null; const sizeClasses = { sm: 'max-w-sm', lg: 'max-w-lg', xl: 'max-w-xl' }; return (

{title}

{children}
); }; // Helper: Reusable Accordion Component const Accordion = ({ title, children }) => { const [isOpen, setIsOpen] = useState(false); return (
{isOpen &&
{children}
}
); }; // Drawing Canvas Component const DrawingCanvas = ({ onSave, initialDrawing }) => { const canvasRef = useRef(null); const [tool, setTool] = useState('pen'); // 'pen', 'marker', 'spray', 'eraser', 'sticker' const [color, setColor] = useState('#4A4A4A'); const [brushSize, setBrushSize] = useState(10); const [selectedSticker, setSelectedSticker] = useState(null); const history = useRef([null]); const historyStep = useRef(0); const isDrawing = useRef(false); const palette = ['#4A4A4A', '#F5A623', '#F8E71C', '#8B572A', '#7ED321', '#4A90E2', '#50E3C2', '#BD10E0', '#9013FE', '#E94E77']; const stickers = { happy: '😊', sad: '😢', angry: '😠', love: '❤️', star: '⭐', spiral: '🌀', lightning: '⚡️', fire: '🔥', water: '💧', broken: '💔' }; // Save drawing history for undo/redo const saveHistory = () => { if (historyStep.current < history.current.length - 1) { history.current = history.current.slice(0, historyStep.current + 1); } const canvas = canvasRef.current; if(canvas) { history.current.push(canvas.toDataURL()); historyStep.current++; } }; // Redraw canvas content const redrawCanvas = (dataUrl, callback) => { const canvas = canvasRef.current; if(!canvas) return; const context = canvas.getContext('2d'); if (!dataUrl) { context.clearRect(0, 0, canvas.width, canvas.height); if (callback) callback(); return; } const image = new Image(); image.onload = () => { context.clearRect(0, 0, canvas.width, canvas.height); context.drawImage(image, 0, 0, canvas.width * 0.5, canvas.height * 0.5); if(callback) callback(); }; image.src = dataUrl; }; // Undo action const handleUndo = () => { if (historyStep.current > 1) { historyStep.current--; const previousState = history.current[historyStep.current - 1]; redrawCanvas(previousState); onSave(previousState); } }; // Redo action const handleRedo = () => { if (historyStep.current < history.current.length) { const nextState = history.current[historyStep.current]; redrawCanvas(nextState); onSave(nextState); historyStep.current++; } }; // Initialize canvas and load saved drawing useEffect(() => { const canvas = canvasRef.current; canvas.width = 500 * 2; canvas.height = 500 * 2; canvas.style.width = `500px`; canvas.style.height = `500px`; const context = canvas.getContext("2d"); context.scale(2, 2); redrawCanvas(initialDrawing, () => { if (canvasRef.current) { history.current = [initialDrawing || canvasRef.current.toDataURL()]; historyStep.current = 1; } }); }, [initialDrawing]); // Get coordinates for mouse/touch events const getCoords = (e) => { const rect = canvasRef.current.getBoundingClientRect(); const event = e.touches && e.touches.length > 0 ? e.touches[0] : e; return { offsetX: event.clientX - rect.left, offsetY: event.clientY - rect.top, }; } // Place sticker on canvas const placeSticker = (e) => { if (!selectedSticker) return; const { offsetX, offsetY } = getCoords(e); const context = canvasRef.current.getContext('2d'); context.font = `${brushSize * 2}px sans-serif`; context.textAlign = 'center'; context.textBaseline = 'middle'; context.globalCompositeOperation = 'source-over'; context.fillStyle = color; context.fillText(stickers[selectedSticker], offsetX, offsetY); saveHistory(); onSave(canvasRef.current.toDataURL("image/png")); setSelectedSticker(null); setTool('pen'); }; // Start drawing const startDrawing = (e) => { e.preventDefault(); if (tool === 'sticker') { placeSticker(e); return; } const { offsetX, offsetY } = getCoords(e); const context = canvasRef.current.getContext('2d'); isDrawing.current = true; context.beginPath(); context.moveTo(offsetX, offsetY); context.lineWidth = brushSize; context.strokeStyle = color; context.globalAlpha = tool === 'marker' ? 0.3 : 1.0; context.lineCap = tool === 'marker' ? 'square' : 'round'; context.lineJoin = tool === 'marker' ? 'miter' : 'round'; context.globalCompositeOperation = tool === 'eraser' ? 'destination-out' : 'source-over'; }; // Continue drawing const draw = (e) => { if (!isDrawing.current) return; e.preventDefault(); const { offsetX, offsetY } = getCoords(e); const context = canvasRef.current.getContext('2d'); if (tool === 'spray') { for (let i = 0; i < 15; i++) { const angle = Math.random() * 2 * Math.PI; const radius = Math.random() * brushSize; const sprayX = offsetX + Math.cos(angle) * radius; const sprayY = offsetY + Math.sin(angle) * radius; context.fillStyle = color; context.fillRect(sprayX, sprayY, 1, 1); } } else { context.lineTo(offsetX, offsetY); context.stroke(); } }; // Finish drawing const finishDrawing = () => { if (!isDrawing.current) return; isDrawing.current = false; const context = canvasRef.current.getContext('2d'); context.closePath(); context.globalAlpha = 1.0; saveHistory(); const dataUrl = canvasRef.current.toDataURL("image/png"); onSave(dataUrl); }; const ToolButton = ({ onClick, currentTool, toolName, title, children }) => ( ); return (
{palette.map(c => ( ))}
setTool('pen')} currentTool={tool} toolName='pen' title="펜"> setTool('marker')} currentTool={tool} toolName='marker' title="마커"> setTool('spray')} currentTool={tool} toolName='spray' title="스프레이"> setTool('eraser')} currentTool={tool} toolName='eraser' title="지우개">
{Object.entries(stickers).map(([name, emoji]) => ( ))}
setBrushSize(e.target.value)} className="w-full accent-orange-500"/>
); }; // Step 1 Component const Step1 = ({ data, onUpdate, onNext }) => { const handleUpdate = (field, value) => onUpdate('step1', { ...data.step1, [field]: value }); const isComplete = data.step1?.topic && data.step1?.drawingA && data.step1?.drawingAName; const [showExampleModal, setShowExampleModal] = useState(false); const [selectedExample, setSelectedExample] = useState(null); // Updated with user-provided image links const examples = [ { title: '조화로운 마음', imgSrc: 'https://postfiles.pstatic.net/MjAyNTA3MDdfMyAg/MDAxNzUxODcyNjM2Mjk2.VchHBlmqLtskbA5fX2xqCsKqloaTPwVpML65qF55tGQg.MDv5Ll0Jo6hrLiRj3n0O2N2Ju_Geq3giXVnbsHz1stog.JPEG/unnamed_(4).jpg?type=w773' }, { title: '기쁨', imgSrc: 'https://postfiles.pstatic.net/MjAyNTA3MDdfMjQ2/MDAxNzUxODcyNjM2Mjk1.EoNEdgfSLJTG5IQ1yOkBZF9ZBOdivORb1RRD11Zkm3Qg._QSTTe93ZLZSyy_q5jmYCJXShufYsjwueHL6COzOn4kg.JPEG/unnamed_(3).jpg?type=w773' }, { title: '갑자기 끓어오르는 분노', imgSrc: 'https://postfiles.pstatic.net/MjAyNTA3MDdfMjc4/MDAxNzUxODcyNjM2Mjk3._zbEJoPQIhmHp3SgvVC8CN69jzo0jVFE5_n3Y9M8N3Ug.Ub6KQTPWt3_a7SDuyWzo6cruGGenw07J_No2cm43IZog.JPEG/unnamed_(2).jpg?type=w773' }, { title: '나를 사랑하기', imgSrc: 'https://postfiles.pstatic.net/MjAyNTA3MDdfMjc0/MDAxNzUxODcyNjM2Mjk2.ylScAm5rxttwGbgP2QX5CTDfJqVcCNhxVYuUEvl_o6gg.WE7g0M64DQmNPwc_R15IKHigQBS94akqPpZmwDh2HFEg.JPEG/unnamed_(1).jpg?type=w773' }, ]; const openExample = (ex) => { setSelectedExample(ex); setShowExampleModal(true); }; return (

1단계: 감정 탐색하기

다루고 싶은 감정을 정하고, 몸으로 느껴 그려봅니다.

1-1. 감정 주제 정하기

  • 최근 나의 관계에 영향을 미친 감정
  • 해결되지 않고 마음에 남아 있는 감정
  • 특정 상황에서 계속 반복되는 감정
  • 이해하지 못해서 혼란스러운 감정
  • 너무 압도적이거나 감당하기 어려운 감정은 피하세요.
  • 과거 기억은 생생하지만 더 이상 감정이 동반되지 않는 경우는 적합하지 않습니다.
handleUpdate('topic', e.target.value)} className="w-full p-4 border-2 border-gray-200 bg-gray-50 rounded-2xl focus:ring-2 focus:ring-orange-400 focus:border-orange-400 transition" />

1-2. 감정 스케치 A: 나의 감정 자화상

주제를 정한 감정을 떠올렸을 때 몸에서 느껴지는 감각을 자유롭게 그려보세요.

다른 사람들의 감정 스케치 예시

{examples.map((ex, i) => (
openExample(ex)}> {ex.title}
))}
handleUpdate('drawingA', drawing)} initialDrawing={data.step1?.drawingA} />
handleUpdate('drawingAName', e.target.value)} className="w-full max-w-sm mx-auto block p-4 border-2 border-gray-200 bg-gray-50 rounded-2xl text-center" />
setShowExampleModal(false)} title={`감정 스케치 예시: ${selectedExample?.title || ''}`} size="xl"> {selectedExample && (
{selectedExample.title}
)}
); }; // Step 2 Component const Step2 = ({ data, onUpdate, onNext, onPrev }) => { const [showModal, setShowModal] = useState(false); const [currentMemory, setCurrentMemory] = useState({ id: null, text: '', age: '', connections: '', intensity: 5 }); const handleAddMemory = () => { setCurrentMemory({ id: null, text: '', age: '', connections: '', intensity: 5 }); setShowModal(true); }; const handleEditMemory = (memory) => { setCurrentMemory(memory); setShowModal(true); }; const handleSaveMemory = () => { const memories = data.step2?.memories || []; if (currentMemory.id) { const updatedMemories = memories.map(mem => mem.id === currentMemory.id ? currentMemory : mem); onUpdate('step2', { ...data.step2, memories: updatedMemories }); } else { const newMemory = { ...currentMemory, id: Date.now() }; onUpdate('step2', { ...data.step2, memories: [...memories, newMemory] }); } setShowModal(false); }; const handleDeleteMemory = (id) => { const memories = data.step2?.memories || []; const updatedMemories = memories.filter(mem => mem.id !== id); onUpdate('step2', { ...data.step2, memories: updatedMemories }); } const memories = data.step2?.memories || []; const isComplete = memories.length > 0; return (

2단계: 감정 히스토리맵

나의 감정의 역사를 타임라인에 기록해봅니다.

나의 감정 연대기

{memories.length === 0 ? (

아직 기록된 기억이 없습니다.

'기억 추가하기' 버튼을 눌러 시작해보세요.

) : (
{memories.map(memory => (

{memory.text}

감정의 나이: {memory.age}살

연결고리: {memory.connections}

강도:

))}
)}
setShowModal(false)} title={currentMemory.id ? "기억 수정하기" : "새로운 기억 추가"}>
setCurrentMemory({...currentMemory, text: e.target.value})} className="w-full p-4 border-2 border-gray-200 bg-gray-50 rounded-2xl" />
setCurrentMemory({...currentMemory, age: e.target.value})} className="w-full p-4 border-2 border-gray-200 bg-gray-50 rounded-2xl" />
setCurrentMemory({...currentMemory, connections: e.target.value})} className="w-full p-4 border-2 border-gray-200 bg-gray-50 rounded-2xl" />
setCurrentMemory({...currentMemory, intensity: e.target.value})} className="w-full accent-orange-500" />
); }; // Step 3 Component const Step3 = ({ data, onUpdate, onNext, onPrev }) => { const handleUpdate = (field, value) => onUpdate('step3', { ...data.step3, [field]: value }); const isComplete = data.step3?.writing; return (

3단계: 감정 글쓰기

표현되지 못하고 쌓여 있는 마음들을 자유롭게 풀어내 봅니다.

글쓰기 가이드

  • 그 감정과 관련해서 떠오르는 생각이나 이야기들은 무엇인가요?
  • 표현되지 못하고 쌓여 있는 마음들은 무엇인가요?
  • 충분히 표현했다는 마음이 들 때까지 자유롭게 기록합니다.
); }; // Step 4 Component const Step4 = ({ data, onUpdate, onNext, onPrev }) => { const [activeTab, setActiveTab] = useState('signal'); const [showNeedsModal, setShowNeedsModal] = useState(false); const needsList = { '자율성': ['자신의 목표/가치 선택', '꿈을 이루기 위한 방법 선택'], '신체적/생존': ['공기', '음식', '물', '주거', '휴식', '수면', '안전', '신체적 접촉', '운동'], '사회적/정서적': ['소통', '연결', '존중', '공감', '이해', '수용', '지지', '협력', '인정', '사랑', '소속감', '공동체', '신뢰'], '놀이/재미': ['즐거움', '재미', '유머'], '삶의 의미': ['기여', '성장', '배움', '보람', '깨달음', '참여', '효능감', '희망'], '진실성': ['정직', '진실', '자기존중'], '아름다움/평화': ['아름다움', '평온함', '조화', '질서'], }; const handleUpdate = (field, value) => onUpdate('step4', { ...data.step4, [field]: value }); const handleNeedClick = (need) => { const currentNeeds = data.step4?.needs || ''; handleUpdate('needs', currentNeeds ? `${currentNeeds}, ${need}` : need); }; const isComplete = data.step4?.signal_self && data.step4?.signal_env && data.step4?.needs && data.step4?.needs_solution && data.step4?.purpose_help && data.step4?.purpose_newName; const TabButton = ({ id, label }) => ( ); return (

4단계: 감정 이해하기

감정이 보내는 신호를 해석하고, 그 안에 숨겨진 필요와 목적을 발견합니다.

{activeTab === 'signal' && (
)} {activeTab === 'needs' && (
handleUpdate('needs', e.target.value)} className="w-full p-4 border-2 border-gray-200 bg-gray-50 rounded-2xl" placeholder="예: 존중, 안정감, 소속감..." />
)} {activeTab === 'purpose' && (
handleUpdate('purpose_newName', e.target.value)} className="w-full p-4 border-2 border-gray-200 bg-gray-50 rounded-2xl" placeholder="예: 신중한 조언자, 나의 경호원..." />
)}
setShowNeedsModal(false)} title="욕구 목록">
{Object.entries(needsList).map(([category, items]) => (

{category}

{items.map(item => ( ))}
))}
); }; // Step 5 Component const Step5 = ({ data, onUpdate, onNext, onPrev }) => { const handleUpdate = (field, value) => onUpdate('step5', { ...data.step5, [field]: value }); useEffect(() => { if (data.step4?.purpose_newName && !data.step5?.final_newName) { handleUpdate('final_newName', data.step4.purpose_newName); } }, [data.step4?.purpose_newName]); const isComplete = data.step5?.message_to_emotion && data.step5?.final_newName && data.step5?.final_purpose && data.step5?.final_action; return (

5단계: 감정 소통하기

나의 감정에게 말을 걸어보고, 감정의 진짜 목적을 다시 한번 확인합니다.

5-1. 내가 감정에게 해주고 싶은 이야기

그 감정을 느끼는 나에게, 혹은 감정 그 자체에게 해주고 싶은 말을 진심을 담아 적어보세요.

5-2. 감정과의 약속

감정과의 소통을 통해 발견한 내용을 바탕으로 앞으로의 다짐을 정리해봅니다.

handleUpdate('final_newName', e.target.value)} className="w-full p-4 border-2 border-gray-200 bg-gray-50 rounded-2xl" />
handleUpdate('final_purpose', e.target.value)} className="w-full p-4 border-2 border-gray-200 bg-gray-50 rounded-2xl" />
handleUpdate('final_action', e.target.value)} className="w-full p-4 border-2 border-gray-200 bg-gray-50 rounded-2xl" />
); }; // Step 6 Component const Step6 = ({ data, onUpdate, onNext, onPrev }) => { const handleUpdate = (field, value) => onUpdate('step6', { ...data.step6, [field]: value }); const isComplete = data.step6?.drawingB && data.step6?.drawingBName; return (

6단계: 감정 정리하기

나의 다짐을 확인하고, 변화된 감정을 다시 한번 그려봅니다.

6-1. 나의 다짐 확인하기

감정의 새로운 이름

{data.step5?.final_newName || '아직 정해지지 않았습니다.'}

감정이 이루고자 하는 진짜 목적

{data.step5?.final_purpose || '아직 정해지지 않았습니다.'}

이 감정이 찾아올 때 나를 위해 할 수 있는 일

{data.step5?.final_action || '아직 정해지지 않았습니다.'}

6-2. 감정 스케치 B: 변화된 감정 그리기

모든 작업을 마친 후, 지금 내 감정이 어떻게 느껴지는지 다시 한번 표현해 보세요.

handleUpdate('drawingB', drawing)} initialDrawing={data.step6?.drawingB} />
handleUpdate('drawingBName', e.target.value)} className="w-full max-w-sm mx-auto block p-4 border-2 border-gray-200 bg-gray-50 rounded-2xl text-center" />
); }; // Final Dashboard Component const FinalDashboard = ({ data, onRestart }) => { const dashboardRef = useRef(null); const [showErrorModal, setShowErrorModal] = useState(false); // Download dashboard as an image using html2canvas const downloadDashboard = () => { if (dashboardRef.current && typeof window.html2canvas === 'function') { window.html2canvas(dashboardRef.current, { useCORS: true, scale: 2, backgroundColor: '#FFF7ED' }).then(canvas => { const link = document.createElement('a'); link.download = '나의_감정워크북_결과.png'; link.href = canvas.toDataURL('image/png'); link.click(); }).catch(err => { console.error("html2canvas error:", err); setShowErrorModal(true); }); } else { console.error("html2canvas function not found or dashboard ref is not set."); setShowErrorModal(true); } }; return ( <>

감정 워크숍 여정을 마쳤습니다!

자신의 감정을 마주하고 이해하려 노력한 당신에게 박수를 보냅니다.

나의 감정 여정 요약

Before: 처음 만난 감정

{data.step1?.drawingA ? 감정 스케치 A :

그림 없음

}

"{data.step1?.drawingAName || '이름 없음'}"

After: 변화된 감정

{data.step6?.drawingB ? 감정 스케치 B :

그림 없음

}

"{data.step6?.drawingBName || '이름 없음'}"

나의 감정 선언문

나는 "{data.step1?.topic || '정했던'}" 이라는 감정을 통해, 나에게 "{data.step5?.final_purpose || '중요한 목적'}"이 있음을 발견했습니다. 이 감정은 이제 나에게 "{data.step5?.final_newName || '새로운 이름'}"(으)로 불릴 것이며, 앞으로 이 감정이 찾아올 때 나는 나를 위해 "{data.step5?.final_action || '정해진 행동'}"을/를 할 것을 약속합니다.

setShowErrorModal(false)} title="오류">

결과를 이미지로 저장하는 기능을 현재 사용할 수 없습니다.

페이지를 새로고침한 후 다시 시도해 주세요.

); }; // Main App Component export default function App() { const [currentStep, setCurrentStep] = useState(0); // Current step const [userData, setUserData] = useState({}); // User data const [userId, setUserId] = useState(null); // Firebase user ID const [isLoading, setIsLoading] = useState(true); // Loading state const [showRestartConfirm, setShowRestartConfirm] = useState(false); // Restart confirmation modal // Dynamically load html2canvas script useEffect(() => { const scriptId = 'html2canvas-script'; if (document.getElementById(scriptId)) return; const script = document.createElement('script'); script.id = scriptId; script.src = "https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"; script.async = true; document.body.appendChild(script); return () => { const scriptElement = document.getElementById(scriptId); if (scriptElement && scriptElement.parentNode) { scriptElement.parentNode.removeChild(scriptElement); } }; }, []); // Set up Firebase auth state listener useEffect(() => { const authListener = onAuthStateChanged(auth, async (user) => { if (user) { setUserId(user.uid); } else { try { if (typeof __initial_auth_token !== 'undefined' && __initial_auth_token) { await signInWithCustomToken(auth, __initial_auth_token); } else { await signInAnonymously(auth); } } catch (error) { console.error("Authentication error:", error); setIsLoading(false); } } }); return () => authListener(); }, []); // Real-time data synchronization with Firestore useEffect(() => { if (!userId) return; const docRef = doc(db, "artifacts", appId, "users", userId, "data", "emotionWorkbook"); const unsubscribe = onSnapshot(docRef, (docSnap) => { if (docSnap.exists()) { const loadedData = docSnap.data(); setUserData(loadedData.data || {}); setCurrentStep(loadedData.step || 0); } else { setUserData({}); setCurrentStep(0); } setIsLoading(false); }, (error) => { console.error("Firestore snapshot error:", error); setIsLoading(false); }); return () => unsubscribe(); }, [userId]); // Save data to Firestore const saveData = async (step, data) => { if (!userId) return; try { const docRef = doc(db, "artifacts", appId, "users", userId, "data", "emotionWorkbook"); await setDoc(docRef, { step, data }); } catch (error) { console.error("Error saving data:", error); } }; // Update data handler const handleUpdate = (stepKey, stepData) => { const updatedData = { ...userData, [stepKey]: stepData }; setUserData(updatedData); saveData(currentStep, updatedData); }; // Move to the next step const nextStep = () => { const next = Math.min(currentStep + 1, 7); setCurrentStep(next); saveData(next, userData); }; // Move to the previous step const prevStep = () => { const prev = Math.max(currentStep - 1, 0); setCurrentStep(prev); saveData(prev, userData); }; // Restart workbook handler const handleRestart = () => setShowRestartConfirm(true); const confirmRestart = () => { setUserData({}); setCurrentStep(1); // Start from step 1 saveData(1, {}); setShowRestartConfirm(false); }; // Render the current step component const renderStep = () => { switch (currentStep) { case 1: return ; case 2: return ; case 3: return ; case 4: return ; case 5: return ; case 6: return ; case 7: return ; default: return ( // Start screen

디지털 감정 워크북

나의 소화되지 않는 감정을 마주하고 이해하는 시간을 가져보세요.

); } }; // Progress indicator const ProgressIndicator = () => { if (currentStep < 1 || currentStep > 6) return null; const totalSteps = 6; const progress = (currentStep / totalSteps) * 100; return (
) } // Loading screen if (isLoading) { return (
); } return (
{currentStep > 0 && currentStep < 7 && (

{currentStep} / 6 단계

)}
{renderStep()}

이 워크북은 '어반몽크'의 감정 워크숍 자료를 기반으로 제작되었습니다.

setShowRestartConfirm(false)} title="새로 시작하기" >

정말로 모든 진행 상황을 초기화하고 새로 시작하시겠습니까? 저장된 데이터는 영구적으로 사라집니다.

); }