Files
2025-08-01 16:45:26 +09:00

826 lines
25 KiB
JavaScript

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: '플랜별 구독 목록 조회 중 오류가 발생했습니다.' });
}
};
// 인앱 결제 영수증 검증
exports.verifyReceipt = async (req, res) => {
try {
const { receipt, transactionId, productId, platform } = req.body;
const userId = req.user.id;
const clubId = req.body.clubId;
if (!receipt) {
return res.status(400).json({ message: '영수증 데이터가 없습니다.' });
}
// 플랫폼별 검증 로직
let verificationResult = false;
let purchaseData = {};
if (platform === 'ios') {
// iOS 영수증 검증
verificationResult = await verifyIosReceipt(receipt);
purchaseData = {
platform: 'ios',
transactionId,
productId,
receipt
};
} else if (platform === 'android') {
// Android 영수증 검증
verificationResult = await verifyAndroidReceipt(receipt, productId);
purchaseData = {
platform: 'android',
transactionId,
productId,
receipt
};
} else {
return res.status(400).json({ message: '지원하지 않는 플랫폼입니다.' });
}
if (!verificationResult) {
return res.status(400).json({ message: '영수증 검증에 실패했습니다.' });
}
// 구독 플랜 타입 확인
const planType = getPlanTypeFromProductId(productId);
if (!planType) {
return res.status(400).json({ message: '유효하지 않은 상품 ID입니다.' });
}
// 해당 플랜 정보 가져오기
const plan = await SubscriptionPlan.findOne({
where: { type: planType }
});
if (!plan) {
return res.status(404).json({ message: '해당 플랜을 찾을 수 없습니다.' });
}
// 기존 구독 정보 확인
let subscription = await ClubSubscription.findOne({
where: { clubId }
});
// 구독 생성 또는 업데이트
const startDate = new Date();
const endDate = new Date();
endDate.setMonth(endDate.getMonth() + 1); // 기본 1개월 구독
if (subscription) {
// 기존 구독 업데이트
await subscription.update({
planId: plan.id,
status: 'active',
startDate,
endDate,
lastPaymentDate: startDate,
transactionId,
receiptData: JSON.stringify(purchaseData)
});
} else {
// 새 구독 생성
subscription = await ClubSubscription.create({
clubId,
planId: plan.id,
status: 'active',
startDate,
endDate,
lastPaymentDate: startDate,
transactionId,
receiptData: JSON.stringify(purchaseData)
});
}
// 검증 결과 및 구독 정보 반환
res.json({
success: true,
message: '영수증 검증 및 구독 처리가 완료되었습니다.',
subscription: {
id: subscription.id,
planId: subscription.planId,
planType: planType,
status: subscription.status,
startDate: subscription.startDate,
endDate: subscription.endDate,
lastPaymentDate: subscription.lastPaymentDate
}
});
} catch (error) {
console.error('Error in verifyReceipt:', error);
res.status(500).json({ message: '영수증 검증 중 오류가 발생했습니다.' });
}
};
// iOS 영수증 검증 함수 (실제 구현에서는 Apple 서버와 통신 필요)
async function verifyIosReceipt(receipt) {
// 실제 구현에서는 Apple 서버에 영수증 검증 요청
// 테스트를 위해 현재는 항상 true 반환
return true;
}
// Android 영수증 검증 함수 (실제 구현에서는 Google Play API와 통신 필요)
async function verifyAndroidReceipt(receipt, productId) {
// 실제 구현에서는 Google Play API를 통해 영수증 검증
// 테스트를 위해 현재는 항상 true 반환
return true;
}
// 상품 ID에서 플랜 타입 추출
function getPlanTypeFromProductId(productId) {
if (productId.includes('basic')) {
return 'basic';
} else if (productId.includes('premium')) {
return 'premium';
} else if (productId.includes('pro')) {
return 'pro';
}
return null;
}