-
Notifications
You must be signed in to change notification settings - Fork 3
feat: 워크스페이스 설정페이지 api연동 #51
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
730ccf9
210db0f
d13a5af
930a15c
d57ddf8
4e07945
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| // 초대 참여 라우트 — 초대 코드로 워크스페이스 요약을 조회해 참여 화면을 보여준다. | ||
| // 비멤버는 RLS로 워크스페이스를 직접 조회할 수 없어 security definer RPC(get_invite_preview)를 사용한다. | ||
| import Link from 'next/link'; | ||
| import { getInvitePreview } from '@/entities/workspace/api/get-invite-preview'; | ||
| import { plusJakartaSans } from '@/shared/lib/fonts'; | ||
| import { InviteAcceptView } from '@/views/invite'; | ||
|
|
||
| interface InvitePageProps { | ||
| params: Promise<{ code: string }>; | ||
| } | ||
|
|
||
| export default async function InvitePage({ params }: InvitePageProps) { | ||
| const { code } = await params; | ||
| const preview = await getInvitePreview(code); | ||
|
|
||
| if (!preview) { | ||
| return ( | ||
| <div | ||
| className={`${plusJakartaSans.className} flex min-h-dvh items-center justify-center px-4`} | ||
| > | ||
| <section className="w-full max-w-md rounded-2xl border border-slate-200 bg-white p-8 text-center shadow-sm"> | ||
| <h1 className="text-xl font-bold text-slate-950">유효하지 않은 초대예요</h1> | ||
| <p className="mt-2 text-sm text-slate-500"> | ||
| 링크가 만료되었거나 초대가 비활성화되었을 수 있어요. 초대한 사람에게 새 링크를 | ||
| 요청해 주세요. | ||
| </p> | ||
| <Link | ||
| href="/workspaces" | ||
| className="mt-8 inline-flex h-11 items-center justify-center rounded-2xl bg-[var(--color-brand)] px-5 text-sm font-bold text-white hover:bg-indigo-500" | ||
| > | ||
| 내 워크스페이스로 | ||
| </Link> | ||
| </section> | ||
| </div> | ||
| ); | ||
| } | ||
|
Comment on lines
+16
to
+36
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win 초대 실패 화면 UI를 app 레이어에 직접 구현했습니다.
♻️ 제안: 뷰로 분리- if (!preview) {
- return (
- <div
- className={`${plusJakartaSans.className} flex min-h-dvh items-center justify-center px-4`}
- >
- <section className="w-full max-w-md rounded-2xl border border-slate-200 bg-white p-8 text-center shadow-sm">
- <h1 className="text-xl font-bold text-slate-950">유효하지 않은 초대예요</h1>
- <p className="mt-2 text-sm text-slate-500">
- 링크가 만료되었거나 초대가 비활성화되었을 수 있어요. 초대한 사람에게 새 링크를
- 요청해 주세요.
- </p>
- <Link
- href="/workspaces"
- className="mt-8 inline-flex h-11 items-center justify-center rounded-2xl bg-[var(--color-brand)] px-5 text-sm font-bold text-white hover:bg-indigo-500"
- >
- 내 워크스페이스로
- </Link>
- </section>
- </div>
- );
- }
+ if (!preview) {
+ return <InviteInvalidView />;
+ }As per path instructions for 🤖 Prompt for AI AgentsSource: Path instructions |
||
|
|
||
| return ( | ||
| <InviteAcceptView | ||
| code={code} | ||
| workspaceName={preview.name} | ||
| memberCount={preview.memberCount} | ||
| /> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| 'use server'; | ||
|
|
||
| // 현재 사용자의 워크스페이스 닉네임 수정 서버액션 — workspace_members update | ||
| // RLS(members_update_self)에 의해 본인 멤버십만 수정할 수 있다. | ||
| import { revalidatePath } from 'next/cache'; | ||
| import { z } from 'zod'; | ||
| import { getCurrentUserId } from '@/shared/api/supabase/current-user'; | ||
| import { createSupabaseServerClient } from '@/shared/api/supabase/server'; | ||
|
|
||
| const updateMyNicknameInputSchema = z.object({ | ||
| workspaceId: z.guid(), | ||
| nickname: z | ||
| .string() | ||
| .trim() | ||
| .min(1, '닉네임을 입력해주세요') | ||
| .max(20, '닉네임은 20자 이내로 입력해주세요'), | ||
| }); | ||
|
|
||
| export type UpdateMyNicknameInput = z.infer<typeof updateMyNicknameInputSchema>; | ||
|
|
||
| export async function updateMyNickname(input: UpdateMyNicknameInput): Promise<void> { | ||
| const parsed = updateMyNicknameInputSchema.safeParse(input); | ||
| if (!parsed.success) { | ||
| throw new Error(parsed.error.issues[0]?.message ?? '입력값이 올바르지 않습니다'); | ||
| } | ||
|
|
||
| const userId = await getCurrentUserId(); | ||
| const supabase = await createSupabaseServerClient(); | ||
| const { error } = await supabase | ||
| .from('workspace_members') | ||
| .update({ workspace_nickname: parsed.data.nickname }) | ||
| .eq('workspace_id', parsed.data.workspaceId) | ||
| .eq('user_id', userId); | ||
|
|
||
| if (error) { | ||
| // (workspace_id, workspace_nickname) 유니크 제약 위반은 사용자 친화적 메시지로 안내한다. | ||
| if (error.code === '23505') { | ||
| throw new Error('이미 사용 중인 닉네임이에요. 다른 닉네임을 입력해주세요.'); | ||
| } | ||
| console.error('[updateMyNickname] update 실패:', error); | ||
| throw new Error('닉네임 저장에 실패했습니다. 잠시 후 다시 시도해주세요.'); | ||
| } | ||
|
|
||
| revalidatePath(`/workspaces/${parsed.data.workspaceId}/settings`); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| // 초대 미리보기 조회 — get_invite_preview RPC (security definer로 비멤버도 활성 코드면 요약 조회) | ||
| // 서버 전용(next/headers 의존)이므로 barrel에 넣지 않고 RSC에서 직접 경로로 import한다. | ||
| import { cache } from 'react'; | ||
| import { createSupabaseServerClient } from '@/shared/api/supabase/server'; | ||
|
|
||
| export interface InvitePreview { | ||
| workspaceId: string; | ||
| name: string; | ||
| memberCount: number; | ||
| } | ||
|
|
||
| export const getInvitePreview = cache(async (code: string): Promise<InvitePreview | null> => { | ||
| const supabase = await createSupabaseServerClient(); | ||
| const { data, error } = await supabase.rpc('get_invite_preview', { p_code: code }); | ||
|
|
||
| if (error) { | ||
| throw new Error(`초대 정보 조회에 실패했습니다: ${error.message}`); | ||
| } | ||
|
|
||
| const preview = data?.[0]; | ||
| if (!preview) { | ||
| return null; | ||
| } | ||
|
|
||
| return { | ||
| workspaceId: preview.workspace_id, | ||
| name: preview.name, | ||
| memberCount: preview.member_count, | ||
| }; | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| 'use server'; | ||
|
|
||
| // 초대 코드로 워크스페이스 참여 서버액션 — join_workspace_by_invite_code RPC | ||
| // (활성/유효 검증 + 멤버십 insert를 security definer 함수가 트랜잭션으로 처리) | ||
| import { getCurrentUserId } from '@/shared/api/supabase/current-user'; | ||
| import { createSupabaseServerClient } from '@/shared/api/supabase/server'; | ||
|
|
||
| export async function joinWorkspaceByInviteCode(code: string): Promise<{ workspaceId: string }> { | ||
| const trimmed = code.trim(); | ||
| if (!trimmed) { | ||
| throw new Error('유효하지 않은 초대 링크입니다.'); | ||
| } | ||
|
|
||
| const userId = await getCurrentUserId(); | ||
| const supabase = await createSupabaseServerClient(); | ||
| const { data, error } = await supabase.rpc('join_workspace_by_invite_code', { | ||
| p_user_id: userId, | ||
| p_code: trimmed, | ||
| }); | ||
|
|
||
| if (error) { | ||
| // 유효하지 않거나 비활성화된 초대는 RPC가 던진 메시지를 그대로 전달한다. | ||
| console.error('[joinWorkspaceByInviteCode] RPC 실패:', error); | ||
| throw new Error(error.message || '워크스페이스 참여에 실패했습니다. 잠시 후 다시 시도해주세요.'); | ||
| } | ||
|
|
||
| return { workspaceId: data }; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: TeampleRun/syncly
Length of output: 186
🏁 Script executed:
Repository: TeampleRun/syncly
Length of output: 2705
🏁 Script executed:
Repository: TeampleRun/syncly
Length of output: 1377
초대 조회 실패도 사용자용 처리로 분기하세요
getInvitePreview가 예외를 던지는데, 여기서는null만 처리해서 RPC 실패 시 기본 에러 화면으로 떨어집니다. 이 라우트에error.tsx를 두거나 여기서 예외를 잡아 초대 화면용 실패 메시지를 보여주세요.🤖 Prompt for AI Agents