2024 라이프처치 감사릴레이

한 해 동안 하나님께 감사했던 것들, 그리고 라이프처치 성도들과 주변 사람들에게 감사했던 것들을 고백해보세요.

내용은 익명으로 접수되며, 여러분의 마음이 하나 하나 모일 때, 풍성하고 따뜻한 추수감사주일을 함께 보낼 수 있습니다.

import React, { useEffect, useMemo, useRef, useState } from "react"; // 단일 파일 React 컴포넌트 (백엔드 없이 로컬 저장) // 기능: 글 작성, 좋아요, 댓글, 삭제, 검색, 정렬, 로컬스토리지 영속화 // Tailwind 사용 (별도 import 불필요) const STORAGE_KEY = "mini_social_board_v1"; function uid() { return Math.random().toString(36).slice(2) + Date.now().toString(36); } function timeAgo(dateNum) { const sec = Math.floor((Date.now() - dateNum) / 1000); if (sec < 60) return `${sec}초 전`; const m = Math.floor(sec / 60); if (m < 60) return `${m}분 전`; const h = Math.floor(m / 60); if (h < 24) return `${h}시간 전`; const d = Math.floor(h / 24); if (d < 7) return `${d}일 전`; const dt = new Date(dateNum); return `${dt.getFullYear()}.${String(dt.getMonth() + 1).padStart(2, "0")}.${String( dt.getDate() ).padStart(2, "0")} ${String(dt.getHours()).padStart(2, "0")}:${String( dt.getMinutes() ).padStart(2, "0")}`; } function classNames(...a) { return a.filter(Boolean).join(" "); } const HeartIcon = ({ filled }) => ( ); const MessageIcon = () => ( ); const TrashIcon = () => ( ); const DotsIcon = () => ( ); export default function SocialBoard() { const [posts, setPosts] = useState([]); const [author, setAuthor] = useState(""); const [content, setContent] = useState(""); const [imageUrl, setImageUrl] = useState(""); const [query, setQuery] = useState(""); const [sort, setSort] = useState("new"); // new | top const contentRef = useRef(null); // 초기 로드 useEffect(() => { try { const raw = localStorage.getItem(STORAGE_KEY); if (raw) setPosts(JSON.parse(raw)); } catch {} }, []); // 저장 useEffect(() => { try { localStorage.setItem(STORAGE_KEY, JSON.stringify(posts)); } catch {} }, [posts]); // textarea 자동 높이 useEffect(() => { const el = contentRef.current; if (!el) return; el.style.height = "auto"; el.style.height = el.scrollHeight + "px"; }, [content]); const filtered = useMemo(() => { const q = query.trim().toLowerCase(); let arr = posts; if (q) { arr = arr.filter( (p) => p.content.toLowerCase().includes(q) || p.author.toLowerCase().includes(q) || p.comments?.some((c) => c.content.toLowerCase().includes(q)) ); } if (sort === "new") { arr = [...arr].sort((a, b) => b.createdAt - a.createdAt); } else if (sort === "top") { arr = [...arr].sort((a, b) => b.likes - a.likes || b.createdAt - a.createdAt); } return arr; }, [posts, query, sort]); function addPost(e) { e?.preventDefault?.(); const body = content.trim(); if (!body) return; const post = { id: uid(), author: author.trim() || "익명", content: body, imageUrl: imageUrl.trim(), createdAt: Date.now(), likes: 0, likedByMe: false, comments: [], }; setPosts((p) => [post, ...p]); setContent(""); setImageUrl(""); } function toggleLike(id) { setPosts((prev) => prev.map((p) => p.id === id ? { ...p, likedByMe: !p.likedByMe, likes: p.likedByMe ? Math.max(0, p.likes - 1) : p.likes + 1, } : p ) ); } function addComment(postId, authorName, text) { const t = text.trim(); if (!t) return; setPosts((prev) => prev.map((p) => p.id === postId ? { ...p, comments: [ ...(p.comments || []), { id: uid(), author: authorName.trim() || "익명", content: t, createdAt: Date.now() }, ], } : p ) ); } function deletePost(id) { if (!confirm("이 게시글을 삭제하시겠습니까?")) return; setPosts((prev) => prev.filter((p) => p.id !== id)); } return (
{/* 헤더 */}

Mini Social Board

{/* 작성 영역 */}
setAuthor(e.target.value)} className="flex-1 rounded-xl border px-3 py-2 text-sm focus:outline-none focus:ring" />