프로젝트 개요
🏢 조직 구성
| 임원실 | 경영진·전사 대시보드·승인 |
| 회계정리 | 입금·지출·커미션 정산 |
| A1팀 | 신규 문의·상담·계약 |
| A2팀 | 계약 고객사 영업운영 |
| O2팀 | 세무사 전용 영업관리 |
✅ 필수 확정 조건
- ✅ 외부 고객사 로그인 불가
- ✅ A1 커미션 = 실제 입금액 기준
- ✅ A2·O2 14일·월간 자동 보고서
- ✅ 팀별·프로젝트별·1:1 채팅
- ✅ 채팅 파일 공유 + 프로젝트 자료실
- ✅ PC 중심 반응형 + 모바일 지원
💡 A1 커미션 핵심 로직
입금(payments) 상태가 CONFIRMED/COMPLETED로 변경되는 순간
DB 트리거 auto_create_commission()가 자동 실행되어
commission_entries에 금액을 계산·삽입합니다.
기준: amount_received × rule_pct (상한 적용)
기술 스택 & 배포 환경
| 레이어 | 기술 | 역할 | 비고 |
|---|---|---|---|
| 프론트엔드 | React 18 + TypeScript | SPA UI | Vite 번들 |
| 라우팅 | React Router v6 | 클라이언트 라우팅 | BrowserRouter |
| 상태관리 | @tanstack/react-query | 서버 상태 캐싱 | Supabase 연동 |
| 스타일 | Tailwind CSS | 유틸리티 CSS | @tailwindcss/forms |
| 폼 | react-hook-form + zod | 유효성 검사 | |
| 차트 | Recharts | 대시보드 시각화 | |
| 아이콘 | Lucide React | UI 아이콘 | |
| 인증 | Supabase Auth | 이메일+비밀번호 | Sign Up 비활성화 |
| DB | Supabase PostgreSQL | 관계형 DB + RLS | 별도 프로젝트 |
| 실시간 | Supabase Realtime | 채팅 실시간 수신 | Websocket 기반 |
| 파일 | Supabase Storage | 파일 업로드/다운 | Private 버킷 |
| 서버리스 | Supabase Edge Functions | 초대·보고서·감사 | Deno 런타임 |
| 스케줄 | pg_cron | 자동 보고서 | 매월 1·15일 |
| 배포 | Cloudflare Pages | SPA 호스팅 | _redirects 필요 |
public/_redirects 파일에 /* /index.html 200 추가 → Cloudflare Pages 새로고침 404 방지부서별 업무 흐름도
🟢 A1팀 — 문의→계약→커미션
출처 기록, 담당자 배정 (status: NEW)
consultation_logs 작성 (status: IN_PROGRESS)
계약서 작성·서명, 문서 첨부 (status: SIGNED)
A2팀 또는 O2팀으로 인계 (lead_deliveries)
회계팀 입금 CONFIRMED → DB 트리거 commission_entries 생성
🟡 A2팀 — 고객사 운영
A1 인계 후 고객사 생성·담당자 배정
KPI 설정, 멤버 구성, 채팅방·자료실 자동 생성
일지 작성, 파일 공유 (자료실)
14일(매 1·15일), 월간(매 1일) 자동 생성
🔴 O2팀 — 세무사 영업
법인명·세무사 정보, 담당 O2직원 배정
방문·미팅·세미나 기록
세무파트너에 고객 리드 배분 (O2_TO_PARTNER)
A2와 동일 주기 (14일·월간)
🔵 회계팀 — 입금·정산
계약 연결, 입금액 기록
status CONFIRMED → A1 커미션 자동 생성
VERIFIED → PAID, 지급일 기록
입금·지출·커미션 합계
ERD — 주요 테이블
32+ 테이블추가 정규화 테이블 — 설계 근거
| 테이블 | 설계 근거 | 핵심 컬럼 |
|---|---|---|
client_contacts | 고객사 담당자 개인정보(이름·전화·이메일)를 clients 테이블에서 분리 → 개인정보 접근 최소화 원칙 | contact_name, contact_phone, contact_email (PII) |
tax_partner_contacts | 동일 이유: 세무파트너 담당자 PII 분리 | contact_name, contact_phone, contact_email (PII) |
user_invitations | Supabase 초대 상태 추적, 역할 사전 배정을 위한 중간 테이블. 초대 이력 감사 가능 | email, intended_role, status, expires_at |
contract_history | 계약 상태 변경 이력 추적 (법적 분쟁·감사 대비). contracts UPDATE 트리거로 자동 기록 | old_status, new_status, old_data, new_data |
kpi_history | KPI 수치 변경 추적 → 보고서 데이터 소급 검증 가능 | recorded_value, recorded_at, recorded_by |
notification_settings | 사용자별 알림 수신 설정 정규화 → 알림 스팸 방지 | chat_mention, task_assigned 등 BOOLEAN |
tags / entity_tags | 문의·고객사·프로젝트·태스크에 유연한 태그 부착 → 다차원 검색·분류 지원 | entity_type, entity_id (범용 참조) |
주요 인덱스 & 제약조건
🔑 UNIQUE 제약
inquiries.inquiry_no | INQ-YYYYMM-XXXX |
contracts.contract_no | CNT-YYYYMM-XXXX |
payments.payment_no | PAY-YYYYMM-XXXX |
projects.project_no | PRJ-YYYYMM-XXXX |
daily_reports(staff_id,date) | 하루 1회 제한 |
project_members(proj,profile) | 중복 참여 방지 |
role_assignments(profile,role) | 역할 중복 방지 |
files.storage_path | Storage 경로 유일 |
clients.business_no | 사업자번호 유일 |
⚡ 핵심 인덱스
inquiries(status) | 상태 필터 |
payments(period) | 월별 입금 조회 |
commission_entries(period_year,month) | 커미션 정산 |
chat_messages(room_id, created_at DESC) | 채팅 페이징 |
notifications(recipient_id) WHERE unread | 미읽음 카운트 |
clients(company_name) GIN trgm | 고객사 검색 |
inquiries(company_name) GIN trgm | 문의 검색 |
chat_messages(mentions) GIN | 멘션 검색 |
🚧 CHECK 제약
payments.amount_due ≥ 0 | 음수 방지 |
commission_entries.commission_amt ≥ 0 | 음수 방지 |
expenses.amount > 0 | 0원 방지 |
contract dates | end ≥ start |
commission_rules | rate OR fixed 필수 |
chat_rooms DIRECT | 1:1은 project 불가 |
lead_deliveries | from ≠ to |
period_month | 1~12 범위 |
권한 매트릭스
| 기능/테이블 | admin | 임원 | 회계 | A1팀장 | A1직원 | A2팀장 | A2직원 | O2팀장 | O2직원 |
|---|---|---|---|---|---|---|---|---|---|
| 부서·사용자 관리 | CRUD | R | — | — | — | — | — | — | — |
| 문의 (inquiries) | CRUD | R | R | CRUD | CRU | R | — | R | — |
| 계약 (contracts) | CRUD | R | R | CRUD | CRU | R | R | R | R |
| 입금 (payments) | CRUD | R | CRUD | R* | — | — | — | — | — |
| 커미션 (commission_entries) | CRUD | R | RU | R(본인) | R(본인) | — | — | R(본인) | R(본인) |
| 고객사 A2 (clients) | CRUD | R | R | R | R | CRUD | R | — | — |
| 고객사 O2 (clients) | CRUD | R | R | R | R | — | — | CRUD | R |
| 프로젝트 (A2) | CRUD | R | R | — | — | CRUD | 멤버 | — | — |
| 세무파트너 (O2) | CRUD | R | — | — | — | — | — | CRUD | R |
| 자동 보고서 | CRUD | R | — | R | R | R | R | R | R |
| 채팅 (chat_messages) | CRUD | CRUD | CRUD | CRUD | CRUD | CRUD | CRUD | CRUD | CRUD |
| 파일 (프로젝트 자료실) | CRUD | R | R | — | — | 멤버 | 멤버 | 멤버 | 멤버 |
| 감사 로그 | R | R | — | — | — | — | — | — | — |
* R(본인): 본인 담당 데이터만 조회, 멤버: 프로젝트 멤버만 접근
RLS 정책 핵심
auth.uid() 기반 현재 사용자 식별.🔧 헬퍼 함수
-- 역할 확인 (캐시 가능)
has_role(user_role) → BOOLEAN
has_any_role(user_role[]) → BOOLEAN
is_executive_or_admin() → BOOLEAN
is_a1() / is_a2() / is_o2() → BOOLEAN
is_manager_or_above() → BOOLEAN
-- 접근 권한 확인
is_project_member(project_id) → BOOLEAN
is_chat_member(room_id) → BOOLEAN
💡 정책 패턴 예시
-- 채팅 메시지: 룸 멤버만
CREATE POLICY "chat_msg_select"
ON chat_messages FOR SELECT
USING (
is_executive_or_admin()
OR is_chat_member(room_id)
);
-- 파일: 프로젝트 멤버만
CREATE POLICY "files_select"
ON files FOR SELECT
USING (
is_project_member(project_id)
OR is_executive_or_admin()
);
인증 & 사용자 초대 절차
이메일, 역할, 부서 입력
invite-user Edge Function 실행 → Supabase Auth 초대 이메일 발송
/auth/reset-password/confirm 페이지
handle_new_user() → profiles 생성 + role_assignments 배정 + notification_settings 초기화
감사 로그 설계
| 이벤트 | audit_action | 기록 주체 |
|---|---|---|
| 로그인/로그아웃 | LOGIN / LOGOUT | Edge Function |
| 사용자 초대 | INVITE_SENT | invite-user Function |
| 역할 변경 | ROLE_CHANGED | DB 트리거 |
| 파일 업로드/다운로드/삭제 | FILE_UPLOAD/DOWNLOAD/DELETE | DB 트리거 + Edge Function |
| 보고서 생성/열람 | REPORT_GENERATED/VIEWED | Edge Function |
| 커미션 계산/지급 | COMMISSION_CALCULATED/PAID | DB 트리거 |
| 계약·입금 상태 변경 | UPDATE | DB 트리거 |
파일 접근 & Storage 설계
🗂️ Storage 버킷 구조
erp-files (Private)
├── contracts/{contract_id}/
├── projects/{project_id}/
├── chat/{room_id}/
├── expenses/{expense_id}/
├── reports/{report_id}/
└── general/{uploader_id}/
🔐 파일 다운로드 흐름
📋 파일 정책
- 최대 파일 크기: 50MB
- 허용 형식: PDF, Word, Excel, PPT, 이미지, CSV
- 소프트 삭제 (30일 후 Storage 실제 삭제)
- Storage RLS: 버킷 레벨 정책 적용
- 개인정보 포함 파일: 별도 접근 제어
단계별 구현 계획 (~29주)
남은 이슈 & 검토 필요 사항
| # | 이슈 | 중요도 | 상태 |
|---|---|---|---|
| 1 | 회사 이메일 도메인 확정 (이메일 도메인 제한 적용) | 높음 | 미결 |
| 2 | 커미션 규칙 세부 요율 확정 (A1·O2 별도 또는 공통) | 높음 | 미결 |
| 3 | O2 커미션 계산 로직 별도 정의 여부 | 높음 | 미결 |
| 4 | 보고서 PDF 생성 도구 선택 (Puppeteer Edge Function vs react-pdf) | 중간 | 미결 |
| 5 | 외부 이메일 발송 서비스 (Resend vs SendGrid) | 중간 | 미결 |
| 6 | pg_cron 보고서 실행 시간 (KST 기준, Supabase UTC+0 주의) | 중간 | 미결 |
| 7 | Supabase Storage 용량 한도 (Pro 플랜 100GB, 초과 시 추가 과금) | 중간 | 미결 |
| 8 | 계약 만료 D-30 자동 알림 설정 | 중간 | 미결 |
| 9 | 임원 대시보드 데이터 갱신 주기 (실시간 vs 5분 캐시) | 낮음 | 미결 |
| 10 | 모바일 PWA 지원 범위 확정 | 낮음 | 미결 |
위 10개 이슈 중 #1(도메인), #2·#3(커미션 규칙)은 마이그레이션 전 확정 필요. 나머지는 구현 단계에서 순차 결정 가능. 설계 승인 전까지 운영 데이터 생성 및 변경 없음.
생성 산출물 목록
| 파일 | 내용 | 크기 |
|---|---|---|
01_menu_routes.md | 전체 메뉴 트리, 페이지 라우트, 역할별 접근 표 | 6.4KB |
02_workflow.md | 5개 부서 업무 흐름도, 자동 보고서 트리거, 초대 흐름 | 4.4KB |
03_erd_migration_part1.sql | ENUM·기반 테이블 (departments~expenses), 트리거, 자동번호 | 21KB |
03_erd_migration_part2.sql | 나머지 테이블 (projects~audit_logs), 추가 정규화 테이블 | 24.5KB |
04_constraints_index.sql | 인덱스, UNIQUE·CHECK 제약, FK 다이어그램 | 14.3KB |
05_rls_permissions.sql | 헬퍼 함수, 전체 RLS 정책, 권한 매트릭스 주석 | 25.2KB |
06_auth_invite.md | 인증 설정, 초대 절차, Edge Function 코드, 보안 체크리스트 | 7.3KB |
07_audit_file.md | 감사 로그 설계, Storage RLS, Signed URL, 개인정보 마스킹 | 10.7KB |
08_implementation_plan.md | 10단계 구현 계획, 테스트 기준, 남은 이슈 | 8.5KB |
index.html | 통합 HTML 설계 보고서 (본 파일) | ~40KB |