안전교육 강사(요원) 배정 업무를 엑셀·수기 처리에서 벗어나게 하기 위한 시스템입니다. 학교의 교육 의뢰 접수부터 강사 배정, 견적서 발행, 정산 수수료 산출, 배정 결과의 모바일 실시간 알림까지 하나의 어드민 웹과 하나의 모바일 앱으로 처리합니다.
🔗 Live Demo: distop-rho.vercel.app (관리자 로그인 필요 — 실제 화면은 아래 데모 GIF 참고)
기획부터 어드민 웹, 모바일 앱, DB 설계까지 전 영역을 단독으로 개발했습니다.
- 도메인 모델 설계: 의뢰(Job) → 배정(Assignment) → 정산(Settlement)으로 이어지는 핵심 플로우와 학교/교사/강사(요원)/견적/공지/푸시 발송이력을 아우르는 Prisma 스키마(20여 개 모델) 설계
- 어드민 웹 기능 구현: 의뢰 접수·자동 견적 계산, 드래그앤드롭 기반 배정 현황 보드(
@dnd-kit), 월별 일정 캘린더, PDF 견적서 생성(@react-pdf/renderer) 및 이메일 발송(Resend) 구현 - 모바일 앱 설계·구현: Expo Router 기반으로 학교(교사)/요원 두 역할의 탭 네비게이션을 분리 설계하고, 로그인부터 배정 확인·정산 조회·공지 열람까지의 REST API 연동 구현
- DB 보안 모델 설계: Supabase Security Advisor가 지적한 RLS 취약점을 SECURITY INVOKER 뷰, 컬럼 단위 SELECT 권한, SECURITY DEFINER 헬퍼 함수로 해결 (자세히)
- 인증/계정 발급 흐름 설계: 전화번호·이메일 기반 가상 계정을 어드민에서 발급하고, 최초 로그인 시 임시 비밀번호 변경을 강제하는 흐름 구현
| Area | Stack |
|---|---|
| Admin Web | Next.js 16 (App Router), TypeScript, TailwindCSS, Recharts, @dnd-kit |
| Mobile App | Expo SDK 57 (Expo Router), React Native, TanStack Query |
| Backend / API | Next.js API Routes, Supabase Auth (JWT/Bearer 인가) |
| Database | Supabase (PostgreSQL), Prisma ORM, Row Level Security |
| 기타 연동 | Resend(이메일 발송), @react-pdf/renderer(견적서 PDF), Expo Notifications(푸시), EAS Update(OTA 배포) |
Distop/
├── distop-admin/ # Next.js 기반 어드민 웹 애플리케이션 & API 백엔드
│ ├── prisma/ # Prisma 스키마 및 DB 마이그레이션 SQL 스크립트
│ └── src/app/ # Next.js App Router 구조 (대시보드, 일정, 의뢰, 정산 관리)
│
├── distop-app/ # React Native / Expo 기반 모바일 애플리케이션
│ ├── app/ # Expo Router 구조 (요원 전용/학교 교사 전용 탭 레이아웃)
│ └── lib/ # Supabase 클라이언트 및 세션 저장소 공통 모듈
│
└── .agents/ # 개발 에이전트를 위한 프로젝트 컨벤션 및 RLS 검증 규칙 (AGENTS.md)
graph TD
%% Clients
Admin[어드민 웹 Panel <br/> Next.js / Tailwind]
App[모바일 APP <br/> Expo / React Native]
%% APIs
AdminAPI[Next.js API Routes <br/> /api/*]
SupaSDK[Supabase Client SDK <br/> authenticated / anon]
%% Database
subgraph Supabase / PostgreSQL
PRISMA_BYPASS[Prisma Client <br/> postgres bypass RLS]
subgraph Security Invoker Views
ViewA[school_assignments_view]
ViewB[staff_settlements_view]
end
subgraph Raw Tables & RLS Policies
TableA[(jobs)]
TableB[(assignments)]
TableC[(settlements)]
TableD[(schools)]
TableE[(notices / push logs)]
end
Helper[check_job_school <br/> SECURITY DEFINER 헬퍼 함수]
end
%% Connections
Admin -->|Prisma| AdminAPI
AdminAPI -->|postgres role| PRISMA_BYPASS
PRISMA_BYPASS --> TableA
PRISMA_BYPASS --> TableB
PRISMA_BYPASS --> TableC
App -->|Next.js REST API| AdminAPI
App -->|Supabase SDK| SupaSDK
%% RLS & Views evaluation
SupaSDK -->|SELECT| ViewA
SupaSDK -->|SELECT| ViewB
SupaSDK -->|Direct Blocked| TableC
ViewB -->|Reads minimum columns| TableC
ViewA -->|check_job_school| Helper
Helper -->|Read bypass recursion| TableA
TableB -->|RLS checking| Helper
jobs 테이블의 RLS 조건이 assignments를 검사하고, 동시에 assignments의 RLS 조건이 jobs를 다시 조회하면서 무한 재귀(infinite recursion detected)가 발생했습니다. 원인을 추적한 뒤 SECURITY DEFINER 권한의 내부 헬퍼 함수(check_job_school())로 조회 단계를 끊어 순환을 해소했습니다. 이어서 매출액·계좌번호 등 민감 컬럼이 원본 테이블 SELECT 권한을 통해 그대로 노출되던 Security Advisor 경고를 해결하기 위해, 원본 테이블은 필수 식별 컬럼(id, jobId 등)에만 SELECT 권한을 남기고 나머지 조회는 호출자 JWT 권한으로 실행되는 SECURITY INVOKER 뷰(school_assignments_view, staff_settlements_view)로 격리했습니다.
Expo 앱을 웹으로도 빌드해야 하는데, 네이티브 전용 모듈인 expo-secure-store를 그대로 사용하면 SSR/웹 빌드 시 컴파일이 깨지는 문제가 있었습니다. Platform.OS를 기준으로 네이티브에서는 SecureStore, 브라우저에서는 localStorage, Node.js SSR 환경에서는 인메모리 MemoryStorage로 자동 분기되는 스토리지 어댑터를 만들어 세 환경 모두에서 빌드가 깨지지 않도록 했습니다.
폼에서 선택하지 않은 외래키 필드(schoolId, teacherId 등)가 빈 문자열("")로 전송되면 DB의 ENUM/외래키 제약조건 에러가 발생했습니다. API 레이어에서 빈 문자열을 null로 정규화하는 처리를 의뢰·강사·교사 등록/수정 API 전반에 일관되게 적용해 재발을 막았습니다.
- 의뢰 관리 (
/admin/jobs): 접수 대기, 견적서 발행 및 자동 금액 계산 - 배정 현황 (
/admin/assignments/status): 실시간 활성 강사 배정 현황 모니터링 및 상태 추적 - 일정 캘린더 (
/admin/calendar): 월별 강습 및 배정 일정 통합 뷰어 - 알림 발송 이력 (
/admin/push-logs): 요원 및 교사 대상 모바일 푸시 발송 결과 및 읽음 트래킹
- 역할 기반 라우팅: 요원/학교(교사) 계정을 자동 분기해 서로 다른 탭 레이아웃과 화면 제공
- 배정·정산·공지 조회: 배정된 일정 확인, 정산 내역 조회, 학교용 견적서 PDF 열람, 공지사항 확인
- Safe Area 반응형 적용: 기기별 노치·하단 홈 제스처 바에 하단 탭 버튼이 가려지지 않도록
useSafeAreaInsets()기반 동적 패딩 적용 - OTA 업데이트: EAS Update로 스토어 심사 없이 프리뷰/프로덕션 채널에 앱 업데이트 배포
cd distop-admin
# 의존성 설치
npm install
# .env.example을 참고해 .env.local 작성 (Supabase URL/키, DATABASE_URL 등)
cp .env.example .env.local
# Prisma 클라이언트 자동 생성 및 데이터베이스 마이그레이션 적용
npx prisma generate
npx prisma db push
# 로컬 개발 서버 실행
npm run dev
# 프로덕션 빌드 검증
npm run buildcd distop-app
# 의존성 설치
npm install
# .env.example을 참고해 .env 작성 (Supabase URL/키, API Base URL)
cp .env.example .env
# 개발 서버 실행
npm run start보안 망 프록시 내부에서 발생할 수 있는 Node.js SSL 인증서 오류 우회 명령어와 함께 프리뷰 브랜치에 배포합니다.
cd distop-app
# Node.js SSL 인증 검사 임시 해제 후 안드로이드 및 iOS 번들 업데이트
$env:NODE_TLS_REJECT_UNAUTHORIZED="0"
eas update --branch preview --platform android --environment preview --message "업데이트 내용 기술"
eas update --branch preview --platform ios --environment preview --message "업데이트 내용 기술"연간 총매출·요원비·순수익, 월별 매출 추이, 의뢰 상태 현황을 한눈에 보는 대시보드입니다.
등록된 의뢰 목록을 접수대기~근무완료 상태별로 필터링해 확인합니다.
의뢰 등록 폼입니다. 저장 시 견적서가 자동 생성됩니다.
의뢰별 배정 인원 현황과 배정된 요원 목록을 조율·관리하는 화면입니다.
월별 캘린더 뷰로 교육 일정과 배정 현황을 파악합니다.
요원/교사 대상 푸시 알림 발송 결과와 읽음 현황을 확인합니다.
요원의 내 프로필 화면입니다. 인적사항과 지급 계좌 정보를 확인합니다.
배정된 일정을 리스트/캘린더 두 가지 뷰로 확인할 수 있습니다.
근무 완료 건에 대한 정산 내역을 조회합니다.
- 커밋 컨벤션:
feat,fix,docs,chore등 Conventional Commits 스타일을 사용하고, 변경 영역이 명확할 때는fix(ui):,feat(mobile):,feat(auth,database):처럼 스코프를 함께 표기했습니다. - 개발 방식: 1인 개발로 별도 브랜치 전략 없이
main브랜치에 직접 커밋하는 방식으로 진행했습니다.
- Email: gks12090607@gmail.com
- GitHub: @gks1209











