Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
4 changes: 4 additions & 0 deletions public/reportCancle.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 0 additions & 3 deletions src/app/(main)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,6 @@ export default function HomePage() {
onSubscribeClick={() => toggleFollow({ nickname: story.authorInfo.nickname, isFollowing: story.authorInfo.following })}
hideSubscribeButton={story.writtenByMe}
onClick={() => router.push(`/stories/${story.bookStoryId}`)}
onLikeClick={() => toggleLike(story.bookStoryId)}
/>
))}
</div>
Expand Down Expand Up @@ -178,7 +177,6 @@ export default function HomePage() {
onSubscribeClick={() => toggleFollow({ nickname: story.authorInfo.nickname, isFollowing: story.authorInfo.following })}
hideSubscribeButton={story.writtenByMe}
onClick={() => router.push(`/stories/${story.bookStoryId}`)}
onLikeClick={() => toggleLike(story.bookStoryId)}
/>
))}
</div>
Expand Down Expand Up @@ -237,7 +235,6 @@ export default function HomePage() {
onSubscribeClick={() => toggleFollow({ nickname: story.authorInfo.nickname, isFollowing: story.authorInfo.following })}
hideSubscribeButton={story.writtenByMe}
onClick={() => router.push(`/stories/${story.bookStoryId}`)}
onLikeClick={() => toggleLike(story.bookStoryId)}
/>
))}
</div>
Expand Down
2 changes: 1 addition & 1 deletion src/app/(main)/profile/[nickname]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export default async function OtherUserProfilePage({ params }: PageProps) {
</div>

<div className="flex flex-col items-center w-full max-w-[1440px] px-4 md:px-0 gap-[24px] mt-[10px] md:mt-[72px]">
<OtherUserProfileTabs />
<OtherUserProfileTabs nickname={nickname} />
</div>
</div>
);
Expand Down
2 changes: 2 additions & 0 deletions src/app/(main)/stories/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ export default function StoriesPage() {
isFollowing={story.authorInfo.following}
onSubscribeClick={() => toggleFollow({ nickname: story.authorInfo.nickname, isFollowing: story.authorInfo.following })}
hideSubscribeButton={story.writtenByMe}
onProfileClick={() => router.push(`/profile/${story.authorInfo.nickname}`)}
onClick={() => handleCardClick(story.bookStoryId)}
onLikeClick={() => toggleLike(story.bookStoryId)}
/>
Expand Down Expand Up @@ -149,6 +150,7 @@ export default function StoriesPage() {
isFollowing={story.authorInfo.following}
onSubscribeClick={() => toggleFollow({ nickname: story.authorInfo.nickname, isFollowing: story.authorInfo.following })}
hideSubscribeButton={story.writtenByMe}
onProfileClick={() => router.push(`/profile/${story.authorInfo.nickname}`)}
onClick={() => handleCardClick(story.bookStoryId)}
onLikeClick={() => toggleLike(story.bookStoryId)}
/>
Expand Down
12 changes: 11 additions & 1 deletion src/components/base-ui/BookStory/bookstory_card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ type Props = {
isFollowing?: boolean;
hideSubscribeButton?: boolean;
onClick?: () => void;
onProfileClick?: () => void;
};

export default function BookStoryCard({
Expand All @@ -41,6 +42,7 @@ export default function BookStoryCard({
isFollowing = false,
hideSubscribeButton = false,
onClick,
onProfileClick,
}: Props) {
const heartIcon = likedByMe ? "/red_heart.svg" : "/gray_heart.svg";

Expand All @@ -54,7 +56,15 @@ export default function BookStoryCard({
md:w-[336px] md:h-[380px]"
>
{/* 1. 상단 프로필 (모바일 숨김 / 데스크탑 노출) */}
<div className="items-center hidden gap-2 px-4 py-3 md:flex">
<div
className="items-center hidden gap-2 px-4 py-3 md:flex group cursor-pointer"
onClick={(e) => {
if (onProfileClick) {
e.stopPropagation();
onProfileClick();
}
}}
>
<div className="relative w-8 h-8 overflow-hidden rounded-full shrink-0">
<Image
src={profileImgSrc}
Expand Down
12 changes: 11 additions & 1 deletion src/components/base-ui/BookStory/bookstory_card_large.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ type Props = {
isFollowing?: boolean;
onClick?: () => void;
hideSubscribeButton?: boolean;
onProfileClick?: () => void;
};

export default function BookStoryCardLarge({
Expand All @@ -41,6 +42,7 @@ export default function BookStoryCardLarge({
isFollowing = false,
onClick,
hideSubscribeButton = false,
onProfileClick,
}: Props) {
const heartIcon = likedByMe ? "/red_heart.svg" : "/gray_heart.svg";

Expand All @@ -51,7 +53,15 @@ export default function BookStoryCardLarge({
w-[336px] h-[380px]"
>
{/* 상단 프로필 */}
<div className="flex items-center gap-2 px-4 py-3">
<div
className="flex items-center gap-2 px-4 py-3 group cursor-pointer"
onClick={(e) => {
if (onProfileClick) {
e.stopPropagation();
onProfileClick();
}
}}
>
Comment on lines +56 to +64
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Use semantic buttons for profile/like actions (keyboard access is currently blocked).

Clickable divs here are not keyboard-focusable/actionable by default, which blocks core interactions for keyboard/screen-reader users.

♿ Suggested fix
-      <div
+      <button
+        type="button"
         className="flex items-center gap-2 px-4 py-3 group cursor-pointer"
         onClick={(e) => {
           if (onProfileClick) {
             e.stopPropagation();
             onProfileClick();
           }
         }}
       >
@@
-      </div>
+      </button>
@@
-        <div
+        <button
+          type="button"
           onClick={(e) => {
             e.stopPropagation();
             onLikeClick?.(e);
           }}
           className="flex items-center justify-center gap-2 pt-1 cursor-pointer hover:bg-gray-100 transition-colors rounded-full px-2 h-10"
+          aria-pressed={likedByMe}
         >
@@
-        </div>
+        </button>

Also applies to: 116-125

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/base-ui/BookStory/bookstory_card_large.tsx` around lines 56 -
64, Replace the non-semantic, clickable divs that handle profile and like
actions with semantic <button type="button"> elements so they are
keyboard-focusable and actionable (preserve visual classes by moving className
to the button), keep the existing handlers (onProfileClick, onLikeClick) but
move e.stopPropagation() into the button onClick and remove any manual onKey
handlers; also add an appropriate aria-label for screen readers (e.g., "Open
profile" / "Like story") and ensure buttons do not submit forms by specifying
type="button". This change applies to the block using onProfileClick and the
analogous like-action block (the second clickable div).

<div className="relative w-8 h-8 overflow-hidden rounded-full shrink-0">
<Image
src={profileImgSrc}
Expand Down
37 changes: 31 additions & 6 deletions src/components/base-ui/BookStory/bookstory_detail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
import { useState, useRef, useEffect } from "react";
import Image from "next/image";
import Link from "next/link";
import ReportModal from "@/components/common/ReportModal";
import { useReportMemberMutation } from "@/hooks/mutations/useMemberMutations";
import { ReportType } from "@/types/member";

type BookstoryDetailProps = {
imageUrl?: string;
Expand Down Expand Up @@ -70,8 +73,10 @@ export default function BookstoryDetail({
}: BookstoryDetailProps) {
const href = authorHref ?? `/profile/${authorId}`;
const [menuOpen, setMenuOpen] = useState(false);
const [isReportModalOpen, setIsReportModalOpen] = useState(false);
const menuRef = useRef<HTMLDivElement>(null);
const heartIcon = likedByMe ? "/red_heart.svg" : "/gray_heart.svg";
const { mutate: reportMember } = useReportMemberMutation();

// 바깥 클릭 시 메뉴 닫기
useEffect(() => {
Expand All @@ -84,6 +89,19 @@ export default function BookstoryDetail({
return () => document.removeEventListener("mousedown", handleClickOutside);
}, []);

const handleReportSubmit = (type: string, content: string) => {
let mappedType: ReportType = "GENERAL";
if (type === "책 이야기") mappedType = "BOOK_STORY";
if (type === "책이야기(댓글)") mappedType = "COMMENT";
if (type === "책모임 내부") mappedType = "CLUB_MEETING";

reportMember({
reportedMemberNickname: authorNickname,
reportType: mappedType,
content,
});
};

return (
<div
className={`flex flex-col t:flex-row w-full px-[20px] items-start gap-4 t:gap-[28px] bg-Background ${className}`}
Expand Down Expand Up @@ -116,8 +134,8 @@ export default function BookstoryDetail({
type="button"
onClick={onSubscribeClick}
className={`flex px-4 py-1.5 justify-center items-center rounded-lg text-White text-[12px] font-medium shrink-0 transition-colors ${isFollowing
? "bg-Subbrown-4 text-primary-3"
: "bg-primary-2 text-White"
? "bg-Subbrown-4 text-primary-3"
: "bg-primary-2 text-White"
}`}
>
{subscribeText}
Expand Down Expand Up @@ -147,7 +165,7 @@ export default function BookstoryDetail({
<button
type="button"
onClick={() => {
console.log("신고하기");
setIsReportModalOpen(true);
setMenuOpen(false);
}}
className="flex w-full items-center gap-2 px-4 py-3 body_1_2 text-Gray-4 hover:text-Gray-7 cursor-pointer"
Expand Down Expand Up @@ -281,8 +299,8 @@ export default function BookstoryDetail({
type="button"
onClick={onSubscribeClick}
className={`flex px-[17px] py-[8px] justify-center items-center rounded-lg bg-primary-2 text-White body_2_1 shrink-0 whitespace-nowrap cursor-pointer transition-colors ${isFollowing
? "bg-Subbrown-4 text-primary-3"
: "bg-primary-2 text-White"
? "bg-Subbrown-4 text-primary-3"
: "bg-primary-2 text-White"
}`}
>
{subscribeText}
Expand All @@ -305,7 +323,7 @@ export default function BookstoryDetail({
<button
type="button"
onClick={() => {
console.log("신고하기");
setIsReportModalOpen(true);
setMenuOpen(false);
}}
className="flex w-full items-center gap-2 px-4 py-3 body_1_2 text-Gray-4 hover:text-Gray-7 cursor-pointer"
Expand Down Expand Up @@ -335,6 +353,13 @@ export default function BookstoryDetail({
</div>
</div>
</div>
{/* Report Modal */}
<ReportModal
isOpen={isReportModalOpen}
onClose={() => setIsReportModalOpen(false)}
onSubmit={handleReportSubmit}
defaultReportType="책 이야기"
/>
</div>
);
}
57 changes: 54 additions & 3 deletions src/components/base-ui/Comment/comment_section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ import {
} from "@/hooks/mutations/useStoryMutations";
import { toast } from "react-hot-toast";
import ConfirmModal from "@/components/common/ConfirmModal";
import ReportModal from "@/components/common/ReportModal";
import { useReportMemberMutation } from "@/hooks/mutations/useMemberMutations";
import { ReportType } from "@/types/member";

// 어떤 글의 댓글인지 구분
type CommentSectionProps = {
Expand All @@ -28,6 +31,7 @@ export default function CommentSection({
const createCommentMutation = useCreateCommentMutation(storyId);
const updateCommentMutation = useUpdateCommentMutation(storyId);
const deleteCommentMutation = useDeleteCommentMutation(storyId);
const { mutate: reportMember } = useReportMemberMutation();

// API 데이터를 UI용 Comment 형식으로 변환 및 계층 구조화
const mapApiToUiComments = (apiComments: CommentInfo[]): Comment[] => {
Expand Down Expand Up @@ -73,6 +77,9 @@ export default function CommentSection({
const [isConfirmOpen, setIsConfirmOpen] = useState(false);
const [commentToDelete, setCommentToDelete] = useState<number | null>(null);

const [isReportModalOpen, setIsReportModalOpen] = useState(false);
const [reportTargetNickname, setReportTargetNickname] = useState<string>("");

// 데이터가 변경되면 상태 업데이트
useEffect(() => {
setComments(mapApiToUiComments(initialComments));
Expand Down Expand Up @@ -144,9 +151,42 @@ export default function CommentSection({
};

const handleReportComment = (id: number) => {
// TODO: 댓글 신고 API 연동
console.log("댓글 신고:", id);
toast.success("신고가 접수되었습니다.");
// 찾기: 최상단 댓글과 대댓글 모두 탐색
let targetComment: Comment | undefined;

for (const c of comments) {
if (c.id === id) {
targetComment = c;
break;
}
if (c.replies) {
const found = c.replies.find(r => r.id === id);
if (found) {
targetComment = found;
break;
}
}
}

if (targetComment) {
setReportTargetNickname(targetComment.authorName);
setIsReportModalOpen(true);
}
};

const handleReportSubmit = (type: string, content: string) => {
let mappedType: ReportType = "GENERAL";
if (type === "책 이야기") mappedType = "BOOK_STORY";
if (type === "책이야기(댓글)") mappedType = "COMMENT";
if (type === "책모임 내부") mappedType = "CLUB_MEETING";

if (reportTargetNickname) {
reportMember({
reportedMemberNickname: reportTargetNickname,
reportType: mappedType,
content,
});
}
};

return (
Expand All @@ -168,6 +208,17 @@ export default function CommentSection({
setCommentToDelete(null);
}}
/>

{/* Report Modal */}
<ReportModal
isOpen={isReportModalOpen}
onClose={() => {
setIsReportModalOpen(false);
setReportTargetNickname("");
}}
onSubmit={handleReportSubmit}
defaultReportType="책이야기(댓글)"
/>
</>
);
}
Expand Down
Loading