Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion 0028.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ create table app.business_licenses (
);
```

- 신청 전제: `business_profiles` 승인 완료(사업자 인증 없이 업종 증빙만 낼 수 없음).
- 신청 전제: `business_profiles` 존재(pending 포함) — 업체 등록 폼에서 등록·허가증을 **동시 제출**할 수 있게(신청 두 번 방지, 6차 개정). 단 업종 **승인**은 업체 인증 승인이 선행돼야 한다(심사 RPC 가 보장).
- 승인 흐름은 0025 관리자 승인과 같은 패턴(admin RPC + 감사 로그). facilities 공공데이터의
업종 컬럼과 대조해 **참고 신호**로 표시하되, 자동승인은 두지 않는다(문서 위조 리스크가
사업자번호 대조보다 크고, 건수가 적어 수동 검토 비용이 낮다).
Expand Down
2 changes: 1 addition & 1 deletion docs/supabase-db.md
Original file line number Diff line number Diff line change
Expand Up @@ -1036,7 +1036,7 @@ refresh 토큰 저장소 (설계: `docs/refresh-token-flow-design.md`). 원문
- **UNIQUE**: (user_id, license_type) — 계정당 업종별 1행(재신청은 갱신).
- 반려 서류는 6개월 후 파기 큐(`business_doc_purge_queue`, 재신청 시 회수), 교체 서류는 1개월.
- 게이트: `app.has_license(p_type)` [SD] — 도구 RPC 첫 줄에서 호출(클라이언트 직접 호출 경로 없음).
- RPC: `apply_business_license(p_type, p_license_no, p_document_path)` (승인 업체 전제, 본인 폴더 경로만), `my_business_licenses()`, `admin_list_business_licenses(p_status='pending', ...)` [admin], `admin_review_business_license(p_license, p_status, p_reason?)` [admin — 반려 사유 필수, business_approved/rejected 알림 재사용, admin_logs 기록].
- RPC: `apply_business_license(p_type, p_license_no, p_document_path)` (업체 인증 **pending 부터** 신청 가능 — 등록 폼 동시 제출 지원, 본인 폴더 경로만), `my_business_licenses()`, `admin_list_business_licenses(p_status='pending', ...)` [admin], `admin_review_business_license(p_license, p_status, p_reason?)` [admin — 반려 사유 필수, **승인은 업체 인증 approved 선행 필수(business_not_approved)**, business_approved/rejected 알림 재사용, admin_logs 기록].

### app.share_links

Expand Down
149 changes: 149 additions & 0 deletions supabase/migrations/20260723030000_license_apply_with_registration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
-- ============================================================================
-- 업종 인증을 업체 등록과 한 번에 신청 (0028 §1 보완)
--
-- 기존: 업체 인증 '승인' 후에만 업종 인증 신청 가능 → 사장님이 서류·심사·대기를
-- 두 번 겪는다(파일럿 온보딩 이탈 포인트). 신청 게이트를 pending 까지 완화해
-- 업체 등록 폼에서 등록·허가증을 함께 제출할 수 있게 한다.
--
-- 순서 보장은 승인 쪽으로 이동: 업종 인증 '승인'은 업체 인증이 approved 일 때만
-- (business_not_approved). 반려는 순서 무관. 서류 정합(무허가 차단)은 그대로.
-- ============================================================================

-- (1) 신청 게이트 완화 — pending 업체도 신청 가능(rejected 는 업체 재신청과 함께)
create or replace function public.apply_business_license(
p_type text,
p_license_no text,
p_document_path text
) returns uuid
language plpgsql
security definer
set search_path to ''
as $function$
declare
v_uid uuid := app.uid();
v_type app.biz_license_type;
v_no text := nullif(btrim(coalesce(p_license_no, '')), '');
v_path text := nullif(btrim(coalesce(p_document_path, '')), '');
v_old app.business_licenses%rowtype;
v_id uuid;
begin
if v_uid is null then
raise exception 'auth required' using errcode = '42501';
end if;
-- 업체 등록과 동시 신청 허용 — pending 포함(승인 순서는 심사 RPC 가 보장).
if not exists (select 1 from public.business_profiles b
where b.user_id = v_uid and b.status in ('pending', 'approved')) then
raise exception 'biz_profile_required' using errcode = 'P0001';
end if;
begin
v_type := p_type::app.biz_license_type;
exception when others then
raise exception 'invalid_type' using errcode = 'P0001';
end;
if v_no is null or length(v_no) < 4 or length(v_no) > 40 then
raise exception 'invalid_license_no' using errcode = 'P0001';
end if;
if v_path is null or position(v_uid::text || '/' in v_path) <> 1 then
raise exception 'invalid_document_path' using errcode = 'P0001';
end if;

select * into v_old from app.business_licenses
where user_id = v_uid and license_type = v_type;
if found and v_old.status = 'approved' then
raise exception 'already_approved' using errcode = 'P0001';
end if;

delete from app.business_doc_purge_queue where path = v_path;
if found and v_old.document_path <> v_path then
insert into app.business_doc_purge_queue (path, reason, purge_after)
values (v_old.document_path, 'replaced', now() + interval '1 month');
end if;

insert into app.business_licenses (user_id, license_type, license_no, document_path)
values (v_uid, v_type, v_no, v_path)
on conflict (user_id, license_type) do update
set license_no = excluded.license_no,
document_path = excluded.document_path,
status = 'pending', reject_reason = null,
reviewed_by = null, reviewed_at = null, updated_at = now()
returning id into v_id;
return v_id;
end;
$function$;

-- (2) 승인 순서 보장 — 업체 인증 미승인 상태의 업종 인증 '승인' 차단
create or replace function public.admin_review_business_license(
p_license uuid,
p_status text,
p_reason text default null
) returns void
language plpgsql
security definer
set search_path to ''
as $function$
declare
v_row app.business_licenses%rowtype;
v_reason text := nullif(btrim(coalesce(p_reason, '')), '');
v_label text;
begin
if not app.is_admin() then
raise exception 'forbidden' using errcode = '42501';
end if;
if p_status not in ('approved', 'rejected') then
raise exception 'invalid_status' using errcode = 'P0001';
end if;
select * into v_row from app.business_licenses where id = p_license;
if not found then
raise exception 'license_not_found' using errcode = 'P0001';
end if;
if v_row.status = p_status then
raise exception 'no_change' using errcode = 'P0001';
end if;
-- 동시 신청 허용의 대가: 업종 승인은 업체 인증 승인이 선행돼야 한다.
if p_status = 'approved' and not exists (
select 1 from public.business_profiles b
where b.user_id = v_row.user_id and b.status = 'approved'
) then
raise exception 'business_not_approved' using errcode = 'P0001';
end if;

v_label := case v_row.license_type
when 'grooming' then '동물미용업' when 'boarding' then '동물위탁관리업'
when 'sales' then '동물판매업' when 'production' then '동물생산업'
when 'exhibition' then '동물전시업' when 'transport' then '동물운송업'
end;

if p_status = 'rejected' then
if v_reason is null then
raise exception 'reason_required' using errcode = 'P0001';
end if;
update app.business_licenses set
status = 'rejected', reject_reason = v_reason,
reviewed_by = app.uid(), reviewed_at = now(), updated_at = now()
where id = p_license;

insert into app.business_doc_purge_queue (path, reason, purge_after)
values (v_row.document_path, 'license_rejected', now() + interval '6 months');

insert into public.notifications (user_id, notification_type, is_system, title, body)
values (v_row.user_id, 'business_rejected', true,
v_label || ' 인증이 반려되었어요',
'사유: ' || v_reason || E'\n업체 관리에서 보완 후 다시 신청할 수 있어요.');
else
update app.business_licenses set
status = 'approved', reject_reason = null,
reviewed_by = app.uid(), reviewed_at = now(), updated_at = now()
where id = p_license;

insert into public.notifications (user_id, notification_type, is_system, title, body)
values (v_row.user_id, 'business_approved', true,
v_label || ' 인증이 완료되었어요',
v_label || ' 인증이 승인되었어요. 해당 업종 기능이 열렸어요.');
end if;

insert into public.admin_logs (admin_id, action_type, target_type, target_id, detail)
values (app.uid(), 'review_business_license', 'user', v_row.user_id,
jsonb_build_object('license_id', p_license, 'type', v_row.license_type,
'from', v_row.status, 'to', p_status, 'reason', v_reason));
end;
$function$;
8 changes: 7 additions & 1 deletion supabase/schema/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -2907,6 +2907,12 @@ begin
if v_row.status = p_status then
raise exception 'no_change' using errcode = 'P0001';
end if;
if p_status = 'approved' and not exists (
select 1 from public.business_profiles b
where b.user_id = v_row.user_id and b.status = 'approved'
) then
raise exception 'business_not_approved' using errcode = 'P0001';
end if;

v_label := case v_row.license_type
when 'grooming' then '동물미용업' when 'boarding' then '동물위탁관리업'
Expand Down Expand Up @@ -3259,7 +3265,7 @@ begin
raise exception 'auth required' using errcode = '42501';
end if;
if not exists (select 1 from public.business_profiles b
where b.user_id = v_uid and b.status = 'approved') then
where b.user_id = v_uid and b.status in ('pending', 'approved')) then
raise exception 'biz_profile_required' using errcode = 'P0001';
end if;
begin
Expand Down
30 changes: 29 additions & 1 deletion supabase/tests/t10_business_licenses_test.sql
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
begin;
set local search_path = public, app, extensions;
\ir helpers/seed.sql
select plan(9);
select plan(11);

-- 추가 시드: 관리자(심사자).
create temp table t10 (k text primary key, id uuid not null);
Expand Down Expand Up @@ -102,5 +102,33 @@ select is(
'재신청 시 pending 복귀 + 파기 큐 회수'
);

-- ⑧ 업체 등록과 동시 신청 — pending 업체도 신청 가능(등록 폼 동시 제출).
insert into public.business_profiles
(user_id, business_reg_no, declared_category, business_name,
business_address, contact_email, license_image_path, status)
select id, '9999999999', 'grooming', '동시신청테스트',
'서울 어딘가 1', 'p@test.co', 'x/p.png', 'pending'
from seed where k = 'friend';
select set_config('request.jwt.claims',
json_build_object('sub', (select id from seed where k='friend'), 'tv', 0)::text, true);
select lives_ok(
$$select public.apply_business_license('grooming', 'C-9999',
(select id::text || '/licenses/c.png' from seed where k='friend'))$$,
'pending 업체의 동시 신청 허용'
);

-- ⑨ 단, 업종 '승인'은 업체 인증 승인이 선행돼야 한다(순서 보장).
select set_config('request.jwt.claims',
json_build_object('sub', (select id from t10 where k='admin'), 'tv', 0)::text, true);
select throws_like(
$$select public.admin_review_business_license(
(select l.id from app.business_licenses l
where l.user_id = (select id from seed where k='friend')
and l.license_type = 'grooming'),
'approved')$$,
'%business_not_approved%',
'업체 미승인 상태의 업종 승인 차단'
);

select * from finish();
rollback;
Loading