Skip to content

Commit 1938cd6

Browse files
committed
✨ Remove/edit comments and various updates
1 parent af5edea commit 1938cd6

15 files changed

Lines changed: 598 additions & 126 deletions

File tree

README.md

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,22 @@
11
# 쉬운 전문용어
22

33
## TODO
4+
45
- [x] 폰트 변경
56
- [ ] 용어 분류 필터를 고려한 단어 수 / 서버 렌더링
6-
- [ ] 댓글 고치기 및 지우기
7+
- [x] 댓글 고치기 및 지우기
78
- [ ] 취지 / 제작기
89
- [ ] 관리자 모드
910
- [ ] 프로필
10-
- [ ] 댓글 고치기 및 지우기
11-
- [ ] 링크 공유
11+
- [x] 링크 공유
1212
- [ ] 관리자 CSV 내보내기
1313
- [ ] 댓글 명령 (/command), 단어 연결 (#jargon)
1414
- [ ] 용어 대 용어 (arena)
15+
16+
## 2025-08-13
17+
18+
- [ ] Removed comments are still viewable (RLS)
19+
- [ ] Don't include removed comments count in the main page
20+
- [ ] Show search bar in the jargon detail page
21+
- [ ] Show more categories in the filter
22+
- [ ] Center the category filter location in mobile

app/actions/createComment.ts

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,21 +28,31 @@ export async function createComment(
2828
if (!content) return { ok: false, error: "내용을 입력해주세요" };
2929

3030
const supabase = await createClient();
31-
const {
32-
data: { user },
33-
error: userErr,
34-
} = await supabase.auth.getUser();
35-
if (userErr || !user) return { ok: false, error: "로그인이 필요해요" };
36-
3731
const { error } = await MUTATIONS.createComment(
3832
supabase,
3933
jargonId,
4034
parentId,
4135
content,
4236
);
43-
if (error) return { ok: false, error: "댓글 다는 중 문제가 생겼어요" };
37+
38+
if (error) {
39+
console.error(error);
40+
41+
switch (error.code) {
42+
case "28000":
43+
return { ok: false, error: "로그인이 필요해요" };
44+
case "22023":
45+
return { ok: false, error: "내용을 입력해주세요" };
46+
case "PARENT_REMOVED":
47+
return { ok: false, error: "지워진 댓글에는 답글을 달 수 없어요" };
48+
case "NO_PARENT":
49+
return { ok: false, error: "부모 댓글을 찾을 수 없어요" };
50+
default:
51+
return { ok: false, error: "댓글을 다는 중 문제가 생겼어요" };
52+
}
53+
}
4454

4555
console.info("createComment", jargonId, parentId, content);
4656

47-
return { ok: true as const, error: null };
57+
return { ok: true, error: null };
4858
}

app/actions/removeComment.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
"use server";
2+
3+
import { MUTATIONS } from "@/lib/supabase/repository";
4+
import { createClient } from "@/lib/supabase/server";
5+
6+
export type RemoveCommentState =
7+
| {
8+
ok: false;
9+
error: string;
10+
}
11+
| {
12+
ok: true;
13+
error: null;
14+
};
15+
16+
export type RemoveCommentAction = (
17+
prevState: RemoveCommentState,
18+
formData: FormData,
19+
) => Promise<RemoveCommentState>;
20+
21+
export async function removeComment(
22+
commentId: string,
23+
_prevState: RemoveCommentState,
24+
_formData: FormData,
25+
) {
26+
const supabase = await createClient();
27+
const { error } = await MUTATIONS.removeComment(supabase, commentId);
28+
29+
if (error) {
30+
console.error(error);
31+
32+
switch (error.code) {
33+
case "28000":
34+
return { ok: false, error: "로그인이 필요해요" };
35+
case "22023":
36+
return { ok: false, error: "댓글 ID가 필요해요" };
37+
case "NO_COMMENT":
38+
return { ok: false, error: "댓글을 찾을 수 없어요" };
39+
case "42501":
40+
return { ok: false, error: "이 댓글을 지울 권한이 없어요" };
41+
default:
42+
return { ok: false, error: "댓글을 지우는 중 문제가 생겼어요" };
43+
}
44+
}
45+
return { ok: true, error: null };
46+
}

app/actions/updateComment.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
"use server";
2+
3+
import { MUTATIONS } from "@/lib/supabase/repository";
4+
import { createClient } from "@/lib/supabase/server";
5+
6+
export type UpdateCommentState =
7+
| {
8+
ok: false;
9+
error: string;
10+
}
11+
| {
12+
ok: true;
13+
error: null;
14+
};
15+
16+
export type UpdateCommentAction = (
17+
prevState: UpdateCommentState,
18+
formData: FormData,
19+
) => Promise<UpdateCommentState>;
20+
21+
export async function updateComment(
22+
commentId: string,
23+
_prevState: UpdateCommentState,
24+
formData: FormData,
25+
) {
26+
const content = (formData.get("content") as string | null)?.trim();
27+
if (!content) return { ok: false, error: "내용을 입력해주세요" };
28+
29+
const supabase = await createClient();
30+
const { error } = await MUTATIONS.updateComment(supabase, commentId, content);
31+
32+
if (error) {
33+
console.error(error);
34+
35+
switch (error.code) {
36+
case "28000":
37+
return { ok: false, error: "로그인이 필요해요" };
38+
case "22023":
39+
return { ok: false, error: "내용을 입력해주세요" };
40+
case "NO_COMMENT":
41+
return { ok: false, error: "댓글을 찾을 수 없어요" };
42+
case "COMMENT_REMOVED":
43+
return { ok: false, error: "지워진 댓글은 고칠 수 없어요" };
44+
case "42501":
45+
return { ok: false, error: "이 댓글을 고칠 권한이 없어요" };
46+
default:
47+
return { ok: false, error: "댓글을 고치는 중 문제가 생겼어요" };
48+
}
49+
}
50+
console.info("updateComment", commentId, content);
51+
52+
return { ok: true, error: null };
53+
}

app/jargon/[slug]/page.tsx

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import { createClient } from "@/lib/supabase/server";
33
import { QUERIES } from "@/lib/supabase/repository";
44
import CommentThread from "@/components/comment/CommentThread";
55
import SuggestTranslationDialog from "@/components/dialogs/SuggestTranslationDialog";
6+
import ShareButton from "@/components/ShareButton";
7+
import { Comment } from "@/types/comment";
68

79
export default async function JargonDetailPage({
810
params,
@@ -36,18 +38,23 @@ export default async function JargonDetailPage({
3638
<div className="flex flex-col gap-2">
3739
{/* categories */}
3840
{jargon.categories.length > 0 ? (
39-
<div className="flex flex-wrap gap-2">
40-
{jargon.categories.map((cat) => (
41-
<span
42-
key={cat.category.acronym}
43-
className="bg-background text-foreground border-accent inline-block rounded-full border px-1 py-0.5 font-mono text-sm"
44-
>
45-
{cat.category.acronym}
46-
</span>
47-
))}
41+
<div className="flex flex-wrap items-center justify-between gap-2">
42+
<div className="flex flex-wrap gap-2">
43+
{jargon.categories.map((cat) => (
44+
<span
45+
key={cat.category.acronym}
46+
className="bg-background text-foreground border-accent inline-block rounded-full border px-1 py-0.5 font-mono text-sm"
47+
>
48+
{cat.category.acronym}
49+
</span>
50+
))}
51+
</div>
4852
</div>
4953
) : null}
50-
<h1 className="text-2xl font-bold">{jargon.name}</h1>
54+
<div className="flex items-center justify-between gap-2">
55+
<h1 className="text-2xl font-bold">{jargon.name}</h1>
56+
<ShareButton label="공유" />
57+
</div>
5158
{jargon.translations.length > 0 ? (
5259
<div className="text-foreground text-base">
5360
<ul className="list-disc pl-5">
@@ -69,7 +76,10 @@ export default async function JargonDetailPage({
6976

7077
{/* Comments section */}
7178
<div className="bg-card rounded-lg p-3">
72-
<CommentThread jargonId={jargon.id} initialComments={comments ?? []} />
79+
<CommentThread
80+
jargonId={jargon.id}
81+
initialComments={comments as Omit<Comment, "replies">[]}
82+
/>
7383
</div>
7484
</div>
7585
);

app/profile/page.tsx

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
11
import { redirect } from "next/navigation";
22

3+
import dayjs from "dayjs";
4+
import relativeTime from "dayjs/plugin/relativeTime";
35
import { LogoutButton } from "@/components/auth/LogoutButton";
46
import { createClient } from "@/lib/supabase/server";
57
import { QUERIES } from "@/lib/supabase/repository";
8+
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
9+
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
10+
import "dayjs/locale/ko";
11+
12+
dayjs.extend(relativeTime);
13+
dayjs.locale("ko");
614

715
export default async function Profile() {
816
const supabase = await createClient();
@@ -13,16 +21,43 @@ export default async function Profile() {
1321
} = await supabase.auth.getUser();
1422
if (error || !user) redirect("/auth/login");
1523

16-
const { data: profile } = await QUERIES.getName(supabase, user.id);
24+
const { data: profile } = await QUERIES.getNameAndPhoto(supabase, user.id);
25+
26+
const initials = (profile?.display_name || (user.email ?? ""))
27+
.slice(0, 2)
28+
.toUpperCase();
1729

1830
return (
19-
<div className="flex w-full items-center justify-center gap-2">
20-
<p>
21-
안녕하세요 <span>{profile?.display_name ?? ""}님!</span>
22-
<br />
23-
이메일: <span>{user.email}</span>
24-
</p>
25-
<LogoutButton />
31+
<div className="mx-auto flex w-full max-w-xl items-center justify-center p-4">
32+
<Card className="w-full">
33+
<CardHeader className="flex flex-row items-center gap-4">
34+
<Avatar className="h-12 w-12">
35+
{profile?.photo_url ? (
36+
<AvatarImage
37+
src={profile.photo_url}
38+
alt={profile?.display_name ?? ""}
39+
/>
40+
) : null}
41+
<AvatarFallback>{initials}</AvatarFallback>
42+
</Avatar>
43+
<div className="flex flex-col">
44+
<CardTitle className="text-xl">
45+
{profile?.display_name ?? "이름 없는 사용자"}
46+
</CardTitle>
47+
<span className="text-muted-foreground text-sm">{user.email}</span>
48+
</div>
49+
<div className="ml-auto">
50+
<LogoutButton />
51+
</div>
52+
</CardHeader>
53+
<CardContent>
54+
<div className="text-muted-foreground text-end text-sm">
55+
함께한 지{" "}
56+
{profile?.created_at ? dayjs(profile.created_at).fromNow(true) : ""}{" "}
57+
되었어요
58+
</div>
59+
</CardContent>
60+
</Card>
2661
</div>
2762
);
2863
}

components/ShareButton.tsx

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
"use client";
2+
3+
import { useState } from "react";
4+
import { Share, ClipboardCheck } from "lucide-react";
5+
import { Button } from "@/components/ui/button";
6+
7+
export default function ShareButton({ label = "공유" }: { label?: string }) {
8+
const [copied, setCopied] = useState(false);
9+
10+
async function onCopy() {
11+
try {
12+
await navigator.clipboard.writeText(window.location.href);
13+
setCopied(true);
14+
setTimeout(() => setCopied(false), 1200);
15+
} catch (error) {
16+
console.error(error);
17+
}
18+
}
19+
20+
return (
21+
<Button variant="outline" size="sm" onClick={onCopy} className="gap-1">
22+
{copied ? (
23+
<ClipboardCheck className="size-3.5" />
24+
) : (
25+
<Share className="size-3.5" />
26+
)}
27+
<span className="text-xs">{copied ? "복사됨" : label}</span>
28+
</Button>
29+
);
30+
}

0 commit comments

Comments
 (0)