diff --git a/index.html b/index.html index 76cbc79a..132d3432 100644 --- a/index.html +++ b/index.html @@ -3,6 +3,7 @@ + EvenTee diff --git a/public/default-event.png b/public/default-event.png new file mode 100644 index 00000000..61b2afb9 Binary files /dev/null and b/public/default-event.png differ diff --git a/public/ticket.png b/public/ticket.png new file mode 100644 index 00000000..1687415c Binary files /dev/null and b/public/ticket.png differ diff --git a/src/contexts/AppContext.tsx b/src/contexts/AppContext.tsx index 8ea8c566..290c06aa 100644 --- a/src/contexts/AppContext.tsx +++ b/src/contexts/AppContext.tsx @@ -1,4 +1,4 @@ -import { createContext, useContext, useState, ReactNode } from 'react'; +import { createContext, useContext, useState, ReactNode, useEffect } from "react"; export type User = { id: string | null; @@ -6,7 +6,7 @@ export type User = { email: string | null; socialId: string | null; profileImageUrl?: string | null; - role: 'user' | 'admin' | 'master_admin' | null; + role: "user" | "admin" | "master_admin" | null; }; export type Event = { @@ -35,27 +35,51 @@ type AppContextType = { logout: () => void; }; - const AppContext = createContext(undefined); export function AppProvider({ children }: { children: ReactNode }) { const [user, setUser] = useState(null); const [accessToken, setAccessToken] = useState(null); - const [currentEvent, setCurrentEvent] = useState(null); - - // inviteCode 기본값: null + const [currentEvent, _setCurrentEvent] = useState(null); const [inviteCode, setInviteCode] = useState(null); + + useEffect(() => { + const saved = localStorage.getItem("currentEvent"); + if (saved) { + try { + const parsed = JSON.parse(saved); + + parsed.startDate = parsed.startDate ? new Date(parsed.startDate) : null; + parsed.endDate = parsed.endDate ? new Date(parsed.endDate) : null; + + _setCurrentEvent(parsed); + } catch (err) { + console.error("currentEvent 복구 실패:", err); + } + } + }, []); + + + const setCurrentEvent = (event: Event | null) => { + _setCurrentEvent(event); + + if (event) { + localStorage.setItem("currentEvent", JSON.stringify(event)); + } else { + localStorage.removeItem("currentEvent"); + } + }; + + const logout = () => { setUser(null); setAccessToken(null); setCurrentEvent(null); - - setInviteCode(null); - // localStorage 정리 localStorage.removeItem("accessToken"); + localStorage.removeItem("currentEvent"); }; return ( @@ -72,16 +96,13 @@ export function AppProvider({ children }: { children: ReactNode }) { logout, }} > - {children} ); } export function useApp() { - const context = useContext(AppContext); - if (!context) { - throw new Error('useApp must be used within an AppProvider'); - } - return context; + const ctx = useContext(AppContext); + if (!ctx) throw new Error("useApp must be used within AppProvider"); + return ctx; } diff --git a/src/pages/AdminDashboard.tsx b/src/pages/AdminDashboard.tsx index 5d19a5f0..aba7793d 100644 --- a/src/pages/AdminDashboard.tsx +++ b/src/pages/AdminDashboard.tsx @@ -1,230 +1,240 @@ -import { useNavigate } from 'react-router-dom'; -import { useApp } from '../contexts/AppContext'; -import EventeeButton from '../components/EventeeButton'; -import { Button } from '../components/ui/button'; -import { Tabs, TabsContent, TabsList, TabsTrigger } from '../components/ui/tabs'; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../components/ui/card'; -import { User, Users, MessageSquare, Gamepad2, Heart, Menu } from 'lucide-react'; -import { Sheet, SheetContent, SheetTrigger } from '../components/ui/sheet'; -import { format } from 'date-fns'; -import { ko } from 'date-fns/locale'; +import { useNavigate } from "react-router-dom"; +import { useApp } from "../contexts/AppContext"; +import EventeeButton from "../components/EventeeButton"; +import { Button } from "../components/ui/button"; +import { + Tabs, + TabsContent, + TabsList, + TabsTrigger, +} from "../components/ui/tabs"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "../components/ui/card"; +import { + Users, + MessageSquare, + Gamepad2, + Heart, + ArrowLeft +} from "lucide-react"; +import { format } from "date-fns"; +import { ko } from "date-fns/locale"; export default function AdminDashboard() { const navigate = useNavigate(); const { user, currentEvent, logout } = useApp(); - + if (!currentEvent || !user) return null; + const event = currentEvent; return ( -
+
{/* Header */} -
+
-
-
-

- Eventee 관리자 +
+ + {/* 이벤트 정보 */} +
+

+ EvenTee{" "} + + 관리자 +

-

{event.title}

+ +

+ {event.title} +

+

- {format(event.startDate, 'yyyy.MM.dd', { locale: ko })} - {format(event.endDate, 'yyyy.MM.dd', { locale: ko })} + {format(event.startDate, "yyyy.MM.dd", { locale: ko })} ~{" "} + {format(event.endDate, "yyyy.MM.dd", { locale: ko })}

- - - - - - -
-
-
- {user.nickname.charAt(0).toUpperCase()} -
-
-
{user.nickname}
-
{user.email}
-
-
- - - - -
-
-
+ + {/* 오른쪽: 뒤로가기 버튼 */} +
+ + +
+

{/* Main Content */} -
+
- - 개요 - 팀 빌딩 - 채팅 - 롤링 페이퍼 - 미니게임 + + {/* Tabs Header */} + + 대시보드 + 참여자 관리 + 공지 작성 + 그룹 관리 + 게임 관리 - - - - 이벤트 정보 - {event.description} + {/* 대시보드 */} + + + {/* 이벤트 정보 */} + + + + 이벤트 정보 + + + {event.description || "이벤트 설명이 아직 입력되지 않았습니다."} + - -
-
-
초대 코드
-
{event.inviteCode}
-
- -
-
참가자 수
-
24명
-
- -
-
팀 수
-
6개 팀
-
+ + +
+ + +
+ {/* 기능 카드 */}
- - - - - 팀 빌딩 - - - -

참가자들과 팀을 구성하고 함께 활동하세요

- 팀 빌딩 시작하기 -
-
- - - - - - 채팅 - - - -

팀원들과 실시간으로 소통하세요

- 채팅 참여하기 -
-
- - - - - - 롤링 페이퍼 - - - -

참가자들에게 응원 메시지를 남겨보세요

- 메시지 작성하기 -
-
- - - - - - 미니게임 - - - -

재미있는 게임으로 팀워크를 높여보세요

- 게임 시작하기 -
-
+ } + title="참여자 관리" + description="참여자 목록을 확인하고 상태를 관리하는 기능입니다." + button="참여자 관리 열기" + /> + } + title="공지 작성" + description="공지사항을 작성하고 참가자에게 전달하는 기능입니다." + button="공지 작성하기" + /> + } + title="그룹 관리" + description="팀/그룹 생성과 구성원 관리를 담당합니다." + button="그룹 관리하기" + /> + } + title="게임 관리" + description="이벤트 진행용 게임 관리 기능입니다." + button="기능 추가 예정입니다" + outline + disabled + />
- - - - - 팀 빌딩 - 팀을 구성하고 관리하세요 - - -
- 팀 빌딩 기능은 곧 추가될 예정입니다 -
-
-
- - - - 채팅 - 참가자들과 실시간으로 소통하세요 - - -
- 채팅 기능은 곧 추가될 예정입니다 -
-
-
-
+ {/* 나머지 탭들 */} + + + + - - - - 롤링 페이퍼 - 참가자들에게 응원 메시지를 남겨보세요 - - -
- 롤링 페이퍼 기능은 곧 추가될 예정입니다 -
-
-
-
- - - - - 미니게임 - 재미있는 게임으로 팀워크를 높여보세요 - - -
- 미니게임 기능은 곧 추가될 예정입니다 -
-
-
-
); } + +/* ----------------- COMPONENTS ----------------- */ + +function TabsTriggerStyled({ value, children }) { + return ( + + {children} + + ); +} + +function InfoBox({ label, value, color }) { + return ( +
+

{label}

+

+ {value} +

+
+ ); +} + +function FeatureCard({ icon, title, description, button, outline, disabled }) { + return ( + + + + {icon} + {title} + + + +

{description}

+ + {button} + +
+
+ ); +} + +function PlaceholderTab({ value, title }) { + return ( + + + + {title} + 기능이 곧 추가될 예정입니다. + + +
+ 준비 중입니다. +
+
+
+
+ ); +} diff --git a/src/pages/EventMainPage.tsx b/src/pages/EventMainPage.tsx index 5da53e8c..4f592768 100644 --- a/src/pages/EventMainPage.tsx +++ b/src/pages/EventMainPage.tsx @@ -30,11 +30,11 @@ type Comment = { }; type PollOption = { - id: string; // "opt1", "opt2" … - text: string; // 옵션 텍스트 - votes: number; // 득표 수 - percent?: number; // (백엔드에서 주는 percent, UI에서는 안 써도 됨) - isMine?: boolean; // 내가 찍은 옵션인지 여부 + id: string; // "opt1", "opt2" … + text: string; + votes: number; + percent?: number; + isMine?: boolean; }; type Post = { @@ -48,7 +48,7 @@ type Post = { type?: "text" | "vote"; pollOptions?: PollOption[]; pollQuestion?: string; - userVote?: string; // "opt1" 같은 형식 + userVote?: string; isWrite: boolean; pollUsesPercent?: boolean; createdAt?: string; @@ -76,7 +76,7 @@ type EventInfo = { thumbnailUrl?: string; teamCount?: number; role?: string; - nickname?: string; // 이벤트 내 닉네임 + nickname?: string; }; type GroupEditFormState = { @@ -90,7 +90,8 @@ type GroupEditFormState = { export default function EventMainPage() { const navigate = useNavigate(); const location = useLocation(); - const { user } = useApp(); + const { user, setCurrentEvent, currentEvent } = useApp(); + const API_URL = import.meta.env.VITE_API_URL; @@ -98,23 +99,36 @@ export default function EventMainPage() { const eventCode = location.state?.eventCode || ""; const eventId = location.state?.eventId || 7; - console.log("[EventMainPage] 렌더링 시작", { - locationState: location.state, - eventTitle, - eventCode, - eventId, - user, - }); - const [eventInfo, setEventInfo] = useState(null); const [teams, setTeams] = useState([]); + // location.state 없으면 currentEvent 사용 + useEffect(() => { + if (!location.state && currentEvent) { + const restoredState = { + eventId: Number(currentEvent.id), + eventTitle: currentEvent.title, + eventCode: currentEvent.inviteCode, + nickname: user?.nickname, + }; + + navigate("/event-main", { state: restoredState, replace: true }); + } + }, []); + + + + const [showAddPostDialog, setShowAddPostDialog] = useState(false); const [newPostContent, setNewPostContent] = useState(""); const [selectedTeamId, setSelectedTeamId] = useState(""); - const [commentInputs, setCommentInputs] = useState<{ [key: string]: string }>({}); + const [commentInputs, setCommentInputs] = useState<{ [key: string]: string }>( + {} + ); const [newPostImage, setNewPostImage] = useState(null); - const [commentImages, setCommentImages] = useState<{ [key: string]: string | null }>({}); + const [commentImages, setCommentImages] = useState<{ + [key: string]: string | null; + }>({}); const [showPostTypeMenu, setShowPostTypeMenu] = useState(false); const [postType, setPostType] = useState<"text" | "vote">("text"); const [pollQuestion, setPollQuestion] = useState(""); @@ -133,53 +147,34 @@ export default function EventMainPage() { const formatDateOnly = (isoString: string) => { if (!isoString) return ""; - const date = new Date(isoString); - const y = date.getFullYear(); const m = String(date.getMonth() + 1).padStart(2, "0"); const d = String(date.getDate()).padStart(2, "0"); - return `${y}. ${m}. ${d}`; }; - const formatEventPeriod = (start?: string, end?: string) => { if (!start || !end) return ""; const s = new Date(start); const e = new Date(end); - const format = (d: Date) => { const yyyy = d.getFullYear(); const mm = String(d.getMonth() + 1).padStart(2, "0"); const dd = String(d.getDate()).padStart(2, "0"); return `${yyyy}-${mm}-${dd}`; }; - return `${format(s)} ~ ${format(e)}`; }; - - - - // ========================== // 그룹 불러오기 // ========================== useEffect(() => { - console.log("[EventMainPage] useEffect(eventId) 실행", { eventId }); loadEventGroups(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [eventId]); - useEffect(() => { - console.log("[EventMainPage] eventInfo 변경됨", eventInfo); - }, [eventInfo]); - - useEffect(() => { - console.log("[EventMainPage] teams 변경됨", teams); - }, [teams]); - const assignGroupColor = (groupNo?: number) => { const palette = ["#FFAB5D", "#E8E4D9", "#F5D0C5", "#C7D2FE", "#FDE68A"]; if (!groupNo) return palette[0]; @@ -188,49 +183,31 @@ export default function EventMainPage() { const loadEventGroups = async () => { try { - console.log("[EventMainPage] 이벤트 그룹 목록 조회 시작", { eventId, API_URL }); const res = await apiFetch(`${API_URL}/api/v1/events/${eventId}/groups`, { method: "GET", }); - - console.log("[EventMainPage] 이벤트 그룹 목록 fetch 응답 객체", res); const data = await res.json(); - console.log("[EventMainPage] 이벤트 그룹 목록 응답 JSON", data); - if (!data.isSuccess) { - console.warn("[EventMainPage] 이벤트 그룹 목록 응답 isSuccess=false", data); - return; - } + if (!data.isSuccess) return; const { title, eventTitle: legacyEventTitle, description, eventDescription, + eventRole, startAt, endAt, thumbnailUrl, teamCount, groups, role, - nickname, // ✅ 백엔드에서 넘어오는 이벤트 내 닉네임 기대 + nickname, } = data.result; const resolvedTitle = title ?? legacyEventTitle ?? eventTitle; const resolvedDescription = description ?? eventDescription; - console.log("[EventMainPage] 파싱된 이벤트 정보", { - eventId, - resolvedTitle, - resolvedDescription, - startAt, - endAt, - thumbnailUrl, - teamCount, - role, - nickname, - }); - setEventInfo({ eventId, title: resolvedTitle, @@ -239,7 +216,7 @@ export default function EventMainPage() { endAt, thumbnailUrl, teamCount, - role, + role: eventRole, nickname, }); @@ -256,23 +233,15 @@ export default function EventMainPage() { img: g.groupImg, })); - console.log("[EventMainPage] 변환된 팀 목록", convertedTeams); - setTeams(convertedTeams); const teamsWithPosts = await Promise.all( convertedTeams.map(async (team) => { const posts = await fetchGroupPosts(team.id); - console.log("[EventMainPage] 팀별 게시글 로딩 완료", { - teamId: team.id, - teamName: team.name, - posts, - }); return { ...team, posts }; }) ); - console.log("[EventMainPage] 게시글 포함 팀 목록 최종", teamsWithPosts); setTeams(teamsWithPosts); } catch (err) { console.error("이벤트 그룹 API 오류:", err); @@ -280,7 +249,6 @@ export default function EventMainPage() { }; const openGroupEditDialog = (team: Team) => { - console.log("[EventMainPage] 그룹 수정 다이얼로그 오픈", team); setGroupEditForm({ groupId: team.id, groupName: team.name, @@ -292,7 +260,6 @@ export default function EventMainPage() { }; const closeGroupEditDialog = () => { - console.log("[EventMainPage] 그룹 수정 다이얼로그 닫기"); setGroupEditDialogOpen(false); setGroupEditForm({ groupId: "", @@ -303,8 +270,10 @@ export default function EventMainPage() { }); }; - const handleGroupEditInputChange = (field: keyof GroupEditFormState, value: string) => { - console.log("[EventMainPage] 그룹 수정 인풋 변경", { field, value }); + const handleGroupEditInputChange = ( + field: keyof GroupEditFormState, + value: string + ) => { setGroupEditForm((prev) => ({ ...prev, [field]: value, @@ -312,10 +281,7 @@ export default function EventMainPage() { }; const handleSubmitGroupEdit = async () => { - if (!groupEditForm.groupId || !groupEditForm.groupName.trim()) { - console.warn("[EventMainPage] 그룹 수정 필수값 누락", groupEditForm); - return; - } + if (!groupEditForm.groupId || !groupEditForm.groupName.trim()) return; const payload = { groupId: Number(groupEditForm.groupId), @@ -326,19 +292,13 @@ export default function EventMainPage() { }; try { - console.log("[EventMainPage] 그룹 수정 요청", payload); const res = await apiFetch(`${API_URL}/api/v1/group`, { method: "PUT", body: JSON.stringify(payload), }); - console.log("[EventMainPage] 그룹 수정 fetch 응답", res); const data = await res.json(); - console.log("[EventMainPage] 그룹 수정 응답 JSON", data); - if (!data.isSuccess) { - console.warn("[EventMainPage] 그룹 수정 실패 isSuccess=false", data); - return; - } + if (!data.isSuccess) return; setTeams((prev) => prev.map((team) => @@ -360,54 +320,56 @@ export default function EventMainPage() { }; // ========================== - // Post 변환 함수 + // Post 변환 함수 (백엔드 여러 버전 안전 대응) // ========================== - const convertPost = (p: any): Post => { - const isVote = (p.type ?? "").toString().toLowerCase() === "vote"; - - const pollOptions = isVote && Array.isArray(p.pollOptions) - ? p.pollOptions.map((opt: any) => ({ - id: `opt${opt.optionNo}`, + const convertPost = (p: any): Post => { + // voteOptions / pollOptions 모두 지원 + const rawVoteOptions: any[] = Array.isArray(p.voteOptions) + ? p.voteOptions + : Array.isArray(p.pollOptions) + ? p.pollOptions + : []; + + const isVote = + (p.type ?? "").toString().toLowerCase() === "vote" || + !!p.voteTitle || + !!p.pollQuestion || + rawVoteOptions.length > 0; + + const pollOptions = + rawVoteOptions.length > 0 + ? rawVoteOptions.map((opt: any) => ({ + id: `opt${opt.optionNo ?? opt.id ?? 1}`, text: opt.text, - votes: opt.votes, - percent: opt.percent, - isMine: opt.isMine + votes: opt.votes ?? 0, + percent: opt.percent ?? 0, + isMine: opt.isMine ?? false, })) : undefined; - const userVote = isVote && p.userVote != null - ? `opt${p.userVote}` - : undefined; - - return { - id: String(p.postId), - author: p.author, - content: p.content, - type: isVote ? "vote" : "text", - pollQuestion: p.pollQuestion, - pollOptions, - userVote, - createdAt: p.createdAt, - comments: (p.comments ?? []).map((c: any) => ({ - id: String(c.commentId), - author: c.writerNickname, - content: c.content, - timestamp: c.createdAt, - imageUrl: undefined, - isWrite: Boolean(c.isMine) - })), - likes: 0, - isLiked: false, - isWrite: Boolean(p.isMine), - }; + return { + id: String(p.postId), + author: p.writerName ?? p.author ?? "익명", + content: p.content, + type: isVote ? "vote" : "text", + pollQuestion: p.voteTitle ?? p.pollQuestion ?? "", + pollOptions: isVote ? pollOptions : undefined, + createdAt: p.createdAt, + comments: (p.comments ?? []).map((c: any) => ({ + id: String(c.commentId ?? c.id), + author: c.writerNickname ?? c.writerName ?? c.author ?? "익명", + content: c.content, + timestamp: c.createdAt, + isWrite: Boolean(c.isMine ?? c.isWrite), + })), + likes: 0, + isLiked: false, + isWrite: Boolean(p.isWrite ?? p.isMine), }; - - - + }; const fetchGroupPosts = async (groupId: string): Promise => { try { - console.log("[EventMainPage] 그룹 게시글 조회 시작", { eventId, groupId }); const res = await apiFetch( `${API_URL}/api/v1/events/${eventId}/groups/${groupId}/posts`, { @@ -415,20 +377,30 @@ export default function EventMainPage() { } ); - console.log("[EventMainPage] 그룹 게시글 fetch 응답", res); const data = await res.json(); - console.log("[EventMainPage] 그룹 게시글 응답 JSON", { groupId, data }); - if (!data.isSuccess) { - console.warn("[EventMainPage] 그룹 게시글 응답 isSuccess=false", { - groupId, - data, - }); - return []; + + console.log("posts raw data:", data.result); + + if (!data.isSuccess) return []; + + let rawPosts: any[] = []; + + // CASE 1: { posts: [...] } + if (Array.isArray(data.result?.posts)) { + rawPosts = data.result.posts; } + // CASE 2: result 자체가 배열 + else if (Array.isArray(data.result)) { + rawPosts = data.result; + } + // CASE 3: 단일 객체 + else if (data.result?.postId) { + rawPosts = [data.result]; + } + + console.log("rawPosts:", rawPosts); - const posts = data.result?.posts ?? []; - const converted = posts.map((post: any) => convertPost(post)); - return converted; + return rawPosts.map((post: any) => convertPost(post)); } catch (err) { console.error("그룹 게시글 API 오류:", err); return []; @@ -436,10 +408,9 @@ export default function EventMainPage() { }; // ========================== - // 게시글 생성 + // 게시글 생성 / 수정 // ========================== const resetPostForm = () => { - console.log("[EventMainPage] 게시글 폼 리셋"); setNewPostContent(""); setSelectedTeamId(""); setNewPostImage(null); @@ -453,11 +424,7 @@ export default function EventMainPage() { }; const refreshTeamPosts = async (teamId: string) => { - if (!teamId) { - console.warn("[EventMainPage] refreshTeamPosts teamId 없음"); - return; - } - console.log("[EventMainPage] 팀 게시글 새로고침 시작", { teamId }); + if (!teamId) return; const posts = await fetchGroupPosts(teamId); setTeams((prev) => prev.map((team) => (team.id === teamId ? { ...team, posts } : team)) @@ -465,13 +432,11 @@ export default function EventMainPage() { }; const closePostDialog = () => { - console.log("[EventMainPage] 게시글 다이얼로그 닫기"); setShowAddPostDialog(false); resetPostForm(); }; const openPostDialog = (teamId: string, post?: Post) => { - console.log("[EventMainPage] 게시글 다이얼로그 오픈", { teamId, post }); if (post) { setEditingPost({ id: post.id, teamId }); setNewPostContent(post.content); @@ -488,28 +453,25 @@ export default function EventMainPage() { } else { resetPostForm(); } + if (teamId) { setSelectedTeamId(teamId); setIsPostTeamLocked(true); } else { setIsPostTeamLocked(false); } + setShowAddPostDialog(true); }; const handleSubmitPost = async () => { - if (!newPostContent.trim() || !selectedTeamId) { - console.warn("[EventMainPage] 게시글 저장 필수값 누락", { - newPostContent, - selectedTeamId, - }); - return; - } + console.log("[handleSubmitPost] 호출됨"); + console.log("selectedTeamId:", selectedTeamId); + console.log("editingPost:", editingPost); const isPoll = postType === "vote"; - const body: Record = { - groupId: Number(selectedTeamId), + const body: any = { type: isPoll ? "VOTE" : "TEXT", content: newPostContent, voteTitle: isPoll ? pollQuestion : null, @@ -521,32 +483,38 @@ export default function EventMainPage() { : null, }; - if (editingPost) { - body.postId = Number(editingPost.id); + if (!editingPost) { + body.groupId = Number(selectedTeamId); } + console.log("[handleSubmitPost] 요청 body:", body); + try { - console.log("[EventMainPage] 게시글 저장 요청", { - mode: editingPost ? "update" : "create", - body, - }); - const res = await apiFetch(`${API_URL}/api/v1/post`, { + const url = editingPost + ? `${API_URL}/api/v1/post/${editingPost.id}` + : `${API_URL}/api/v1/post`; + + console.log("[handleSubmitPost] request URL:", url); + + const res = await apiFetch(url, { method: editingPost ? "PATCH" : "POST", body: JSON.stringify(body), }); - console.log("[EventMainPage] 게시글 저장 fetch 응답", res); + console.log("[handleSubmitPost] response:", res); + const data = await res.json(); - console.log("[EventMainPage] 게시글 저장 응답 JSON", data); + console.log("[handleSubmitPost] json:", data); + if (!data.isSuccess) { - console.warn("[EventMainPage] 게시글 저장 실패 isSuccess=false", data); + console.warn("[handleSubmitPost] 실패:", data); return; } await refreshTeamPosts(selectedTeamId); closePostDialog(); } catch (err) { - console.error("게시글 저장 오류:", err); + console.error("[handleSubmitPost] 오류:", err); } }; @@ -555,10 +523,7 @@ export default function EventMainPage() { // ========================== const handleAddComment = async (teamId: string, postId: string) => { const commentText = commentInputs[postId]; - if (!commentText?.trim()) { - console.warn("[EventMainPage] 댓글 내용 없음", { postId, commentText }); - return; - } + if (!commentText?.trim()) return; const body = { postId: Number(postId), @@ -566,19 +531,13 @@ export default function EventMainPage() { }; try { - console.log("[EventMainPage] 댓글 생성 요청", body); const res = await apiFetch(`${API_URL}/api/v1/comment`, { method: "POST", body: JSON.stringify(body), }); - console.log("[EventMainPage] 댓글 생성 fetch 응답", res); const data = await res.json(); - console.log("[EventMainPage] 댓글 생성 응답 JSON", data); - if (!data.isSuccess) { - console.warn("[EventMainPage] 댓글 생성 실패 isSuccess=false", data); - return; - } + if (!data.isSuccess) return; await refreshTeamPosts(teamId); @@ -594,18 +553,12 @@ export default function EventMainPage() { // ========================== const handleDeleteComment = async (teamId: string, commentId: string) => { try { - console.log("[EventMainPage] 댓글 삭제 요청", { commentId }); const res = await apiFetch(`${API_URL}/api/v1/comment/${commentId}`, { method: "DELETE", }); - console.log("[EventMainPage] 댓글 삭제 fetch 응답", res); const data = await res.json(); - console.log("[EventMainPage] 댓글 삭제 응답 JSON", data); - if (!data.isSuccess) { - console.warn("[EventMainPage] 댓글 삭제 실패 isSuccess=false", data); - return; - } + if (!data.isSuccess) return; await refreshTeamPosts(teamId); } catch (err) { @@ -619,23 +572,12 @@ export default function EventMainPage() { const handleVote = async (teamId: string, postId: string, optionId: string) => { const team = teams.find((t) => t.id === teamId); const post = team?.posts.find((p) => p.id === postId); - if (!post || !post.pollOptions) { - console.warn("[EventMainPage] 투표 대상 게시글/옵션 없음", { - teamId, - postId, - post, - }); - return; - } + if (!post || !post.pollOptions) return; const option = post.pollOptions.find((o) => o.id === optionId); const voteText = option?.text ?? optionId; try { - console.log("[EventMainPage] 투표 요청", { - postId, - voteText, - }); const res = await apiFetch(`${API_URL}/api/v1/post/vote`, { method: "POST", body: JSON.stringify({ @@ -644,13 +586,8 @@ export default function EventMainPage() { }), }); - console.log("[EventMainPage] 투표 fetch 응답", res); const data = await res.json(); - console.log("[EventMainPage] 투표 응답 JSON", data); - if (!data.isSuccess) { - console.warn("[EventMainPage] 투표 실패 isSuccess=false", data); - return; - } + if (!data.isSuccess) return; await refreshTeamPosts(teamId); } catch (err) { @@ -664,18 +601,12 @@ export default function EventMainPage() { const handleDeletePost = async (teamId: string, postId: string) => { if (!window.confirm("게시글을 삭제하시겠어요?")) return; try { - console.log("[EventMainPage] 게시글 삭제 요청", { postId }); const res = await apiFetch(`${API_URL}/api/v1/post/${postId}`, { method: "DELETE", }); - console.log("[EventMainPage] 게시글 삭제 fetch 응답", res); const data = await res.json(); - console.log("[EventMainPage] 게시글 삭제 응답 JSON", data); - if (!data.isSuccess) { - console.warn("[EventMainPage] 게시글 삭제 실패 isSuccess=false", data); - return; - } + if (!data.isSuccess) return; await refreshTeamPosts(teamId); } catch (err) { @@ -699,388 +630,434 @@ export default function EventMainPage() { .charAt(0) .toUpperCase(); - const isEventHost = (eventInfo?.role ?? user?.role)?.toUpperCase() === "HOST"; - console.log("[EventMainPage] isEventHost 계산", { - eventRole: eventInfo?.role, - userRole: user?.role, - isEventHost, - }); + const isEventHost = + (eventInfo?.role ?? user?.role)?.toUpperCase() === "HOST"; + + const displayNickname = location.state?.nickname ?? eventInfo?.nickname ?? "닉네임"; - // ✅ 이벤트 닉네임 우선 사용 (join 응답의 nickname) - const displayNickname = - eventInfo?.nickname ?? - location.state?.eventNickname ?? // 혹시 state로도 넘어온 경우 대비 - user?.nickname ?? - user?.email ?? - "닉네임"; - // =================================================== - // ==================== UI 시작 ====================== - // =================================================== return (
{/* 헤더 */} -
-
-

- EventTee -

-
-

- {headerTitleText} - {eventPeriod && ( - - ({eventPeriod}) - - )} -

- {headerSubtitleText && ( -

- {headerSubtitleText} -

- )} -
-
- -
- {isEventHost && ( - { - console.log("[EventMainPage] 운영자 페이지 버튼 클릭", { - eventId, - }); - navigate("/admin-dashboard", { state: { eventId } }); - }} - > - 운영자 페이지 - +
+ {/* 왼쪽: 제목 + 설명 */} +
+ {/* 로고 + 이벤트 타이틀 */} +
+

+ Eventee +

+ +
+

+ + {headerTitleText} + {eventPeriod && ( + + ({eventPeriod}) + )} +

+
+
- + {/* 이벤트 설명 — 간격 줄임 */} + {headerSubtitleText && ( +

+ {headerSubtitleText} +

+ )} +
+ + {/* 오른쪽: 운영자 버튼 + 프로필 */} +
+ {isEventHost && ( + { + const eventData = { + id: String(eventInfo?.eventId), + title: eventInfo?.title ?? "", + description: eventInfo?.description ?? "", + inviteCode: eventCode, + startDate: eventInfo?.startAt ? new Date(eventInfo.startAt) : null, + endDate: eventInfo?.endAt ? new Date(eventInfo.endAt) : null, + createdBy: user?.id ?? "", + }; + setCurrentEvent(eventData); + localStorage.setItem("currentEvent", JSON.stringify(eventData)); + navigate("/admin-dashboard"); + }} + > + 운영자 페이지 + + )} + + {/* 프로필 */} + +
+
+ {/* 메인 컨텐츠 */}
- {/* 팀 컬럼 */} + {/* 팀 컬럼 영역 (가로 스크롤) */}
{teams.map((team) => ( -
-
-
-

{team.name}

- - {team.isMyTeam && ( - - 내 팀 - - )} -
+
+ {/* 팀 헤더 */} +
+ {/* 상단 이미지 영역 */} +
+ {`${team.name} +
+ + {/* 텍스트 정보 */} +
+ +
+

+ {team.name} +

+ + +
+ + {team.description ? ( +

+ {team.description} +

+ ) : ( +

소개가 없습니다

+ )} +
+
+ + {/* 그룹 내부 스크롤 영역 */} +
+ {team.posts.map((post) => { + const isVotePost = + post.type === "vote" && + Array.isArray(post.pollOptions) && + post.pollOptions.length > 0; + + return ( +
+ {/* 게시글 헤더 */} +
+
+
+ + {post.author} + +
+ {post.isWrite && ( +
+ +
+ )} +
- {/* Post 리스트 */} -
- {team.posts.map((post) => ( -
-
-
-
- - {post.author} - -
- {post.isWrite && ( -
- - -
- )} -
+ {isVotePost ? ( + <> + {/* 투표 게시글 UI */} +
+

+ {post.pollQuestion} +

+
- {/* 투표 게시글 */} - {post.type === "vote" && post.pollOptions ? ( - <> -
-

- {post.pollQuestion} -

-
- -

- {post.content} -

- -
- {post.pollOptions.map((option) => { - const totalVotes = post.pollOptions!.reduce( - (sum, opt) => sum + opt.votes, - 0 - ); - - const percentage = - totalVotes > 0 - ? Math.round( - (option.votes / totalVotes) * 100 - ) - : 0; - - const isVoted = post.userVote === option.id; - - return ( - + {post.content && ( +

+ {post.content} +

+ )} - ); - })} -
- - ) : ( - <> - {/* 내용 */} -

{post.content}

- - {/* 이미지 */} - {post.imageUrl && ( - post - )} - - {/* 댓글 목록 */} - {post.comments.length > 0 && ( -
- {post.comments.map((comment) => ( -
-
-
-
-
- - {comment.author} - +
+ {post.pollOptions!.map((option) => { + const totalVotes = post.pollOptions!.reduce( + (sum, opt) => sum + (opt.votes ?? 0), + 0 + ); + + const percentage = + totalVotes > 0 + ? Math.round( + ((option.votes ?? 0) / totalVotes) * + 100 + ) + : option.percent ?? 0; + + return ( + + ); + })} +
+ + ) : ( + <> + {/* 일반 게시글 내용 */} +

{post.content}

+ + {/* 이미지 */} + {post.imageUrl && ( + post + )} + + {/* 댓글 목록 */} + {post.comments.length > 0 && ( +
+ {post.comments.map((comment) => ( +
+
+
+
+
+ + {comment.author} + - {formatDateOnly(comment.timestamp)} + {formatDateOnly( + comment.timestamp + )} - +
+ {comment.isWrite && ( + + )}
- {comment.isWrite && ( - + +

+ {comment.content} +

+ + {comment.imageUrl && ( + comment )}
- -

- {comment.content} -

- - {comment.imageUrl && ( - comment - )}
+ ))} +
+ )} + + {/* 댓글 입력 */} +
+ {commentImages[post.id] && ( +
+ preview +
- ))} -
- )} - - {/* 댓글 입력 */} -
- {commentImages[post.id] && ( -
- preview + { + const file = e.target.files?.[0]; + if (file) { + const reader = new FileReader(); + reader.onloadend = () => { + setCommentImages({ + ...commentImages, + [post.id]: + reader.result as string, + }); + }; + reader.readAsDataURL(file); + } + }} /> -
- )} -
- { - const file = e.target.files?.[0]; - if (file) { - const reader = new FileReader(); - reader.onloadend = () => { - setCommentImages({ - ...commentImages, - [post.id]: reader.result as string, - }); - }; - reader.readAsDataURL(file); - } - }} - /> - - - - setCommentInputs({ - ...commentInputs, - [post.id]: e.target.value, - }) - } - onKeyPress={(e) => { - if (e.key === "Enter") { - handleAddComment(team.id, post.id); + + setCommentInputs({ + ...commentInputs, + [post.id]: e.target.value, + }) } - }} - placeholder="댓글 입력..." - className="flex-1 bg-transparent text-xs outline-none" - /> + onKeyPress={(e) => { + if (e.key === "Enter") { + handleAddComment(team.id, post.id); + } + }} + placeholder="댓글 입력..." + className="flex-1 bg-transparent text-xs outline-none" + /> - + +
-
- - )} -
- ))} + + )} +
+ ); + })} + {/* 게시글 추가 버튼 */}
- {/* 게시글 추가 다이얼로그 */} + {/* 게시글 추가/수정 다이얼로그 */} { - console.log("[EventMainPage] 게시글 다이얼로그 openChange", open); if (!open) { closePostDialog(); } else { @@ -1138,10 +1114,6 @@ export default function EventMainPage() { id="team" value={selectedTeamId} onChange={(e) => { - console.log( - "[EventMainPage] 게시글 작성 팀 선택 변경", - e.target.value - ); setSelectedTeamId(e.target.value); setIsPostTeamLocked(false); }} @@ -1170,25 +1142,30 @@ export default function EventMainPage() {
-
- {showPostTypeMenu && ( + {!editingPost && showPostTypeMenu && (
+ {/* 그룹 수정 다이얼로그 */} { - console.log("[EventMainPage] 그룹 수정 다이얼로그 openChange", open); if (!open) { closeGroupEditDialog(); } else { diff --git a/src/pages/MyPage.tsx b/src/pages/MyPage.tsx index 6d24cf4b..b2076fcd 100644 --- a/src/pages/MyPage.tsx +++ b/src/pages/MyPage.tsx @@ -152,12 +152,16 @@ export default function MyPage() { {/* 썸네일 */}
{event.title} { + e.currentTarget.src = "/default-event.png"; + }} />
+ {/* 내용 */}
@@ -217,13 +221,25 @@ export default function MyPage() { )}
- {/* 이벤트 생성 Floating 버튼 */} - +
+ + {/* 참여하기 버튼 → /join-event */} + + + {/* 기존 이벤트 생성 버튼 */} + + +
diff --git a/src/pages/SignupPage.tsx b/src/pages/SignupPage.tsx index b99a96be..66a0d469 100644 --- a/src/pages/SignupPage.tsx +++ b/src/pages/SignupPage.tsx @@ -77,7 +77,7 @@ export default function SignupPage() { return; } - // ★ currentEvent 타입과 맞도록 저장 (Event 타입에 맞게 축약) + // currentEvent 타입과 맞도록 저장 setCurrentEvent({ id: data.result.eventId, title: data.result.title, @@ -94,6 +94,7 @@ export default function SignupPage() { eventId: data.result.eventId, eventTitle: data.result.title, eventCode: inviteCode, + nickname: nickname }, });