From 7795037d6725586cb23ff7c4fc9f1ccbf5ce7f44 Mon Sep 17 00:00:00 2001 From: choiboa Date: Wed, 14 Jan 2026 02:57:26 +0900 Subject: [PATCH 1/4] =?UTF-8?q?=F0=9F=92=84=20Style=20:=20=ED=97=A4?= =?UTF-8?q?=EB=8D=94-cmdk=20=EC=8A=A4=ED=83=80=EC=9D=BC=EB=A7=81=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/(layout)/(shell)/_components/layout/SiteHeader.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/(layout)/(shell)/_components/layout/SiteHeader.tsx b/src/app/(layout)/(shell)/_components/layout/SiteHeader.tsx index fe124f2..7c1a3b4 100644 --- a/src/app/(layout)/(shell)/_components/layout/SiteHeader.tsx +++ b/src/app/(layout)/(shell)/_components/layout/SiteHeader.tsx @@ -38,14 +38,14 @@ export default function SiteHeader() { "min-w-46 cursor-pointer ", "hidden items-center gap-2 rounded-full", "bg-neutral-200/50 ", - "dark:border dark:border-foreground/15 dark:bg-background/40 pl-3 pr-2 py-1.5", + "dark:border dark:border-foreground/15 dark:bg-foreground/7 pl-3 pr-2 py-1", "text-xs text-foreground/70 md:inline-flex" )} >
검색 - + ⌘K
From 09974e76948847cefaa48c03e464f46f2fc36554 Mon Sep 17 00:00:00 2001 From: choiboa Date: Wed, 14 Jan 2026 17:33:58 +0900 Subject: [PATCH 2/4] =?UTF-8?q?=E2=9C=A8=20feat:=20=20=EC=A2=8B=EC=95=84?= =?UTF-8?q?=EC=9A=94=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../posts/[slug]/_components/PostActions.tsx | 10 +- .../_components/post-actions/LikeButton.tsx | 28 ++-- .../_components/post-actions/ShareButton.tsx | 2 +- src/app/(layout)/posts/[slug]/page.tsx | 2 +- src/hooks/usePostLike.ts | 127 ++++++++++++++++++ src/lib/supabase/viewerId.ts | 19 +++ 6 files changed, 172 insertions(+), 16 deletions(-) create mode 100644 src/hooks/usePostLike.ts create mode 100644 src/lib/supabase/viewerId.ts diff --git a/src/app/(layout)/posts/[slug]/_components/PostActions.tsx b/src/app/(layout)/posts/[slug]/_components/PostActions.tsx index a8d4485..100174d 100644 --- a/src/app/(layout)/posts/[slug]/_components/PostActions.tsx +++ b/src/app/(layout)/posts/[slug]/_components/PostActions.tsx @@ -5,9 +5,13 @@ interface PostActionsProps { variant?: "desktop" | "mobile"; title: string; thumbnail?: string; + post: { + slug: string + }; } export default function PostActions({ + post, variant = "desktop", title, thumbnail, @@ -15,7 +19,7 @@ export default function PostActions({ if (variant === "mobile") { return ( ); @@ -23,9 +27,9 @@ export default function PostActions({ return ( ); diff --git a/src/app/(layout)/posts/[slug]/_components/post-actions/LikeButton.tsx b/src/app/(layout)/posts/[slug]/_components/post-actions/LikeButton.tsx index 49d0bee..69694e5 100644 --- a/src/app/(layout)/posts/[slug]/_components/post-actions/LikeButton.tsx +++ b/src/app/(layout)/posts/[slug]/_components/post-actions/LikeButton.tsx @@ -1,34 +1,40 @@ "use client"; +import { usePostLike } from "@/hooks/usePostLike"; import clsx from "clsx"; import { Heart } from "lucide-react"; import { useState } from "react"; -export default function LikeButton() { - const [isLiked, setIsLiked] = useState(false); - const [likeCount] = useState(0); +interface PostLikeButtonProps { + postId: string; +} + +export default function LikeButton({ postId }: PostLikeButtonProps) { + const { count, liked, loading, toggleLike } = usePostLike(postId); + const [isPopping, setIsPopping] = useState(false); const handleClick = () => { - setIsLiked((v) => !v); - + toggleLike(); setIsPopping(true); window.setTimeout(() => setIsPopping(false), 180); }; return ( ); diff --git a/src/app/(layout)/posts/[slug]/_components/post-actions/ShareButton.tsx b/src/app/(layout)/posts/[slug]/_components/post-actions/ShareButton.tsx index 7751855..45e7076 100644 --- a/src/app/(layout)/posts/[slug]/_components/post-actions/ShareButton.tsx +++ b/src/app/(layout)/posts/[slug]/_components/post-actions/ShareButton.tsx @@ -38,7 +38,7 @@ export default function ShareButton({ title, thumbnail }: ShareButtonProps) {
- +
diff --git a/src/hooks/usePostLike.ts b/src/hooks/usePostLike.ts new file mode 100644 index 0000000..7fae34b --- /dev/null +++ b/src/hooks/usePostLike.ts @@ -0,0 +1,127 @@ +"use client"; + +import { useEffect, useRef, useState } from "react"; +import type { PostgrestError } from "@supabase/supabase-js"; +import { supabase } from "@/lib/supabase/client"; +import { getOrCreateViewerId } from "@/lib/supabase/viewerId"; + +type UsePostLikeResult = { + count: number; + liked: boolean; + loading: boolean; + toggleLike: () => Promise; +}; + +function isDuplicateKeyError(error: PostgrestError | null): boolean { + return !!error && error.code === "23505"; +} + +export function usePostLike(postId: string): UsePostLikeResult { + const [count, setCount] = useState(0); + const [liked, setLiked] = useState(false); + const [loading, setLoading] = useState(true); + + const viewerIdRef = useRef(null); + + useEffect(() => { + viewerIdRef.current = getOrCreateViewerId(); + }, []); + + useEffect(() => { + if (!postId) return; + + const fetchLikeState = async () => { + const viewerId = viewerIdRef.current ?? getOrCreateViewerId(); + if (!viewerId) { + setLoading(false); + return; + } + + const { + data, + count: total, + error, + } = await supabase + .from("post_likes") + .select("viewer_id", { count: "exact" }) + .eq("post_id", postId); + + if (error) { + console.error("좋아요 조회 실패:", error); + setLoading(false); + return; + } + + const rows = data ?? []; + + setCount(total ?? rows.length); + setLiked(rows.some((row) => row.viewer_id === viewerId)); + setLoading(false); + }; + + void fetchLikeState(); + }, [postId]); + + const refreshCount = async () => { + const { count: total, error } = await supabase + .from("post_likes") + .select("*", { count: "exact", head: true }) + .eq("post_id", postId); + + if (error) { + console.error("좋아요 카운트 재조회 실패:", error); + return; + } + + if (typeof total === "number") { + setCount(total); + } + }; + + const toggleLike = async () => { + const viewerId = viewerIdRef.current ?? getOrCreateViewerId(); + if (!viewerId) return; + + if (liked) { + const { error } = await supabase + .from("post_likes") + .delete() + .eq("post_id", postId) + .eq("viewer_id", viewerId); + + if (error) { + console.error("좋아요 취소 실패:", error); + return; + } + + setLiked(false); + setCount((prev) => Math.max(0, prev - 1)); + return; + } + + const { error } = await supabase.from("post_likes").insert({ + post_id: postId, + viewer_id: viewerId, + }); + + if (error) { + if (isDuplicateKeyError(error)) { + setLiked(true); + await refreshCount(); + } else { + console.error("좋아요 추가 실패:", error); + } + return; + } + + setLiked(true); + setCount((prev) => prev + 1); + }; + + return { + count, + liked, + loading, + toggleLike, + }; +} \ No newline at end of file diff --git a/src/lib/supabase/viewerId.ts b/src/lib/supabase/viewerId.ts new file mode 100644 index 0000000..15e5e0f --- /dev/null +++ b/src/lib/supabase/viewerId.ts @@ -0,0 +1,19 @@ +const VIEWER_ID_KEY = "b_log_viewer_id"; + +export function getOrCreateViewerId(): string | null { + if (typeof window === "undefined") return null; + + let id = window.localStorage.getItem(VIEWER_ID_KEY); + + if (!id) { + if (window.crypto?.randomUUID) { + id = window.crypto.randomUUID(); + } else { + id = `${Date.now()}-${Math.random().toString(16).slice(2)}`; + } + + window.localStorage.setItem(VIEWER_ID_KEY, id); + } + + return id; +} From 4d19b6c9ae4d61caa450904686fdd419aadbdc48 Mon Sep 17 00:00:00 2001 From: choiboa Date: Wed, 14 Jan 2026 17:40:12 +0900 Subject: [PATCH 3/4] =?UTF-8?q?=F0=9F=93=9D=20Chore=20:=20ci=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EB=B0=8F=20=EB=B6=88=ED=95=84=EC=9A=94=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/labeler.yml | 47 ------------------------------- .github/labels.yml | 45 ----------------------------- .github/workflows/ci.yml | 1 - .github/workflows/labeler.yml | 17 ----------- .github/workflows/labels-sync.yml | 22 --------------- 5 files changed, 132 deletions(-) delete mode 100644 .github/labeler.yml delete mode 100644 .github/labels.yml delete mode 100644 .github/workflows/labeler.yml delete mode 100644 .github/workflows/labels-sync.yml diff --git a/.github/labeler.yml b/.github/labeler.yml deleted file mode 100644 index 9302245..0000000 --- a/.github/labeler.yml +++ /dev/null @@ -1,47 +0,0 @@ -home: - - changed-files: - - any-glob-to-any-file: "app/(home)/**" - -post: - - changed-files: - - any-glob-to-any-file: "app/post/**" - -category: - - changed-files: - - any-glob-to-any-file: "app/(category)/**" - -dock: - - changed-files: - - any-glob-to-any-file: "components/dock/**" - -pagination: - - changed-files: - - any-glob-to-any-file: "components/pagination/**" - -widget: - - changed-files: - - any-glob-to-any-file: "components/widgets/**" - -series: - - changed-files: - - any-glob-to-any-file: "components/series/**" - -comments: - - changed-files: - - any-glob-to-any-file: "components/comments/**" - -storybook: - - changed-files: - - any-glob-to-any-file: "stories/**" - -infra: - - changed-files: - - any-glob-to-any-file: ".github/**" - -test: - - changed-files: - - any-glob-to-any-file: "tests/**" - -util: - - changed-files: - - any-glob-to-any-file: "lib/**" diff --git a/.github/labels.yml b/.github/labels.yml deleted file mode 100644 index c15c138..0000000 --- a/.github/labels.yml +++ /dev/null @@ -1,45 +0,0 @@ -- name: feat - color: "1d76db" - description: New feature -- name: chore - color: "cccccc" - description: Chore / infra -- name: test - color: "0e8a16" - description: Testing -- name: a11y - color: "5319e7" - description: Accessibility -- name: home - color: "0052cc" - description: Home page -- name: post - color: "fbca04" - description: Post detail -- name: category - color: "d93f0b" - description: Category page -- name: dock - color: "5319e7" - description: DockMenu -- name: comments - color: "0366d6" - description: Giscus / Comments -- name: widget - color: "0b7285" - description: Widgets / metrics -- name: series - color: "a2eeef" - description: Series -- name: pagination - color: "7057ff" - description: Pagination -- name: util - color: "6f42c1" - description: Utils / helpers -- name: infra - color: "24292e" - description: Infra / CI -- name: storybook - color: "ededed" - description: Storybook diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 92f27f7..ef5564c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,7 +24,6 @@ jobs: - run: pnpm typecheck - run: pnpm lint - - run: pnpm test:ci - run: pnpm build diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml deleted file mode 100644 index 37ae8e6..0000000 --- a/.github/workflows/labeler.yml +++ /dev/null @@ -1,17 +0,0 @@ -name: PR Labeler -on: - pull_request_target: - types: [opened, synchronize, reopened] - -permissions: - contents: read - pull-requests: write - -jobs: - label: - runs-on: ubuntu-latest - steps: - - uses: actions/labeler@v5 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - configuration-path: .github/labeler.yml diff --git a/.github/workflows/labels-sync.yml b/.github/workflows/labels-sync.yml deleted file mode 100644 index 907e1c6..0000000 --- a/.github/workflows/labels-sync.yml +++ /dev/null @@ -1,22 +0,0 @@ -name: Sync labels -on: - workflow_dispatch: - push: - paths: - - .github/labels.yml - -permissions: - issues: write - contents: read - -jobs: - labels: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - sparse-checkout: .github/labels.yml - - uses: EndBug/label-sync@v2 - with: - config-file: .github/labels.yml - delete-other-labels: false From b8259f0ae687c20f798051827c243b116c2e2086 Mon Sep 17 00:00:00 2001 From: choiboa Date: Wed, 14 Jan 2026 17:43:06 +0900 Subject: [PATCH 4/4] =?UTF-8?q?=F0=9F=9A=A8=20Fix=20:=20=EB=B9=84=EB=8F=99?= =?UTF-8?q?=EA=B8=B0=20=EB=88=84=EB=9D=BD=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../posts/[slug]/_components/post-actions/LikeButton.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/(layout)/posts/[slug]/_components/post-actions/LikeButton.tsx b/src/app/(layout)/posts/[slug]/_components/post-actions/LikeButton.tsx index 69694e5..3b17d85 100644 --- a/src/app/(layout)/posts/[slug]/_components/post-actions/LikeButton.tsx +++ b/src/app/(layout)/posts/[slug]/_components/post-actions/LikeButton.tsx @@ -14,8 +14,8 @@ export default function LikeButton({ postId }: PostLikeButtonProps) { const [isPopping, setIsPopping] = useState(false); - const handleClick = () => { - toggleLike(); + const handleClick = async () => { + await toggleLike(); setIsPopping(true); window.setTimeout(() => setIsPopping(false), 180); };