Skip to content

Commit 9ceed95

Browse files
committed
✨ Allow authors and admins to remove/update jargons
1 parent be74b2b commit 9ceed95

File tree

11 files changed

+468
-82
lines changed

11 files changed

+468
-82
lines changed

app/actions/create-comment.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,8 @@ import { MUTATIONS } from "@/lib/supabase/repository";
44
import { createClient } from "@/lib/supabase/server";
55

66
export type CreateCommentState =
7-
| {
8-
ok: false;
9-
error: string;
10-
}
11-
| {
12-
ok: true;
13-
error: null;
14-
};
7+
| { ok: false; error: string }
8+
| { ok: true; error: null };
159

1610
export type CreateCommentAction = (
1711
prevState: CreateCommentState,

app/actions/remove-comment.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,8 @@ import { MUTATIONS } from "@/lib/supabase/repository";
44
import { createClient } from "@/lib/supabase/server";
55

66
export type RemoveCommentState =
7-
| {
8-
ok: false;
9-
error: string;
10-
}
11-
| {
12-
ok: true;
13-
error: null;
14-
};
7+
| { ok: false; error: string }
8+
| { ok: true; error: null };
159

1610
export type RemoveCommentAction = (
1711
prevState: RemoveCommentState,

app/actions/remove-jargon.ts

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

app/actions/suggest-jargon.ts

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,8 @@ import { createClient } from "@/lib/supabase/server";
55
import { eulLeul } from "@/lib/utils";
66

77
export type SuggestJargonState =
8-
| {
9-
ok: false;
10-
error: string;
11-
}
12-
| {
13-
ok: true;
14-
error: null;
15-
jargonSlug: string;
16-
};
8+
| { ok: false; error: string }
9+
| { ok: true; error: null; jargonSlug: string };
1710

1811
export type SuggestJargonAction = (
1912
prevState: SuggestJargonState,

app/actions/update-comment.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,8 @@ import { MUTATIONS } from "@/lib/supabase/repository";
44
import { createClient } from "@/lib/supabase/server";
55

66
export type UpdateCommentState =
7-
| {
8-
ok: false;
9-
error: string;
10-
}
11-
| {
12-
ok: true;
13-
error: null;
14-
};
7+
| { ok: false; error: string }
8+
| { ok: true; error: null };
159

1610
export type UpdateCommentAction = (
1711
prevState: UpdateCommentState,

app/actions/update-jargon.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
"use server";
2+
3+
import { MUTATIONS } from "@/lib/supabase/repository";
4+
import { createClient } from "@/lib/supabase/server";
5+
6+
export type UpdateJargonState =
7+
| { ok: false; error: string }
8+
| { ok: true; error: null; jargonSlug: string };
9+
10+
export type UpdateJargonAction = (
11+
prevState: UpdateJargonState,
12+
formData: FormData,
13+
) => Promise<UpdateJargonState>;
14+
15+
export async function updateJargon(
16+
jargonId: string,
17+
_prevState: UpdateJargonState,
18+
formData: FormData,
19+
): Promise<UpdateJargonState> {
20+
const name = (formData.get("name") as string | null)?.trim();
21+
if (!name) return { ok: false, error: "용어 이름을 입력해주세요" };
22+
23+
const supabase = await createClient();
24+
const { data, error } = await MUTATIONS.updateJargon(
25+
supabase,
26+
jargonId,
27+
name,
28+
);
29+
30+
if (error) {
31+
switch (error.code) {
32+
case "28000":
33+
return { ok: false, error: "로그인이 필요해요" };
34+
case "22023":
35+
return { ok: false, error: "올바른 값을 입력해주세요" };
36+
case "NO_JARGON":
37+
return { ok: false, error: "용어를 찾을 수 없어요" };
38+
case "23505":
39+
return {
40+
ok: false,
41+
error: "이미 존재하는 용어이거나 짧은 이름(slug)이 겹쳐요",
42+
};
43+
case "42501":
44+
return { ok: false, error: "이 용어를 고칠 권한이 없어요" };
45+
default:
46+
return { ok: false, error: "용어를 고치는 중 문제가 생겼어요" };
47+
}
48+
}
49+
50+
return { ok: true, error: null, jargonSlug: data.jargon_slug };
51+
}

app/jargon/[slug]/page.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import CommentThread from "@/components/comment/comment-thread";
55
import SuggestTranslationDialog from "@/components/dialogs/suggest-translation-dialog";
66
import ShareButton from "@/components/share-button";
77
import { Comment } from "@/types/comment";
8+
import JargonActions from "@/components/jargon/jargon-actions";
89

910
export default async function JargonDetailPage({
1011
params,
@@ -52,7 +53,14 @@ export default async function JargonDetailPage({
5253
</div>
5354
) : null}
5455
<div className="flex items-center justify-between gap-2">
55-
<h1 className="text-2xl font-bold">{jargon.name}</h1>
56+
<div className="flex items-center gap-2">
57+
<h1 className="text-2xl font-bold">{jargon.name}</h1>
58+
<JargonActions
59+
jargonId={jargon.id}
60+
authorId={jargon.author_id}
61+
name={jargon.name}
62+
/>
63+
</div>
5664
<ShareButton label="공유" />
5765
</div>
5866
{jargon.translations.length > 0 ? (
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
"use client";
2+
3+
import { useActionState, useEffect } from "react";
4+
import { redirect, RedirectType } from "next/navigation";
5+
import Form from "next/form";
6+
import { useQueryClient } from "@tanstack/react-query";
7+
import { Button } from "@/components/ui/button";
8+
import { Input } from "@/components/ui/input";
9+
import { useUserQuery } from "@/hooks/use-user-query";
10+
import {
11+
AlertDialog,
12+
AlertDialogTrigger,
13+
AlertDialogContent,
14+
AlertDialogHeader,
15+
AlertDialogTitle,
16+
AlertDialogDescription,
17+
AlertDialogFooter,
18+
AlertDialogCancel,
19+
AlertDialogAction,
20+
} from "@/components/ui/alert-dialog";
21+
import {
22+
Dialog,
23+
DialogTrigger,
24+
DialogContent,
25+
DialogHeader,
26+
DialogTitle,
27+
DialogDescription,
28+
DialogFooter,
29+
DialogClose,
30+
} from "@/components/ui/dialog";
31+
import { updateJargon } from "@/app/actions/update-jargon";
32+
import { removeJargon } from "@/app/actions/remove-jargon";
33+
34+
export default function JargonActions({
35+
jargonId,
36+
authorId,
37+
name,
38+
}: {
39+
jargonId: string;
40+
authorId: string;
41+
name: string;
42+
}) {
43+
const queryClient = useQueryClient();
44+
45+
const { data: user } = useUserQuery();
46+
const isAdmin = user?.app_metadata?.userrole === "admin";
47+
const canManage = !!user && (user.id === authorId || isAdmin);
48+
49+
const [updateState, updateAction] = useActionState(
50+
updateJargon.bind(null, jargonId),
51+
{ ok: false, error: "" },
52+
);
53+
const [removeState, removeAction] = useActionState(
54+
removeJargon.bind(null, jargonId),
55+
{ ok: false, error: "" },
56+
);
57+
58+
useEffect(() => {
59+
if (removeState.ok) {
60+
queryClient.invalidateQueries({ queryKey: ["jargons"] });
61+
redirect("/", RedirectType.replace);
62+
}
63+
}, [removeState.ok, queryClient]);
64+
65+
useEffect(() => {
66+
if (updateState.ok) {
67+
queryClient.invalidateQueries({ queryKey: ["jargons"] });
68+
redirect(`/jargon/${updateState.jargonSlug}`, RedirectType.replace);
69+
}
70+
}, [updateState.ok, updateState, queryClient]);
71+
72+
if (!canManage) return null;
73+
74+
return (
75+
<div className="flex items-center gap-2">
76+
<Dialog>
77+
<DialogTrigger asChild>
78+
<Button type="button" variant="ghost" className="h-7 px-2.5 text-xs">
79+
고치기
80+
</Button>
81+
</DialogTrigger>
82+
<DialogContent className="sm:max-w-[420px]">
83+
<DialogHeader>
84+
<DialogTitle>용어 이름 고치기</DialogTitle>
85+
<DialogDescription>
86+
새로운 용어 이름을 입력해 주세요. 저장하면 짧은 이름(slug)도 함께
87+
바뀌어요.
88+
</DialogDescription>
89+
</DialogHeader>
90+
<Form action={updateAction} className="grid gap-3">
91+
<Input name="name" defaultValue={name} autoFocus />
92+
{updateState?.error && (
93+
<span className="text-xs text-red-600">{updateState.error}</span>
94+
)}
95+
<DialogFooter>
96+
<DialogClose asChild>
97+
<Button type="button" variant="outline">
98+
닫기
99+
</Button>
100+
</DialogClose>
101+
<Button type="submit">저장</Button>
102+
</DialogFooter>
103+
</Form>
104+
</DialogContent>
105+
</Dialog>
106+
107+
<AlertDialog>
108+
<AlertDialogTrigger asChild>
109+
<Button
110+
type="button"
111+
variant="ghost"
112+
className="h-7 px-2.5 text-xs text-red-600 hover:text-red-700"
113+
>
114+
지우기
115+
</Button>
116+
</AlertDialogTrigger>
117+
<AlertDialogContent>
118+
<AlertDialogHeader>
119+
<AlertDialogTitle>정말로 용어를 지울까요?</AlertDialogTitle>
120+
<AlertDialogDescription>
121+
이 작업은 되돌릴 수 없어요. 관련 댓글과 번역도 함께 지워져요.
122+
</AlertDialogDescription>
123+
</AlertDialogHeader>
124+
{removeState?.error && (
125+
<span className="text-xs text-red-600">{removeState.error}</span>
126+
)}
127+
<AlertDialogFooter>
128+
<AlertDialogCancel>닫기</AlertDialogCancel>
129+
<Form action={removeAction}>
130+
<AlertDialogAction
131+
type="submit"
132+
className="bg-red-600 hover:bg-red-700"
133+
>
134+
지우기
135+
</AlertDialogAction>
136+
</Form>
137+
</AlertDialogFooter>
138+
</AlertDialogContent>
139+
</AlertDialog>
140+
</div>
141+
);
142+
}

lib/supabase/repository.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export const QUERIES = {
3535
return supabase
3636
.from("jargon")
3737
.select(
38-
"id, name, slug, created_at, translations:translation(name), categories:jargon_category(category:category(acronym))",
38+
"id, name, slug, created_at, author_id, translations:translation(name), categories:jargon_category(category:category(acronym))",
3939
)
4040
.eq("slug", slug)
4141
.limit(1)
@@ -207,4 +207,24 @@ export const MUTATIONS = {
207207
) {
208208
return supabase.rpc("remove_comment", { p_comment_id: commentId });
209209
},
210+
211+
updateJargon: function (
212+
supabase: SupabaseClient<Database>,
213+
jargonId: string,
214+
name: string,
215+
) {
216+
return supabase
217+
.rpc("update_jargon", {
218+
p_jargon_id: jargonId,
219+
p_name: name,
220+
})
221+
.single();
222+
},
223+
224+
removeJargon: function (
225+
supabase: SupabaseClient<Database>,
226+
jargonId: string,
227+
) {
228+
return supabase.rpc("remove_jargon", { p_jargon_id: jargonId });
229+
},
210230
};

0 commit comments

Comments
 (0)