결제, 구독 기능
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
const { Feature, ClubFeature, Club } = require('../models');
|
const { Feature, ClubFeature, Club, ClubSubscription, SubscriptionPlan, SubscriptionPlanFeature } = require('../models');
|
||||||
const { Op } = require('sequelize');
|
const { Op } = require('sequelize');
|
||||||
|
|
||||||
// 전체 기능 목록 조회
|
// 전체 기능 목록 조회
|
||||||
@@ -20,7 +20,7 @@ exports.getFeatures = async (req, res) => {
|
|||||||
// 사용 가능한 기능 목록 조회
|
// 사용 가능한 기능 목록 조회
|
||||||
exports.getAvailableFeatures = async (req, res) => {
|
exports.getAvailableFeatures = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { clubId } = req.query;
|
const { clubId } = req.body;
|
||||||
|
|
||||||
if (!clubId) {
|
if (!clubId) {
|
||||||
return res.status(400).json({ message: '클럽 ID가 필요합니다.' });
|
return res.status(400).json({ message: '클럽 ID가 필요합니다.' });
|
||||||
@@ -47,10 +47,40 @@ exports.getAvailableFeatures = async (req, res) => {
|
|||||||
|
|
||||||
const activeFeatureIds = activeFeatures.map(af => af.featureId);
|
const activeFeatureIds = activeFeatures.map(af => af.featureId);
|
||||||
|
|
||||||
// 기능 목록에 활성화 상태 추가
|
// 현재 구독 중인 플랜에 포함된 기능 조회
|
||||||
|
const { ClubSubscription, SubscriptionPlan, SubscriptionPlanFeature } = require('../models');
|
||||||
|
|
||||||
|
// 현재 활성화된 구독 조회
|
||||||
|
const activeSubscription = await ClubSubscription.findOne({
|
||||||
|
where: {
|
||||||
|
clubId,
|
||||||
|
status: 'active',
|
||||||
|
endDate: {
|
||||||
|
[Op.gt]: new Date()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
attributes: ['planId']
|
||||||
|
});
|
||||||
|
|
||||||
|
let planFeatureIds = [];
|
||||||
|
|
||||||
|
// 현재 구독 플랜이 있는 경우, 해당 플랜에 포함된 기능 ID 목록 조회
|
||||||
|
if (activeSubscription) {
|
||||||
|
const planFeatures = await SubscriptionPlanFeature.findAll({
|
||||||
|
where: {
|
||||||
|
planId: activeSubscription.planId
|
||||||
|
},
|
||||||
|
attributes: ['featureId']
|
||||||
|
});
|
||||||
|
|
||||||
|
planFeatureIds = planFeatures.map(pf => pf.featureId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 기능 목록에 활성화 상태 및 플랜 포함 여부 추가
|
||||||
const formattedFeatures = features.map(feature => ({
|
const formattedFeatures = features.map(feature => ({
|
||||||
...feature.toJSON(),
|
...feature.toJSON(),
|
||||||
isActive: activeFeatureIds.includes(feature.id)
|
isActive: activeFeatureIds.includes(feature.id),
|
||||||
|
isIncludedInPlan: planFeatureIds.includes(feature.id)
|
||||||
}));
|
}));
|
||||||
|
|
||||||
res.json(formattedFeatures);
|
res.json(formattedFeatures);
|
||||||
@@ -63,7 +93,7 @@ exports.getAvailableFeatures = async (req, res) => {
|
|||||||
// 활성화된 기능 목록 조회
|
// 활성화된 기능 목록 조회
|
||||||
exports.getActiveFeatures = async (req, res) => {
|
exports.getActiveFeatures = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { clubId } = req.query;
|
const { clubId } = req.body;
|
||||||
|
|
||||||
if (!clubId) {
|
if (!clubId) {
|
||||||
return res.status(400).json({ message: '클럽 ID가 필요합니다.' });
|
return res.status(400).json({ message: '클럽 ID가 필요합니다.' });
|
||||||
@@ -80,10 +110,19 @@ exports.getActiveFeatures = async (req, res) => {
|
|||||||
model: Feature,
|
model: Feature,
|
||||||
attributes: ['name', 'description']
|
attributes: ['name', 'description']
|
||||||
}],
|
}],
|
||||||
attributes: ['id', 'featureId', 'purchaseDate', 'expiryDate']
|
attributes: ['id', 'featureId', 'createdAt', 'expiryDate']
|
||||||
});
|
});
|
||||||
|
|
||||||
res.json(activeFeatures);
|
// createdAt을 purchaseDate로 매핑
|
||||||
|
const formattedFeatures = activeFeatures.map(feature => ({
|
||||||
|
id: feature.id,
|
||||||
|
featureId: feature.featureId,
|
||||||
|
purchaseDate: feature.createdAt,
|
||||||
|
expiryDate: feature.expiryDate,
|
||||||
|
Feature: feature.Feature
|
||||||
|
}));
|
||||||
|
|
||||||
|
res.json(formattedFeatures);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('활성화된 기능 목록 조회 오류:', error);
|
console.error('활성화된 기능 목록 조회 오류:', error);
|
||||||
res.status(500).json({ message: '활성화된 기능 목록을 가져오는 중 오류가 발생했습니다.' });
|
res.status(500).json({ message: '활성화된 기능 목록을 가져오는 중 오류가 발생했습니다.' });
|
||||||
@@ -93,7 +132,7 @@ exports.getActiveFeatures = async (req, res) => {
|
|||||||
// 기능 구매 내역 조회
|
// 기능 구매 내역 조회
|
||||||
exports.getFeaturePayments = async (req, res) => {
|
exports.getFeaturePayments = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { clubId } = req.query;
|
const { clubId } = req.body;
|
||||||
|
|
||||||
if (!clubId) {
|
if (!clubId) {
|
||||||
return res.status(400).json({ message: '클럽 ID가 필요합니다.' });
|
return res.status(400).json({ message: '클럽 ID가 필요합니다.' });
|
||||||
@@ -105,29 +144,41 @@ exports.getFeaturePayments = async (req, res) => {
|
|||||||
},
|
},
|
||||||
include: [{
|
include: [{
|
||||||
model: Feature,
|
model: Feature,
|
||||||
attributes: ['name']
|
attributes: ['name', 'price']
|
||||||
}],
|
}],
|
||||||
attributes: [
|
attributes: [
|
||||||
'id',
|
'id',
|
||||||
'featureId',
|
'featureId',
|
||||||
'purchaseDate',
|
'createdAt',
|
||||||
'expiryDate',
|
'expiryDate',
|
||||||
'amount',
|
'enabled'
|
||||||
'duration',
|
|
||||||
'status'
|
|
||||||
],
|
],
|
||||||
order: [['purchaseDate', 'DESC']]
|
order: [['createdAt', 'DESC']]
|
||||||
});
|
});
|
||||||
|
|
||||||
const formattedPayments = payments.map(payment => ({
|
// 기간 계산 (월 단위)
|
||||||
id: payment.id,
|
const calculateDuration = (startDate, endDate) => {
|
||||||
featureName: payment.feature.name,
|
const start = new Date(startDate);
|
||||||
purchaseDate: payment.purchaseDate,
|
const end = new Date(endDate);
|
||||||
expiryDate: payment.expiryDate,
|
const diffTime = Math.abs(end - start);
|
||||||
amount: payment.amount,
|
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||||
duration: payment.duration,
|
return Math.ceil(diffDays / 30); // 대략적인 월 수 계산
|
||||||
status: payment.status
|
};
|
||||||
}));
|
|
||||||
|
const formattedPayments = payments.map(payment => {
|
||||||
|
const duration = calculateDuration(payment.createdAt, payment.expiryDate);
|
||||||
|
const amount = payment.Feature.price * duration;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: payment.id,
|
||||||
|
featureName: payment.Feature.name,
|
||||||
|
purchaseDate: payment.createdAt,
|
||||||
|
expiryDate: payment.expiryDate,
|
||||||
|
amount: amount,
|
||||||
|
duration: duration,
|
||||||
|
status: payment.enabled ? 'active' : 'inactive'
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
res.json(formattedPayments);
|
res.json(formattedPayments);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -172,27 +223,32 @@ exports.purchaseFeature = async (req, res) => {
|
|||||||
return res.status(400).json({ message: '이미 활성화된 기능입니다.' });
|
return res.status(400).json({ message: '이미 활성화된 기능입니다.' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 구매 금액 계산 (duration에 따른 할인 적용 가능)
|
// 만료일 계산
|
||||||
const amount = feature.price * duration;
|
|
||||||
|
|
||||||
// 기능 구매 기록 생성
|
|
||||||
const purchaseDate = new Date();
|
|
||||||
const expiryDate = new Date();
|
const expiryDate = new Date();
|
||||||
expiryDate.setMonth(expiryDate.getMonth() + duration);
|
expiryDate.setMonth(expiryDate.getMonth() + parseInt(duration));
|
||||||
|
|
||||||
const clubFeature = await ClubFeature.create({
|
const clubFeature = await ClubFeature.create({
|
||||||
clubId,
|
clubId,
|
||||||
featureId,
|
featureId,
|
||||||
purchaseDate,
|
|
||||||
expiryDate,
|
expiryDate,
|
||||||
amount,
|
enabled: true
|
||||||
duration,
|
|
||||||
status: 'active'
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 응답용 객체 생성
|
||||||
|
const purchaseInfo = {
|
||||||
|
id: clubFeature.id,
|
||||||
|
clubId: clubFeature.clubId,
|
||||||
|
featureId: clubFeature.featureId,
|
||||||
|
purchaseDate: clubFeature.createdAt,
|
||||||
|
expiryDate: clubFeature.expiryDate,
|
||||||
|
amount: feature.price * parseInt(duration),
|
||||||
|
duration: parseInt(duration),
|
||||||
|
status: 'active'
|
||||||
|
};
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
message: '기능 구매가 완료되었습니다.',
|
message: '기능 구매가 완료되었습니다.',
|
||||||
purchaseInfo: clubFeature
|
purchaseInfo
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('기능 구매 오류:', error);
|
console.error('기능 구매 오류:', error);
|
||||||
|
|||||||
@@ -21,6 +21,17 @@ exports.publicEventEntry = async (req, res) => {
|
|||||||
if (event.status === '취소') {
|
if (event.status === '취소') {
|
||||||
return res.status(403).json({ message: '취소된 이벤트입니다.' });
|
return res.status(403).json({ message: '취소된 이벤트입니다.' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 클럽의 구독 활성 여부 확인
|
||||||
|
const clubSubscription = await sequelize.models.ClubSubscription.findOne({
|
||||||
|
where: { clubId: event.clubId, status: 'active' },
|
||||||
|
include: [{ model: sequelize.models.SubscriptionPlan }]
|
||||||
|
});
|
||||||
|
|
||||||
|
// 구독이 비활성화된 경우 접근 제한
|
||||||
|
if (!clubSubscription) {
|
||||||
|
return res.status(403).json({ message: '클럽의 구독이 비활성화되어 이벤트에 접근할 수 없습니다. 클럽 관리자에게 문의하세요.' });
|
||||||
|
}
|
||||||
|
|
||||||
let token = null;
|
let token = null;
|
||||||
let isAuthenticated = false;
|
let isAuthenticated = false;
|
||||||
@@ -248,6 +259,26 @@ exports.addPublicGuest = async (req, res) => {
|
|||||||
const event = await Event.findOne({ where: { publicHash } });
|
const event = await Event.findOne({ where: { publicHash } });
|
||||||
if (!event) return res.status(404).json({ message: '이벤트를 찾을 수 없습니다.' });
|
if (!event) return res.status(404).json({ message: '이벤트를 찾을 수 없습니다.' });
|
||||||
if (event.status === '취소') return res.status(403).json({ message: '취소된 이벤트입니다.' });
|
if (event.status === '취소') return res.status(403).json({ message: '취소된 이벤트입니다.' });
|
||||||
|
|
||||||
|
// 클럽의 회원 수와 구독 플랜의 최대 회원 수 확인
|
||||||
|
const memberCount = await Member.count({ where: { clubId: event.clubId } });
|
||||||
|
|
||||||
|
// 클럽의 구독 정보 가져오기
|
||||||
|
const clubSubscription = await sequelize.models.ClubSubscription.findOne({
|
||||||
|
where: { clubId: event.clubId, status: 'active' },
|
||||||
|
include: [{ model: sequelize.models.SubscriptionPlan }]
|
||||||
|
});
|
||||||
|
|
||||||
|
// 구독 플랜이 있고 회원 수가 최대치를 초과하는 경우
|
||||||
|
if (clubSubscription && clubSubscription.SubscriptionPlan) {
|
||||||
|
const maxMembers = clubSubscription.SubscriptionPlan.maxMembers;
|
||||||
|
if (memberCount >= maxMembers) {
|
||||||
|
return res.status(403).json({
|
||||||
|
message: `회원 수가 구독 플랜의 최대 허용 인원(${maxMembers}명)을 초과했습니다. 구독 업그레이드가 필요합니다.`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// publicAuth 미들웨어로 인증됨
|
// publicAuth 미들웨어로 인증됨
|
||||||
const member = await Member.create({
|
const member = await Member.create({
|
||||||
name,
|
name,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const { SubscriptionPlan, SubscriptionPlanFeature, ClubSubscription, Feature, Club, User } = require('../models');
|
const { SubscriptionPlan, SubscriptionPlanFeature, ClubSubscription, Feature, Club, User, PaymentRequest } = require('../models');
|
||||||
|
|
||||||
// 전체 구독 목록 조회
|
// 전체 구독 목록 조회
|
||||||
exports.getClubSubscriptions = async (req, res) => {
|
exports.getClubSubscriptions = async (req, res) => {
|
||||||
@@ -45,6 +45,29 @@ exports.getSubscriptionPlans = async (req, res) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 플랜 상세 정보 조회 (포함된 기능 포함)
|
||||||
|
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) => {
|
exports.createSubscriptionPlan = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
@@ -192,7 +215,7 @@ exports.subscribeClub = async (req, res) => {
|
|||||||
// 클럽의 현재 구독 정보 조회
|
// 클럽의 현재 구독 정보 조회
|
||||||
exports.getClubSubscription = async (req, res) => {
|
exports.getClubSubscription = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { clubId } = req.params;
|
const { clubId } = req.body;
|
||||||
|
|
||||||
const subscription = await ClubSubscription.findOne({
|
const subscription = await ClubSubscription.findOne({
|
||||||
where: { clubId, status: 'active' },
|
where: { clubId, status: 'active' },
|
||||||
@@ -238,7 +261,9 @@ exports.getCurrentSubscription = async (req, res) => {
|
|||||||
planId: subscription.SubscriptionPlan.id,
|
planId: subscription.SubscriptionPlan.id,
|
||||||
status: subscription.status,
|
status: subscription.status,
|
||||||
startDate: subscription.startDate,
|
startDate: subscription.startDate,
|
||||||
endDate: subscription.expiryDate
|
endDate: subscription.endDate,
|
||||||
|
planPrice: subscription.SubscriptionPlan.price,
|
||||||
|
price: subscription.price
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error in getCurrentSubscription:', error);
|
console.error('Error in getCurrentSubscription:', error);
|
||||||
@@ -267,7 +292,368 @@ exports.getSubscriptionPayments = async (req, res) => {
|
|||||||
})));
|
})));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error in getSubscriptionPayments:', error);
|
console.error('Error in getSubscriptionPayments:', error);
|
||||||
res.status(500).json({ message: '결제 내역 조회 중 오류가 발생했습니다.' });
|
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
|
||||||
|
});
|
||||||
|
|
||||||
|
// 토스페이먼츠에 전송할 데이터 구성
|
||||||
|
const paymentData = {
|
||||||
|
orderId: paymentInfo.orderId,
|
||||||
|
amount: totalAmount,
|
||||||
|
orderName: planId ? `구독 플랜: ${planDuration}개월` : '추가 기능 구매',
|
||||||
|
customerName: club.name,
|
||||||
|
successUrl: successUrl || `${process.env.FRONTEND_URL}/payment/success`,
|
||||||
|
failUrl: failUrl || `${process.env.FRONTEND_URL}/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=결제 취소 처리 중 오류가 발생했습니다.`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
const { DataTypes } = require('sequelize');
|
||||||
|
const { sequelize } = require('../config/database');
|
||||||
|
|
||||||
|
const PaymentRequest = sequelize.define('PaymentRequest', {
|
||||||
|
id: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
primaryKey: true,
|
||||||
|
autoIncrement: true,
|
||||||
|
comment: '결제 요청 고유 ID'
|
||||||
|
},
|
||||||
|
clubId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
comment: '클럽 ID'
|
||||||
|
},
|
||||||
|
planId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: true,
|
||||||
|
comment: '선택한 구독 플랜 ID'
|
||||||
|
},
|
||||||
|
currentPlanId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: true,
|
||||||
|
comment: '현재 구독 중인 플랜 ID'
|
||||||
|
},
|
||||||
|
isUpgrade: {
|
||||||
|
type: DataTypes.BOOLEAN,
|
||||||
|
defaultValue: false,
|
||||||
|
comment: '업그레이드 여부 (true: 업그레이드, false: 다운그레이드 또는 신규)'
|
||||||
|
},
|
||||||
|
planDuration: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
defaultValue: 1,
|
||||||
|
comment: '구독 기간 (개월 단위)'
|
||||||
|
},
|
||||||
|
features: {
|
||||||
|
type: DataTypes.TEXT,
|
||||||
|
allowNull: true,
|
||||||
|
comment: '추가 기능 목록 (JSON 문자열)'
|
||||||
|
},
|
||||||
|
totalAmount: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
comment: '총 결제 금액 (원)'
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: 'pending',
|
||||||
|
comment: '결제 상태 (pending: 대기, success: 성공, failed: 실패, cancelled: 취소)'
|
||||||
|
},
|
||||||
|
orderId: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false,
|
||||||
|
unique: true,
|
||||||
|
comment: '주문 고유 ID'
|
||||||
|
},
|
||||||
|
paymentKey: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
comment: '결제 서비스 제공자의 결제 키'
|
||||||
|
},
|
||||||
|
paymentData: {
|
||||||
|
type: DataTypes.TEXT,
|
||||||
|
allowNull: true,
|
||||||
|
comment: '결제 관련 추가 데이터 (JSON 문자열)'
|
||||||
|
},
|
||||||
|
planChange: {
|
||||||
|
type: DataTypes.BOOLEAN,
|
||||||
|
defaultValue: false,
|
||||||
|
comment: '플랜 변경 여부 (true: 플랜 변경, false: 신규 구독 또는 기능만 추가)'
|
||||||
|
},
|
||||||
|
failReason: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
comment: '결제 실패 사유'
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
timestamps: true
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = PaymentRequest;
|
||||||
@@ -16,6 +16,7 @@ const SubscriptionPlanFeature = require('./SubscriptionPlanFeature');
|
|||||||
const ClubSubscription = require('./ClubSubscription');
|
const ClubSubscription = require('./ClubSubscription');
|
||||||
const ActivityLog = require('./activityLog');
|
const ActivityLog = require('./activityLog');
|
||||||
const EventTeamMember = require('./EventTeamMember');
|
const EventTeamMember = require('./EventTeamMember');
|
||||||
|
const PaymentRequest = require('./PaymentRequest');
|
||||||
|
|
||||||
// 모델 동기화 함수
|
// 모델 동기화 함수
|
||||||
const syncModels = async () => {
|
const syncModels = async () => {
|
||||||
@@ -52,6 +53,7 @@ const db = {
|
|||||||
ClubSubscription,
|
ClubSubscription,
|
||||||
ActivityLog,
|
ActivityLog,
|
||||||
EventTeamMember,
|
EventTeamMember,
|
||||||
|
PaymentRequest,
|
||||||
};
|
};
|
||||||
Object.values(db).forEach(model => {
|
Object.values(db).forEach(model => {
|
||||||
if (model.associate) model.associate(db);
|
if (model.associate) model.associate(db);
|
||||||
@@ -75,5 +77,6 @@ module.exports = {
|
|||||||
ClubSubscription,
|
ClubSubscription,
|
||||||
ActivityLog,
|
ActivityLog,
|
||||||
EventTeamMember,
|
EventTeamMember,
|
||||||
|
PaymentRequest,
|
||||||
syncModels
|
syncModels
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,23 +1,32 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const subscriptionController = require('../controllers/subscriptionController');
|
const subscriptionController = require('../controllers/subscriptionController');
|
||||||
const authMiddleware = require('../middleware/auth');
|
const { authenticateJWT } = require('../middleware/authMiddleware');
|
||||||
// 구독 목록 조회
|
// 구독 목록 조회
|
||||||
router.get('/', authMiddleware, subscriptionController.getClubSubscriptions);
|
router.get('/', authenticateJWT, subscriptionController.getClubSubscriptions);
|
||||||
|
|
||||||
// 구독 플랜 관리 (관리자 전용)
|
// 구독 플랜 관리 (관리자 전용)
|
||||||
router.get('/plans', authMiddleware, subscriptionController.getSubscriptionPlans);
|
router.get('/plans', authenticateJWT, subscriptionController.getSubscriptionPlans);
|
||||||
router.post('/plans', authMiddleware, subscriptionController.createSubscriptionPlan);
|
router.post('/plans', authenticateJWT, subscriptionController.createSubscriptionPlan);
|
||||||
router.put('/plans/:id', authMiddleware, subscriptionController.updateSubscriptionPlan);
|
router.put('/plans/:id', authenticateJWT, subscriptionController.updateSubscriptionPlan);
|
||||||
router.delete('/plans/:id', authMiddleware, subscriptionController.deleteSubscriptionPlan);
|
router.delete('/plans/:id', authenticateJWT, subscriptionController.deleteSubscriptionPlan);
|
||||||
router.get('/plans/:planId/subscriptions', authMiddleware, subscriptionController.getPlanSubscriptions);
|
router.get('/plans/:planId/subscriptions', authenticateJWT, subscriptionController.getPlanSubscriptions);
|
||||||
|
|
||||||
|
// 플랜 상세 정보 조회 (포함된 기능 포함)
|
||||||
|
router.get('/plans/:planId', authenticateJWT, subscriptionController.getPlanDetail);
|
||||||
|
|
||||||
// 클럽 구독 관리
|
// 클럽 구독 관리
|
||||||
router.post('/subscribe', authMiddleware, subscriptionController.subscribeClub);
|
router.post('/subscribe', authenticateJWT, subscriptionController.subscribeClub);
|
||||||
router.get('/clubs/:clubId', authMiddleware, subscriptionController.getClubSubscription);
|
router.post('/clubs', authenticateJWT, subscriptionController.getClubSubscription);
|
||||||
|
|
||||||
// 클럽 구독 정보 및 결제 내역 (POST, 클럽 기준)
|
// 클럽 구독 정보 및 결제 내역 (POST, 클럽 기준)
|
||||||
router.post('/current', authMiddleware, subscriptionController.getCurrentSubscription);
|
router.post('/current', authenticateJWT, subscriptionController.getCurrentSubscription);
|
||||||
router.post('/payments', authMiddleware, subscriptionController.getSubscriptionPayments);
|
router.post('/payments', authenticateJWT, subscriptionController.getSubscriptionPayments);
|
||||||
|
router.post('/change-with-features', authenticateJWT, subscriptionController.changeWithFeatures);
|
||||||
|
|
||||||
|
// 토스페이먼츠 결제 관련 라우트
|
||||||
|
router.post('/payment/request', authenticateJWT, subscriptionController.requestPayment);
|
||||||
|
router.post('/payment/success', subscriptionController.processPaymentSuccess);
|
||||||
|
router.post('/payment/fail', subscriptionController.processPaymentFail);
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
Generated
+40
@@ -15,6 +15,7 @@
|
|||||||
"@fullcalendar/vue3": "^6.1.15",
|
"@fullcalendar/vue3": "^6.1.15",
|
||||||
"@primeuix/themes": "^1.0.3",
|
"@primeuix/themes": "^1.0.3",
|
||||||
"@primevue/themes": "^4.3.3",
|
"@primevue/themes": "^4.3.3",
|
||||||
|
"@tosspayments/payment-sdk": "^1.9.1",
|
||||||
"@vuelidate/core": "^2.0.3",
|
"@vuelidate/core": "^2.0.3",
|
||||||
"@vuelidate/validators": "^2.0.4",
|
"@vuelidate/validators": "^2.0.4",
|
||||||
"axios": "^1.8.2",
|
"axios": "^1.8.2",
|
||||||
@@ -1459,6 +1460,45 @@
|
|||||||
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
|
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@tosspayments/payment__types": {
|
||||||
|
"version": "1.69.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tosspayments/payment__types/-/payment__types-1.69.0.tgz",
|
||||||
|
"integrity": "sha512-EbU36vLEWfhRpX/g4V0DyRiUmTkNeeVDYjJamD9aJiLmRbLl9PVdXi5QOQTMYma2RGRgA8drHY4yDMsjvKFwCg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@tosspayments/sdk-constants": "^0.2.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tosspayments/payment-sdk": {
|
||||||
|
"version": "1.9.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tosspayments/payment-sdk/-/payment-sdk-1.9.1.tgz",
|
||||||
|
"integrity": "sha512-OzDG2kOGOrb5JRvprR8R/mQzXFNnLW/6X09uLY1KF4+O7ASu6LrMoFCERd+WeAJMtRAARkclGDcJ/GSu+OHzzg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@tosspayments/payment__types": "1.69.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tosspayments/sdk-constants": {
|
||||||
|
"version": "0.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tosspayments/sdk-constants/-/sdk-constants-0.2.2.tgz",
|
||||||
|
"integrity": "sha512-qTIcD9JnVtN65/yiW5sPAlvK5fpQtDluv4IEyslubncbQLQKE+ePSnGZU1fLNPxcchv6QTnwmgj9Ndm6EN/Ymg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"type-fest": "^2.11.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tosspayments/sdk-constants/node_modules/type-fest": {
|
||||||
|
"version": "2.19.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz",
|
||||||
|
"integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==",
|
||||||
|
"license": "(MIT OR CC0-1.0)",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.20"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/estree": {
|
"node_modules/@types/estree": {
|
||||||
"version": "1.0.7",
|
"version": "1.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
"@fullcalendar/vue3": "^6.1.15",
|
"@fullcalendar/vue3": "^6.1.15",
|
||||||
"@primeuix/themes": "^1.0.3",
|
"@primeuix/themes": "^1.0.3",
|
||||||
"@primevue/themes": "^4.3.3",
|
"@primevue/themes": "^4.3.3",
|
||||||
|
"@tosspayments/payment-sdk": "^1.9.1",
|
||||||
"@vuelidate/core": "^2.0.3",
|
"@vuelidate/core": "^2.0.3",
|
||||||
"@vuelidate/validators": "^2.0.4",
|
"@vuelidate/validators": "^2.0.4",
|
||||||
"axios": "^1.8.2",
|
"axios": "^1.8.2",
|
||||||
|
|||||||
+30
-12
@@ -86,11 +86,13 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted, onBeforeUnmount, provide } from 'vue';
|
import { ref, computed, onMounted, onBeforeUnmount, provide, watch } from 'vue';
|
||||||
import { useRouter, useRoute } from 'vue-router';
|
import { useRouter, useRoute } from 'vue-router';
|
||||||
import AppHeader from './components/AppHeader.vue';
|
|
||||||
import AppPublicLayout from './layouts/AppPublicLayout.vue';
|
|
||||||
import apiClient from './services/api';
|
import apiClient from './services/api';
|
||||||
|
import authService from './services/authService';
|
||||||
|
import { useClubStore } from './stores/clubStore';
|
||||||
|
import AppPublicLayout from './layouts/AppPublicLayout.vue';
|
||||||
|
import AppHeader from './components/AppHeader.vue';
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const layoutComponent = computed(() => route.meta.layout === 'public' ? AppPublicLayout : 'div');
|
const layoutComponent = computed(() => route.meta.layout === 'public' ? AppPublicLayout : 'div');
|
||||||
@@ -98,7 +100,9 @@ const isPublicLayout = computed(() => {
|
|||||||
// 라우트 meta.layout이 public이거나, 경로가 /public/로 시작하면 public 레이아웃으로 간주
|
// 라우트 meta.layout이 public이거나, 경로가 /public/로 시작하면 public 레이아웃으로 간주
|
||||||
return route.meta.layout === 'public' || route.path.startsWith('/public/');
|
return route.meta.layout === 'public' || route.path.startsWith('/public/');
|
||||||
});
|
});
|
||||||
import authService from './services/authService';
|
|
||||||
|
// 클럽 스토어 초기화
|
||||||
|
const clubStore = useClubStore();
|
||||||
|
|
||||||
// 상태 변수
|
// 상태 변수
|
||||||
const user = ref(null);
|
const user = ref(null);
|
||||||
@@ -188,16 +192,21 @@ const fetchClubs = async () => {
|
|||||||
const fetchClubInfo = async () => {
|
const fetchClubInfo = async () => {
|
||||||
if (isPublicLayout.value) return;
|
if (isPublicLayout.value) return;
|
||||||
try {
|
try {
|
||||||
const response = await apiClient.post(`/api/club`, { clubId: selectedClubId.value ?? clubs.value[0].id });
|
// 클럽 스토어를 통해 클럽 정보 가져오기
|
||||||
|
const clubId = selectedClubId.value ?? (clubs.value.length > 0 ? clubs.value[0].id : null);
|
||||||
|
if (!clubId) return;
|
||||||
|
|
||||||
|
const clubData = await clubStore.fetchClubById(clubId);
|
||||||
|
|
||||||
if (response.data) {
|
if (clubData) {
|
||||||
selectedClub.value = {
|
selectedClub.value = {
|
||||||
id: response.data.id,
|
id: clubData.id,
|
||||||
name: response.data.name,
|
name: clubData.name,
|
||||||
location: response.data.location,
|
location: clubData.location,
|
||||||
|
subscription: clubData.subscription || null
|
||||||
};
|
};
|
||||||
memberCount.value = response.data.memberCount;
|
memberCount.value = clubData.memberCount;
|
||||||
ownerName.value = response.data.ownerName;
|
ownerName.value = clubData.ownerName;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('클럽 정보 가져오기 오류:', error);
|
console.error('클럽 정보 가져오기 오류:', error);
|
||||||
@@ -287,7 +296,16 @@ const loadMenuItems = async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
menuItems.value = groupMenus;
|
menuItems.value = groupMenus;
|
||||||
singleMenuItems.value = singleMenus;
|
|
||||||
|
// 구독관리 메뉴 하드코딩으로 추가
|
||||||
|
singleMenuItems.value = [
|
||||||
|
...singleMenus,
|
||||||
|
{
|
||||||
|
label: '구독관리',
|
||||||
|
icon: 'pi pi-credit-card',
|
||||||
|
to: '/club/subscription'
|
||||||
|
}
|
||||||
|
];
|
||||||
} else {
|
} else {
|
||||||
menuItems.value = [];
|
menuItems.value = [];
|
||||||
singleMenuItems.value = [];
|
singleMenuItems.value = [];
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
<Tab value="기본정보"><i class="pi pi-info-circle mr-2" />기본정보</Tab>
|
<Tab value="기본정보"><i class="pi pi-info-circle mr-2" />기본정보</Tab>
|
||||||
<Tab value="참가자 관리"><i class="pi pi-users mr-2" />참가자 관리</Tab>
|
<Tab value="참가자 관리"><i class="pi pi-users mr-2" />참가자 관리</Tab>
|
||||||
<Tab value="점수 관리"><i class="pi pi-chart-bar mr-2" />점수 관리</Tab>
|
<Tab value="점수 관리"><i class="pi pi-chart-bar mr-2" />점수 관리</Tab>
|
||||||
<Tab value="팀 편성"><i class="pi pi-sitemap mr-2" />팀 편성</Tab>
|
<Tab v-if="hasTeamFeature" value="팀 편성"><i class="pi pi-sitemap mr-2" />팀 편성</Tab>
|
||||||
</TabList>
|
</TabList>
|
||||||
<TabPanels>
|
<TabPanels>
|
||||||
<!-- 기본 정보 탭 -->
|
<!-- 기본 정보 탭 -->
|
||||||
@@ -77,7 +77,7 @@
|
|||||||
/>
|
/>
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
<!-- 팀 편성 탭 -->
|
<!-- 팀 편성 탭 -->
|
||||||
<TabPanel value="팀 편성">
|
<TabPanel v-if="hasTeamFeature" value="팀 편성">
|
||||||
<TeamGeneration :participants="confirmedParticipants" :teams="teams" @save-teams="handleSaveTeams" />
|
<TeamGeneration :participants="confirmedParticipants" :teams="teams" @save-teams="handleSaveTeams" />
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
</TabPanels>
|
</TabPanels>
|
||||||
@@ -105,6 +105,8 @@ const props = defineProps({
|
|||||||
getStatusName: { type: Function, required: true },
|
getStatusName: { type: Function, required: true },
|
||||||
getStatusSeverity: { type: Function, required: true },
|
getStatusSeverity: { type: Function, required: true },
|
||||||
formatDate: { type: Function, required: true },
|
formatDate: { type: Function, required: true },
|
||||||
|
// 팀 기능 사용 권한 여부
|
||||||
|
hasTeamFeature: { type: Boolean, default: false },
|
||||||
|
|
||||||
// (권장) 상태명이 매칭 안 될 경우 실제 상태 문자열 반환
|
// (권장) 상태명이 매칭 안 될 경우 실제 상태 문자열 반환
|
||||||
// getStatusName 함수 예시:
|
// getStatusName 함수 예시:
|
||||||
@@ -139,11 +141,16 @@ const hideDialog = () => {
|
|||||||
// 팀 생성 결과 저장
|
// 팀 생성 결과 저장
|
||||||
const teams = ref([]);
|
const teams = ref([]);
|
||||||
|
|
||||||
// 이벤트 상세 다이얼로그가 열릴 때마다 팀 목록 불러오기
|
// 이벤트 상세 다이얼로그가 열릴 때마다 팀 목록 불러오기 (팀 기능 사용 권한이 있는 경우에만)
|
||||||
watch(() => eventModel.value.id, async (eventId) => {
|
watch(() => eventModel.value.id, async (eventId) => {
|
||||||
if (eventId) {
|
if (eventId && props.hasTeamFeature) {
|
||||||
const latestTeams = await teamService.getTeams(eventId);
|
try {
|
||||||
teams.value = latestTeams;
|
const latestTeams = await teamService.getTeams(eventId);
|
||||||
|
teams.value = latestTeams;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('팀 정보를 불러오는 중 오류가 발생했습니다:', error);
|
||||||
|
teams.value = [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, { immediate: true });
|
}, { immediate: true });
|
||||||
|
|
||||||
@@ -166,6 +173,12 @@ import { useToast } from 'primevue/usetoast';
|
|||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
|
||||||
async function handleSaveTeams(savedTeams) {
|
async function handleSaveTeams(savedTeams) {
|
||||||
|
// 팀 기능 사용 권한이 없는 경우 저장하지 않음
|
||||||
|
if (!props.hasTeamFeature) {
|
||||||
|
toast.add({ severity: 'error', summary: '권한 없음', detail: '팀 기능을 사용할 수 있는 권한이 없습니다.', life: 3000 });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const eventId = eventModel.value.id;
|
const eventId = eventModel.value.id;
|
||||||
try {
|
try {
|
||||||
await teamService.saveTeams(eventId, savedTeams);
|
await teamService.saveTeams(eventId, savedTeams);
|
||||||
|
|||||||
@@ -23,6 +23,10 @@ const UserClubCreate = () => import('@/views/club/ClubCreate.vue');
|
|||||||
// 구독 관리 컴포넌트 import
|
// 구독 관리 컴포넌트 import
|
||||||
const ClubSubscriptionManagement = () => import('@/views/club/subscription/SubscriptionManagement.vue');
|
const ClubSubscriptionManagement = () => import('@/views/club/subscription/SubscriptionManagement.vue');
|
||||||
|
|
||||||
|
// 결제 관련 컴포넌트 import
|
||||||
|
const PaymentSuccess = () => import('@/views/payment/PaymentSuccess.vue');
|
||||||
|
const PaymentFail = () => import('@/views/payment/PaymentFail.vue');
|
||||||
|
|
||||||
const EventPublicPage = () => import('@/views/event/EventPublicPage.vue');
|
const EventPublicPage = () => import('@/views/event/EventPublicPage.vue');
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
@@ -137,6 +141,20 @@ const routes = [
|
|||||||
name: 'UserClubCreate',
|
name: 'UserClubCreate',
|
||||||
component: () => import('@/views/club/ClubCreate.vue'),
|
component: () => import('@/views/club/ClubCreate.vue'),
|
||||||
meta: { requiresAuth: true }
|
meta: { requiresAuth: true }
|
||||||
|
},
|
||||||
|
|
||||||
|
// 결제 관련 경로
|
||||||
|
{
|
||||||
|
path: '/payment/success',
|
||||||
|
name: 'PaymentSuccess',
|
||||||
|
component: PaymentSuccess,
|
||||||
|
meta: { requiresAuth: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/payment/fail',
|
||||||
|
name: 'PaymentFail',
|
||||||
|
component: PaymentFail,
|
||||||
|
meta: { requiresAuth: true }
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -37,9 +37,42 @@ apiClient.interceptors.response.use(
|
|||||||
if (error.response && error.response.status === 401) {
|
if (error.response && error.response.status === 401) {
|
||||||
// 로컬 스토리지에서 토큰 제거
|
// 로컬 스토리지에서 토큰 제거
|
||||||
localStorage.removeItem('token');
|
localStorage.removeItem('token');
|
||||||
// 로그인 페이지로 리디렉션
|
localStorage.removeItem('user');
|
||||||
window.location.href = '/login';
|
|
||||||
|
// 현재 페이지가 이미 로그인 페이지가 아닌 경우에만 리디렉션
|
||||||
|
const currentPath = window.location.pathname;
|
||||||
|
if (currentPath !== '/login') {
|
||||||
|
// 로그인 페이지로 리디렉션
|
||||||
|
window.location.href = '/login';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 403 에러 (권한 없음) 처리
|
||||||
|
if (error.response && error.response.status === 403) {
|
||||||
|
// 구독 관련 오류 메시지인지 확인
|
||||||
|
const errorMessage = error.response.data?.error || '';
|
||||||
|
if (errorMessage.includes('기능을 사용할 수 없습니다') ||
|
||||||
|
errorMessage.includes('권한이 없습니다')) {
|
||||||
|
|
||||||
|
// 사용자에게 구독 안내 표시
|
||||||
|
import('primevue/usetoast').then(({ useToast }) => {
|
||||||
|
const toast = useToast();
|
||||||
|
toast.add({
|
||||||
|
severity: 'warn',
|
||||||
|
summary: '권한 없음',
|
||||||
|
detail: '해당 기능을 사용하려면 구독이 필요합니다. 구독 페이지로 이동합니다.',
|
||||||
|
life: 5000
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3초 후 구독 페이지로 이동
|
||||||
|
setTimeout(() => {
|
||||||
|
const clubId = localStorage.getItem('clubId');
|
||||||
|
window.location.href = `/club/subscription`;
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -19,8 +19,13 @@ export default {
|
|||||||
const response = await apiClient.post('/api/club/events', { clubId });
|
const response = await apiClient.post('/api/club/events', { clubId });
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.response && error.response.data && error.response.data.message) {
|
// 원래 오류 객체 그대로 전달하여 HTTP 상태 코드 확인 가능하게 함
|
||||||
throw new Error(error.response.data.message);
|
if (error.response) {
|
||||||
|
console.error('이벤트 목록 오류:', {
|
||||||
|
status: error.response.status,
|
||||||
|
data: error.response.data
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ export const useClubStore = defineStore('club', {
|
|||||||
clubs: [],
|
clubs: [],
|
||||||
selectedClub: null,
|
selectedClub: null,
|
||||||
loading: false,
|
loading: false,
|
||||||
error: null
|
error: null,
|
||||||
|
subscription: null,
|
||||||
|
subscriptionFeatures: []
|
||||||
}),
|
}),
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
@@ -22,13 +24,43 @@ export const useClubStore = defineStore('club', {
|
|||||||
this.loading = false;
|
this.loading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 클럽의 구독 정보 가져오기
|
||||||
|
async fetchClubSubscription(clubId) {
|
||||||
|
try {
|
||||||
|
const response = await apiClient.post('/api/subscriptions/clubs', { clubId });
|
||||||
|
this.subscription = response.data;
|
||||||
|
// 구독 기능 정보 처리
|
||||||
|
if (response.data && response.data.SubscriptionPlan && response.data.SubscriptionPlan.Features) {
|
||||||
|
this.subscriptionFeatures = response.data.SubscriptionPlan.Features;
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('구독 정보를 가져오는 중 오류가 발생했습니다:', error);
|
||||||
|
this.subscription = null;
|
||||||
|
this.subscriptionFeatures = [];
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
async fetchClubById(clubId) {
|
async fetchClubById(clubId) {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
try {
|
try {
|
||||||
const response = await apiClient.post(`/api/club`, { clubId });
|
// 클럽 정보와 구독 정보를 동시에 가져오기
|
||||||
this.selectedClub = response.data;
|
const [clubResponse, subscriptionResponse] = await Promise.all([
|
||||||
return response.data;
|
apiClient.post(`/api/club`, { clubId }),
|
||||||
|
this.fetchClubSubscription(clubId)
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 클럽 정보와 구독 정보 합치기
|
||||||
|
const clubData = clubResponse.data;
|
||||||
|
if (subscriptionResponse) {
|
||||||
|
clubData.subscription = subscriptionResponse;
|
||||||
|
clubData.subscriptionFeatures = subscriptionResponse.SubscriptionPlan?.Features || [];
|
||||||
|
}
|
||||||
|
this.selectedClub = clubData;
|
||||||
|
return clubData;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.error = error.response?.data?.message || '클럽 정보를 가져오는데 실패했습니다.';
|
this.error = error.response?.data?.message || '클럽 정보를 가져오는데 실패했습니다.';
|
||||||
throw error;
|
throw error;
|
||||||
@@ -44,5 +76,26 @@ export const useClubStore = defineStore('club', {
|
|||||||
clearSelectedClub() {
|
clearSelectedClub() {
|
||||||
this.selectedClub = null;
|
this.selectedClub = null;
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getters: {
|
||||||
|
// 현재 구독 정보 가져오기
|
||||||
|
getCurrentSubscription() {
|
||||||
|
return this.subscription;
|
||||||
|
},
|
||||||
|
|
||||||
|
// 특정 기능 사용 권한 확인
|
||||||
|
hasFeature() {
|
||||||
|
return (featureCode) => {
|
||||||
|
if (!this.subscriptionFeatures || this.subscriptionFeatures.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return this.subscriptionFeatures.some(feature =>
|
||||||
|
feature.code === featureCode && feature.status === 'active'
|
||||||
|
);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -83,6 +83,7 @@
|
|||||||
:getStatusSeverity="getStatusSeverity"
|
:getStatusSeverity="getStatusSeverity"
|
||||||
:formatDate="formatDate"
|
:formatDate="formatDate"
|
||||||
:availableMembers="clubMembers"
|
:availableMembers="clubMembers"
|
||||||
|
:hasTeamFeature="hasTeamFeature"
|
||||||
@edit="editFromDetails"
|
@edit="editFromDetails"
|
||||||
@hide="hideDialog"
|
@hide="hideDialog"
|
||||||
@participant-updated="fetchEvents"
|
@participant-updated="fetchEvents"
|
||||||
@@ -105,11 +106,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue';
|
import { ref, onMounted, computed } from 'vue';
|
||||||
import { useToast } from 'primevue/usetoast';
|
import { useToast } from 'primevue/usetoast';
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
import { useClubStore } from '@/stores/clubStore';
|
import { useClubStore } from '@/stores/clubStore';
|
||||||
import eventService from '@/services/eventService';
|
import eventService from '@/services/eventService';
|
||||||
|
import apiClient from '@/services/api';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import clubService from '@/services/clubService';
|
import clubService from '@/services/clubService';
|
||||||
import ScoreManagement from '@/components/event/ScoreManagement.vue';
|
import ScoreManagement from '@/components/event/ScoreManagement.vue';
|
||||||
@@ -142,6 +144,9 @@ const filters = ref({
|
|||||||
startDate: { value: null, matchMode: 'equals' }
|
startDate: { value: null, matchMode: 'equals' }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 팀 기능 사용 권한 여부
|
||||||
|
const hasTeamFeature = computed(() => clubStore.hasFeature('team_create'));
|
||||||
|
|
||||||
// 이벤트 유형 옵션
|
// 이벤트 유형 옵션
|
||||||
const eventTypeOptions = [
|
const eventTypeOptions = [
|
||||||
{ name: '정기전', value: '정기전' },
|
{ name: '정기전', value: '정기전' },
|
||||||
@@ -156,17 +161,22 @@ const clubMembers = ref([]);
|
|||||||
|
|
||||||
// 이벤트 목록 가져오기
|
// 이벤트 목록 가져오기
|
||||||
const fetchEvents = async () => {
|
const fetchEvents = async () => {
|
||||||
loading.value = true;
|
|
||||||
try {
|
try {
|
||||||
const response = await eventService.getEvents(clubId.value);
|
loading.value = true;
|
||||||
events.value = response;
|
const data = await eventService.getEvents(clubId.value);
|
||||||
|
events.value = data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.add({
|
console.error('이벤트 목록 로드 실패:', error);
|
||||||
severity: 'error',
|
|
||||||
summary: '오류',
|
// 403 오류는 중앙에서 처리하민로 여기서는 기타 오류만 처리
|
||||||
detail: '이벤트 목록을 가져오는데 실패했습니다.',
|
if (!error.response) {
|
||||||
life: 3000
|
toast.add({
|
||||||
});
|
severity: 'error',
|
||||||
|
summary: '오류',
|
||||||
|
detail: '이벤트 목록을 불러오는 데 실패했습니다.',
|
||||||
|
life: 3000
|
||||||
|
});
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,132 @@
|
|||||||
|
<template>
|
||||||
|
<div class="payment-result fail">
|
||||||
|
<div class="result-container">
|
||||||
|
<div class="icon-container">
|
||||||
|
<i class="pi pi-times-circle"></i>
|
||||||
|
</div>
|
||||||
|
<h1>결제가 취소되었습니다</h1>
|
||||||
|
<p v-if="message">{{ message }}</p>
|
||||||
|
<div class="buttons">
|
||||||
|
<button class="primary-btn" @click="goToSubscription">구독 관리로 이동</button>
|
||||||
|
<button class="secondary-btn" @click="goToHome">홈으로 이동</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue';
|
||||||
|
import { useRouter, useRoute } from 'vue-router';
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const route = useRoute();
|
||||||
|
const message = ref('');
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
// URL에서 메시지 가져오기
|
||||||
|
message.value = route.query.message || '결제가 취소되었습니다.';
|
||||||
|
|
||||||
|
// 부모 창에 결제 실패 메시지 전달
|
||||||
|
if (window.opener && window.opener.handlePaymentFail) {
|
||||||
|
window.opener.handlePaymentFail(message.value);
|
||||||
|
// 3초 후 창 닫기
|
||||||
|
setTimeout(() => {
|
||||||
|
window.close();
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const goToSubscription = () => {
|
||||||
|
if (window.opener) {
|
||||||
|
window.opener.location.href = '/club/subscription';
|
||||||
|
window.close();
|
||||||
|
} else {
|
||||||
|
router.push('/club/subscription');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const goToHome = () => {
|
||||||
|
if (window.opener) {
|
||||||
|
window.opener.location.href = '/';
|
||||||
|
window.close();
|
||||||
|
} else {
|
||||||
|
router.push('/');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.payment-result {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-container {
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
padding: 40px;
|
||||||
|
text-align: center;
|
||||||
|
max-width: 500px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-container {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fail .icon-container i {
|
||||||
|
font-size: 80px;
|
||||||
|
color: #f44336;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 24px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary-btn, .secondary-btn {
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary-btn {
|
||||||
|
background-color: #4caf50;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary-btn:hover {
|
||||||
|
background-color: #43a047;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.secondary-btn {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
color: #333;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.secondary-btn:hover {
|
||||||
|
background-color: #e9e9e9;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
<template>
|
||||||
|
<div class="payment-result success">
|
||||||
|
<div class="result-container">
|
||||||
|
<div class="icon-container">
|
||||||
|
<i class="pi pi-check-circle"></i>
|
||||||
|
</div>
|
||||||
|
<h1>결제가 완료되었습니다</h1>
|
||||||
|
<p>주문번호: {{ orderId }}</p>
|
||||||
|
<div class="buttons">
|
||||||
|
<button class="primary-btn" @click="goToSubscription">구독 관리로 이동</button>
|
||||||
|
<button class="secondary-btn" @click="goToHome">홈으로 이동</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue';
|
||||||
|
import { useRouter, useRoute } from 'vue-router';
|
||||||
|
import apiClient from '@/services/api';
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const route = useRoute();
|
||||||
|
const orderId = ref('');
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
// URL에서 주문 ID 가져오기
|
||||||
|
orderId.value = route.query.orderId || '';
|
||||||
|
|
||||||
|
// 부모 창에 결제 성공 메시지 전달
|
||||||
|
if (window.opener && window.opener.handlePaymentSuccess) {
|
||||||
|
window.opener.handlePaymentSuccess(orderId.value);
|
||||||
|
// 3초 후 창 닫기
|
||||||
|
setTimeout(() => {
|
||||||
|
window.close();
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const goToSubscription = () => {
|
||||||
|
if (window.opener) {
|
||||||
|
window.opener.location.href = '/club/subscription';
|
||||||
|
window.close();
|
||||||
|
} else {
|
||||||
|
router.push('/club/subscription');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const goToHome = () => {
|
||||||
|
if (window.opener) {
|
||||||
|
window.opener.location.href = '/';
|
||||||
|
window.close();
|
||||||
|
} else {
|
||||||
|
router.push('/');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.payment-result {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-container {
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
padding: 40px;
|
||||||
|
text-align: center;
|
||||||
|
max-width: 500px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-container {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success .icon-container i {
|
||||||
|
font-size: 80px;
|
||||||
|
color: #4caf50;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 24px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary-btn, .secondary-btn {
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary-btn {
|
||||||
|
background-color: #4caf50;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary-btn:hover {
|
||||||
|
background-color: #43a047;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.secondary-btn {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
color: #333;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.secondary-btn:hover {
|
||||||
|
background-color: #e9e9e9;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user