111 lines
4 KiB
PL/PgSQL
111 lines
4 KiB
PL/PgSQL
-- 회의 생성 시 문서 템플릿은 선택 사항이다. 템플릿 없이도 회의를 시작할 수
|
|
-- 있어야 하며(데스크톱 동작과 동일), 문서 생성 시점에 사용자 템플릿 라이브러리
|
|
-- 에서 템플릿이 해결된다. meetings.template_id 컬럼과 소유권/생성 무결성
|
|
-- 트리거(enforce_meeting_template_owner_v1, enforce_meeting_creation_identity_v1)
|
|
-- 는 이미 NULL을 허용하므로 생성 RPC의 인자 검증만 완화한다.
|
|
--
|
|
-- 주의: idempotency 요청 해시에 template_id가 포함되므로, 같은 idempotency
|
|
-- 키를 NULL/NOT NULL 템플릿으로 재시도하면 creation_request_hash 불일치로
|
|
-- meeting_creation_idempotency_conflict가 발생한다 — 의도된 방어 동작이다.
|
|
|
|
CREATE OR REPLACE FUNCTION public.mobile_create_meeting_workspace_v2(
|
|
p_title text,
|
|
p_attendees jsonb,
|
|
p_language text,
|
|
p_template_id uuid,
|
|
p_idempotency_key uuid
|
|
)
|
|
RETURNS public.meetings
|
|
LANGUAGE plpgsql
|
|
SECURITY DEFINER
|
|
SET search_path = pg_catalog, public, auth, extensions
|
|
AS $$
|
|
DECLARE
|
|
actor_id uuid := auth.uid();
|
|
safe_title text := regexp_replace(trim(p_title), '[[:space:]]+', ' ', 'g');
|
|
safe_language text := lower(trim(p_language));
|
|
safe_attendees jsonb;
|
|
request_digest text;
|
|
created public.meetings;
|
|
BEGIN
|
|
IF actor_id IS NULL THEN
|
|
RAISE EXCEPTION 'authentication_required' USING ERRCODE = '42501';
|
|
END IF;
|
|
IF p_idempotency_key IS NULL THEN
|
|
RAISE EXCEPTION 'meeting_creation_identifiers_required' USING ERRCODE = '22023';
|
|
END IF;
|
|
IF p_title IS NULL OR char_length(safe_title) NOT BETWEEN 1 AND 160 THEN
|
|
RAISE EXCEPTION 'invalid_meeting_title' USING ERRCODE = '22023';
|
|
END IF;
|
|
IF p_language IS NULL OR safe_language NOT IN ('ko', 'en', 'ja', 'zh-cn') THEN
|
|
RAISE EXCEPTION 'invalid_meeting_language' USING ERRCODE = '22023';
|
|
END IF;
|
|
IF NOT public.is_valid_meeting_attendees_v1(p_attendees) THEN
|
|
RAISE EXCEPTION 'invalid_meeting_attendees' USING ERRCODE = '22023';
|
|
END IF;
|
|
IF p_template_id IS NOT NULL AND NOT EXISTS (
|
|
SELECT 1
|
|
FROM public.user_templates
|
|
WHERE id = p_template_id
|
|
AND user_id = actor_id
|
|
AND template_kind = 'meeting_document'
|
|
) THEN
|
|
RAISE EXCEPTION 'meeting_template_not_found' USING ERRCODE = 'P0002';
|
|
END IF;
|
|
|
|
PERFORM set_config('d3ro.meeting_creation_actor', actor_id::text, true);
|
|
|
|
SELECT coalesce(
|
|
jsonb_agg(
|
|
to_jsonb(regexp_replace(trim(item.value #>> '{}'), '[[:space:]]+', ' ', 'g'))
|
|
ORDER BY item.ordinality
|
|
),
|
|
'[]'::jsonb
|
|
)
|
|
INTO safe_attendees
|
|
FROM jsonb_array_elements(p_attendees) WITH ORDINALITY AS item(value, ordinality);
|
|
|
|
request_digest := encode(extensions.digest(
|
|
jsonb_build_object(
|
|
'title', safe_title,
|
|
'attendees', safe_attendees,
|
|
'language', safe_language,
|
|
'template_id', p_template_id
|
|
)::text,
|
|
'sha256'
|
|
), 'hex');
|
|
|
|
INSERT INTO public.meetings(
|
|
user_id, title, status, language, attendees, template_id,
|
|
creation_idempotency_key, creation_request_hash
|
|
)
|
|
VALUES (
|
|
actor_id, safe_title, 'recording', safe_language, safe_attendees,
|
|
p_template_id, p_idempotency_key, request_digest
|
|
)
|
|
ON CONFLICT (user_id, creation_idempotency_key)
|
|
WHERE creation_idempotency_key IS NOT NULL
|
|
DO NOTHING
|
|
RETURNING * INTO created;
|
|
|
|
IF created.id IS NULL THEN
|
|
SELECT * INTO created
|
|
FROM public.meetings
|
|
WHERE user_id = actor_id
|
|
AND creation_idempotency_key = p_idempotency_key;
|
|
IF created.id IS NULL THEN
|
|
RAISE EXCEPTION 'meeting_creation_retry_state_missing' USING ERRCODE = 'PT409';
|
|
END IF;
|
|
IF created.creation_request_hash <> request_digest THEN
|
|
RAISE EXCEPTION 'meeting_creation_idempotency_conflict' USING ERRCODE = 'PT409';
|
|
END IF;
|
|
END IF;
|
|
|
|
RETURN created;
|
|
END;
|
|
$$;
|
|
|
|
REVOKE ALL ON FUNCTION public.mobile_create_meeting_workspace_v2(text, jsonb, text, uuid, uuid)
|
|
FROM PUBLIC, anon;
|
|
GRANT EXECUTE ON FUNCTION public.mobile_create_meeting_workspace_v2(text, jsonb, text, uuid, uuid)
|
|
TO authenticated;
|