Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions LP/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
"@tanstack/react-query": "^5.75.2",
"axios": "^1.9.0",
"clsx": "^2.1.1",
"lodash.debounce": "^4.0.8",
"lucide-react": "^0.510.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-hook-form": "^7.56.1",
Expand Down
Binary file added LP/public/cd.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added LP/public/lp.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 3 additions & 3 deletions LP/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ import SignupPage from "./pages/SignupPage";
import Mypage from "./pages/MyPage.tsx";
import GoogleCallback from "./pages/GoogleCallback";
import LPDetail from "./pages/LPDetail.tsx";
import SearchResult from "./pages/SearchResult";

import ProtectedRoute from "../src/components/ProtectedRoute";
import { AuthProvider } from "./context/AuthContext";
import { AuthProvider } from "./context/AuthContext";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";


const queryClient = new QueryClient();

const router = createBrowserRouter([
Expand All @@ -25,6 +25,7 @@ const router = createBrowserRouter([
{ index: true, element: <HomePage /> },
{ path: "login", element: <LoginPage /> },
{ path: "signup", element: <SignupPage /> },
{ path: "search", element: <SearchResult /> },
{
path: "mypage",
element: (
Expand Down Expand Up @@ -59,5 +60,4 @@ function App() {
);
}


export default App;
39 changes: 22 additions & 17 deletions LP/src/api/comments.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
import api from './axios';
import api from "./axios";

export const getComments = async ({
lpId,
cursor = 0,
limit = 10,
order = 'desc',
}: {
lpId: number;
cursor?: number;
limit?: number;
order?: 'asc' | 'desc';
}) => {
const res = await api.get(`/lps/${lpId}/comments`, {
params: { cursor, limit, order },
});
await new Promise(resolve => setTimeout(resolve, 1000)); //로딩바 확인용 지연
lpId,
cursor = 0,
limit = 10,
order = "desc",
}: {
lpId: number;
cursor?: number;
limit?: number;
order?: "asc" | "desc";
}) => {
const res = await api.get(`/lps/${lpId}/comments`, {
params: { cursor, limit, order },
});
await new Promise((resolve) => setTimeout(resolve, 1000)); //로딩바 확인용 지연

return res.data.data;
};
console.log("getComments 반환값:", res.data);
return {
comments: res.data.data.data, //댓글 리스트
nextCursor: res.data.data.nextCursor,
hasNext: res.data.data.hasNext,
};
};
2 changes: 1 addition & 1 deletion LP/src/api/lp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,4 @@ export const fetchLpDetail = async (lpId: string | number): Promise<Lp> => {
};

return withTagIds;
};
};
253 changes: 253 additions & 0 deletions LP/src/components/CommentSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
import React, { useState, useEffect, useRef } from "react";
import { useComments } from "../hooks/useComments";
import api from "../api/axios";

interface CommentSectionProps {
LPid: number;
user: { id: number; name: string } | null;
}

const CommentSection = ({ LPid, user }: CommentSectionProps) => {
const [order, setOrder] = useState<"asc" | "desc">("desc");
const {
comments,
comment,
setComment,
submitComment,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
isLoading,
} = useComments(LPid, order);

const [menuOpenId, setMenuOpenId] = useState<number | null>(null);
const [editingCommentId, setEditingCommentId] = useState<number | null>(null);
const [editingContent, setEditingContent] = useState("");

const loadMoreRef = useRef<HTMLDivElement | null>(null);

useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting && hasNextPage) {
fetchNextPage();
}
},
{ threshold: 1.0 }
);
if (loadMoreRef.current) observer.observe(loadMoreRef.current);
return () => {
if (loadMoreRef.current) observer.unobserve(loadMoreRef.current);
};
}, [hasNextPage, fetchNextPage]);

