const { SubscriptionPlan, SubscriptionPlanFeature, ClubSubscription, Feature, Club, User, PaymentRequest } = require('../models'); // 전체 구독 목록 조회 exports.getClubSubscriptions = async (req, res) => { try { const subscriptions = await ClubSubscription.findAll({ include: [ { model: SubscriptionPlan, include: [{ model: Feature, through: { attributes: ['status'] } }] }, { model: Club, include: [{ model: User, as: 'owner', attributes: ['id', 'name', 'email'] }] } ] }); res.json(subscriptions); } catch (error) { console.error('Error in getClubSubscriptions:', error); res.status(500).json({ message: '구독 목록 조회 중 오류가 발생했습니다.' }); } }; // 구독 플랜 목록 조회 exports.getSubscriptionPlans = async (req, res) => { try { const plans = await SubscriptionPlan.findAll({ include: [{ model: Feature, through: { attributes: ['status'] } }] }); res.json(plans); } catch (error) { console.error('Error in getSubscriptionPlans:', error); res.status(500).json({ message: '구독 플랜 목록 조회 중 오류가 발생했습니다.' }); } }; // 플랜 상세 정보 조회 (포함된 기능 포함) exports.getPlanDetail = async (req, res) => { try { const { planId } = req.params; const plan = await SubscriptionPlan.findByPk(planId, { include: [{ model: Feature, through: { attributes: ['status'] } }] }); if (!plan) { return res.status(404).json({ message: '해당 플랜을 찾을 수 없습니다.' }); } res.json(plan); } catch (error) { console.error('Error in getPlanDetail:', error); res.status(500).json({ message: '플랜 상세 정보 조회 중 오류가 발생했습니다.' }); } }; // 구독 플랜 생성 exports.createSubscriptionPlan = async (req, res) => { try { const { name, description, maxMembers, price, features } = req.body; const plan = await SubscriptionPlan.create({ name, description, maxMembers, price }); if (features && features.length > 0) { const featureAssociations = features.map(featureId => ({ planId: plan.id, featureId, status: 'active' })); await SubscriptionPlanFeature.bulkCreate(featureAssociations); } const createdPlan = await SubscriptionPlan.findByPk(plan.id, { include: [{ model: Feature, through: { attributes: ['status'] } }] }); res.status(201).json(createdPlan); } catch (error) { console.error('Error in createSubscriptionPlan:', error); res.status(500).json({ message: '구독 플랜 생성 중 오류가 발생했습니다.' }); } }; // 구독 플랜 수정 exports.updateSubscriptionPlan = async (req, res) => { try { const { id } = req.params; const { name, description, maxMembers, price, features } = req.body; const plan = await SubscriptionPlan.findByPk(id); if (!plan) { return res.status(404).json({ message: '구독 플랜을 찾을 수 없습니다.' }); } await plan.update({ name, description, maxMembers, price }); if (features) { // 기존 기능 연결 삭제 await SubscriptionPlanFeature.destroy({ where: { planId: id } }); // 새로운 기능 연결 생성 if (features.length > 0) { const featureAssociations = features.map(featureId => ({ planId: id, featureId, status: 'active' })); await SubscriptionPlanFeature.bulkCreate(featureAssociations); } } const updatedPlan = await SubscriptionPlan.findByPk(id, { include: [{ model: Feature, through: { attributes: ['status'] } }] }); res.json(updatedPlan); } catch (error) { console.error('Error in updateSubscriptionPlan:', error); res.status(500).json({ message: '구독 플랜 수정 중 오류가 발생했습니다.' }); } }; // 구독 플랜 삭제 exports.deleteSubscriptionPlan = async (req, res) => { try { const { id } = req.params; // 플랜이 존재하는지 확인 const plan = await SubscriptionPlan.findByPk(id); if (!plan) { return res.status(404).json({ message: '구독 플랜을 찾을 수 없습니다.' }); } // 플랜과 연결된 기능 관계 삭제 await SubscriptionPlanFeature.destroy({ where: { planId: id } }); // 플랜 삭제 await plan.destroy(); res.json({ message: '구독 플랜이 삭제되었습니다.' }); } catch (error) { console.error('Error in deleteSubscriptionPlan:', error); res.status(500).json({ message: '구독 플랜 삭제 중 오류가 발생했습니다.' }); } }; // 클럽 구독 생성/수정 exports.subscribeClub = async (req, res) => { try { const { clubId, planId, startDate, expiryDate } = req.body; // 기존 구독 확인 let subscription = await ClubSubscription.findOne({ where: { clubId, status: 'active' } }); if (subscription) { // 기존 구독 만료 처리 await subscription.update({ status: 'expired', updatedAt: new Date() }); } // 새로운 구독 생성 subscription = await ClubSubscription.create({ clubId, planId, startDate: new Date(startDate), expiryDate: new Date(expiryDate), status: 'active' }); res.status(201).json(subscription); } catch (error) { console.error('Error in subscribeClub:', error); res.status(500).json({ message: '클럽 구독 처리 중 오류가 발생했습니다.' }); } }; // 클럽의 현재 구독 정보 조회 exports.getClubSubscription = async (req, res) => { try { const { clubId } = req.body; const subscription = await ClubSubscription.findOne({ where: { clubId, status: 'active' }, include: [{ model: SubscriptionPlan, include: [{ model: Feature, through: { attributes: ['status'] } }] }] }); if (!subscription) { return res.status(404).json({ message: '활성화된 구독 정보가 없습니다.' }); } res.json(subscription); } catch (error) { console.error('Error in getClubSubscription:', error); res.status(500).json({ message: '구독 정보 조회 중 오류가 발생했습니다.' }); } }; // 클럽의 현재 구독 정보 반환 exports.getCurrentSubscription = async (req, res) => { const { clubId } = req.body; if (!clubId) { return res.status(400).json({ message: '클럽 ID가 필요합니다.' }); } try { const subscription = await ClubSubscription.findOne({ where: { clubId, status: 'active' }, include: [{ model: SubscriptionPlan, include: [{ model: Feature, through: { attributes: ['status'] } }] }] }); if (!subscription) { return res.status(404).json({ message: '활성화된 구독 정보가 없습니다.' }); } res.json({ planName: subscription.SubscriptionPlan.name, planId: subscription.SubscriptionPlan.id, status: subscription.status, startDate: subscription.startDate, endDate: subscription.endDate, planPrice: subscription.SubscriptionPlan.price, price: subscription.price }); } catch (error) { console.error('Error in getCurrentSubscription:', error); res.status(500).json({ message: '구독 정보 조회 중 오류가 발생했습니다.' }); } }; // 클럽의 구독 결제 내역 반환 exports.getSubscriptionPayments = async (req, res) => { const { clubId } = req.body; if (!clubId) { return res.status(400).json({ message: '클럽 ID가 필요합니다.' }); } try { const payments = await ClubSubscription.findAll({ where: { clubId }, include: [{ model: SubscriptionPlan }], order: [['createdAt', 'DESC']] }); res.json(payments.map(payment => ({ id: payment.id, paymentDate: payment.createdAt, planName: payment.SubscriptionPlan?.name || '', amount: payment.SubscriptionPlan?.price || 0, status: payment.status }))); } catch (error) { console.error('Error in getSubscriptionPayments:', error); res.status(500).json({ message: '구독 결제 내역 조회 중 오류가 발생했습니다.' }); } }; // 플랜 변경과 추가 기능을 함께 처리 exports.changeWithFeatures = async (req, res) => { try { const { clubId, currentPlanId, newPlanId, isUpgrade, refundAmount, proRatedNewPlanPrice, planPriceDifference, features, totalFeaturesPrice, finalAmount, remainingDays } = req.body; // 클럽과 구독 정보 확인 const club = await Club.findByPk(clubId); if (!club) { return res.status(404).json({ message: '클럽을 찾을 수 없습니다.' }); } const currentSubscription = await ClubSubscription.findOne({ where: { clubId, planId: currentPlanId, status: 'active' } }); if (!currentSubscription) { return res.status(404).json({ message: '현재 활성화된 구독을 찾을 수 없습니다.' }); } const newPlan = await SubscriptionPlan.findByPk(newPlanId); if (!newPlan) { return res.status(404).json({ message: '선택한 플랜을 찾을 수 없습니다.' }); } // 트랜잭션 시작 const transaction = await sequelize.transaction(); try { if (isUpgrade) { // 업그레이드: 현재 구독 업데이트 currentSubscription.planId = newPlanId; currentSubscription.price = newPlan.price; await currentSubscription.save({ transaction }); // 결제 기록 생성 await Payment.create({ clubId, amount: planPriceDifference, description: `플랜 업그레이드: ${currentPlanId} -> ${newPlanId}`, status: 'completed', paymentDate: new Date() }, { transaction }); } else { // 다운그레이드: 다음 결제일에 적용될 구독 생성 await ClubSubscription.create({ clubId, planId: newPlanId, status: 'pending', startDate: currentSubscription.endDate, endDate: new Date(new Date(currentSubscription.endDate).setMonth( new Date(currentSubscription.endDate).getMonth() + 1 )), price: newPlan.price }, { transaction }); } // 추가 기능 처리 if (features && features.length > 0) { for (const feature of features) { // 현재 구독 만료일 계산 const expiryDate = new Date(currentSubscription.endDate); // 1개월 초과 구독인 경우 해당 기간만큼 추가 if (feature.duration > 1) { expiryDate.setMonth(expiryDate.getMonth() + feature.duration - 1); } // 기능 구매 기록 생성 await ClubFeature.create({ clubId, featureId: feature.featureId, expiryDate, enabled: true }, { transaction }); // 기능 결제 기록 생성 await Payment.create({ clubId, amount: feature.price, description: `추가 기능 구매: ${feature.featureId} (${feature.duration}개월)`, status: 'completed', paymentDate: new Date() }, { transaction }); } } // 트랜잭션 커밋 await transaction.commit(); res.json({ message: '플랜 변경 및 추가 기능 구매가 완료되었습니다.', isUpgrade, planChanged: true, featuresAdded: features.length > 0, totalAmount: finalAmount }); } catch (error) { // 트랜잭션 롤백 await transaction.rollback(); throw error; } } catch (error) { console.error('플랜 변경 및 추가 기능 구매 오류:', error); res.status(500).json({ message: '처리 중 오류가 발생했습니다.' }); } }; // 토스페이먼츠 결제 요청 생성 exports.requestPayment = async (req, res) => { try { const { clubId, planId, features, totalAmount, planDuration, planChange, currentPlanId, isUpgrade, successUrl, failUrl } = req.body; if (!clubId || !totalAmount) { return res.status(400).json({ message: '필수 데이터가 누락되었습니다.' }); } // 클럽 정보 확인 const club = await Club.findByPk(clubId); if (!club) { return res.status(404).json({ message: '클럽을 찾을 수 없습니다.' }); } // 결제 정보 저장 const paymentInfo = await PaymentRequest.create({ clubId, planId: planId || null, currentPlanId: currentPlanId || null, isUpgrade: isUpgrade || false, planDuration: planDuration || 1, features: features ? JSON.stringify(features) : null, totalAmount, status: 'pending', orderId: `order_${Date.now()}_${clubId}`, planChange: planChange || false }); // 플랜 정보 조회 (planId가 있는 경우) let planName = ''; if (planId) { const plan = await SubscriptionPlan.findByPk(planId); if (plan) { planName = plan.name; // 베이직 또는 프리미엄 등의 플랜 이름 } } // 토스페이먼츠에 전송할 데이터 구성 const paymentData = { orderId: paymentInfo.orderId, amount: totalAmount, orderName: planId ? `${planName} 플랜: ${planDuration}개월` : '추가 기능 구매', customerName: club.name, successUrl: successUrl || `${req.headers.origin}/payment/success`, failUrl: failUrl || `${req.headers.origin}/payment/fail`, customerEmail: req.user.email }; res.json({ paymentData, clientKey: process.env.TOSS_PAYMENTS_CLIENT_KEY }); } catch (error) { console.error('결제 요청 생성 오류:', error); res.status(500).json({ message: '결제 요청 생성 중 오류가 발생했습니다.' }); } }; // 결제 성공 처리 exports.processPaymentSuccess = async (req, res) => { try { const { orderId, paymentKey, amount } = req.query; // 결제 요청 정보 조회 const paymentRequest = await PaymentRequest.findOne({ where: { orderId } }); if (!paymentRequest) { return res.status(404).json({ message: '결제 정보를 찾을 수 없습니다.' }); } // 금액 확인 if (paymentRequest.totalAmount !== parseInt(amount)) { return res.status(400).json({ message: '결제 금액이 일치하지 않습니다.' }); } // 토스페이먼츠 API로 결제 승인 요청 const tossPaymentsSecretKey = process.env.TOSS_PAYMENTS_SECRET_KEY; const encryptedSecretKey = Buffer.from(`${tossPaymentsSecretKey}:`).toString('base64'); const response = await axios({ method: 'post', url: 'https://api.tosspayments.com/v1/payments/confirm', headers: { Authorization: `Basic ${encryptedSecretKey}`, 'Content-Type': 'application/json' }, data: { orderId, amount, paymentKey } }); // 결제 요청 상태 업데이트 paymentRequest.status = 'completed'; paymentRequest.paymentKey = paymentKey; paymentRequest.paymentData = JSON.stringify(response.data); await paymentRequest.save(); // 트랜잭션 시작 const transaction = await sequelize.transaction(); try { // 플랜 변경 처리 if (paymentRequest.planId) { if (paymentRequest.planChange) { if (paymentRequest.isUpgrade) { // 업그레이드: 현재 구독 업데이트 const currentSubscription = await ClubSubscription.findOne({ where: { clubId: paymentRequest.clubId, planId: paymentRequest.currentPlanId, status: 'active' } }); if (currentSubscription) { currentSubscription.planId = paymentRequest.planId; await currentSubscription.save({ transaction }); } } else { // 다운그레이드: 다음 결제일에 적용될 구독 생성 const currentSubscription = await ClubSubscription.findOne({ where: { clubId: paymentRequest.clubId, planId: paymentRequest.currentPlanId, status: 'active' } }); if (currentSubscription) { await ClubSubscription.create({ clubId: paymentRequest.clubId, planId: paymentRequest.planId, status: 'pending', startDate: currentSubscription.endDate, endDate: new Date(new Date(currentSubscription.endDate).setMonth( new Date(currentSubscription.endDate).getMonth() + paymentRequest.planDuration )), price: (await SubscriptionPlan.findByPk(paymentRequest.planId))?.price || 0 }, { transaction }); } } } else { // 새 구독 생성 const startDate = new Date(); const endDate = new Date(); endDate.setMonth(endDate.getMonth() + paymentRequest.planDuration); await ClubSubscription.create({ clubId: paymentRequest.clubId, planId: paymentRequest.planId, status: 'active', startDate, endDate, price: (await SubscriptionPlan.findByPk(paymentRequest.planId))?.price || 0 }, { transaction }); } } // 추가 기능 처리 if (paymentRequest.features) { const features = JSON.parse(paymentRequest.features); for (const feature of features) { const expiryDate = new Date(); expiryDate.setMonth(expiryDate.getMonth() + (feature.duration || paymentRequest.planDuration)); await ClubFeature.create({ clubId: paymentRequest.clubId, featureId: feature.featureId, expiryDate, enabled: true }, { transaction }); } } // 결제 기록 생성 await Payment.create({ clubId: paymentRequest.clubId, amount: paymentRequest.totalAmount, description: paymentRequest.planId ? `구독 플랜 ${paymentRequest.planDuration}개월` : '추가 기능 구매', status: 'completed', paymentDate: new Date(), paymentMethod: response.data.method || 'card', paymentKey }, { transaction }); // 트랜잭션 커밋 await transaction.commit(); // 성공 페이지로 리디렉션 res.redirect(`${process.env.FRONTEND_URL}/payment/success?orderId=${orderId}`); } catch (error) { // 트랜잭션 롤백 await transaction.rollback(); console.error('결제 처리 오류:', error); res.redirect(`${process.env.FRONTEND_URL}/payment/fail?message=결제는 성공했지만 처리 중 오류가 발생했습니다.`); } } catch (error) { console.error('결제 성공 처리 오류:', error); res.redirect(`${process.env.FRONTEND_URL}/payment/fail?message=결제 처리 중 오류가 발생했습니다.`); } }; // 결제 실패 처리 exports.processPaymentFail = async (req, res) => { try { const { orderId, message } = req.query; // 결제 요청 정보 조회 및 업데이트 if (orderId) { const paymentRequest = await PaymentRequest.findOne({ where: { orderId } }); if (paymentRequest) { paymentRequest.status = 'failed'; paymentRequest.failReason = message || '사용자가 결제를 취소했습니다.'; await paymentRequest.save(); } } // 실패 페이지로 리디렉션 res.redirect(`${process.env.FRONTEND_URL}/payment/fail?message=${encodeURIComponent(message || '결제가 취소되었습니다.')}`); } catch (error) { console.error('결제 실패 처리 오류:', error); res.redirect(`${process.env.FRONTEND_URL}/payment/fail?message=결제 취소 처리 중 오류가 발생했습니다.`); } }; // 플랜별 구독 목록 조회 exports.getPlanSubscriptions = async (req, res) => { try { const { planId } = req.params; const subscriptions = await ClubSubscription.findAll({ where: { planId }, include: [{ model: Club, attributes: ['id', 'name'] }] }); res.json(subscriptions); } catch (error) { console.error('Error in getPlanSubscriptions:', error); res.status(500).json({ message: '플랜별 구독 목록 조회 중 오류가 발생했습니다.' }); } };