Skip to content

feat: 워크스페이스 설정페이지 api연동 - #51

Closed
Kwon812 wants to merge 6 commits into
developfrom
feat/#47/workspace-setting-page
Closed

feat: 워크스페이스 설정페이지 api연동#51
Kwon812 wants to merge 6 commits into
developfrom
feat/#47/workspace-setting-page

Conversation

@Kwon812

@Kwon812 Kwon812 commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Pull Request

작업 내용

  • 목업이던 워크스페이스 설정 페이지를 Supabase 실 API로 연동
  • 초대 링크 / 이메일 기반 팀원 초대 참여 플로우 구현

작업 결과

  • 설정 페이지에서 워크스페이스 정보·프로필 닉네임 수정이 실제 DB에 반영됨
  • 초대 링크 활성화 후 /invite/[code]로 워크스페이스 참여 가능
  • 입력한 이메일로 초대 링크 발송 (Resend)

변경 사항

Added

  • 초대 참여 라우트/뷰: /invite/[code], InviteAcceptView
  • RPC: get_invite_preview, join_workspace_by_invite_code (SECURITY DEFINER)
  • 서버액션: updateWorkspaceInfo, updateMyNickname, setWorkspaceInviteEnabled, joinWorkspaceByInviteCode, sendInviteEmail
  • 이메일 발송 유틸(Resend) 및 env 헬퍼(RESEND_API_KEY, INVITE_EMAIL_FROM)
  • 마이그레이션: 20260713010000_create_invite_rpcs.sql

Changed

  • 워크스페이스 정보/멤버/현재 사용자 조회를 목업 → 실 API로 교체
  • getWorkspaceByIdinvite_code, invite_enabled 추가
  • 이메일 초대: 로컬 목업 → 실제 초대 링크 발송으로 전환
  • 초대 링크 UI에 활성화 토글 추가

Fixed

  • 워크스페이스 정보 수정 / 초대 링크 토글을 소유자만 가능하도록 제한 (RLS와 UI 일치)

실행화면

테스트

  • 로컬 실행 확인
  • 주요 시나리오 확인 (초대 링크 생성 → 참여, 이메일 발송)
  • 영향 범위 확인 (타입체크 / 린트 / 프로덕션 빌드 통과)

