From 3b7e09cc682e5249cf702a5668842245dfc5f46c Mon Sep 17 00:00:00 2001 From: FF Date: Mon, 15 Sep 2025 09:33:50 +0900 Subject: [PATCH 1/3] =?UTF-8?q?refactor:=20community=20=ED=8E=B8=EC=A7=91?= =?UTF-8?q?=20=EA=B6=8C=ED=95=9C=20ui=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/src/api/posts.ts | 4 ++-- .../CommunityDetail/_components/PostHeader.tsx | 12 ++++++++---- .../src/pages/Community/CommunityDetail/index.tsx | 11 ++++++++++- 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/apps/web/src/api/posts.ts b/apps/web/src/api/posts.ts index de69889..dabdf7b 100644 --- a/apps/web/src/api/posts.ts +++ b/apps/web/src/api/posts.ts @@ -136,7 +136,7 @@ function mapListItemToEntity(res: PostResListItem | PostResListItemAlt): Post { /** --- 서버 → 클라이언트 엔티티 매퍼 --- */ function mapPostResToEntity(res: PostRes): Post { - const author = res.authorInfo.nickname; + const author = res.authorInfo?.nickname ?? ''; const content = 'content' in res ? res.content : undefined; return { @@ -275,7 +275,7 @@ export function getReadablePostError(e: unknown): string { return '요청 처리에 실패했습니다. 잠시 후 다시 시도해주세요.'; } } - return '알 수 없는 오류가 발생했습니다.'; + return `알 수 없는 오류가 발생했습니다. ${e}`; } /* ------------------------------------------- diff --git a/apps/web/src/pages/Community/CommunityDetail/_components/PostHeader.tsx b/apps/web/src/pages/Community/CommunityDetail/_components/PostHeader.tsx index d8331eb..7624d29 100644 --- a/apps/web/src/pages/Community/CommunityDetail/_components/PostHeader.tsx +++ b/apps/web/src/pages/Community/CommunityDetail/_components/PostHeader.tsx @@ -12,10 +12,12 @@ export default function PostHeader({ post, onEdit, onDelete, + canEdit = false, }: { post: Post; onEdit: () => void; onDelete: () => void; + canEdit?: boolean; }) { return ( @@ -23,10 +25,12 @@ export default function PostHeader({
{CATEGORY_LABEL_MAP[post.category]}
- - 수정 - 삭제 - + {canEdit && ( + + 수정 + 삭제 + + )} {post.title} {post.authorInfo?.nickname} diff --git a/apps/web/src/pages/Community/CommunityDetail/index.tsx b/apps/web/src/pages/Community/CommunityDetail/index.tsx index 86a2fab..66ce641 100644 --- a/apps/web/src/pages/Community/CommunityDetail/index.tsx +++ b/apps/web/src/pages/Community/CommunityDetail/index.tsx @@ -19,6 +19,7 @@ import { deleteComment, } from '@/api/comments'; import { useModal } from '@/components/common/modal/modalContext'; +import { useNativeBridgeStore } from '@/stores/nativeBridgeStore'; export default function CommunityDetail() { const nav = useNavigate(); @@ -41,11 +42,18 @@ export default function CommunityDetail() { const [deleting, setDeleting] = useState(false); const [submittingComment, setSubmittingComment] = useState(false); - // 개별 댓글 작업 상태 const [editingId, setEditingId] = useState(null); const [deletingCommentId, setDeletingCommentId] = useState( null, ); + const myIdRaw = useNativeBridgeStore((s) => s.init?.user?.id ?? null); + + const toStr = (v: unknown) => (v == null ? null : String(v)); + + const myId = toStr(myIdRaw); + const authorId = toStr(post?.authorInfo?.id) ?? null; + + const canEdit = myId !== null && authorId !== null && myId === authorId; useEffect(() => { let mounted = true; @@ -264,6 +272,7 @@ export default function CommunityDetail() { post={post} onEdit={() => nav(`/community/edit/${post.id}`)} onDelete={handleDeletePost} + canEdit={canEdit} /> From 5520daa08a76c0e5ecec4532d301a669ca0b9729 Mon Sep 17 00:00:00 2001 From: FF Date: Mon, 15 Sep 2025 11:54:20 +0900 Subject: [PATCH 2/3] =?UTF-8?q?refactor:=20=EB=8C=93=EA=B8=80=20=ED=8E=B8?= =?UTF-8?q?=EC=A7=91=20=EA=B6=8C=ED=95=9C=20=EC=A0=81=EC=9A=A9=20=EB=B0=8F?= =?UTF-8?q?=20ui=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/src/api/comments.ts | 37 +++----- apps/web/src/api/mypage.ts | 2 +- .../_components/CommentList.tsx | 92 +++++++++++-------- .../pages/Community/CommunityDetail/index.tsx | 71 +++++++++----- 4 files changed, 117 insertions(+), 85 deletions(-) diff --git a/apps/web/src/api/comments.ts b/apps/web/src/api/comments.ts index 535a3cf..d277bbe 100644 --- a/apps/web/src/api/comments.ts +++ b/apps/web/src/api/comments.ts @@ -1,18 +1,21 @@ import api from '@/lib/api'; import type { Comment } from '@/types/community'; -// ----- 서버 응답 타입 ----- -type AuthorObj = { id: number; nickname: string; avatarUrl?: string | null }; - +// ----- 서버 응답 타입 (커서 기반) ----- type CommentListItemRes = { id: number; postId: number; - author: AuthorObj; + authorId: number; content: string; createdAt: string; updatedAt: string; }; +type CommentListRes = { + items: CommentListItemRes[]; + nextCursor: string | null; +}; + type CommentCreateRes = { id: number; postId: number; @@ -22,13 +25,6 @@ type CommentCreateRes = { updatedAt: string; }; -type CommentListRes = { - items: CommentListItemRes[]; - page: number; - limit: number; - total: number; -}; - type CommentUpdateRes = { ok: boolean; comment: { id: number; content: string; updatedAt: string }; @@ -37,12 +33,11 @@ type CommentUpdateRes = { type OkRes = { ok: boolean }; // ----- 매퍼 (서버 → 클라 엔티티) ----- -// 프로젝트의 Comment 타입이 { id, postId, author, content } 기준이라면, -// createdAt/updatedAt은 필요 시 확장하세요. +// 주의: UI에서 c.author를 "작성자 ID"로 쓰는 흐름에 맞춰 문자열 ID로 매핑 const mapListItemToEntity = (r: CommentListItemRes): Comment => ({ id: String(r.id), postId: String(r.postId), - author: r.author?.nickname, + author: String(r.authorId), // ← authorId를 문자열로 content: r.content, updatedAt: r.updatedAt, }); @@ -50,26 +45,25 @@ const mapListItemToEntity = (r: CommentListItemRes): Comment => ({ const mapCreateItemToEntity = (r: CommentCreateRes): Comment => ({ id: String(r.id), postId: String(r.postId), - author: String(r.authorId), + author: String(r.authorId), // ← 동일하게 문자열 ID content: r.content, updatedAt: r.updatedAt, }); // ----- API ----- +// 커서 기반 목록 export async function getPostComments( postId: number | string, - page = 1, limit = 20, -): Promise<{ items: Comment[]; page: number; limit: number; total: number }> { + cursor?: string | null, +): Promise<{ items: Comment[]; nextCursor: string | null }> { const { data } = await api.get( `/community/posts/${postId}/comments`, - { params: { page, limit } }, + { params: { limit, cursor: cursor ?? undefined } }, ); return { items: data.items.map(mapListItemToEntity), - page: data.page, - limit: data.limit, - total: data.total, + nextCursor: data.nextCursor ?? null, }; } @@ -92,7 +86,6 @@ export async function updateComment( `/community/comments/${id}`, { content }, ); - console.log(data); return { id: String(data.comment.id), content: data.comment.content, diff --git a/apps/web/src/api/mypage.ts b/apps/web/src/api/mypage.ts index a4692f5..edca524 100644 --- a/apps/web/src/api/mypage.ts +++ b/apps/web/src/api/mypage.ts @@ -253,5 +253,5 @@ export function getReadableUserError(e: unknown): string { if (status === 404) return '사용자 정보를 찾을 수 없습니다.'; return '요청 처리에 실패했습니다. 잠시 후 다시 시도해 주세요.'; } - return '알 수 없는 오류가 발생했습니다.'; + return `알 수 없는 오류가 발생했습니다. ${e}`; } diff --git a/apps/web/src/pages/Community/CommunityDetail/_components/CommentList.tsx b/apps/web/src/pages/Community/CommunityDetail/_components/CommentList.tsx index 61878cb..62deb95 100644 --- a/apps/web/src/pages/Community/CommunityDetail/_components/CommentList.tsx +++ b/apps/web/src/pages/Community/CommunityDetail/_components/CommentList.tsx @@ -21,31 +21,43 @@ export default function CommentList({ {comments.map((c) => { const manageable = canManage ? canManage(c) : true; const busy = workingId === c.id; + return ( - - {c.author} - - {c.content} - {manageable && (onEdit || onDelete) && ( - - + + + {c.author} + + + {manageable && (onEdit || onDelete) && ( + {onEdit && ( - onEdit(c.id)}> - 수정 - + onEdit(c.id)} + $variant="edit" + > + + )} {onDelete && ( - onDelete(c.id)} + $variant="delete" > - 삭제 - + + )} - - - )} + + )} + + + {c.content} ); })} @@ -61,11 +73,18 @@ const Wrap = styled.div` const Row = styled.div` display: grid; - gap: 6px; + gap: 8px; border-bottom: 1px solid ${({ theme }) => theme.colors.surface}; padding-bottom: 12px; `; +const HeaderRow = styled.div` + display: flex; + align-items: center; + justify-content: space-between; /* 좌: 작성자 / 우: 버튼들 */ + gap: 12px; +`; + const Meta = styled.div` font-size: 12px; color: ${({ theme }) => theme.colors.primary}; @@ -80,31 +99,28 @@ const Body = styled.div` white-space: pre-wrap; `; -/* 버튼 줄을 오른쪽 끝으로 */ -const BtnRow = styled.div` - display: flex; - justify-content: flex-end; -`; - -const Btns = styled.div` +const Actions = styled.div` display: flex; - gap: 8px; + align-items: end; + gap: 6px; `; -const baseBtn = ` - padding: 6px 10px; - border-radius: 999px; - border: 1px solid rgba(0, 0, 0, 0.08); +const IconButton = styled.button<{ $variant: 'edit' | 'delete' }>` + width: 40px; + height: 40px; + display: inline-flex; + align-items: center; + justify-content: center; + border: 0; background: transparent; - font-weight: 600; -`; + font-size: 20px; -const EditButton = styled.button` - ${baseBtn} - color: ${({ theme }) => theme.colors.subtext}; -`; + /* 색상은 기존과 동일한 토큰 사용 */ + color: ${({ theme, $variant }) => + $variant === 'edit' ? theme.colors.primary : theme.colors.danger}; -const DeleteButton = styled.button` - ${baseBtn} - color: ${({ theme }) => theme.colors.danger}; + &[disabled] { + opacity: 0.6; + pointer-events: none; + } `; diff --git a/apps/web/src/pages/Community/CommunityDetail/index.tsx b/apps/web/src/pages/Community/CommunityDetail/index.tsx index 66ce641..dfa4e38 100644 --- a/apps/web/src/pages/Community/CommunityDetail/index.tsx +++ b/apps/web/src/pages/Community/CommunityDetail/index.tsx @@ -13,7 +13,7 @@ import { getReadablePostError, } from '@/api/posts'; import { - getPostComments, + getPostComments, // ← 커서 기반 시그니처: (postId, limit?, cursor?) createPostComment, updateComment, deleteComment, @@ -33,9 +33,8 @@ export default function CommunityDetail() { const [error, setError] = useState(null); const [comments, setComments] = useState([]); - const [cPage, setCPage] = useState(1); const [cLimit] = useState(20); - const [cTotal, setCTotal] = useState(0); + const [cCursor, setCCursor] = useState(null); // ✅ 커서 const [cLoading, setCLoading] = useState(false); const [liking, setLiking] = useState(false); @@ -46,15 +45,17 @@ export default function CommunityDetail() { const [deletingCommentId, setDeletingCommentId] = useState( null, ); - const myIdRaw = useNativeBridgeStore((s) => s.init?.user?.id ?? null); + // 현재 사용자 ID (RN → Web 브리지) + const myIdRaw = useNativeBridgeStore((s) => s.init?.user?.id ?? null); const toStr = (v: unknown) => (v == null ? null : String(v)); - const myId = toStr(myIdRaw); - const authorId = toStr(post?.authorInfo?.id) ?? null; + // 게시글 수정/삭제 권한 (작성자 본인만) + const authorId = toStr(post?.authorInfo?.id) ?? null; const canEdit = myId !== null && authorId !== null && myId === authorId; + // ---- 게시글 로드 ---- useEffect(() => { let mounted = true; (async () => { @@ -80,17 +81,17 @@ export default function CommunityDetail() { }; }, [id]); + // ---- 댓글 최초 로드 (커서 기반) ---- useEffect(() => { let mounted = true; (async () => { if (!id) return; try { setCLoading(true); - const res = await getPostComments(id, 1, cLimit); + const { items, nextCursor } = await getPostComments(id, cLimit); if (mounted) { - setComments(res.items); - setCPage(res.page); - setCTotal(res.total); + setComments(items); + setCCursor(nextCursor ?? null); } } catch (e) { console.error('[comments] load error:', getReadablePostError(e)); @@ -103,6 +104,7 @@ export default function CommunityDetail() { }; }, [id, cLimit]); + // ---- 좋아요 ---- const handleToggleLike = async () => { if (!post || liking) return; setLiking(true); @@ -125,16 +127,19 @@ export default function CommunityDetail() { } }; + // ---- 댓글 작성 ---- const handleSubmitComment = async () => { const v = inputRef.current?.value?.trim(); if (!v || !id) return; try { setSubmittingComment(true); const created = await createPostComment(id, v); - setComments((prev) => [...prev, created]); // ASC 정렬 가정 → 뒤에 붙임 + setComments((prev) => [...prev, created]); // ASC 가정 → 뒤에 추가 inputRef.current!.value = ''; - setPost((p) => (p ? { ...p, commentsCount: p.commentsCount + 1 } : p)); - setCTotal((t) => t + 1); + setPost((p) => + p ? { ...p, commentsCount: (p.commentsCount ?? 0) + 1 } : p, + ); + // 커서는 서버가 새로 계산하므로 유지(append만 했으니 nextCursor 변화 없음) } catch (e) { alert(getReadablePostError(e)); } finally { @@ -142,6 +147,7 @@ export default function CommunityDetail() { } }; + // ---- 댓글 수정 ---- const handleEditComment = async (commentId: string) => { const target = comments.find((c) => c.id === commentId); if (!target) return; @@ -153,7 +159,7 @@ export default function CommunityDetail() { confirmText: '수정', cancelText: '취소', }); - if (next == null) return; // 취소 + if (next == null) return; const content = next.trim(); if (!content) { await alert({ @@ -168,7 +174,9 @@ export default function CommunityDetail() { const res = await updateComment(commentId, content); setComments((prev) => prev.map((c) => - c.id === commentId ? { ...c, content: res.content } : c, + c.id === commentId + ? { ...c, content: res.content, updatedAt: res.updatedAt } + : c, ), ); } catch (e) { @@ -178,6 +186,7 @@ export default function CommunityDetail() { } }; + // ---- 게시글 삭제 ---- const handleDeletePost = useCallback(async () => { if (!post || deleting) return; @@ -204,6 +213,7 @@ export default function CommunityDetail() { } }, [post, deleting, nav, confirm]); + // ---- 댓글 삭제 ---- const handleDeleteComment = async (commentId: string) => { const ok = await confirm({ title: '댓글을 삭제하시겠습니까?', @@ -219,9 +229,10 @@ export default function CommunityDetail() { if (done) { setComments((prev) => prev.filter((c) => c.id !== commentId)); setPost((p) => - p ? { ...p, commentsCount: Math.max(0, p.commentsCount - 1) } : p, + p + ? { ...p, commentsCount: Math.max(0, (p.commentsCount ?? 1) - 1) } + : p, ); - setCTotal((t) => Math.max(0, t - 1)); } else { await alert({ title: '실패', description: '삭제에 실패했습니다.' }); } @@ -232,15 +243,19 @@ export default function CommunityDetail() { } }; + // ---- 더 보기(커서 페이징) ---- const handleLoadMore = async () => { - if (!post) return; - const next = cPage + 1; - const res = await getPostComments(post.id, next, cLimit); - setComments((prev) => [...prev, ...res.items]); - setCPage(res.page); - setCTotal(res.total); + if (!post || !cCursor) return; // 더 불러올 것 없으면 종료 + const { items, nextCursor } = await getPostComments( + post.id, + cLimit, + cCursor, + ); + setComments((prev) => [...prev, ...items]); + setCCursor(nextCursor ?? null); }; + // ---- early returns ---- if (loading) { return ( @@ -264,7 +279,13 @@ export default function CommunityDetail() { } const imageUrl = post.imageUrl; - const hasMore = comments.length < cTotal; + const hasMore = cCursor !== null; // ✅ 커서가 있으면 더 불러올 수 있음 + + // 댓글 개별 권한: 본인만 관리 가능 + const canEditComment = (c: Comment) => { + const cid = toStr(c.author) ?? null; // Comment.author는 문자열 ID로 매핑됨 + return myId !== null && cid !== null && myId === cid; + }; return ( @@ -296,6 +317,7 @@ export default function CommunityDetail() { onEdit={handleEditComment} onDelete={handleDeleteComment} workingId={editingId ?? deletingCommentId ?? null} + canManage={canEditComment} // ✅ 댓글도 권한 체크 적용 /> {cLoading && 댓글 불러오는 중…} @@ -319,6 +341,7 @@ export default function CommunityDetail() { ); } +/* ---- styles 동일 ---- */ const Content = styled.section` padding: 16px; color: ${({ theme }) => theme.colors.text}; From 8e1fc8377ad5193aaf6faba548c26122b8c2fc00 Mon Sep 17 00:00:00 2001 From: FF Date: Mon, 15 Sep 2025 12:22:08 +0900 Subject: [PATCH 3/3] =?UTF-8?q?refactor:=20=EC=98=A4=EB=A5=98=20=EB=A9=94?= =?UTF-8?q?=EC=8B=9C=EC=A7=80=20=EC=B6=9C=EB=A0=A5=20=EA=B0=84=EB=8B=A8?= =?UTF-8?q?=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/src/api/mypage.ts | 2 +- apps/web/src/api/posts.ts | 45 ++++--------------- .../pages/Community/CommunityDetail/index.tsx | 12 +++-- 3 files changed, 15 insertions(+), 44 deletions(-) diff --git a/apps/web/src/api/mypage.ts b/apps/web/src/api/mypage.ts index edca524..6ad4676 100644 --- a/apps/web/src/api/mypage.ts +++ b/apps/web/src/api/mypage.ts @@ -253,5 +253,5 @@ export function getReadableUserError(e: unknown): string { if (status === 404) return '사용자 정보를 찾을 수 없습니다.'; return '요청 처리에 실패했습니다. 잠시 후 다시 시도해 주세요.'; } - return `알 수 없는 오류가 발생했습니다. ${e}`; + return `알 수 없는 오류가 발생했습니다.`; } diff --git a/apps/web/src/api/posts.ts b/apps/web/src/api/posts.ts index dabdf7b..ac25eae 100644 --- a/apps/web/src/api/posts.ts +++ b/apps/web/src/api/posts.ts @@ -2,15 +2,13 @@ import axios from 'axios'; import api from '@/lib/api'; import type { Category, Post } from '@/types/community'; -/** 서버 enum (클라 Category에서 'ALL' 제외) */ -export type ServerPostType = Exclude; // 'FREE' | 'PROOF' | 'SHARE' | 'MATE' +export type ServerPostType = Exclude; -/** --- 요청/응답 타입: 서버 스펙 기준 --- */ export type CreatePostReq = { type: ServerPostType; title: string; content: string; - imageUrl: string; // 단일 이미지 URL + imageUrl: string; routeId?: number; }; @@ -18,7 +16,7 @@ export type UpdatePostReq = Partial<{ type: ServerPostType; title: string; content: string; - imageUrl: string | null; // 단일 이미지 URL 또는 null + imageUrl: string | null; routeId: number | null; }>; @@ -28,8 +26,8 @@ type PostResBase = { id: number; type: ServerPostType; title: string; - imageUrls?: string[]; // 서버가 과거 호환 위해 줄 수 있음 - imageUrl?: string; // 표준: 단일 + imageUrls?: string[]; + imageUrl?: string; routeId?: number | null; likeCount: number; commentCount: number; @@ -40,12 +38,12 @@ type PostResBase = { }; type PostResCreate = PostResBase & { - authorId: number; // 생성 응답 + authorId: number; content: string; }; type PostResDetail = PostResBase & { - author: AuthorObj; // 상세 응답 + author: AuthorObj; content: string; }; @@ -62,7 +60,7 @@ type UpdatePostRes = { type: ServerPostType; title: string; content: string; - imageUrl: string; // 서버 표준 + imageUrl: string; routeId?: number; updatedAt: string; }; @@ -77,18 +75,15 @@ export type GetPostsQuery = { limit?: number; }; -/** 목록 아이템 (author 객체 버전) */ type PostResListItem = PostResBase & { content?: string; // 서버가 줄 수도 있어 optional }; -/** 목록 아이템 (authorId만 있는 버전) */ type PostResListItemAlt = PostResBase & { authorId: number; content?: string; }; -/** 커서 목록 응답 */ type PostListCursorRes = { items: Array; nextCursor: number | string | null; @@ -103,7 +98,6 @@ export type GetPostsCursorQuery = { limit?: number; }; -/** 서버 응답에서 단일 imageUrl 계산 (imageUrl 우선, 없으면 imageUrls[0]) */ function pickImageUrl(res: { imageUrl?: string; imageUrls?: string[]; @@ -115,7 +109,6 @@ function pickImageUrl(res: { return ''; } -/** 목록 아이템 매퍼 (author/authorId 모두 대응) */ function mapListItemToEntity(res: PostResListItem | PostResListItemAlt): Post { const author = res.authorInfo?.nickname; @@ -134,7 +127,6 @@ function mapListItemToEntity(res: PostResListItem | PostResListItemAlt): Post { }; } -/** --- 서버 → 클라이언트 엔티티 매퍼 --- */ function mapPostResToEntity(res: PostRes): Post { const author = res.authorInfo?.nickname ?? ''; const content = 'content' in res ? res.content : undefined; @@ -154,13 +146,11 @@ function mapPostResToEntity(res: PostRes): Post { }; } -/** --- 게시글 생성 --- */ export async function createPost(body: CreatePostReq): Promise { const { data } = await api.post('/community/posts', body); return mapPostResToEntity(data); } -/** --- 게시글 목록 --- */ export async function getPosts(query: GetPostsQuery = {}): Promise { const q: string[] = []; @@ -184,7 +174,6 @@ export async function getPosts(query: GetPostsQuery = {}): Promise { return (data.items ?? []).map(mapListItemToEntity); } -/** 커서 기반 목록 */ export async function getPostsCursor( query: GetPostsCursorQuery = {}, ): Promise<{ items: Post[]; nextCursor: number | string | null }> { @@ -214,13 +203,11 @@ export async function getPostsCursor( }; } -/** --- 게시글 상세 --- */ export async function getPost(id: number | string): Promise { const { data } = await api.get(`/community/posts/${id}`); return mapPostResToEntity(data); } -/** 게시글 수정 */ export async function updatePostById( id: number | string, body: UpdatePostReq, @@ -232,19 +219,16 @@ export async function updatePostById( return data; } -/** 좋아요 토글 */ export async function togglePostLike(id: number | string): Promise { const { data } = await api.post(`/community/posts/${id}/like`); return data; } -/** 게시글 삭제 (soft) */ export async function deletePostById(id: number | string): Promise { const { data } = await api.delete(`/community/posts/${id}`); return !!data?.ok; } -/** --- (선택) 에러 코드 헬퍼: 필요시 사용 --- */ export type PostErrorCode = | 'AUTH.UNAUTHORIZED' | 'COMMUNITY.POST_TYPE_INVALID' @@ -275,23 +259,16 @@ export function getReadablePostError(e: unknown): string { return '요청 처리에 실패했습니다. 잠시 후 다시 시도해주세요.'; } } - return `알 수 없는 오류가 발생했습니다. ${e}`; + return `알 수 없는 오류가 발생했습니다.`; } -/* ------------------------------------------- - * 🔽 PROOF 이미지 presign & 업로드 유틸 - * (아바타 presign 로직과 동일한 패턴) - * ----------------------------------------- */ - export type PresignReq = { - /** 예: 'community-proof' */ type: string; contentType: string; size: number; }; export type PresignRes = { - /** PUT 대상 URL (S3 presigned URL 등) */ url: string; }; @@ -304,19 +281,16 @@ export function objectUrlFromPresign(url: string): string { return url.split('?')[0]; } -/** 파일 업로드(put) 후 public URL(단일) 반환 */ export async function uploadProofWithFile(file: File): Promise { const contentType = file.type || 'image/jpeg'; const size = file.size; - // 1) presign const presign = await getProofPresign({ type: 'verify', contentType, size, }); - // 2) PUT 업로드 const putRes = await fetch(presign.url, { method: 'PUT', headers: { 'Content-Type': contentType }, @@ -326,6 +300,5 @@ export async function uploadProofWithFile(file: File): Promise { throw new Error(`이미지 업로드 실패: ${putRes.status}`); } - // 3) 업로드된 public URL 반환 return objectUrlFromPresign(presign.url); } diff --git a/apps/web/src/pages/Community/CommunityDetail/index.tsx b/apps/web/src/pages/Community/CommunityDetail/index.tsx index dfa4e38..cd254e7 100644 --- a/apps/web/src/pages/Community/CommunityDetail/index.tsx +++ b/apps/web/src/pages/Community/CommunityDetail/index.tsx @@ -48,7 +48,7 @@ export default function CommunityDetail() { // 현재 사용자 ID (RN → Web 브리지) const myIdRaw = useNativeBridgeStore((s) => s.init?.user?.id ?? null); - const toStr = (v: unknown) => (v == null ? null : String(v)); + const toStr = useCallback((v: unknown) => (v == null ? null : String(v)), []); const myId = toStr(myIdRaw); // 게시글 수정/삭제 권한 (작성자 본인만) @@ -230,7 +230,7 @@ export default function CommunityDetail() { setComments((prev) => prev.filter((c) => c.id !== commentId)); setPost((p) => p - ? { ...p, commentsCount: Math.max(0, (p.commentsCount ?? 1) - 1) } + ? { ...p, commentsCount: Math.max(0, (p.commentsCount ?? 0) - 1) } : p, ); } else { @@ -279,11 +279,10 @@ export default function CommunityDetail() { } const imageUrl = post.imageUrl; - const hasMore = cCursor !== null; // ✅ 커서가 있으면 더 불러올 수 있음 + const hasMore = cCursor !== null; - // 댓글 개별 권한: 본인만 관리 가능 const canEditComment = (c: Comment) => { - const cid = toStr(c.author) ?? null; // Comment.author는 문자열 ID로 매핑됨 + const cid = toStr(c.author) ?? null; return myId !== null && cid !== null && myId === cid; }; @@ -317,7 +316,7 @@ export default function CommunityDetail() { onEdit={handleEditComment} onDelete={handleDeleteComment} workingId={editingId ?? deletingCommentId ?? null} - canManage={canEditComment} // ✅ 댓글도 권한 체크 적용 + canManage={canEditComment} /> {cLoading && 댓글 불러오는 중…} @@ -341,7 +340,6 @@ export default function CommunityDetail() { ); } -/* ---- styles 동일 ---- */ const Content = styled.section` padding: 16px; color: ${({ theme }) => theme.colors.text};