feat: 워크스페이스 설정페이지 api연동 - #51
Conversation
📝 WalkthroughWalkthrough워크스페이스 설정 페이지를 실제 Supabase 조회·저장 방식으로 전환하고, 소유자 권한 기반 편집 및 초대 링크·이메일 초대 기능을 추가했다. 초대 코드 미리보기와 참여 RPC, 참여 화면, Resend 이메일 발송 흐름도 구현했다. Changes워크스페이스 설정 및 초대
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: 참여 결과 반환
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
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
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json,!package-lock.json
📒 Files selected for processing (25)
.env.examplepackage.jsonsrc/app/invite/[code]/page.tsxsrc/app/workspaces/[workspaceId]/settings/page.tsxsrc/entities/workspace-member/api/update-my-nickname.tssrc/entities/workspace-member/index.tssrc/entities/workspace/api/get-invite-preview.tssrc/entities/workspace/api/get-workspace-by-id.tssrc/entities/workspace/api/join-workspace-by-invite-code.tssrc/entities/workspace/api/send-invite-email.tssrc/entities/workspace/api/set-workspace-invite-enabled.tssrc/entities/workspace/api/update-workspace-info.tssrc/entities/workspace/index.tssrc/entities/workspace/model/workspace.types.tssrc/features/manage-member-profile/ui/MemberProfileForm.tsxsrc/features/manage-workspace-info/ui/WorkspaceInfoForm.tsxsrc/features/manage-workspace-members/model/use-member-management.tssrc/features/manage-workspace-members/ui/MemberInviteSection.tsxsrc/features/manage-workspace-members/ui/MemberManagementPanel.tsxsrc/shared/api/email/env.tssrc/shared/model/database.types.tssrc/views/invite/index.tssrc/views/invite/ui/InviteAcceptView.tsxsrc/views/settings/ui/SettingsView.tsxsupabase/migrations/20260713010000_create_invite_rpcs.sql
|
|
||
| export default async function InvitePage({ params }: InvitePageProps) { | ||
| const { code } = await params; | ||
| const preview = await getInvitePreview(code); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
fd -p 'src/app/invite' --type fRepository: 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/**' || trueRepository: 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/**' || trueRepository: 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.
| 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> | ||
| ); | ||
| } |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
초대 실패 화면 UI를 app 레이어에 직접 구현했습니다.
InvitePage는 유효한 초대는 InviteAcceptView로 위임하지만, "유효하지 않은 초대" 화면은 app 레이어에서 직접 마크업을 구현하고 있습니다. src/views/invite에 InviteInvalidView 같은 뷰 컴포넌트를 추가해 이 분기도 위임하면 일관성이 생기고, 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
| 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}` : ''; | ||
| } |
There was a problem hiding this comment.
🔒 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.
| 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> | ||
| `, | ||
| }); |
There was a problem hiding this comment.
🔒 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 이름이 사용되도록 처리합니다.
| 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; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
canInvite에 isInviteEnabled 조건 누락.
초대 링크가 비활성화된 상태에서도 이메일 초대 버튼이 활성화되어 제출이 가능합니다. sendInviteEmail은 workspace.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.
| 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.
| 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; |
There was a problem hiding this comment.
🔒 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/migrationsRepository: 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/migrationsRepository: 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)}')
PYRepository: 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/migrationsRepository: 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.
| if v_workspace_id is null then | ||
| raise exception 'invalid_or_disabled_invite' | ||
| using message = '유효하지 않거나 비활성화된 초대 링크입니다.'; | ||
| end if; |
There was a problem hiding this comment.
🎯 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 -SRepository: 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 -SRepository: 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:
- 1: https://docs.yugabyte.com/stable/api/ysql/user-defined-subprograms-and-anon-blocks/language-plpgsql-subprograms/plpgsql-syntax-and-semantics/executable-section/basic-statements/raise/
- 2: https://marc.info/?l=pgsql-hackers&m=147369551921902
- 3: https://www.postgresql.org/docs/18/plpgsql-errors-and-messages.html
- 4: https://www.postgresql.org/docs/17/plpgsql-errors-and-messages.html
- 5: https://www.postgresql.org/docs/15/plpgsql-errors-and-messages.html
- 6: https://www.postgresql.org/docs/16/plpgsql-errors-and-messages.html
🏁 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' -nRepository: 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 -nRepository: 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.
Pull Request
작업 내용
작업 결과
/invite/[code]로 워크스페이스 참여 가능변경 사항
Added
/invite/[code],InviteAcceptViewget_invite_preview,join_workspace_by_invite_code(SECURITY DEFINER)updateWorkspaceInfo,updateMyNickname,setWorkspaceInviteEnabled,joinWorkspaceByInviteCode,sendInviteEmailRESEND_API_KEY,INVITE_EMAIL_FROM)20260713010000_create_invite_rpcs.sqlChanged
getWorkspaceById에invite_code,invite_enabled추가Fixed
실행화면
테스트
리뷰 체크리스트
feature/*->develop, 배포 시develop또는release/*->main)Type/#issue-number/description형식을 따릅니다.console.log, 주석, 임시 코드를 제거했습니다.리뷰 요청사항
SECURITY DEFINERRPC(get_invite_preview,join_workspace_by_invite_code) 내부 검증 로직참고
npm run gen:types필요RESEND_API_KEY미설정 시 이메일 발송 불가 (도메인 인증 전에는 Resend 테스트 모드)DEV_USER_ID사용 → 연동 시auth.uid()로 교체 예정관련 이슈
Closes #47
Summary by CodeRabbit
새로운 기능
개선 사항