레스토랑 업계가 직면한 디지털 전환의 어려움을 해결하고자 통합 마케팅 플랫폼을 개발하였습니다. 현장에서 식당 운영자들과 대화를 나누며, 고객과의 소통, 마케팅, 로열티 프로그램 운영 등에서 겪는 어려움을 직접 확인할 수 있었습니다. 특히 디지털 도구의 부재로 인한 비효율적인 운영과 고객 관리의 한계를 극복하고자, 식당 운영자와 고객 모두에게 도움이 되는 솔루션을 만들어보고자 했습니다.
- 👤 레스토랑 소유자와 고객 간의 직접적인 소통 채널 제공
- 🎫 디지털 쿠폰 및 스탬프 시스템을 통한 고객 충성도 프로그램
- 📱 실시간 SMS 알림 서비스로 즉각적인 정보 전달
- 📋 개인화된 구독 관리 시스템
- 🔐 강력한 보안 및 인증 시스템
- 📊 상세한 분석 및 통계 기능
- 🌐 다국어 지원 시스템
- 💳 안전한 결제 시스템 연동
-
고객 경험 향상
- 맞춤형 프로모션 제공
- 간편한 예약 시스템
- 실시간 메뉴 업데이트
- 특별 이벤트 알림
-
레스토랑 운영 최적화
- 고객 데이터 분석
- 마케팅 효과 측정
- 예약 관리 자동화
- 매출 증대 지원
-
커뮤니티 형성
- 고객 리뷰 및 평가
- 맞춤형 추천 시스템
- 소셜 미디어 통합
- 이벤트 참여 유도
계정 관리
- 회원가입 및 로그인
- 프로필 관리
- 비밀번호 변경
- 계정 삭제 기능
텍스트 메시지 관리
- 마케팅 메시지 작성 및 예약 발송
- 메시지 템플릿 저장
- 이미지 첨부 기능
- 반복 발송 설정 (일간/주간/월간)
- Twilio API 연동
- 예약 발송 시스템
- 대량 메시지 발송/처리
- 발송 상태 추적
스탬프 시스템
- 스탬프 적립 규칙 설정
- 일일 제한 및 쿨다운 시스템
- 리워드 교환 시스템
- 고객별 스탬프 현황 확인
- NFC 태그 기반 스탬프 적립
- 만료일 관리
사용자 인증 시스템
- JWT 기반 멀티 유저 인증 시스템 설계 및 구현
- OTP 기반 휴대폰 인증 시스템 개발
- 권한 기반 접근 제어 시스템 구축
- 세션 관리 및 토큰 검증 로직 구현
구독자 관리
- 구독자 목록 확인
- 구독자 통계
- 구독 해지 관리
식당 관리
- 식당 정보 등록/수정/삭제
- 식당 프로필 이미지 업로드
- QR 코드 자동 생성
- 영업 시간 및 위치 정보 관리
쿠폰 시스템
- 쿠폰 생성 및 관리
- QR 코드 기반 쿠폰 인증
- 쿠폰 만료일 관리 로직 구현
- 사용 현황 추적
- 자동 만료 설정
- 사용 제한 설정
- 할인율/금액 설정
- 타겟팅 옵션
이미지 처리 시스템
- AWS S3 기반 이미지 업로드
- 이미지 보안 검증
- 파일 관리 시스템
- 동적 이미지 URL 처리
검색 및 필터링
- 고급 검색 기능
- 다중 필터링 옵션
- 정렬 기능
- 실시간 검색 결과
구독 시스템 아키텍처 설계 및 구현
- 무료/유료 구독 플랜 관리 시스템 구축
- 플랜 변경 시 자동 비례 배분(proration) 시스템 개발
- 구독 갱신, 업그레이드, 다운그레드 로직 구현
- 무료 플랜 및 3가지 유료 구독 플랜 (Basic, Pro, Premium) 구현
- 플랜 업그레이드시 즉시 반영 및 차액 정산
- 다운그레이드시 현재 구독 기간 종료 후 자동 변경
- 구독 취소 및 재활성화 기능
크레딧 시스템 개발
- 크레딧 시스템(구독/구매) 설계 및 구현
- 자동 크레딧 갱신 및 이월 시스템 개발 (Webhook)
- 크레딧 사용 우선순위 로직 구현
- 구독 플랜별 기본 크레딧 제공
- 구독 크레딧과 구매 크레딧의 분리 관리
결제 시스템 통합
- Stripe Elements를 활용한 PCI 준수 결제 프로세스 구현 및 안전한 카드 결제 시스템
- 다중 결제 수단 관리 기능 개발
- 자동 결제 및 실패 복구 시스템 구축
- 기본 결제 수단 설정 기능
- 결제 내역 조회 및 영수증/인보이스 다운로드
- 결제 실패 자동 복구 시스템
- 환불 처리 자동화
- 결제 데이터 동기화 시스템
- 다중 통화 지원 시스템
- 휴대폰 번호 기반 회원가입
- OTP 인증
- 프로필 설정
- 계정 설정 관리
- QR 코드 스캔으로 구독
- 구독 중인 식당 목록 확인
- 구독 취소 기능
- 좋아하는 식당 저장
- 커스텀 플레이리스트 생성
- 플레이리스트 관리
- 발급받은 쿠폰 목록 확인
- 쿠폰 사용 및 관리
- 만료 예정 쿠폰 확인
- NFC 태그로 스탬프 적립
- 적립 현황 확인
- 리워드 교환
-
레스토랑 프로필 관리
- 기본 정보 설정
const restaurantSchema = new mongoose.Schema({ name: { type: String, required: true }, address: { type: String, required: true }, description: String, category: String, openingHours: [{ day: String, open: String, close: String }], contacts: { phone: String, email: String, website: String } });
- 메뉴 관리 시스템
- 영업 시간 설정
- 위치 정보 관리
- 사진 갤러리 관리
- 기본 정보 설정
-
마케팅 도구
- 쿠폰 시스템
- 자동 만료 설정
- 사용 제한 설정
- 할인율/금액 설정
- 타겟팅 옵션
- 문자 메시지 캠페인
- 예약 발송
- 대량 발송
- 템플릿 관리
- 발송 결과 분석
- 웰컴 메시지
- 자동 발송 설정
- 맞춤형 메시지
- A/B 테스팅
- 스탬프 카드
- 적립 규칙 설정
- 리워드 설정
- 만료일 관리
- 쿠폰 시스템
-
고객 관리
- 고객 데이터베이스
const customerSchema = new mongoose.Schema({ name: String, phoneNumber: { type: String, required: true }, email: String, joinDate: { type: Date, default: Date.now }, preferences: [{ type: String }], visitHistory: [{ date: Date, amount: Number, items: Array }] });
- 구독자 분석
- 방문 이력 관리
- VIP 고객 관리
- 고객 데이터베이스
-
통계 및 분석
- 실시간 대시보드
- 매출 리포트
- 고객 행동 분석
- 캠페인 성과 분석
-
레스토랑 구독
- 구독 관리
const subscriptionSchema = new mongoose.Schema({ customer: { type: mongoose.Schema.Types.ObjectId, ref: 'Customer', required: true }, restaurant: { type: mongoose.Schema.Types.ObjectId, ref: 'Restaurant', required: true }, status: { type: String, enum: ['active', 'paused', 'cancelled'], default: 'active' }, preferences: { notifications: { sms: Boolean, email: Boolean }, frequency: { type: String, enum: ['daily', 'weekly', 'monthly'] } } });
- 알림 설정
- 맞춤 추천
- 구독 관리
-
플레이리스트 기능
- 즐겨찾기
- 커스텀 리스트
- 방문 예정 리스트
- 방문 이력
-
리워드 시스템
- 쿠폰 관리
- 스탬프 적립
- 포인트 시스템
- 리워드 교환
-
개인화 기능
- 취향 설정
- 알림 관리
- 방문 이력
- 리뷰 관리
성능 최적화 및 보안 강화
-
React 컴포넌트 메모이제이션을 통한 렌더링 최적화
-
사용자 인터페이스의 성능 최적화를 위해 React.memo, useCallback, useMemo 등의 메모이제이션 기법을 적극 활용
-
커스텀 훅을 개발하여 비즈니스 로직의 재사용성을 높였습니다. 예를 들어, useCustomerAuthCheck라는 커스텀 훅을 개발하여 사용자 인증 상태 관리와 토큰 검증을 효율적으로 처리
-
상태 관리 측면에서는 Context API를 활용하여 전역 상태를 관리했으며, 특히 인증 상태와 같은 중요 데이터의 일관성을 유지하기 위해 노력했습니다.
-
로딩 상태, 에러 처리, 성공/실패 메시지 등을 toast 알림으로 구현하여 사용자에게 즉각적인 피드백을 제공
-
MongoDB 트랜잭션 처리로 데이터 일관성 보장
-
결제 실패 시 자동 복구 메커니즘 구현
-
데이터베이스 설계에서는 MongoDB를 사용하여 유연하고 확장 가능한 스키마를 구현했습니다. 특히 매장, 고객, 쿠폰, 스탬프 등 복잡한 관계를 가진 데이터 모델을 효율적으로 설계했으며, 인덱싱을 통해 쿼리 성능을 최적화했습니다. 또한 트랜잭션을 활용하여 데이터 일관성을 보장하고, 동시성 문제를 해결했습니다.
-
보안 측면에서는 JWT를 활용한 토큰 기반 인증 시스템을 구현했으며, bcrypt를 사용하여 비밀번호를 안전하게 해시화했습니다. 특히 사용자 인증 과정에서 OTP 시스템을 구현하여 이중 인증을 제공했으며, rate limiting을 적용하여 무차별 공격을 방지했습니다.
-
문자 메시지 발송 시스템은 Twilio API를 활용하여 구현했으며, 특히 대량 발송 시 발생할 수 있는 성능 문제를 해결하기 위해 큐 시스템을 도입했습니다. 또한 예약 발송 기능을 구현하여 매장 오너가 효율적으로 마케팅을 진행할 수 있도록 했습니다.
-
파일 업로드 시스템에서는 multer를 사용하여 이미지 업로드를 구현했으며, sharp 라이브러리를 활용하여 이미지 최적화를 진행했습니다. 특히 프로필 이미지나 쿠폰 이미지와 같은 미디어 파일의 효율적인 저장과 관리를 위해 AWS S3를 활용했습니다.
-
성능 최적화 측면에서는 데이터베이스 쿼리 최적화, 캐싱 전략 수립, 페이지네이션 구현 등을 통해 대규모 데이터 처리 시에도 빠른 응답 속도를 보장했습니다. 특히 Redis를 활용하여 자주 접근하는 데이터를 캐싱하고, 세션 관리를 효율적으로 수행했습니다.
동시성 제어 문제 해결
결제 처리 과정에서 발생할 수 있는 동시성 문제를 해결하기 위해 다음과 같은 방법을 적용
-
락 메커니즘 구현으로 동시 접근 제어
- 결제 처리 시 고유한 idempotency key 사용
- 데이터베이스 레벨의 트랜잭션 락 적용
- 동시 요청에 대한 대기열 처리
-
중복 결제 방지
-
분산 락 시스템 구현
데이터 정합성 보장
구독 상태 변경과 결제 처리가 동시에 일어날 때의 데이터 정합성을 보장하기 위해:
- 트랜잭션 처리
트랜잭션 정합성 문제 해결
구독 플랜 변경 시 결제와 데이터 업데이트의 정합성 문제가 발생했습니다. 이를 해결하기 위해:
- MongoDB 트랜잭션 도입
- 결제 실패 시 자동 롤백 메커니즘 구현
- 멱등성을 보장하는 API 설계
대량의 쿠폰과 메시지 데이터 처리 시 성능 저하 & 실시간 데이터 동기화로 인한 서버 부하
- Redis 캐싱 도입으로 빈번한 조회 데이터 최적화
- 데이터베이스 인덱싱 전략 수립
- React 컴포넌트 메모이제이션으로 불필요한 리렌더링 방지
- 페이지네이션 구현으로 대량 데이터 처리 최적화
결제 정보 보안 & PCI DSS 컴플라이언스 준수 & 결제 실패 시 복구 처리
- Stripe Elements 도입으로 카드 정보 직접 처리 회피
- 토큰 기반 결제 처리 구현
- 자동 복구 메커니즘 구현으로 결제 실패 대응
- 트랜잭션 롤백 시스템 구축
-
핵심 프레임워크/라이브러리
- React.js 18
- React Router DOM v6
- Redux Toolkit
- Axios
-
UI/UX
- Tailwind CSS
- Framer Motion
- Headless UI
- React Icons
-
상태 관리
- Redux Toolkit
- React Query
- Context API
-
유틸리티
- Lodash
- Date-fns
- QRCode.js
- Chart.js
-
서버
- Node.js
- Express.js
- MongoDB
- Mongoose
-
인증/보안
- JWT
- Bcrypt
- Helmet
- CORS
-
외부 서비스
- Twilio API
- AWS S3
- Stripe
- SendGrid
-
개발 도구
- ESLint
- Prettier
- Husky
- Jest
/api/auth/owner/*- 점주 인증 관련/api/restaurants/*- 식당 관리/api/messages/*- 메시지 관리/api/coupons/*- 쿠폰 관리
/api/auth/customer/*- 고객 인증 관련/api/customers/*- 고객 정보 관리/api/subscriptions/*- 구독 관리/api/playlists/*- 플레이리스트 관리
- 반응형 디자인
- 모바일 친화적 UI/UX
- 터치 제스처 지원
- JWT 기반 인증
- OTP 인증
- 비밀번호 암호화
- API 요청 제한
- 실시간 데이터 업데이트
- 자동 세션 관리
- 오프라인 데이터 캐싱
- 구독자 통계
- 쿠폰 사용 분석
- 메시지 효과 측정
- 고객 참여도 추적
- 다크/라이트 모드 지원
- 애니메이션 효과
- 직관적인 네비게이션
- 사용자 친화적 인터페이스
- 고급 검색 기능
- 다중 필터링 옵션
- 정렬 기능
- 실시간 검색 결과
-
클라우드 서비스
- AWS EC2
- AWS S3
- AWS CloudFront
- MongoDB Atlas
-
DevOps
- Docker
- Nginx
- PM2
- GitHub Actions
-
모니터링
- Sentry
- NewRelic
- Datadog
- Grafana
Client App (React)
↕
API Gateway
↕
Microservices
├── Auth Service
├── Restaurant Service
├── Customer Service
├── Notification Service
└── Analytics Service
↕
Databases
├── MongoDB (Main DB)
├── Redis (Cache)
└── S3 (File Storage)
-
사용자 관련
// User Schema const userSchema = new mongoose.Schema({ name: { type: String, required: true }, email: { type: String, required: true, unique: true }, password: { type: String, required: true }, role: { type: String, enum: ['owner', 'admin'], default: 'owner' }, restaurants: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Restaurant' }], createdAt: { type: Date, default: Date.now }, lastLogin: Date, status: { type: String, default: 'active' } }); // Customer Schema const customerSchema = new mongoose.Schema({ phoneNumber: { type: String, required: true, unique: true }, email: String, name: String, preferences: { language: String, notifications: { sms: Boolean, email: Boolean } } });
-
레스토랑 관련
// Restaurant Schema const restaurantSchema = new mongoose.Schema({ name: { type: String, required: true }, owner: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true }, location: { address: String, coordinates: { type: { type: String, enum: ['Point'], default: 'Point' }, coordinates: [Number] } }, menu: [{ category: String, items: [{ name: String, price: Number, description: String, image: String }] }], settings: { stamps: { enabled: Boolean, pointsPerStamp: Number, rewardThreshold: Number } } });
-
마케팅 관련
// Campaign Schema const campaignSchema = new mongoose.Schema({ restaurant: { type: mongoose.Schema.Types.ObjectId, ref: 'Restaurant' }, type: { type: String, enum: ['coupon', 'event', 'announcement'] }, title: String, content: String, startDate: Date, endDate: Date, targetAudience: { type: String, enum: ['all', 'subscribers', 'vip'] }, status: { type: String, enum: ['draft', 'scheduled', 'active', 'completed'] } });
- Node.js v14 이상
- MongoDB v4.4 이상
- Redis v6 이상
- AWS 계정
- Stripe 계정
- Twilio 계정
# 서버 설정
NODE_ENV=development
PORT=5000
FRONTEND_URL=http://localhost:3000
# 데이터베이스
MONGODB_URI=mongodb://localhost:27017/smsstartup
REDIS_URL=redis://localhost:6379
# JWT 설정
JWT_SECRET=your_jwt_secret
JWT_EXPIRATION=30d
# AWS
AWS_ACCESS_KEY_ID=your_access_key
AWS_SECRET_ACCESS_KEY=your_secret_key
AWS_REGION=ap-northeast-2
S3_BUCKET_NAME=your_bucket_name
# Stripe
STRIPE_SECRET_KEY=your_stripe_secret
STRIPE_WEBHOOK_SECRET=your_webhook_secret
# Twilio
TWILIO_ACCOUNT_SID=your_account_sid
TWILIO_AUTH_TOKEN=your_auth_token
TWILIO_PHONE_NUMBER=your_phone_number
# 이메일
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USER=your_email
SMTP_PASS=your_password-
도커 컴포즈 파일
version: '3.8' services: backend: build: ./backend ports: - "5000:5000" environment: - NODE_ENV=development - MONGODB_URI=mongodb://mongo:27017/twiqli depends_on: - mongo - redis frontend: build: ./frontend ports: - "3000:3000" environment: - REACT_APP_API_URL=http://localhost:5000 mongo: image: mongo:4.4 ports: - "27017:27017" volumes: - mongodb_data:/data/db redis: image: redis:6 ports: - "6379:6379" volumes: - redis_data:/data volumes: mongodb_data: redis_data:
-
실행 명령어
# 도커 컴포즈로 서비스 실행 docker-compose up -d # 로그 확인 docker-compose logs -f # 서비스 중지 docker-compose down
-
JavaScript/React 코딩 컨벤션
// 컴포넌트 네이밍 const RestaurantCard = ({ restaurant, onSubscribe }) => { // 상태 관리 const [isLoading, setIsLoading] = useState(false); // 이벤트 핸들러 const handleSubscribe = async () => { try { setIsLoading(true); await onSubscribe(restaurant.id); } catch (error) { console.error('구독 오류:', error); } finally { setIsLoading(false); } }; // JSX 렌더링 return ( <div className="restaurant-card"> {/* 컴포넌트 내용 */} </div> ); };
-
백엔드 코딩 컨벤션
// 컨트롤러 구조 const restaurantController = { // 비동기 함수 사용 async createRestaurant(req, res) { try { const restaurantData = req.body; const newRestaurant = await Restaurant.create(restaurantData); res.status(201).json({ success: true, data: newRestaurant }); } catch (error) { res.status(500).json({ success: false, error: error.message }); } } };
-
CSS/Tailwind 스타일 가이드
/* 공통 스타일 */ .btn { @apply px-4 py-2 rounded-lg font-medium transition-colors duration-200; } .btn-primary { @apply bg-blue-500 text-white hover:bg-blue-600; } .btn-secondary { @apply bg-gray-500 text-white hover:bg-gray-600; }
-
커스텀 훅 예제
// useSubscription.js const useSubscription = (restaurantId) => { const [isSubscribed, setIsSubscribed] = useState(false); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const subscribe = async () => { try { setLoading(true); await subscriptionService.subscribe(restaurantId); setIsSubscribed(true); } catch (err) { setError(err.message); } finally { setLoading(false); } }; const unsubscribe = async () => { // 구독 취소 로직 }; return { isSubscribed, loading, error, subscribe, unsubscribe }; };
-
소유자 인증
/** * @route POST /api/auth/owner/login * @desc 레스토랑 소유자 로그인 * @access Public */ { "email": "owner@example.com", "password": "securepassword123" }
-
고객 인증
/** * @route POST /api/auth/customer/login * @desc 고객 로그인 (OTP 발송) * @access Public */ { "phoneNumber": "+82-10-1234-5678" }
-
레스토랑 등록
/** * @route POST /api/restaurants * @desc 새로운 레스토랑 등록 * @access Private (Owner) */ { "name": "맛있는 레스토랑", "address": "서울시 강남구", "description": "정통 한식 전문점", "category": "한식", "openingHours": { "mon": "09:00-22:00", "tue": "09:00-22:00" } }
-
메뉴 관리
/** * @route PUT /api/restaurants/:id/menu * @desc 메뉴 업데이트 * @access Private (Owner) */ { "menuItems": [ { "name": "김치찌개", "price": 8000, "description": "돼지고기가 들어간 매콤한 김치찌개", "category": "찌개" } ] }
-
구독 신청
/** * @route POST /api/subscriptions * @desc 레스토랑 구독 신청 * @access Private (Customer) */ { "restaurantId": "restaurant_id", "preferences": { "smsNotifications": true, "emailNotifications": false } }
-
구독 관리
/** * @route GET /api/subscriptions * @desc 구독 목록 조회 * @access Private (Customer) */ { "subscriptions": [ { "restaurantId": "restaurant_id", "restaurantName": "맛있는 레스토랑", "subscribedAt": "2024-01-01T00:00:00.000Z", "status": "active" } ] }
-
EC2 인스턴스 설정
# Ubuntu 서버 업데이트 sudo apt-get update sudo apt-get upgrade # Node.js 설치 curl -fsSL https://deb.nodesource.com/setup_14.x | sudo -E bash - sudo apt-get install -y nodejs # PM2 설치 sudo npm install -g pm2
-
Nginx 설정
server { listen 80; server_name your-domain.com; location / { proxy_pass http://localhost:3000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; proxy_set_header Host $host; proxy_cache_bypass $http_upgrade; } location /api { proxy_pass http://localhost:5000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; proxy_set_header Host $host; proxy_cache_bypass $http_upgrade; } }
-
SSL 설정
# Certbot 설치 sudo apt-get install certbot python3-certbot-nginx # SSL 인증서 발급 sudo certbot --nginx -d your-domain.com
- GitHub Actions 워크플로우
name: Deploy to Production on: push: branches: [ main ] jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Setup Node.js uses: actions/setup-node@v2 with: node-version: '14' - name: Install dependencies run: | npm ci cd frontend && npm ci cd ../backend && npm ci - name: Build frontend run: cd frontend && npm run build - name: Deploy to EC2 uses: appleboy/ssh-action@master with: host: ${{ secrets.HOST }} username: ${{ secrets.USERNAME }} key: ${{ secrets.SSH_KEY }} script: | cd /var/www/twiqli git pull npm ci pm2 restart all
-
데이터베이스 연결 오류
// 문제: MongoDB 연결 실패 // 해결 방법: mongoose.connect(process.env.MONGODB_URI, { useNewUrlParser: true, useUnifiedTopology: true, serverSelectionTimeoutMS: 5000, retryWrites: true, retryReads: true }).catch(err => { console.error('MongoDB 연결 오류:', err); process.exit(1); });
-
JWT 인증 오류
// 문제: 토큰 만료 또는 유효하지 않은 토큰 // 해결 방법: const verifyToken = (token) => { try { return jwt.verify(token, process.env.JWT_SECRET); } catch (error) { if (error.name === 'TokenExpiredError') { throw new Error('토큰이 만료되었습니다'); } throw new Error('유효하지 않은 토큰입니다'); } };
-
이미지 최적화
// 이미지 압축 및 리사이징 const sharp = require('sharp'); const optimizeImage = async (buffer) => { return await sharp(buffer) .resize(800, 600, { fit: 'inside', withoutEnlargement: true }) .jpeg({ quality: 80 }) .toBuffer(); };
-
캐싱 전략
// Redis 캐싱 구현 const redis = require('redis'); const client = redis.createClient(); const cacheMiddleware = async (req, res, next) => { const key = req.originalUrl; try { const data = await client.get(key); if (data) { return res.json(JSON.parse(data)); } next(); } catch (error) { next(error); } };
-
헬멧 미들웨어 설정
const helmet = require('helmet'); app.use(helmet()); app.use(helmet.contentSecurityPolicy({ directives: { defaultSrc: ["'self'"], styleSrc: ["'self'", "'unsafe-inline'"], scriptSrc: ["'self'", "'unsafe-inline'", "'unsafe-eval'"], imgSrc: ["'self'", "data:", "https:"], connectSrc: ["'self'", "https://api.your-domain.com"] } }));
-
CORS 설정
const cors = require('cors'); app.use(cors({ origin: process.env.FRONTEND_URL, methods: ['GET', 'POST', 'PUT', 'DELETE'], allowedHeaders: ['Content-Type', 'Authorization'], credentials: true }));
-
비밀번호 암호화
const bcrypt = require('bcrypt'); const hashPassword = async (password) => { const salt = await bcrypt.genSalt(10); return await bcrypt.hash(password, salt); }; const verifyPassword = async (password, hash) => { return await bcrypt.compare(password, hash); };
-
입력값 검증
const Joi = require('joi'); const validateRestaurant = (data) => { const schema = Joi.object({ name: Joi.string().required().min(2).max(50), address: Joi.string().required(), phone: Joi.string().pattern(/^\+?[\d\s-]+$/), email: Joi.string().email(), description: Joi.string().max(500) }); return schema.validate(data); };
-
Jest 설정
// jest.config.js module.exports = { testEnvironment: 'node', collectCoverage: true, coverageDirectory: 'coverage', coveragePathIgnorePatterns: [ '/node_modules/', '/dist/' ], setupFiles: ['./jest.setup.js'] };
-
컴포넌트 테스트
// RestaurantCard.test.js import { render, screen, fireEvent } from '@testing-library/react'; import RestaurantCard from './RestaurantCard'; describe('RestaurantCard', () => { const mockRestaurant = { id: '1', name: '테스트 레스토랑', description: '테스트 설명' }; test('renders restaurant information', () => { render(<RestaurantCard restaurant={mockRestaurant} />); expect(screen.getByText('테스트 레스토랑')).toBeInTheDocument(); expect(screen.getByText('테스트 설명')).toBeInTheDocument(); }); test('handles subscribe button click', () => { const mockOnSubscribe = jest.fn(); render( <RestaurantCard restaurant={mockRestaurant} onSubscribe={mockOnSubscribe} /> ); fireEvent.click(screen.getByText('구독하기')); expect(mockOnSubscribe).toHaveBeenCalledWith('1'); }); });
-
API 테스트
// restaurantApi.test.js const request = require('supertest'); const app = require('../app'); const mongoose = require('mongoose'); describe('Restaurant API', () => { beforeAll(async () => { await mongoose.connect(process.env.TEST_MONGODB_URI); }); afterAll(async () => { await mongoose.connection.close(); }); describe('GET /api/restaurants', () => { it('should return all restaurants', async () => { const res = await request(app) .get('/api/restaurants') .expect(200); expect(Array.isArray(res.body)).toBeTruthy(); expect(res.body.length).toBeGreaterThan(0); }); it('should filter restaurants by category', async () => { const res = await request(app) .get('/api/restaurants?category=한식') .expect(200); expect(res.body.every(r => r.category === '한식')).toBeTruthy(); }); }); });
-
서비스 레이어 테스트
// subscriptionService.test.js const subscriptionService = require('../services/subscriptionService'); const Customer = require('../models/Customer'); const Restaurant = require('../models/Restaurant'); describe('Subscription Service', () => { beforeEach(async () => { await Customer.deleteMany({}); await Restaurant.deleteMany({}); }); test('should create new subscription', async () => { // 테스트 데이터 설정 const customer = await Customer.create({ phoneNumber: '010-1234-5678', name: '테스트 고객' }); const restaurant = await Restaurant.create({ name: '테스트 레스토랑', address: '서울시 강남구' }); // 구독 생성 테스트 const subscription = await subscriptionService.createSubscription( customer._id, restaurant._id ); expect(subscription).toBeDefined(); expect(subscription.customer).toEqual(customer._id); expect(subscription.restaurant).toEqual(restaurant._id); }); });
-
Cypress 설정
// cypress.config.js const { defineConfig } = require('cypress'); module.exports = defineConfig({ e2e: { baseUrl: 'http://localhost:3000', setupNodeEvents(on, config) { require('@cypress/code-coverage/task')(on, config); return config; }, }, env: { apiUrl: 'http://localhost:5000', }, });
-
E2E 테스트 시나리오
// cypress/e2e/subscription.cy.js describe('구독 프로세스', () => { beforeEach(() => { cy.intercept('GET', '/api/restaurants*').as('getRestaurants'); cy.intercept('POST', '/api/subscriptions').as('subscribe'); }); it('레스토랑 구독 프로세스 완료', () => { // 로그인 cy.login('010-1234-5678'); // 레스토랑 목록 페이지 방문 cy.visit('/restaurants'); cy.wait('@getRestaurants'); // 첫 번째 레스토랑 선택 cy.get('.restaurant-card').first().click(); // 구독 버튼 클릭 cy.get('[data-cy=subscribe-button]').click(); // OTP 입력 cy.get('[data-cy=otp-input]').type('123456'); cy.get('[data-cy=verify-button]').click(); // 구독 완료 확인 cy.wait('@subscribe'); cy.get('[data-cy=subscription-success]').should('be.visible'); }); });
-
성능 메트릭 수집
// performanceMonitor.js export const measurePageLoad = () => { const performance = window.performance; return { timeToFirstByte: performance.timing.responseStart - performance.timing.navigationStart, domContentLoaded: performance.timing.domContentLoadedEventEnd - performance.timing.navigationStart, fullPageLoad: performance.timing.loadEventEnd - performance.timing.navigationStart }; }; export const trackUserInteraction = (event) => { const startTime = performance.now(); return { measure() { const duration = performance.now() - startTime; return { event, duration, timestamp: new Date().toISOString() }; } }; };
-
에러 트래킹
// errorTracker.js class ErrorTracker { constructor() { this.errors = []; this.init(); } init() { window.onerror = (message, source, lineno, colno, error) => { this.logError({ message, source, lineno, colno, stack: error?.stack }); }; window.addEventListener('unhandledrejection', (event) => { this.logError({ message: 'Unhandled Promise Rejection', error: event.reason }); }); } logError(error) { this.errors.push({ ...error, timestamp: new Date().toISOString(), userAgent: navigator.userAgent, url: window.location.href }); // 에러 리포팅 서비스로 전송 this.sendToErrorService(error); } sendToErrorService(error) { // Sentry나 다른 에러 트래킹 서비스로 전송 } }



