//
-// {post.title}
+// {p.title ?? "(title 없음)"}
//
-
-//
-// {post.content}
-//
-
-//
-//
-// {formatDate(post.createdAt)}
-
-//
-
-// 댓글 {post.commentCount ?? 0}
-//
+//
+// {p.content ?? "(content 없음)"}
//
-
-//
+//
//
// ))
// )}
@@ -189,188 +98,236 @@
// );
// }
-
-
-import React, { useEffect, useState } from "react";
+import React, { useEffect, useMemo, useState } from "react";
import { Link, useNavigate } from "react-router-dom";
+import api from "@api/api.js";
-/**
- * localStorage keys
- */
-const LS_POSTS = "community_posts_v1";
-
-
-const SEED_POSTS = [
- {
- id: "1",
- author: "zelsa",
- title: "SSAFY VS SKALA",
- content: "둘 다 붙으면 어디를 가는 게 좋을까요?\n의견 부탁드립니다!",
- createdAt: "2024-11-11",
- likeCount: 2,
- commentCount: 1,
- },
- {
- id: "2",
- author: "zelsa",
- title: "SSAFY VS SKALA",
- content: "둘 다 붙으면 어디를 가는 게 좋을까요?\n(중복 테스트)",
- createdAt: "2024-11-11",
- likeCount: 2,
- commentCount: 1,
- },
- {
- id: "3",
- author: "zelsa",
- title: "SSAFY VS SKALA",
- content: "둘 다 붙으면 어디를 가는 게 좋을까요?\n(리스트 UI 테스트)",
- createdAt: "2024-11-11",
- likeCount: 2,
- commentCount: 0,
- },
-];
-
-function loadPosts() {
- const raw = localStorage.getItem(LS_POSTS);
- if (!raw) {
- localStorage.setItem(LS_POSTS, JSON.stringify(SEED_POSTS));
- return SEED_POSTS;
- }
- try {
- const parsed = JSON.parse(raw);
- return Array.isArray(parsed) ? parsed : [];
- } catch {
- return [];
- }
-}
-
-function savePosts(posts) {
- localStorage.setItem(LS_POSTS, JSON.stringify(posts));
-}
-
+/** =========================
+ * 날짜 포맷 유틸 (예: 2026-01-28T02:06:52 -> Jan.28.2026)
+ * ========================= */
function formatDate(yyyyMmDd) {
- const [y, m, d] = yyyyMmDd.split("-").map(Number);
+ const [y, m, d] = String(yyyyMmDd || "").split("-").map(Number);
const monthNames = [
"Jan.", "Feb.", "Mar.", "Apr.", "May.", "Jun.",
- "Jul.", "Aug.", "Sep.", "Oct.", "Nov.", "Dec."
+ "Jul.", "Aug.", "Sep.", "Oct.", "Nov.", "Dec.",
];
- if (!y || !m || !d) return yyyyMmDd;
+ if (!y || !m || !d) return yyyyMmDd || "";
return `${monthNames[m - 1]}${d}.${y}`;
}
+/** =========================
+ * 리스트 응답 형태 정규화
+ * - 백엔드가 다양한 형태로 내려줘도 content 배열만 뽑아오는 역할
+ * ========================= */
+function normalizeListResponse(data) {
+ if (Array.isArray(data)) return data;
+ if (Array.isArray(data?.content)) return data.content; // Page 형태
+ if (Array.isArray(data?.data)) return data.data; // Wrapper 형태
+ if (Array.isArray(data?.result)) return data.result; // Wrapper 형태
+ if (Array.isArray(data?.data?.content)) return data.data.content;
+ return [];
+}
+
+/** createdAt이 ISO("2026-01-28T...")일 수 있어서 앞 10자리만 잘라 YYYY-MM-DD로 */
+function toYmd(createdAtLike) {
+ const s = String(createdAtLike || "");
+ return s.length >= 10 ? s.slice(0, 10) : s;
+}
+
export default function CommunityListPage() {
const navigate = useNavigate();
+
+ // 1) 화면에 뿌릴 게시글 목록 상태
const [posts, setPosts] = useState([]);
- const [q, setQ] = useState("");
+ // 2) 검색어 상태 (UI + 필터링)
+ const [q, setQ] = useState("");
+
+ // 3) 로딩/에러/디버그 상태
+ const [loading, setLoading] = useState(false);
+ const [errorMsg, setErrorMsg] = useState("");
+ const [debug, setDebug] = useState("INIT");
+
+ // 4) 원본 응답 확인용 (문제 생기면 구조 파악하려고)
+ //const [raw, setRaw] = useState(null);
+
+ // 5) 페이지 진입 시 게시글 목록 조회
useEffect(() => {
- console.log("[CommunityListPage] mounted", new Date().toISOString());
+ let cancelled = false;
+
+ (async () => {
+ try {
+ setLoading(true);
+ setErrorMsg("");
+ setDebug("FETCH_STARTED");
+
+ const res = await api.get("/api/posts");
+
+ if (cancelled) return;
+
+ setDebug(`FETCH_SUCCESS_${res.status}`);
+ //setRaw(res.data);
+
+ const list = normalizeListResponse(res.data);
+
+
+ // 6) 서버 응답을 UI에서 쓰기 쉬운 형태로 매핑
+ const mapped = list.map((p) => ({
+ id: String(p.id ?? p.postId ?? ""),
+ title: p.title ?? "",
+ content: p.content ?? "",
+
+ // author는 객체일 수도 문자열일 수도 있어서 방어적으로 처리
+ author:
+ typeof p.author === "string"
+ ? p.author
+ : (p.author?.nickname ??
+ p.user?.nickname ??
+ p.writer?.nickname ??
+ "unknown"),
+
+ createdAt: toYmd(p.createdAt ?? p.created_at ?? p.createdDate ?? p.created_date),
+
+ likeCount: p.likeCount ?? p.like_count ?? p.likes ?? 0,
+ commentCount: p.commentCount ?? p.comment_count ?? p.comments ?? 0,
+ }));
+
+ // id가 없으면 링크가 깨질 수 있어서 필터
+ const safe = mapped.filter((p) => p.id);
+
+ setPosts(safe);
+ } catch (e) {
+ if (cancelled) return;
+
+ const status = e?.response?.status;
+ const data = e?.response?.data;
+
+ //setRaw(data ?? null);
+ setDebug(`FETCH_FAILED_${status ?? "?"}`);
+
+ if (status === 401) setErrorMsg("로그인이 필요합니다.");
+ else if (status === 403) setErrorMsg("권한이 없습니다.");
+ else setErrorMsg("게시글을 불러오지 못했습니다.");
+
+ setPosts([]);
+ } finally {
+ if (!cancelled) setLoading(false);
+ }
+ })();
+
+ return () => {
+ cancelled = true;
+ };
}, []);
- useEffect(() => {
- (async () => {
- try {
- const res = await fetch("/api/posts"); // nginx가 backend로 프록시
- if (!res.ok) throw new Error(`HTTP ${res.status}`);
- const data = await res.json();
-
- // ✅ 서버 응답 형태에 맞게 매핑 (필드명은 실제 응답 보고 조정)
- const mapped = (Array.isArray(data) ? data : data?.content ?? []).map((p) => ({
- id: String(p.id),
- author:
- typeof p.author === "string"
- ? p.author
- : (p.author?.nickname ?? p.user?.nickname ?? "unknown"),
- title: p.title,
- content: p.content,
- createdAt: (p.createdAt ?? p.created_at ?? "").slice(0, 10), // "YYYY-MM-DD"
- likeCount: p.likeCount ?? p.like_count ?? 0,
- commentCount: p.commentCount ?? p.comment_count ?? 0,
- }));
-
- setPosts(mapped);
- } catch (e) {
- console.error("Failed to load posts:", e);
- // 서버 장애 시 임시 데이터 fallback 쓰고 싶으면 아래 주석 해제
- // setPosts(loadPosts());
- setPosts([]);
- }
- })();
-}, []);
+ // 7) 검색어로 목록 필터링 (제목/내용/작성자)
+ const filtered = useMemo(() => {
+ const keyword = q.trim().toLowerCase();
+ if (!keyword) return posts;
+
+ return posts.filter((p) => {
+ const t = String(p.title || "").toLowerCase();
+ const c = String(p.content || "").toLowerCase();
+ const a = String(p.author || "").toLowerCase();
+ return t.includes(keyword) || c.includes(keyword) || a.includes(keyword);
+ });
+ }, [posts, q]);
+ // =========================
+ // 8) 추천(좋아요) 버튼 클릭 핸들러
+ // =========================
+ const onLike = async (postId) => {
+ alert("추천 API 연결 전입니다.");
+ };
return (
+ {/* =========================
+ 상단 타이틀 + 검색 + 글쓰기 버튼
+ ========================= */}
커뮤니티
-
+ {/* 검색 입력 */}
setQ(e.target.value)}
/>
+ {/* 글쓰기 이동 */}
-
-
- {/* 리스트 */}
-
- {posts.length === 0 ? (
-
- 게시글이 없습니다.
-
+ {/* =========================
+ 로딩/에러 메시지
+ ========================= */}
+
+ {loading ? (
+
불러오는 중...
+ ) : errorMsg ? (
+
{errorMsg}
+ ) : null}
+
+
+ {/* =========================
+ (선택) 디버그 영역: 서버 응답 구조 확인용
+ 안정화되면 지워도 됨
+ ========================= */}
+ {/*
DEBUG: {debug}
*/}
+ {/*
+{JSON.stringify(raw, null, 2)}
+ */}
+
+ {/* =========================
+ 게시글 리스트 UI
+ - 작성자, 제목, 내용
+ - 날짜, 추천 수, 댓글 수 표시
+ ========================= */}
+
+ {filtered.length === 0 && !loading ? (
+
게시글이 없습니다.
) : (
- posts.map((post) => (
+ filtered.map((post) => (
-
+ {/* 작성자 */}
- 👤
+ face
{post.author}
+ {/* 제목 */}
- {post.title}
+ {post.title || "(title 없음)"}
+ {/* 내용 미리보기 */}
- {post.content}
+ {post.content || "(content 없음)"}
+ {/* 우측 하단 메타정보: 날짜/추천/댓글 */}
{formatDate(post.createdAt)}
@@ -379,12 +336,8 @@ export default function CommunityListPage() {
type="button"
onClick={() => onLike(post.id)}
className="
- bg-transparent
- p-0
- border-0
- text-gray-500
- hover:text-black
- focus:outline-none
+ bg-transparent p-0 border-0 text-gray-500
+ hover:text-black focus:outline-none
"
aria-label="like"
>
@@ -395,6 +348,7 @@ export default function CommunityListPage() {
+ {/* 구분선 */}
))
@@ -403,4 +357,4 @@ export default function CommunityListPage() {
);
-}
\ No newline at end of file
+}
diff --git a/Frontend/src/pages/Community/CommunityWritePage.jsx b/Frontend/src/pages/Community/CommunityWritePage.jsx
index 4c133d7..155269a 100644
--- a/Frontend/src/pages/Community/CommunityWritePage.jsx
+++ b/Frontend/src/pages/Community/CommunityWritePage.jsx
@@ -1,98 +1,155 @@
import React, { useState } from "react";
-import { Link, useNavigate } from "react-router-dom";
-
-const LS_POSTS = "community_posts_v1";
-
-function loadPosts() {
- const raw = localStorage.getItem(LS_POSTS);
- if (!raw) return [];
- try {
- const parsed = JSON.parse(raw);
- return Array.isArray(parsed) ? parsed : [];
- } catch {
- return [];
- }
-}
-
-function savePosts(posts) {
- localStorage.setItem(LS_POSTS, JSON.stringify(posts));
-}
+import api from "@api/api.js";
+import { useNavigate } from "react-router-dom";
-function todayString() {
- const d = new 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}`;
-}
export default function CommunityWritePage() {
- const navigate = useNavigate();
+ // 1) 폼 입력값 상태 (제목/내용)
const [title, setTitle] = useState("");
const [content, setContent] = useState("");
+ // 2) UI 상태 (로딩/에러)
+ const [isSubmitting, setIsSubmitting] = useState(false);
+ const [errorMsg, setErrorMsg] = useState("");
+ // const [lastPostRes, setLastPostRes] = useState(null);
+ // const [lastGetRes, setLastGetRes] = useState(null);
+ // const [isLoadingGet, setIsLoadingGet] = useState(false);
+ const navigate = useNavigate();
- const onSubmit = () => {
- const t = title.trim();
- const c = content.trim();
- if (!t || !c) return;
-
- const posts = loadPosts();
- const newPost = {
- id: String(Date.now()),
- author: "USERNAME",
- title: t,
- content: c,
- createdAt: todayString(),
- likeCount: 0,
- dislikeCount: 0,
- commentCount: 0,
+
+ const submit = async () => {
+ const body = {
+ title: title.trim() || "테스트 게시물 제목 입니다.",
+ content: content.trim() || "테스트 게시물 내용 입니다.",
};
- // 최신 글이 위로
- savePosts([newPost, ...posts]);
- navigate("/community");
+ if (!body.title || !body.content) {
+ setErrorMsg("제목과 내용을 모두 입력해주세요.");
+ return;
+ }
+
+ // 요청 시작: 로딩 ON / 에러 초기화
+ setIsSubmitting(true);
+ setErrorMsg("");
+ //setLastPostRes(null);
+
+ try {
+ // 등록 요청
+ const res = await api.post("/api/posts", body);
+ //setLastPostRes({ ok: true, status: res.status, data: res.data, sentBody: body });
+ navigate("/community");
+ } catch (err) {
+ const status = err?.response?.status;
+ const data = err?.response?.data;
+
+ setErrorMsg(
+ `POST 실패 (status=${status ?? "?"})\n` +
+ (data?.message || data?.error || JSON.stringify(data || {}, null, 2) || err.message)
+ );
+ } finally { // 요청 종료: 로딩 OFF
+ setIsSubmitting(false);
+ }
+ };
+
+ const refetch = async () => {
+ //setIsLoadingGet(true);
+ setErrorMsg("");
+ //setLastGetRes(null);
+
+ try {
+ const res = await api.get("/api/posts");
+ //setLastGetRes({ ok: true, status: res.status, data: res.data });
+ alert("GET 성공. 아래 응답 확인!");
+ } catch (err) {
+ const status = err?.response?.status;
+ const data = err?.response?.data;
+
+ setErrorMsg(
+ `GET 실패 (status=${status ?? "?"})\n` +
+ (data?.message || data?.error || JSON.stringify(data || {}, null, 2) || err.message)
+ );
+ } finally {
+ //setIsLoadingGet(false);
+ }
};
return (
-
- 커뮤니티 글쓰기
+
+ 커뮤니티 글쓰기
+
-
+ {/* 폼 */}
+
-
- setTitle(e.target.value)}
- />
-
+
setTitle(e.target.value)}
+ disabled={isSubmitting}
+ placeholder="제목을 입력하세요"
+ />
-
-
-
+
+
diff --git a/Frontend/src/pages/MyPage/MyPageLoggedOut.jsx b/Frontend/src/pages/MyPage/MyPageLoggedOut.jsx
index d829267..58fbc48 100644
--- a/Frontend/src/pages/MyPage/MyPageLoggedOut.jsx
+++ b/Frontend/src/pages/MyPage/MyPageLoggedOut.jsx
@@ -4,7 +4,7 @@ const MyPageLoggedOut = () => {
const handleGithubLogin = () => {
// OAuth 시작 url
- window.location.href = "http://solvemeup.com/oauth2/authorization/github"
+ window.location.href = `${import.meta.env.VITE_API_URL}/oauth2/authorization/github`
}
diff --git a/Frontend/src/router/index.jsx b/Frontend/src/router/index.jsx
index 80b96bd..c951f32 100644
--- a/Frontend/src/router/index.jsx
+++ b/Frontend/src/router/index.jsx
@@ -1,7 +1,8 @@
import MainLayout from "@layout/index";
import CommunityListPage from "@pages/Community/CommunityListPage";
import CommunityWritePage from "@pages/Community/CommunityWritePage";
-import CommunityDetailPage from "@pages/Community/CommunityDeatailPage";
+import CommunityEditPage from "@pages/Community/CommunityEditPage";
+import CommunityDetailPage from "@pages/Community/CommunityDetailPage";
import ProblemListPage from "@pages/Problems/ProblemListPage";
import MyPage from "@pages/MyPage/MyPageGate";
@@ -22,6 +23,10 @@ const routerInfo = [
path: "communitywrite",
element:
,
},
+ {
+ path: "community/:postId/edit",
+ element:
,
+ },
{
path: "community/:postId",
element:
,
diff --git a/Frontend/vite.config.js b/Frontend/vite.config.js
index 3b738e8..3442a8b 100644
--- a/Frontend/vite.config.js
+++ b/Frontend/vite.config.js
@@ -29,5 +29,9 @@ export default defineConfig({
changeOrigin: true,
},
},
+ watch: {
+ usePolling: true,
+ interval: 100,
+ },
},
});