return (
<div className="mt-6 w-full max-w-xl mx-auto px-4">
<div className="flex gap-2 mb-2">
<button
onClick={() => setOrder("desc")}
className={`px-3 py-1 rounded ${
order === "desc"
? "bg-gray-600 text-white"
: "bg-gray-700 text-gray-300"
}`}
>
최신순
</button>
<button
onClick={() => setOrder("asc")}
className={`px-3 py-1 rounded ${
order === "asc"
? "bg-gray-600 text-white"
: "bg-gray-700 text-gray-300"
}`}
>
오래된순
</button>
</div>
<form
onSubmit={async (e) => {
e.preventDefault();
if (!comment.trim()) return;

if (editingCommentId) {
// Only patch if content changed
const editingComment = comments?.find(
(c) => c.id === editingCommentId
);
if (editingComment && editingComment.content === comment) {
setEditingCommentId(null);
setComment("");
return;
}
try {
await api.patch(`/lps/${LPid}/comments/${editingCommentId}`, {
content: comment,
});
setEditingCommentId(null);
setComment("");
await fetchNextPage({ pageParam: 0 }); // simulate refetch
} catch (err) {
console.error("댓글 수정 실패:", err);
}
} else {
await submitComment(comment);
}
}}
className="flex items-center gap-2 mb-4"
>
<input
type="text"
value={comment}
onChange={(e) => setComment(e.target.value)}
placeholder="댓글을 입력하세요"
className="flex-1 p-2 rounded bg-gray-800 text-white border border-gray-600"
/>
<button
type="submit"
className="px-4 py-2 rounded bg-gray-600 text-white"
disabled={
!comment?.trim() ||
(editingCommentId
? comments?.find((c) => c.id === editingCommentId)?.content ===
comment
: false)
}
>
등록
</button>
</form>
{/* Conditional rendering: skeleton or comments */}
<div>
{isLoading ? (
<ul className="space-y-4 min-h-[300px]">
{Array.from({ length: 5 }).map((_, idx) => (
<li
key={idx}
className="animate-pulse flex gap-3 items-start bg-gray-600 p-3 rounded text-sm text-white"
>
<div className="w-8 h-8 bg-gray-500 rounded-full" />
<div className="flex flex-col gap-2 flex-1">
<div className="bg-gray-500 h-4 w-1/3 rounded" />
<div className="bg-gray-500 h-3 w-full rounded" />
</div>
</li>
))}
</ul>
) : (
<ul className="space-y-2 min-h-[300px]">
{comments?.map((c) =>
c && c.author ? (
<li
key={c.id}
className="flex gap-3 items-start bg-gray-800 p-3 rounded text-sm text-white relative"
>
<img
src={c.author.avatar || "/default-avatar.png"}
alt={`${c.author.name} 프로필`}
className="w-8 h-8 rounded-full object-cover"
/>
<div className="flex-1">
<div className="flex items-center justify-between">
<div className="font-semibold">{c.author.name}</div>
{user?.id === c.author.id && (
<div className="relative">
<button
onClick={() =>
setMenuOpenId((prev) =>
prev === c.id ? null : c.id
)
}
className="text-gray-400 hover:text-white text-xl"
>
</button>
{menuOpenId === c.id && (
<div className="absolute right-0 mt-2 w-24 bg-gray-900 border border-gray-700 rounded-md shadow-lg z-10">
<button
onClick={() => {
setEditingCommentId(c.id);
setEditingContent(c.content);
setMenuOpenId(null);
}}
className="block w-full px-4 py-2 text-sm text-left text-white hover:bg-gray-700 transition-colors"
>
✏️
</button>
<button
onClick={async () => {
if (confirm("정말 삭제하시겠습니까?")) {
try {
await api.delete(
`/lps/${LPid}/comments/${c.id}`
);
await fetchNextPage({ pageParam: 0 }); // simulate refetch
} catch (err) {
console.error("삭제 실패:", err);
}
}
setMenuOpenId(null);
}}
className="block w-full px-4 py-2 text-sm text-left text-red-400 hover:bg-gray-700 transition-colors"
>
🗑️
</button>
</div>
)}
</div>
)}
</div>
{editingCommentId === c.id ? (
<div className="flex items-center gap-2 mt-1">
<input
value={editingContent}
onChange={(e) => setEditingContent(e.target.value)}
className="flex-1 p-1 px-2 text-sm text-white bg-transparent border border-gray-500 rounded"
/>
<button
onClick={async () => {
if (!editingContent.trim()) return;
try {
await api.patch(
`/lps/${LPid}/comments/${editingCommentId}`,
{
content: editingContent,
}
);
setEditingCommentId(null);
setEditingContent("");
await fetchNextPage({ pageParam: 0 });
} catch (err) {
console.error("댓글 수정 실패:", err);
}
}}
className="text-white text-xl"
>
</button>
<button
onClick={() => {
setEditingCommentId(null);
setEditingContent("");
}}
className="text-gray-400 hover:text-white text-xl"
>
</button>
</div>
) : (
<div className="text-gray-300">{c.content}</div>
)}
</div>
</li>
) : null
)}
</ul>
)}
</div>
<div ref={loadMoreRef} className="h-10" />
</div>
);
};

export default CommentSection;
36 changes: 36 additions & 0 deletions LP/src/components/DeleteUserModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// src/components/DleteUserModal.tsx
import React from "react";

interface DeleteUserModalProps {
onDelete: () => void;
onCancel: () => void;
}

const DeleteUserModal = ({ onDelete, onCancel }: DeleteUserModalProps) => {
return (
<div className="fixed inset-0 bg-[rgba(0,0,0,0.5)] flex justify-center items-center z-50">
<div className="relative bg-[#2c2c2c] p-6 rounded-xl text-center text-white w-[300px] shadow-lg">
<button onClick={onCancel} className="absolute top-2 right-2 text-xl">
</button>
<p className="mb-6 mt-4">정말 탈퇴하시겠습니까?</p>
<div className="flex justify-center gap-4">
<button
onClick={onDelete}
className="bg-gray-300 text-black px-4 py-2 rounded"
>
</button>
<button
onClick={onCancel}
className="bg-pink-500 text-white px-4 py-2 rounded"
>
아니요
</button>
</div>
</div>
</div>
);
};

export default DeleteUserModal;
Loading
Loading