리뷰 체크리스트

  • PR base branch가 올바릅니다. (feature/* -> develop, 배포 시 develop 또는 release/* -> main)
  • 브랜치명이 Type/#issue-number/description 형식을 따릅니다.
  • 커밋 메시지가 컨벤션을 따릅니다.
  • 불필요한 console.log, 주석, 임시 코드를 제거했습니다.
  • 타입 에러와 린트 에러를 확인했습니다.
  • CodeRabbit 1차 리뷰를 확인했습니다.
  • CodeRabbit 리뷰 반영 후 Discord에 공유했습니다.
  • 최소 1명 이상의 approve 후 merge합니다.

리뷰 요청사항

  • SECURITY DEFINER RPC(get_invite_preview, join_workspace_by_invite_code) 내부 검증 로직

참고

  • 마이그레이션 적용 후 npm run gen:types 필요
  • RESEND_API_KEY 미설정 시 이메일 발송 불가 (도메인 인증 전에는 Resend 테스트 모드)
  • auth 연동 전이라 DEV_USER_ID 사용 → 연동 시 auth.uid()로 교체 예정

관련 이슈

Closes #47

Summary by CodeRabbit

  • 새로운 기능

    • 이메일 또는 초대 링크로 워크스페이스에 초대할 수 있습니다.
    • 초대 링크를 활성화·비활성화하고 링크를 복사할 수 있습니다.
    • 초대 페이지에서 워크스페이스 정보를 확인한 뒤 바로 참여할 수 있습니다.
    • 워크스페이스 정보와 내 닉네임을 실제로 저장할 수 있습니다.
  • 개선 사항

    • 초대 및 설정 변경 시 저장 중 상태와 성공·실패 안내를 제공합니다.
    • 권한이 없는 멤버는 워크스페이스 정보를 수정할 수 없습니다.
    • 유효하지 않거나 비활성화된 초대 링크에 안내 화면을 표시합니다.

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

워크스페이스 설정 페이지를 실제 Supabase 조회·저장 방식으로 전환하고, 소유자 권한 기반 편집 및 초대 링크·이메일 초대 기능을 추가했다. 초대 코드 미리보기와 참여 RPC, 참여 화면, Resend 이메일 발송 흐름도 구현했다.

Changes

워크스페이스 설정 및 초대

Layer / File(s) Summary
초대 RPC 및 데이터 계약
supabase/migrations/..., src/shared/model/database.types.ts, src/entities/workspace/model/workspace.types.ts, .env.example, package.json
초대 미리보기·참여 RPC와 관련 타입을 추가하고, 워크스페이스 초대 필드 및 Resend 설정을 정의했다.
설정 및 초대 서버 액션
src/entities/workspace/api/*, src/entities/workspace-member/api/*, src/shared/api/email/env.ts, src/entities/workspace/index.ts, src/entities/workspace-member/index.ts
워크스페이스 정보·닉네임·초대 상태 저장, 초대 코드 조회·참여, Resend 이메일 발송 서버 액션을 구현했다.
설정 조회 및 저장 UI
src/app/workspaces/.../settings/page.tsx, src/views/settings/*, src/features/manage-workspace-info/*, src/features/manage-member-profile/*
설정 페이지 데이터를 실제 API에서 조회하고, 소유자 전용 워크스페이스 수정과 본인 닉네임 저장을 연결했다.
멤버 초대 관리 UI
src/features/manage-workspace-members/*
초대 링크 활성화 토글, 링크 복사 제어, 이메일 발송 상태 및 권한 처리를 추가했다.
초대 링크 참여 화면
src/app/invite/..., src/views/invite/*
초대 코드 미리보기 결과에 따라 유효하지 않은 안내 또는 참여 화면을 렌더링하고, 참여 성공 시 워크스페이스로 이동하도록 했다.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant InvitePage
  participant getInvitePreview
  participant InviteAcceptView
  participant joinWorkspaceByInviteCode
  participant SupabaseRPC
  InvitePage->>getInvitePreview: 초대 코드 미리보기 조회
  getInvitePreview-->>InvitePage: 워크스페이스 요약 반환
  InvitePage->>InviteAcceptView: 초대 정보 전달
  InviteAcceptView->>joinWorkspaceByInviteCode: 참여 요청
  joinWorkspaceByInviteCode->>SupabaseRPC: 참여 RPC 호출
  SupabaseRPC-->>joinWorkspaceByInviteCode: workspace_id 반환
  joinWorkspaceByInviteCode-->>InviteAcceptView: 참여 결과 반환
Loading

Possibly related PRs

Suggested reviewers: jiwoonge, 0011810

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.11% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed 제목이 워크스페이스 설정 페이지의 API 연동을 핵심으로 잘 요약하며, 초대 플로우도 같은 변경 묶음으로 볼 수 있습니다.
Description check ✅ Passed 필수 섹션과 관련 이슈, 테스트/체크리스트가 대부분 채워져 있어 템플릿 요건을 충분히 만족합니다.
Linked Issues check ✅ Passed 설정 페이지 실 API 전환, 소유자/본인 권한, 초대 링크 참여와 Resend 이메일 발송이 요건과 일치합니다.
Out of Scope Changes check ✅ Passed 패키지 추가, env 예시, RPC/서버 액션/뷰 변경이 모두 설정·초대 기능 범위 안에 있습니다.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/#47/workspace-setting-page

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/app/invite/`[code]/page.tsx:
- Line 14: Update the invite route around getInvitePreview(code) so RPC
exceptions follow the invite-specific user-facing failure path instead of
reaching the generic error screen. Either add the route’s error.tsx handling or
catch the exception in the page and render the existing invite failure message,
while preserving the current null-result handling.
- Around line 16-36: Move the invalid-invite markup from the InvitePage !preview
branch into a new InviteInvalidView under the invite views layer, then render
that view from InvitePage. Preserve the existing Korean text, workspace link,
and visual styling, while reusing shared invite card/layout styles where
appropriate.

In `@src/entities/workspace/api/send-invite-email.ts`:
- Around line 51-69: HTML 이메일을 구성하는 send-invite-email 흐름에서 workspace.name을 직접
삽입하지 말고, 발송 전에 HTML 이스케이프된 값을 생성해 subject 외 HTML 본문 삽입 위치에 사용하세요. 기존 초대 링크와 이메일
내용은 유지하고, HTML 컨텍스트에 안전한 workspace 이름이 사용되도록 처리합니다.
- Around line 19-24: Update resolveOrigin to use a trusted fixed base URL from
the established NEXT_PUBLIC_APP_URL or SITE_URL configuration instead of host
and x-forwarded-proto headers. Before inserting workspace.name into the
invitation email HTML, escape it with the project’s existing HTML-escaping
utility and preserve the intended displayed name.

In `@src/features/manage-workspace-members/model/use-member-management.ts`:
- Around line 77-82: Update the canInvite calculation in the member invitation
flow to require isInviteEnabled in addition to valid, non-duplicate email and
!isSendingInvite conditions. Keep the existing validation logic unchanged so the
invite action remains disabled whenever workspace invitations are disabled.

In `@supabase/migrations/20260713010000_create_invite_rpcs.sql`:
- Around line 30-47: Update join_workspace_by_invite_code to derive the joining
user exclusively from auth.uid() instead of trusting p_user_id, and use that
authenticated ID for all membership creation and related checks. Remove the
user-controlled parameter if the function no longer needs it, and restrict
execution with REVOKE EXECUTE from PUBLIC and anon as appropriate.
- Around line 49-52: Update the invalid-invite branch in the invite RPC to use
only the RAISE EXCEPTION USING MESSAGE form, removing the separate
'invalid_or_disabled_invite' format string so the exception message is assigned
exactly once.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 23ebaf8b-e43f-46e7-b9e1-2ed2eb78bb0d

📥 Commits

Reviewing files that changed from the base of the PR and between c4a8d24 and 4e07945.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json, !package-lock.json
📒 Files selected for processing (25)
  • .env.example
  • package.json
  • src/app/invite/[code]/page.tsx
  • src/app/workspaces/[workspaceId]/settings/page.tsx
  • src/entities/workspace-member/api/update-my-nickname.ts
  • src/entities/workspace-member/index.ts
  • src/entities/workspace/api/get-invite-preview.ts
  • src/entities/workspace/api/get-workspace-by-id.ts
  • src/entities/workspace/api/join-workspace-by-invite-code.ts
  • src/entities/workspace/api/send-invite-email.ts
  • src/entities/workspace/api/set-workspace-invite-enabled.ts
  • src/entities/workspace/api/update-workspace-info.ts
  • src/entities/workspace/index.ts
  • src/entities/workspace/model/workspace.types.ts
  • src/features/manage-member-profile/ui/MemberProfileForm.tsx
  • src/features/manage-workspace-info/ui/WorkspaceInfoForm.tsx
  • src/features/manage-workspace-members/model/use-member-management.ts
  • src/features/manage-workspace-members/ui/MemberInviteSection.tsx
  • src/features/manage-workspace-members/ui/MemberManagementPanel.tsx
  • src/shared/api/email/env.ts
  • src/shared/model/database.types.ts
  • src/views/invite/index.ts
  • src/views/invite/ui/InviteAcceptView.tsx
  • src/views/settings/ui/SettingsView.tsx
  • supabase/migrations/20260713010000_create_invite_rpcs.sql


export default async function InvitePage({ params }: InvitePageProps) {
const { code } = await params;
const preview = await getInvitePreview(code);

Copy link
Copy Markdown

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:

#!/bin/bash
fd -p 'src/app/invite' --type f

Repository: TeampleRun/syncly

Length of output: 186


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== invite route files =="
fd -a . src/app/invite

echo
echo "== page.tsx (numbered) =="
cat -n src/app/invite/[code]/page.tsx

echo
echo "== search getInvitePreview usage/definition =="
rg -n "getInvitePreview|error\.tsx" src/app src -g '!**/node_modules/**' || true

Repository: TeampleRun/syncly

Length of output: 2705


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== get-invite-preview implementation =="
cat -n src/entities/workspace/api/get-invite-preview.ts

echo
echo "== error.tsx under src/app/invite =="
fd -a 'error.tsx' src/app/invite || true

echo
echo "== error.tsx under src/app =="
fd -a 'error.tsx' src/app || true

echo
echo "== search for invite route error boundaries =="
rg -n "error\.tsx|ErrorBoundary" src/app/invite src/app -g '!**/node_modules/**' || true

Repository: TeampleRun/syncly

Length of output: 1377


초대 조회 실패도 사용자용 처리로 분기하세요 getInvitePreview가 예외를 던지는데, 여기서는 null만 처리해서 RPC 실패 시 기본 에러 화면으로 떨어집니다. 이 라우트에 error.tsx를 두거나 여기서 예외를 잡아 초대 화면용 실패 메시지를 보여주세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/invite/`[code]/page.tsx at line 14, Update the invite route around
getInvitePreview(code) so RPC exceptions follow the invite-specific user-facing
failure path instead of reaching the generic error screen. Either add the
route’s error.tsx handling or catch the exception in the page and render the
existing invite failure message, while preserving the current null-result
handling.

Comment on lines +16 to +36
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>
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

초대 실패 화면 UI를 app 레이어에 직접 구현했습니다.

InvitePage는 유효한 초대는 InviteAcceptView로 위임하지만, "유효하지 않은 초대" 화면은 app 레이어에서 직접 마크업을 구현하고 있습니다. src/views/inviteInviteInvalidView 같은 뷰 컴포넌트를 추가해 이 분기도 위임하면 일관성이 생기고, InviteAcceptView와 공유하는 카드/레이아웃 스타일도 재사용할 수 있습니다.

♻️ 제안: 뷰로 분리
-  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 src/app/**/*.{ts,tsx}: "compose screens by importing views rather than implementing page UI directly in the app layer."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/invite/`[code]/page.tsx around lines 16 - 36, Move the invalid-invite
markup from the InvitePage !preview branch into a new InviteInvalidView under
the invite views layer, then render that view from InvitePage. Preserve the
existing Korean text, workspace link, and visual styling, while reusing shared
invite card/layout styles where appropriate.

Source: Path instructions

Comment on lines +19 to +24
async function resolveOrigin(): Promise<string> {
const headerList = await headers();
const host = headerList.get('host');
const proto = headerList.get('x-forwarded-proto') ?? 'http';
return host ? `${proto}://${host}` : '';
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

초대 링크 origin은 신뢰된 base URL로 고정하고, 메일 본문에 삽입하는 값은 이스케이프하세요.

headers()host/x-forwarded-proto를 그대로 사용하면 조작된 도메인으로 초대 링크가 발송될 수 있습니다. NEXT_PUBLIC_APP_URL/SITE_URL 같은 고정 base URL을 사용하세요.

workspace.name도 HTML 메일 본문에 직접 넣지 말고 이스케이프가 필요합니다. 임의 HTML이 들어가면 수신자에게 보내는 메일에 피싱용 콘텐츠를 주입할 수 있습니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/entities/workspace/api/send-invite-email.ts` around lines 19 - 24, Update
resolveOrigin to use a trusted fixed base URL from the established
NEXT_PUBLIC_APP_URL or SITE_URL configuration instead of host and
x-forwarded-proto headers. Before inserting workspace.name into the invitation
email HTML, escape it with the project’s existing HTML-escaping utility and
preserve the intended displayed name.

Comment on lines +51 to +69
const { error } = await resend.emails.send({
from,
to: parsed.data.email,
subject: `[Syncly] ${workspace.name} 워크스페이스에 초대되었어요`,
text: `${workspace.name} 워크스페이스에 초대되었습니다.\n아래 링크로 참여하세요:\n${inviteUrl}`,
html: `
<div style="font-family: sans-serif; line-height: 1.6; color: #0f172a;">
<p><strong>${workspace.name}</strong> 워크스페이스에 초대되었어요.</p>
<p>아래 버튼을 눌러 참여하세요.</p>
<p style="margin: 24px 0;">
<a href="${inviteUrl}"
style="display:inline-block; padding:12px 20px; border-radius:12px; background:#6366f1; color:#fff; text-decoration:none; font-weight:bold;">
워크스페이스 참여하기
</a>
</p>
<p style="color:#64748b; font-size:13px;">버튼이 동작하지 않으면 이 링크를 복사해 열어주세요:<br>${inviteUrl}</p>
</div>
`,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

이메일 HTML에 workspace.name을 이스케이프 없이 삽입 — HTML 인젝션 위험.

workspace.name은 워크스페이스 소유자가 자유롭게 지정하는 값인데, <strong>${workspace.name}</strong> 등으로 이스케이프 없이 HTML 본문에 직접 삽입됩니다. 악의적인 워크스페이스 이름(HTML 태그 포함)이 그대로 제3자(초대 수신자)에게 발송되는 메일에 삽입될 수 있어, 가짜 버튼/링크로 실제 초대 링크를 가리는 등의 피싱에 악용될 수 있습니다.

발송 전 workspace.name을 HTML 이스케이프 처리해 주세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/entities/workspace/api/send-invite-email.ts` around lines 51 - 69, HTML
이메일을 구성하는 send-invite-email 흐름에서 workspace.name을 직접 삽입하지 말고, 발송 전에 HTML 이스케이프된
값을 생성해 subject 외 HTML 본문 삽입 위치에 사용하세요. 기존 초대 링크와 이메일 내용은 유지하고, HTML 컨텍스트에 안전한
workspace 이름이 사용되도록 처리합니다.

Comment on lines 77 to +82
const trimmedEmail = email.trim();
const isDuplicate = useMemo(
() => members.some((member) => member.email.toLowerCase() === trimmedEmail.toLowerCase()),
[members, trimmedEmail],
);
const canInvite = EMAIL_PATTERN.test(trimmedEmail) && !isDuplicate;
const canInvite = EMAIL_PATTERN.test(trimmedEmail) && !isDuplicate && !isSendingInvite;

Copy link
Copy Markdown

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

canInviteisInviteEnabled 조건 누락.

초대 링크가 비활성화된 상태에서도 이메일 초대 버튼이 활성화되어 제출이 가능합니다. sendInviteEmailworkspace.inviteEnabled가 false면 항상 실패하므로('초대 링크를 먼저 활성화해주세요.'), 사용자는 실패할 것이 확실한 요청을 보내고 에러 토스트만 받게 됩니다. MemberInviteSection의 안내 문구("초대 링크가 활성화되어 있어야 발송됩니다")와도 실제 버튼 상태가 불일치합니다.

🐛 제안 수정
-  const canInvite = EMAIL_PATTERN.test(trimmedEmail) && !isDuplicate && !isSendingInvite;
+  const canInvite =
+    isInviteEnabled && EMAIL_PATTERN.test(trimmedEmail) && !isDuplicate && !isSendingInvite;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const trimmedEmail = email.trim();
const isDuplicate = useMemo(
() => members.some((member) => member.email.toLowerCase() === trimmedEmail.toLowerCase()),
[members, trimmedEmail],
);
const canInvite = EMAIL_PATTERN.test(trimmedEmail) && !isDuplicate;
const canInvite = EMAIL_PATTERN.test(trimmedEmail) && !isDuplicate && !isSendingInvite;
const trimmedEmail = email.trim();
const isDuplicate = useMemo(
() => members.some((member) => member.email.toLowerCase() === trimmedEmail.toLowerCase()),
[members, trimmedEmail],
);
const canInvite =
isInviteEnabled && EMAIL_PATTERN.test(trimmedEmail) && !isDuplicate && !isSendingInvite;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/features/manage-workspace-members/model/use-member-management.ts` around
lines 77 - 82, Update the canInvite calculation in the member invitation flow to
require isInviteEnabled in addition to valid, non-duplicate email and
!isSendingInvite conditions. Keep the existing validation logic unchanged so the
invite action remains disabled whenever workspace invitations are disabled.

Comment on lines +30 to +47
create or replace function public.join_workspace_by_invite_code(
p_user_id uuid,
p_code text
)
returns uuid
language plpgsql
security definer
set search_path = public, pg_temp
as $$
declare
v_workspace_id uuid;
v_nickname text;
begin
-- 활성 + 유효 코드의 워크스페이스만 대상으로 한다.
select w.id into v_workspace_id
from workspaces w
where w.invite_code = p_code
and w.invite_enabled = true;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== target file ==\n'
sed -n '1,220p' supabase/migrations/20260713010000_create_invite_rpcs.sql | cat -n

printf '\n== revoke/grant occurrences in migrations ==\n'
rg -n --hidden --glob 'supabase/migrations/*.sql' 'revoke\s+execute|grant\s+execute|security definer|join_workspace_by_invite_code|auth\.uid\(\)' supabase/migrations

Repository: TeampleRun/syncly

Length of output: 6879


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== invite rpc migration ==\n'
sed -n '1,220p' supabase/migrations/20260713010000_create_invite_rpcs.sql | cat -n

printf '\n== nearby rpc migration patterns ==\n'
sed -n '1,220p' supabase/migrations/20260709054147_create_workspace_rpcs.sql | cat -n

printf '\n== explicit grants/revokes for join_workspace_by_invite_code or public schema/function defaults ==\n'
rg -n --hidden --glob 'supabase/migrations/*.sql' \
  'join_workspace_by_invite_code|revoke\s+all|revoke\s+execute|grant\s+execute|alter default privileges|grant\s+usage\s+on\s+schema\s+public|revoke\s+usage\s+on\s+schema\s+public' \
  supabase/migrations

Repository: TeampleRun/syncly

Length of output: 7720


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import pathlib, re
root = pathlib.Path('supabase/migrations')
for path in sorted(root.glob('*.sql')):
    txt = path.read_text()
    if 'create or replace function public.join_workspace_by_invite_code' in txt:
        print(f'FOUND:{path}')
        for m in re.finditer(r'(?im)^(revoke|grant|alter default privileges).*$|join_workspace_by_invite_code.*$', txt):
            line = txt[:m.start()].count('\n') + 1
            print(f'{line}: {m.group(0)}')
PY

Repository: TeampleRun/syncly

Length of output: 254


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== create_workspace_rpcs.sql ==\n'
sed -n '1,220p' supabase/migrations/20260709054147_create_workspace_rpcs.sql | cat -n

printf '\n== execute/grant/revoke search across migrations ==\n'
rg -n --hidden --glob 'supabase/migrations/*.sql' \
  'create or replace function public\.[A-Za-z0-9_]+\(|grant\s+execute\s+on\s+function|revoke\s+execute\s+on\s+function|grant\s+all\s+on\s+function|revoke\s+all\s+on\s+function' \
  supabase/migrations

Repository: TeampleRun/syncly

Length of output: 5661


join_workspace_by_invite_code에서 p_user_id를 그대로 쓰지 마세요. security definer인데 이 함수에 별도 REVOKE/GRANT가 없어 기본 PUBLIC EXECUTE가 남아 있고, 호출자가 임의의 user_id로 멤버십을 만들 수 있습니다. auth.uid()로 가입 대상을 고정하고, 필요하면 REVOKE EXECUTE ... FROM PUBLIC, anon;도 추가하세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@supabase/migrations/20260713010000_create_invite_rpcs.sql` around lines 30 -
47, Update join_workspace_by_invite_code to derive the joining user exclusively
from auth.uid() instead of trusting p_user_id, and use that authenticated ID for
all membership creation and related checks. Remove the user-controlled parameter
if the function no longer needs it, and restrict execution with REVOKE EXECUTE
from PUBLIC and anon as appropriate.

Comment on lines +49 to +52
if v_workspace_id is null then
raise exception 'invalid_or_disabled_invite'
using message = '유효하지 않거나 비활성화된 초대 링크입니다.';
end if;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## Target file context\n'
nl -ba supabase/migrations/20260713010000_create_invite_rpcs.sql | sed -n '1,140p'

printf '\n## Other RAISE EXCEPTION usages in repo\n'
rg -n "RAISE EXCEPTION|raise exception" supabase -S

Repository: TeampleRun/syncly

Length of output: 219


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## Target file context\n'
sed -n '1,140p' supabase/migrations/20260713010000_create_invite_rpcs.sql | cat -n

printf '\n## Other RAISE EXCEPTION usages in repo\n'
rg -n "RAISE EXCEPTION|raise exception" supabase -S

Repository: TeampleRun/syncly

Length of output: 3814


🌐 Web query:

PostgreSQL PL/pgSQL RAISE USING MESSAGE format string option already specified MESSAGE

💡 Result:

In PostgreSQL PL/pgSQL, the error "RAISE option already specified: MESSAGE" occurs when you attempt to define an error message using both the standard format string syntax and the USING MESSAGE option within the same RAISE statement [1][2]. The RAISE statement has several syntax variants [3][4][5]. In the variant that includes a format string (e.g., RAISE EXCEPTION 'Something went wrong'), the message is already supplied by that string [3]. You cannot add a second message definition using USING MESSAGE = '...' because the MESSAGE option is redundant and not permitted when a format string is already present [3][6]. To resolve this, choose one of the following approaches: 1. Use the format string syntax if you do not need other options (like DETAIL or HINT): RAISE EXCEPTION 'Something went wrong'; 2. Use the USING MESSAGE syntax if you are also providing other USING options (e.g., DETAIL, HINT): RAISE EXCEPTION USING MESSAGE = 'Something went wrong', DETAIL = 'Additional details'; 3. If you need to include variable values in your message while using other USING options, include the variables directly in the MESSAGE expression string: RAISE EXCEPTION USING MESSAGE = 'Error: ' || var_name, DETAIL = 'Check your input'; In summary, the MESSAGE option is mutually exclusive with the format string syntax in a RAISE statement [3][4].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## Search for join-workspace-by-invite-code references\n'
rg -n "join-workspace-by-invite-code|invite_code|error\.message|invalid_or_disabled_invite" -S .

printf '\n## File list around invite join flow\n'
git ls-files | rg 'join-workspace|invite|workspace' -n

Repository: TeampleRun/syncly

Length of output: 11916


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## join-workspace-by-invite-code.ts\n'
sed -n '1,120p' src/entities/workspace/api/join-workspace-by-invite-code.ts | cat -n

printf '\n## InviteAcceptView.tsx\n'
sed -n '1,120p' src/views/invite/ui/InviteAcceptView.tsx | cat -n

Repository: TeampleRun/syncly

Length of output: 3772


RAISE EXCEPTION 문법을 하나로 정리하세요.
'invalid_or_disabled_invite'를 포맷 문자열로 넣은 뒤 USING MESSAGE를 추가하면 MESSAGE가 중복 지정되어 함수 생성이 실패합니다. 안내 문구만 필요하면 RAISE EXCEPTION USING MESSAGE = '유효하지 않거나 비활성화된 초대 링크입니다.';처럼 한 방식만 쓰세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@supabase/migrations/20260713010000_create_invite_rpcs.sql` around lines 49 -
52, Update the invalid-invite branch in the invite RPC to use only the RAISE
EXCEPTION USING MESSAGE form, removing the separate 'invalid_or_disabled_invite'
format string so the exception message is assigned exactly once.

@Kwon812 Kwon812 closed this Jul 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: 설정페이지 mock -> api 전환

1 participant