From 7fa99e6988a878b6f9adc06f9f5bcb7d73465478 Mon Sep 17 00:00:00 2001 From: Jaybe Date: Thu, 24 Apr 2025 12:01:51 +0900 Subject: [PATCH] =?UTF-8?q?=EC=8B=9C=EB=93=9C=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/controllers/clubController.js | 15 +- backend/controllers/eventController.js | 25 +- backend/controllers/featureController.js | 2 - backend/controllers/menuController.js | 146 +-- backend/middleware/requireFeature.js | 23 + backend/models/Club.js | 24 +- backend/models/ClubFeature.js | 11 +- backend/models/ClubSubscription.js | 11 +- backend/models/Event.js | 25 +- backend/models/EventParticipant.js | 33 +- backend/models/EventScore.js | 19 +- backend/models/EventTeam.js | 36 + backend/models/Feature.js | 44 +- backend/models/Member.js | 16 +- backend/models/Menu.js | 27 +- backend/models/Notification.js | 8 +- backend/models/Participant.js | 8 +- backend/models/SubscriptionPlan.js | 18 +- backend/models/SubscriptionPlanFeature.js | 6 +- backend/models/User.js | 6 +- backend/models/activityLog.js | 4 +- backend/models/index.js | 840 +----------------- backend/routes/adminRoutes.js | 61 +- backend/routes/clubRoutes.js | 10 +- backend/routes/eventRoutes.js | 31 +- backend/routes/featureRoutes.js | 1 + backend/routes/menuRoutes.js | 1 + backend/seeders/20250313-admin-user.js | 27 - backend/seeders/20250423-init-admin-user.js | 45 + backend/seeders/20250423-init-features.js | 49 + .../20250423-init-subscription-01-plans.js | 36 + ...0423-init-subscription-02-plan-features.js | 23 + backend/seeders/2025042300-sample-users.js | 26 + backend/seeders/2025042301-sample-clubs.js | 22 + .../2025042302-sample-club-subscriptions.js | 24 + backend/seeders/2025042303-sample-events.js | 31 + backend/seeders/2025042304-sample-members.js | 27 + backend/seeders/20250424-init-menus.js | 95 ++ backend/utils/featureAccess.js | 40 + frontend/src/App.vue | 16 +- frontend/src/components/AppHeader.vue | 2 +- .../components/event/EventDetailsDialog.vue | 194 +++- 42 files changed, 938 insertions(+), 1170 deletions(-) create mode 100644 backend/middleware/requireFeature.js create mode 100644 backend/models/EventTeam.js delete mode 100644 backend/seeders/20250313-admin-user.js create mode 100644 backend/seeders/20250423-init-admin-user.js create mode 100644 backend/seeders/20250423-init-features.js create mode 100644 backend/seeders/20250423-init-subscription-01-plans.js create mode 100644 backend/seeders/20250423-init-subscription-02-plan-features.js create mode 100644 backend/seeders/2025042300-sample-users.js create mode 100644 backend/seeders/2025042301-sample-clubs.js create mode 100644 backend/seeders/2025042302-sample-club-subscriptions.js create mode 100644 backend/seeders/2025042303-sample-events.js create mode 100644 backend/seeders/2025042304-sample-members.js create mode 100644 backend/seeders/20250424-init-menus.js create mode 100644 backend/utils/featureAccess.js diff --git a/backend/controllers/clubController.js b/backend/controllers/clubController.js index 08d7521..57cece3 100644 --- a/backend/controllers/clubController.js +++ b/backend/controllers/clubController.js @@ -156,8 +156,8 @@ exports.createClub = async (req, res) => { // 클럽 정보 업데이트 exports.updateClub = async (req, res) => { try { - const { clubId, name, location, description, contact, ownerEmail, status } = req.body; - + const { id, name, location, description, contact, ownerEmail, status } = req.body; + const clubId = id || req.params.id; const club = await Club.findByPk(clubId); if (!club) { @@ -519,7 +519,6 @@ exports.getNextMeeting = async (req, res) => { }, include: [{ model: EventParticipant, - as: 'participants', where: { status: { [Op.ne]: '취소' } }, @@ -563,7 +562,6 @@ exports.getLastMeeting = async (req, res) => { }, include: [{ model: EventParticipant, - as: 'participants', where: { status: { [Op.ne]: '취소' } }, @@ -611,10 +609,8 @@ exports.getScoreStats = async (req, res) => { }, include: [{ model: EventParticipant, - as: 'participants', include: [{ - model: EventScore, - as: 'scores' + model: EventScore }] }] }); @@ -690,7 +686,6 @@ exports.getRecentMeetings = async (req, res) => { }, include: [{ model: EventParticipant, - as: 'participants', where: { status: { [Op.ne]: '취소' } }, @@ -796,10 +791,8 @@ exports.getStatistics = async (req, res) => { }, include: [{ model: EventParticipant, - as: 'participants', include: [{ - model: EventScore, - as: 'scores' + model: EventScore }] }] }); diff --git a/backend/controllers/eventController.js b/backend/controllers/eventController.js index 9ebd836..5a476c6 100644 --- a/backend/controllers/eventController.js +++ b/backend/controllers/eventController.js @@ -27,7 +27,6 @@ exports.getAllEvents = async (req, res) => { }, { model: EventParticipant, - as: 'participants', attributes: ['id', 'status', 'paymentStatus'], where: { status: { [Op.ne]: '취소' } @@ -79,16 +78,15 @@ exports.getEventById = async (req, res) => { }, { model: EventParticipant, - as: 'participants', - attributes: ['id', 'status', 'paymentStatus'], - where: { - status: { [Op.ne]: '취소' } - }, + attributes: ['id', 'status', 'paymentStatus', 'teamNumber'], + where: { status: { [Op.ne]: '취소' } }, required: false, - include: [{ - model: Member, - attributes: ['id', 'name'] - }] + include: [ + { + model: Member, + attributes: ['id', 'name', 'average'] + } + ] } ] }); @@ -244,7 +242,6 @@ exports.updateEvent = async (req, res) => { }, { model: EventParticipant, - as: 'participants', attributes: ['id', 'status'], where: { status: { [Op.ne]: '취소' } @@ -355,7 +352,6 @@ exports.getEventScores = async (req, res) => { include: [ { model: EventParticipant, - as: 'participant', attributes: ['id', 'status'], include: [{ model: Member, @@ -618,7 +614,6 @@ exports.deleteEventScore = async (req, res) => { include: [ { model: EventParticipant, - as: 'participant', attributes: ['id', 'status'], include: [{ model: Member, @@ -702,7 +697,6 @@ exports.getEventStats = async (req, res) => { ], include: [{ model: EventParticipant, - as: 'participant', attributes: [], include: [{ model: Member, @@ -934,7 +928,6 @@ exports.getUserEvents = async (req, res) => { include: [ { model: EventParticipant, - as: 'participants', where: { userId }, attributes: [] }, @@ -983,7 +976,7 @@ exports.getEventParticipants = async (req, res) => { include: [ { model: Member, - attributes: ['id', 'name', 'email', 'phone', 'memberType'] + attributes: ['id', 'name', 'email', 'phone', 'memberType', 'average'] } ], order: [ diff --git a/backend/controllers/featureController.js b/backend/controllers/featureController.js index 6adfd47..15bc09c 100644 --- a/backend/controllers/featureController.js +++ b/backend/controllers/featureController.js @@ -78,7 +78,6 @@ exports.getActiveFeatures = async (req, res) => { }, include: [{ model: Feature, - as: 'feature', attributes: ['name', 'description'] }], attributes: ['id', 'featureId', 'purchaseDate', 'expiryDate'] @@ -106,7 +105,6 @@ exports.getFeaturePayments = async (req, res) => { }, include: [{ model: Feature, - as: 'feature', attributes: ['name'] }], attributes: [ diff --git a/backend/controllers/menuController.js b/backend/controllers/menuController.js index ead1d3e..9ec2978 100644 --- a/backend/controllers/menuController.js +++ b/backend/controllers/menuController.js @@ -1,4 +1,4 @@ -const { Menu, Feature, ClubFeature, ClubSubscription, SubscriptionPlan, Member } = require('../models'); +const { Menu, Member } = require('../models'); const { Op } = require('sequelize'); /** @@ -42,125 +42,31 @@ exports.getUserMenus = async (req, res) => { /** * 사용자 권한과 클럽의 구독/기능 상태에 따라 접근 가능한 메뉴 목록을 조회합니다. */ +const { clubHasFeature } = require('../utils/featureAccess'); + async function getAccessibleMenus(clubId, userRole, member) { - try { - // 1. 클럽의 현재 구독 정보 조회 - const clubSubscription = await ClubSubscription.findOne({ - where: { - clubId, - status: 'active', - endDate: { - [Op.gt]: new Date() - } - }, - include: [{ - model: SubscriptionPlan, - attributes: ['name'] - }] - }); + // 1. 모든 visible 메뉴 조회 + const allMenus = await Menu.findAll({ where: { visible: true }, order: [['order', 'ASC']] }); - // 2. 클럽의 활성화된 기능 목록 조회 - const activeFeatures = await ClubFeature.findAll({ - where: { - clubId, - enabled: true, - [Op.or]: [ - { expiryDate: null }, - { expiryDate: { [Op.gt]: new Date() } } - ] - }, - include: [{ - model: Feature, - as: 'feature', - attributes: ['code'] - }] - }); - - const activeFeatureCodes = activeFeatures.map(f => f.feature.code); - const subscriptionTier = clubSubscription ? clubSubscription.SubscriptionPlan.name.toLowerCase() : 'basic'; - - // 3. 메뉴 조회 및 필터링 - const allMenus = await Menu.findAll({ - where: { - visible: true, - [Op.and]: [ - // 역할 기반 필터링 - { - [Op.or]: [ - { role: userRole }, - { role: 'user' } - ] - }, - // 구독 등급 기반 필터링 - { - [Op.or]: [ - { requiredSubscriptionTier: null }, - { requiredSubscriptionTier: subscriptionTier } - ] - } - ] - }, - include: [{ - model: Menu, - as: 'children', - required: false - }], - order: [ - ['order', 'ASC'], - [{ model: Menu, as: 'children' }, 'order', 'ASC'] - ] - }); - - // 4. 기능 코드 기반 필터링 및 메뉴 구조화 - const processMenu = (menu) => { - // 기능 코드가 있는 경우 접근 권한 확인 - if (menu.featureCode && !activeFeatureCodes.includes(menu.featureCode)) { - return null; - } - - const processedMenu = { - id: menu.id, - title: menu.title, - icon: menu.icon, - to: menu.to, - order: menu.order - }; - - // 하위 메뉴 처리 - if (menu.children && menu.children.length > 0) { - const children = menu.children - .map(processMenu) - .filter(child => child !== null); - - if (children.length > 0) { - processedMenu.children = children; - } - } - - return processedMenu; - }; - - // 최상위 메뉴만 필터링 (parentId가 null인 항목) - const rootMenus = allMenus - .filter(menu => !menu.parentId) - .map(processMenu) - .filter(menu => menu !== null); - - // 구독 관리 메뉴 추가 (모임장, 운영진만 접근 가능) - if (member && (member.memberType === '모임장' || member.memberType === '운영진')) { - rootMenus.push({ - id: 'subscription', - title: '구독/결제 관리', - icon: 'pi pi-credit-card', - to: '/club/subscription', - order: 90, - children: [] - }); - } - - return rootMenus; - } catch (error) { - console.error('메뉴 조회 중 오류 발생:', error); - throw error; + // 2. 각 메뉴별로 featureCode가 있으면 clubHasFeature로 체크 + async function isAccessible(menu) { + if (!menu.featureCode) return true; + return await clubHasFeature(clubId, menu.featureCode); } -} \ No newline at end of file + + // 3. 트리 구조로 접근 가능한 메뉴만 반환 + async function buildMenuTree(parentId = null) { + const filteredMenus = []; + for (const menu of allMenus.filter(m => m.parentId === parentId)) { + if (await isAccessible(menu)) { + const children = await buildMenuTree(menu.id); + filteredMenus.push({ ...menu.toJSON(), children }); + } + } + return filteredMenus; + } + + return await buildMenuTree(); +} + +exports.getAccessibleMenus = getAccessibleMenus; \ No newline at end of file diff --git a/backend/middleware/requireFeature.js b/backend/middleware/requireFeature.js new file mode 100644 index 0000000..2d70e30 --- /dev/null +++ b/backend/middleware/requireFeature.js @@ -0,0 +1,23 @@ +const { clubHasFeature } = require('../utils/featureAccess'); + +// featureCode: 예) 'event_management', 'team_create', 'statistics' 등 +function requireFeature(featureCode) { + return async function(req, res, next) { + try { + const clubId = req.clubId || (req.user && req.user.clubId); + if (!clubId) { + return res.status(400).json({ error: '클럽 정보가 필요합니다.' }); + } + + const hasFeature = await clubHasFeature(clubId, featureCode); + if (!hasFeature) { + return res.status(403).json({ error: '해당 기능을 사용할 수 없습니다.' }); + } + next(); + } catch (err) { + next(err); + } + }; +} + +module.exports = requireFeature; diff --git a/backend/models/Club.js b/backend/models/Club.js index ae4d759..e8b9ee1 100644 --- a/backend/models/Club.js +++ b/backend/models/Club.js @@ -3,7 +3,7 @@ const { sequelize } = require('../config/database'); const Club = sequelize.define('Club', { id: { - type: DataTypes.INTEGER, + type: DataTypes.INTEGER.UNSIGNED, primaryKey: true, autoIncrement: true }, @@ -16,7 +16,7 @@ const Club = sequelize.define('Club', { allowNull: true }, ownerId: { - type: DataTypes.INTEGER, + type: DataTypes.INTEGER.UNSIGNED, allowNull: false, references: { model: 'Users', @@ -54,4 +54,24 @@ const Club = sequelize.define('Club', { timestamps: true }); +Club.associate = (models) => { + Club.hasMany(models.ClubFeature, { foreignKey: 'clubId' }); + Club.hasMany(models.ClubSubscription, { foreignKey: 'clubId' }); + Club.belongsTo(models.User, { foreignKey: 'ownerId', as: 'owner' }); + Club.belongsToMany(models.Feature, { + through: models.ClubFeature, + foreignKey: 'clubId', + }); + Club.belongsToMany(models.SubscriptionPlan, { + through: models.ClubSubscription, + foreignKey: 'clubId', + otherKey: 'planId', + }); + Club.hasMany(models.ClubSubscription, { foreignKey: 'clubId' }); + Club.belongsToMany(models.Feature, { + through: models.ClubFeature, + foreignKey: 'clubId', + }); +}; + module.exports = Club; diff --git a/backend/models/ClubFeature.js b/backend/models/ClubFeature.js index bf0a036..c7f6205 100644 --- a/backend/models/ClubFeature.js +++ b/backend/models/ClubFeature.js @@ -3,12 +3,12 @@ const { sequelize } = require('../config/database'); const ClubFeature = sequelize.define('ClubFeature', { id: { - type: DataTypes.INTEGER, + type: DataTypes.INTEGER.UNSIGNED, primaryKey: true, autoIncrement: true }, clubId: { - type: DataTypes.INTEGER, + type: DataTypes.INTEGER.UNSIGNED, allowNull: false, references: { model: 'Clubs', @@ -16,7 +16,7 @@ const ClubFeature = sequelize.define('ClubFeature', { } }, featureId: { - type: DataTypes.INTEGER, + type: DataTypes.INTEGER.UNSIGNED, allowNull: false, references: { model: 'Features', @@ -45,4 +45,9 @@ const ClubFeature = sequelize.define('ClubFeature', { timestamps: true }); +ClubFeature.associate = (models) => { + ClubFeature.belongsTo(models.Club, { foreignKey: 'clubId' }); + ClubFeature.belongsTo(models.Feature, { foreignKey: 'featureId' }); +}; + module.exports = ClubFeature; diff --git a/backend/models/ClubSubscription.js b/backend/models/ClubSubscription.js index 5139cc9..fa831d5 100644 --- a/backend/models/ClubSubscription.js +++ b/backend/models/ClubSubscription.js @@ -3,12 +3,12 @@ const { sequelize } = require('../config/database'); const ClubSubscription = sequelize.define('ClubSubscription', { id: { - type: DataTypes.INTEGER, + type: DataTypes.INTEGER.UNSIGNED, primaryKey: true, autoIncrement: true }, clubId: { - type: DataTypes.INTEGER, + type: DataTypes.INTEGER.UNSIGNED, allowNull: false, references: { model: 'Clubs', @@ -16,7 +16,7 @@ const ClubSubscription = sequelize.define('ClubSubscription', { } }, planId: { - type: DataTypes.INTEGER, + type: DataTypes.INTEGER.UNSIGNED, allowNull: false, references: { model: 'SubscriptionPlans', @@ -46,4 +46,9 @@ const ClubSubscription = sequelize.define('ClubSubscription', { timestamps: true }); +ClubSubscription.associate = (models) => { + ClubSubscription.belongsTo(models.SubscriptionPlan, { foreignKey: 'planId' }); + ClubSubscription.belongsTo(models.Club, { foreignKey: 'clubId' }); +}; + module.exports = ClubSubscription; diff --git a/backend/models/Event.js b/backend/models/Event.js index 97dcfbf..7454477 100644 --- a/backend/models/Event.js +++ b/backend/models/Event.js @@ -3,12 +3,12 @@ const { sequelize } = require('../config/database'); const Event = sequelize.define('Event', { id: { - type: DataTypes.INTEGER, + type: DataTypes.INTEGER.UNSIGNED, primaryKey: true, autoIncrement: true }, clubId: { - type: DataTypes.INTEGER, + type: DataTypes.INTEGER.UNSIGNED, allowNull: false, references: { model: 'Clubs', @@ -85,18 +85,27 @@ const Event = sequelize.define('Event', { // 관계 설정 Event.associate = (models) => { Event.belongsTo(models.Club, { - foreignKey: 'clubId', - as: 'club' + foreignKey: 'clubId' }); Event.hasMany(models.EventParticipant, { - foreignKey: 'eventId', - as: 'participants' + foreignKey: 'eventId' }); Event.hasMany(models.EventScore, { - foreignKey: 'eventId', - as: 'scores' + foreignKey: 'eventId' + }); +}; + +Event.associate = (models) => { + Event.belongsTo(models.Club, { + foreignKey: 'clubId' + }); + Event.hasMany(models.EventParticipant, { + foreignKey: 'eventId' + }); + Event.hasMany(models.EventScore, { + foreignKey: 'eventId' }); }; diff --git a/backend/models/EventParticipant.js b/backend/models/EventParticipant.js index 61ce63c..a5e4d8c 100644 --- a/backend/models/EventParticipant.js +++ b/backend/models/EventParticipant.js @@ -3,12 +3,12 @@ const { sequelize } = require('../config/database'); const EventParticipant = sequelize.define('EventParticipant', { id: { - type: DataTypes.INTEGER, + type: DataTypes.INTEGER.UNSIGNED, primaryKey: true, autoIncrement: true }, eventId: { - type: DataTypes.INTEGER, + type: DataTypes.INTEGER.UNSIGNED, allowNull: false, references: { model: 'Events', @@ -16,13 +16,21 @@ const EventParticipant = sequelize.define('EventParticipant', { } }, memberId: { - type: DataTypes.INTEGER, + type: DataTypes.INTEGER.UNSIGNED, allowNull: false, references: { model: 'Members', key: 'id' } }, + teamId: { + type: DataTypes.INTEGER.UNSIGNED, + allowNull: true, + references: { + model: 'EventTeams', + key: 'id' + } + }, status: { type: DataTypes.ENUM('참가예정', '참가확정', '취소'), allowNull: false, @@ -41,18 +49,25 @@ const EventParticipant = sequelize.define('EventParticipant', { // 관계 설정 EventParticipant.associate = (models) => { EventParticipant.belongsTo(models.Event, { - foreignKey: 'eventId', - as: 'event' + foreignKey: 'eventId' }); EventParticipant.belongsTo(models.Member, { - foreignKey: 'memberId', - as: 'member' + foreignKey: 'memberId' + }); + + EventParticipant.belongsTo(models.EventTeam, { + foreignKey: 'teamId' }); EventParticipant.hasMany(models.EventScore, { - foreignKey: 'participantId', - as: 'scores' + foreignKey: 'participantId' + }); +}; + +EventParticipant.associate = (models) => { + EventParticipant.hasMany(models.EventScore, { + foreignKey: 'participantId' }); }; diff --git a/backend/models/EventScore.js b/backend/models/EventScore.js index 96d0d90..ed9488f 100644 --- a/backend/models/EventScore.js +++ b/backend/models/EventScore.js @@ -3,12 +3,12 @@ const { sequelize } = require('../config/database'); const EventScore = sequelize.define('EventScore', { id: { - type: DataTypes.INTEGER, + type: DataTypes.INTEGER.UNSIGNED, primaryKey: true, autoIncrement: true }, eventId: { - type: DataTypes.INTEGER, + type: DataTypes.INTEGER.UNSIGNED, allowNull: false, references: { model: 'Events', @@ -16,7 +16,7 @@ const EventScore = sequelize.define('EventScore', { } }, participantId: { - type: DataTypes.INTEGER, + type: DataTypes.INTEGER.UNSIGNED, allowNull: false, references: { model: 'EventParticipants', @@ -24,11 +24,11 @@ const EventScore = sequelize.define('EventScore', { } }, teamNumber: { - type: DataTypes.INTEGER, + type: DataTypes.INTEGER.UNSIGNED, allowNull: true, }, gameNumber: { - type: DataTypes.INTEGER, + type: DataTypes.INTEGER.UNSIGNED, allowNull: false }, score: { @@ -49,4 +49,13 @@ const EventScore = sequelize.define('EventScore', { timestamps: true }); +EventScore.associate = (models) => { + EventScore.belongsTo(models.Event, { + foreignKey: 'eventId' + }); + EventScore.belongsTo(models.EventParticipant, { + foreignKey: 'participantId' + }); +}; + module.exports = EventScore; diff --git a/backend/models/EventTeam.js b/backend/models/EventTeam.js new file mode 100644 index 0000000..86c2ec2 --- /dev/null +++ b/backend/models/EventTeam.js @@ -0,0 +1,36 @@ +const { DataTypes } = require('sequelize'); +const { sequelize } = require('../config/database'); + +const EventTeam = sequelize.define('EventTeam', { + id: { + type: DataTypes.INTEGER.UNSIGNED, + primaryKey: true, + autoIncrement: true, + }, + eventId: { + type: DataTypes.INTEGER.UNSIGNED, + allowNull: false, + }, + teamNumber: { + type: DataTypes.INTEGER.UNSIGNED, + allowNull: false, + }, + name: { + type: DataTypes.STRING, + allowNull: true, + }, + handicap: { + type: DataTypes.INTEGER, + allowNull: true, + }, +}, { + tableName: 'EventTeams', + timestamps: true, +}); + +EventTeam.associate = (models) => { + EventTeam.belongsTo(models.Event, { foreignKey: 'eventId' }); + EventTeam.hasMany(models.EventParticipant, { foreignKey: 'teamId' }); +}; + +module.exports = EventTeam; \ No newline at end of file diff --git a/backend/models/Feature.js b/backend/models/Feature.js index 7c607b0..c636d76 100644 --- a/backend/models/Feature.js +++ b/backend/models/Feature.js @@ -3,10 +3,15 @@ const { sequelize } = require('../config/database'); const Feature = sequelize.define('Feature', { id: { - type: DataTypes.INTEGER, + type: DataTypes.INTEGER.UNSIGNED, primaryKey: true, autoIncrement: true }, + code: { + type: DataTypes.STRING, + allowNull: false, + unique: true + }, name: { type: DataTypes.STRING, allowNull: false @@ -15,29 +20,11 @@ const Feature = sequelize.define('Feature', { type: DataTypes.TEXT, allowNull: true }, - code: { - type: DataTypes.STRING, - allowNull: false, - unique: true - }, - purchaseType: { - type: DataTypes.ENUM('subscription_only', 'separate_only', 'both'), - allowNull: false, - defaultValue: 'both' - }, price: { type: DataTypes.INTEGER, allowNull: false, defaultValue: 0 }, - oneTimePurchasePrice: { - type: DataTypes.INTEGER, - allowNull: true - }, - monthlySubscriptionPrice: { - type: DataTypes.INTEGER, - allowNull: true - }, status: { type: DataTypes.ENUM('active', 'inactive', 'suspended'), allowNull: false, @@ -56,4 +43,23 @@ const Feature = sequelize.define('Feature', { timestamps: true }); +Feature.associate = (models) => { + Feature.hasMany(models.ClubFeature, { foreignKey: 'featureId' }); + Feature.belongsToMany(models.Club, { + through: models.ClubFeature, + foreignKey: 'featureId', + }); + Feature.hasMany(models.Menu, { + foreignKey: 'featureCode', + sourceKey: 'code', + }); + // SubscriptionPlan 관계는 이미 있음 + + Feature.belongsToMany(models.SubscriptionPlan, { + through: models.SubscriptionPlanFeature, + foreignKey: 'featureId', + otherKey: 'planId', + }); +}; + module.exports = Feature; diff --git a/backend/models/Member.js b/backend/models/Member.js index d297d8c..bea71f2 100644 --- a/backend/models/Member.js +++ b/backend/models/Member.js @@ -5,12 +5,12 @@ const Club = require('./Club'); const Member = sequelize.define('Member', { id: { - type: DataTypes.INTEGER, + type: DataTypes.INTEGER.UNSIGNED, primaryKey: true, autoIncrement: true }, clubId: { - type: DataTypes.INTEGER, + type: DataTypes.INTEGER.UNSIGNED, allowNull: false, references: { model: 'Clubs', @@ -18,7 +18,7 @@ const Member = sequelize.define('Member', { } }, userId: { - type: DataTypes.INTEGER, + type: DataTypes.INTEGER.UNSIGNED, allowNull: true, references: { model: 'Users', @@ -77,15 +77,9 @@ const Member = sequelize.define('Member', { }); // User와의 관계 설정 -Member.belongsTo(User, { - foreignKey: 'userId', - as: 'user' -}); +Member.belongsTo(User, {foreignKey: 'userId'}); // Club과의 관계 설정 -Member.belongsTo(Club, { - foreignKey: 'clubId', - as: 'club' -}); +Member.belongsTo(Club, {foreignKey: 'clubId'}); module.exports = Member; diff --git a/backend/models/Menu.js b/backend/models/Menu.js index a747b52..8b0716a 100644 --- a/backend/models/Menu.js +++ b/backend/models/Menu.js @@ -3,15 +3,15 @@ const { sequelize } = require('../config/database'); const Menu = sequelize.define('Menu', { id: { - type: DataTypes.INTEGER, + type: DataTypes.INTEGER.UNSIGNED, primaryKey: true, autoIncrement: true }, parentId: { - type: DataTypes.INTEGER, + type: DataTypes.INTEGER.UNSIGNED, allowNull: true, references: { - model: 'Menus', // 변경 + model: 'Menus', // 실제 테이블명 key: 'id' } }, @@ -67,4 +67,25 @@ const Menu = sequelize.define('Menu', { timestamps: true }); +Menu.associate = (models) => { + Menu.belongsTo(models.Feature, { + foreignKey: 'featureCode', + targetKey: 'code', + }); + Menu.belongsTo(models.Menu, { + as: 'parent', + foreignKey: 'parentId', + }); + Menu.hasMany(models.Menu, { + as: 'children', + foreignKey: 'parentId', + }); +}; + +// 자기참조 관계를 associate에서 명확히 선언 +Menu.associate = (models) => { + Menu.hasMany(models.Menu, { foreignKey: 'parentId', as: 'children' }); + Menu.belongsTo(models.Menu, { foreignKey: 'parentId', as: 'parent' }); +}; + module.exports = Menu; diff --git a/backend/models/Notification.js b/backend/models/Notification.js index fc26db0..97bc168 100644 --- a/backend/models/Notification.js +++ b/backend/models/Notification.js @@ -3,12 +3,12 @@ const { sequelize } = require('../config/database'); const Notification = sequelize.define('Notification', { id: { - type: DataTypes.INTEGER, + type: DataTypes.INTEGER.UNSIGNED, primaryKey: true, autoIncrement: true }, userId: { - type: DataTypes.INTEGER, + type: DataTypes.INTEGER.UNSIGNED, allowNull: false, references: { model: 'Users', @@ -56,4 +56,8 @@ const Notification = sequelize.define('Notification', { tableName: 'Notifications' }); +Notification.associate = (models) => { + Notification.belongsTo(models.User, { foreignKey: 'userId' }); +}; + module.exports = Notification; diff --git a/backend/models/Participant.js b/backend/models/Participant.js index 2f6da9d..6863cac 100644 --- a/backend/models/Participant.js +++ b/backend/models/Participant.js @@ -3,12 +3,12 @@ const { sequelize } = require('../config/database'); const Participant = sequelize.define('Participant', { id: { - type: DataTypes.INTEGER, + type: DataTypes.INTEGER.UNSIGNED, primaryKey: true, autoIncrement: true }, meetingId: { - type: DataTypes.INTEGER, + type: DataTypes.INTEGER.UNSIGNED, allowNull: false, references: { model: 'Meetings', @@ -16,7 +16,7 @@ const Participant = sequelize.define('Participant', { } }, memberId: { - type: DataTypes.INTEGER, + type: DataTypes.INTEGER.UNSIGNED, allowNull: false, references: { model: 'Members', @@ -24,7 +24,7 @@ const Participant = sequelize.define('Participant', { } }, teamNumber: { - type: DataTypes.INTEGER, + type: DataTypes.INTEGER.UNSIGNED, allowNull: false, defaultValue: 1 }, diff --git a/backend/models/SubscriptionPlan.js b/backend/models/SubscriptionPlan.js index ba030c9..30093ea 100644 --- a/backend/models/SubscriptionPlan.js +++ b/backend/models/SubscriptionPlan.js @@ -3,7 +3,7 @@ const { sequelize } = require('../config/database'); const SubscriptionPlan = sequelize.define('SubscriptionPlan', { id: { - type: DataTypes.INTEGER, + type: DataTypes.INTEGER.UNSIGNED, primaryKey: true, autoIncrement: true }, @@ -33,4 +33,20 @@ const SubscriptionPlan = sequelize.define('SubscriptionPlan', { timestamps: true }); +SubscriptionPlan.associate = (models) => { + SubscriptionPlan.belongsToMany(models.Club, { + through: models.ClubSubscription, + foreignKey: 'planId', + otherKey: 'clubId', + }); + SubscriptionPlan.belongsToMany(models.Feature, { + through: models.SubscriptionPlanFeature, + foreignKey: 'planId', + otherKey: 'featureId', + }); + SubscriptionPlan.hasMany(models.ClubSubscription, { + foreignKey: 'planId', + }); +}; + module.exports = SubscriptionPlan; diff --git a/backend/models/SubscriptionPlanFeature.js b/backend/models/SubscriptionPlanFeature.js index 1a2ad6d..c9f055a 100644 --- a/backend/models/SubscriptionPlanFeature.js +++ b/backend/models/SubscriptionPlanFeature.js @@ -3,12 +3,12 @@ const { sequelize } = require('../config/database'); const SubscriptionPlanFeature = sequelize.define('SubscriptionPlanFeature', { id: { - type: DataTypes.INTEGER, + type: DataTypes.INTEGER.UNSIGNED, primaryKey: true, autoIncrement: true }, planId: { - type: DataTypes.INTEGER, + type: DataTypes.INTEGER.UNSIGNED, allowNull: false, references: { model: 'SubscriptionPlans', @@ -16,7 +16,7 @@ const SubscriptionPlanFeature = sequelize.define('SubscriptionPlanFeature', { } }, featureId: { - type: DataTypes.INTEGER, + type: DataTypes.INTEGER.UNSIGNED, allowNull: false, references: { model: 'Features', diff --git a/backend/models/User.js b/backend/models/User.js index 555e046..be40e7c 100644 --- a/backend/models/User.js +++ b/backend/models/User.js @@ -4,7 +4,7 @@ const bcrypt = require('bcrypt'); const User = sequelize.define('User', { id: { - type: DataTypes.INTEGER, + type: DataTypes.INTEGER.UNSIGNED, primaryKey: true, autoIncrement: true }, @@ -75,4 +75,8 @@ User.prototype.validatePassword = async function(password) { return await bcrypt.compare(password, this.password); }; +User.associate = (models) => { + User.hasMany(models.Notification, { foreignKey: 'userId' }); +}; + module.exports = User; diff --git a/backend/models/activityLog.js b/backend/models/activityLog.js index b7acd48..f1c485b 100644 --- a/backend/models/activityLog.js +++ b/backend/models/activityLog.js @@ -5,12 +5,12 @@ class ActivityLog extends Model {} ActivityLog.init({ id: { - type: DataTypes.INTEGER, + type: DataTypes.INTEGER.UNSIGNED, primaryKey: true, autoIncrement: true }, userId: { - type: DataTypes.INTEGER, + type: DataTypes.INTEGER.UNSIGNED, allowNull: false, references: { model: 'Users', diff --git a/backend/models/index.js b/backend/models/index.js index 6246b82..bf32edc 100644 --- a/backend/models/index.js +++ b/backend/models/index.js @@ -3,6 +3,7 @@ const { Op } = require('sequelize'); const User = require('./User'); const Club = require('./Club'); const Event = require('./Event'); +const EventTeam = require('./EventTeam'); const EventParticipant = require('./EventParticipant'); const EventScore = require('./EventScore'); const Member = require('./Member'); @@ -15,99 +16,6 @@ const SubscriptionPlanFeature = require('./SubscriptionPlanFeature'); const ClubSubscription = require('./ClubSubscription'); const ActivityLog = require('./activityLog'); -// 관계 설정 -// User와 Member 관계 -User.hasMany(Member, { foreignKey: 'userId' }); -Member.belongsTo(User, { foreignKey: 'userId' }); - -// Club과 Member 관계 -Club.hasMany(Member, { foreignKey: 'clubId' }); -Member.belongsTo(Club, { foreignKey: 'clubId' }); - -// Club과 User 사이의 다대다 관계 추가 -User.belongsToMany(Club, { through: Member, foreignKey: 'userId' }); -Club.belongsToMany(User, { through: Member, foreignKey: 'clubId' }); - -// Club과 User 사이의 소유자 관계 추가 -Club.belongsTo(User, { as: 'owner', foreignKey: 'ownerId' }); -User.hasMany(Club, { as: 'ownedClubs', foreignKey: 'ownerId' }); - -// Club과 Event 관계 -Club.hasMany(Event, { foreignKey: 'clubId' }); -Event.belongsTo(Club, { foreignKey: 'clubId' }); - -// Event와 EventParticipant 관계 -Event.hasMany(EventParticipant, { foreignKey: 'eventId', as: 'participants' }); -EventParticipant.belongsTo(Event, { foreignKey: 'eventId' }); - -// Member와 EventParticipant 관계 -Member.hasMany(EventParticipant, { foreignKey: 'memberId' }); -EventParticipant.belongsTo(Member, { foreignKey: 'memberId' }); - -// Event와 EventScore 관계 -Event.hasMany(EventScore, { foreignKey: 'eventId', as: 'scores' }); -EventScore.belongsTo(Event, { foreignKey: 'eventId', as: 'event' }); - -// EventParticipant와 EventScore 관계 -EventParticipant.hasMany(EventScore, { foreignKey: 'participantId', as: 'scores' }); -EventScore.belongsTo(EventParticipant, { foreignKey: 'participantId', as: 'participant' }); - -// Feature와 Menu 관계 -Feature.hasMany(Menu, { foreignKey: 'featureCode', sourceKey: 'code' }); -Menu.belongsTo(Feature, { foreignKey: 'featureCode', targetKey: 'code' }); - -// Menu 자기 참조 관계 -Menu.belongsTo(Menu, { as: 'parent', foreignKey: 'parentId' }); -Menu.hasMany(Menu, { as: 'children', foreignKey: 'parentId' }); - -// Club과 Feature의 관계 -Club.belongsToMany(Feature, { through: ClubFeature, foreignKey: 'clubId' }); -Feature.belongsToMany(Club, { through: ClubFeature, foreignKey: 'featureId' }); - -// Feature와 ClubFeature 관계 -Feature.hasMany(ClubFeature, { foreignKey: 'featureId', as: 'clubFeatures' }); -ClubFeature.belongsTo(Feature, { foreignKey: 'featureId', as: 'feature' }); - -// Club과 ClubFeature 관계 -Club.hasMany(ClubFeature, { foreignKey: 'clubId', as: 'features' }); -ClubFeature.belongsTo(Club, { foreignKey: 'clubId', as: 'club' }); - -// User와 Notification 관계 -User.hasMany(Notification, { foreignKey: 'userId' }); -Notification.belongsTo(User, { foreignKey: 'userId' }); - -// Club과 SubscriptionPlan의 관계 (through ClubSubscription) -Club.belongsToMany(SubscriptionPlan, { - through: ClubSubscription, - foreignKey: 'clubId', - otherKey: 'planId' -}); -SubscriptionPlan.belongsToMany(Club, { - through: ClubSubscription, - foreignKey: 'planId', - otherKey: 'clubId' -}); - -// SubscriptionPlan과 Feature의 관계 (through SubscriptionPlanFeature) -SubscriptionPlan.belongsToMany(Feature, { - through: SubscriptionPlanFeature, - foreignKey: 'planId', - otherKey: 'featureId' -}); -Feature.belongsToMany(SubscriptionPlan, { - through: SubscriptionPlanFeature, - foreignKey: 'featureId', - otherKey: 'planId' -}); - -// ClubSubscription과 SubscriptionPlan의 관계 -ClubSubscription.belongsTo(SubscriptionPlan, { foreignKey: 'planId' }); -SubscriptionPlan.hasMany(ClubSubscription, { foreignKey: 'planId' }); - -// ClubSubscription과 Club의 관계 -ClubSubscription.belongsTo(Club, { foreignKey: 'clubId' }); -Club.hasMany(ClubSubscription, { foreignKey: 'clubId' }); - // 모델 동기화 함수 const syncModels = async () => { try { @@ -116,729 +24,21 @@ const syncModels = async () => { // await sequelize.query('SET FOREIGN_KEY_CHECKS = 0'); // await sequelize.sync({ force: true }); // await sequelize.query('SET FOREIGN_KEY_CHECKS = 1'); - // await createAdminUser(); - // await createSampleData(); // 운영 alter: true - 구조변경만 alter처리 - await sequelize.sync({ alter: true }); -} catch (error) { + // await sequelize.sync({ alter: true }); + await sequelize.sync(); + } catch (error) { console.error('모델 동기화 중 오류 발생:', error); } }; -// 초기 관리자 계정 생성 함수 -const createAdminUser = async () => { - try { - const adminExists = await User.findOne({ - where: { email: process.env.ADMIN_EMAIL } - }); - - if (!adminExists) { - await User.create({ - username: 'master', - name: '관리자', - email: process.env.ADMIN_EMAIL, - password: process.env.ADMIN_PASSWORD, - role: 'admin', - status: 'active', - createdAt: new Date(), - updatedAt: new Date() - }); - console.log('관리자 계정이 생성되었습니다.'); - } - } catch (error) { - console.error('관리자 계정 생성 중 오류 발생:', error); - } -}; - -// 초기 샘플 데이터 생성 함수 -const createSampleData = async () => { - try { - // 1. 기존 데이터 삭제 (순서 중요) - await ClubFeature.destroy({ where: {} }); - await ClubSubscription.destroy({ where: {} }); - await Member.destroy({ where: {} }); - await Club.destroy({ where: {} }); - await Feature.destroy({ where: {} }); - await SubscriptionPlan.destroy({ where: {} }); - await User.destroy({ where: { email: { [Op.ne]: process.env.ADMIN_EMAIL } } }); - console.log('기존 데이터가 모두 삭제되었습니다.'); - - // 2. 샘플 사용자 데이터 생성 - const sampleUsers = await User.bulkCreate([ - { - username: 'hong', - name: '홍길동', - email: 'hong@test.com', - password: '1234', - role: 'user', - status: 'active', - createdAt: new Date(), - updatedAt: new Date() - }, - { - username: 'kim', - name: '김철수', - email: 'kim@test.com', - password: '1234', - role: 'user', - status: 'active', - createdAt: new Date(), - updatedAt: new Date() - }, - { - username: 'lee', - name: '이영희', - email: 'lee@test.com', - password: '1234', - role: 'user', - status: 'active', - createdAt: new Date(), - updatedAt: new Date() - } - ]); - console.log('샘플 사용자 데이터가 생성되었습니다:', sampleUsers.map(user => user.id)); - - // 3. 샘플 클럽 데이터 생성 - const clubs = await Club.bulkCreate([ - { - name: '서울 볼링 클럽', - description: '서울 지역 볼링 동호회입니다.', - location: '서울시 강남구', - ownerId: sampleUsers[0].id, - status: 'active', - memberCount: 3, - createdAt: new Date(), - updatedAt: new Date() - }, - { - name: '부산 볼링 클럽', - description: '부산 지역 볼링 동호회입니다.', - location: '부산시 해운대구', - ownerId: sampleUsers[1].id, - status: 'active', - memberCount: 3, - createdAt: new Date(), - updatedAt: new Date() - }, - { - name: '대전 볼링 클럽', - description: '대전 지역 볼링 동호회입니다.', - location: '대전시 유성구', - ownerId: sampleUsers[2].id, - status: 'active', - memberCount: 3, - createdAt: new Date(), - updatedAt: new Date() - } - ]); - console.log('샘플 클럽 데이터가 생성되었습니다:', clubs.map(club => club.id)); - - // 샘플 구독 플랜 데이터 생성 - const subscriptionPlans = [ - { - name: 'Basic', - description: '기본적인 클럽 관리 기능을 제공합니다.', - price: 30000, - duration: 1, - maxMembers: 30, - features: ['member_management', 'event_basic'], - status: 'active' - }, - { - name: 'Premium', - description: '고급 분석 기능과 함께 더 많은 회원을 관리할 수 있습니다.', - price: 50000, - duration: 1, - maxMembers: 100, - features: ['member_management', 'event_basic', 'analytics_basic', 'lane_booking'], - status: 'active' - }, - { - name: 'Enterprise', - description: '무제한 회원 관리와 모든 프리미엄 기능을 제공합니다.', - price: 100000, - duration: 1, - maxMembers: 999999, - features: ['member_management', 'event_basic', 'analytics_basic', 'lane_booking', 'tournament', 'analytics_advanced'], - status: 'active' - } - ]; - - await SubscriptionPlan.bulkCreate(subscriptionPlans); - console.log('샘플 구독 플랜 데이터가 생성되었습니다.'); - - // 샘플 클럽 구독 데이터 생성 - const clubSubscriptions = [ - { - clubId: 1, - planId: 3, // Enterprise 플랜 - startDate: new Date(), - endDate: new Date(new Date().setMonth(new Date().getMonth() + 12)), - amount: 1200000, // 연간 구독 - status: 'active', - paymentStatus: 'paid' - }, - { - clubId: 2, - planId: 2, // Premium 플랜 - startDate: new Date(), - endDate: new Date(new Date().setMonth(new Date().getMonth() + 6)), - amount: 300000, // 6개월 구독 - status: 'active', - paymentStatus: 'paid' - }, - { - clubId: 3, - planId: 1, // Basic 플랜 - startDate: new Date(), - endDate: new Date(new Date().setMonth(new Date().getMonth() + 3)), - amount: 90000, // 3개월 구독 - status: 'active', - paymentStatus: 'paid' - } - ]; - - await ClubSubscription.bulkCreate(clubSubscriptions); - console.log('샘플 클럽 구독 데이터가 생성되었습니다.'); - - // 샘플 기능 데이터 생성 - const features = [ - { - code: 'member_management', - name: '회원 관리', - description: '클럽 회원 관리 기능', - purchaseType: 'subscription_only', - monthlySubscriptionPrice: 0, - createdAt: new Date(), - updatedAt: new Date() - }, - { - code: 'event_management', - name: '이벤트 관리', - description: '클럽 이벤트 관리 기능', - purchaseType: 'subscription_only', - monthlySubscriptionPrice: 5000, - createdAt: new Date(), - updatedAt: new Date() - }, - { - code: 'event_calendar', - name: '이벤트 캘린더', - description: '클럽 이벤트 캘린더 기능', - purchaseType: 'subscription_only', - monthlySubscriptionPrice: 5000, - createdAt: new Date(), - updatedAt: new Date() - }, - { - code: 'statistics', - name: '통계', - description: '클럽 통계 기능', - purchaseType: 'subscription_only', - monthlySubscriptionPrice: 10000, - createdAt: new Date(), - updatedAt: new Date() - } - ]; - - // 기능 데이터 생성 - await Feature.bulkCreate(features); - console.log('샘플 기능 데이터가 생성되었습니다.'); - - // 샘플 클럽 회원 데이터 생성 - const members = [ - { - clubId: 1, - userId: 2, - name: '홍길동', - gender: '남성', - memberType: '모임장', - status: 'active', - joinDate: new Date(), - createdAt: new Date(), - updatedAt: new Date() - }, - { - clubId: 1, - userId: 3, - name: '김철수', - gender: '남성', - memberType: '정회원', - status: 'active', - joinDate: new Date(), - createdAt: new Date(), - updatedAt: new Date() - }, - { - clubId: 2, - userId: 3, - name: '김철수', - gender: '남성', - memberType: '모임장', - status: 'active', - joinDate: new Date(), - createdAt: new Date(), - updatedAt: new Date() - }, - { - clubId: 2, - userId: 4, - name: '이영희', - gender: '여성', - memberType: '정회원', - status: 'active', - joinDate: new Date(), - createdAt: new Date(), - updatedAt: new Date() - }, - { - clubId: 3, - userId: 4, - name: '이영희', - gender: '여성', - memberType: '모임장', - status: 'active', - joinDate: new Date(), - createdAt: new Date(), - updatedAt: new Date() - }, - { - clubId: 3, - userId: 2, - name: '홍길동', - gender: '남성', - memberType: '정회원', - status: 'active', - joinDate: new Date(), - createdAt: new Date(), - updatedAt: new Date() - }, - { - clubId: 1, - userId: null, - name: '정회원A', - gender: '여성', - memberType: '정회원', - status: 'active', - joinDate: new Date('2023-01-15'), - createdAt: new Date('2023-01-16'), - updatedAt: new Date('2023-01-17') - }, - { - clubId: 2, - userId: null, - name: '준회원B', - gender: '남성', - memberType: '준회원', - status: 'inactive', - joinDate: new Date('2023-02-20'), - createdAt: new Date('2023-02-21'), - updatedAt: new Date('2023-02-22') - }, - { - clubId: 3, - userId: null, - name: '정회원C', - gender: '여성', - memberType: '정회원', - status: 'suspended', - joinDate: new Date('2023-03-10'), - createdAt: new Date('2023-03-11'), - updatedAt: new Date('2023-03-12') - }, - { - clubId: 1, - userId: null, - name: '준회원D', - gender: '남성', - memberType: '준회원', - status: 'active', - joinDate: new Date('2023-04-05'), - createdAt: new Date('2023-04-06'), - updatedAt: new Date('2023-04-07') - }, - { - clubId: 2, - userId: null, - name: '정회원E', - gender: '여성', - memberType: '정회원', - status: 'suspended', - joinDate: new Date('2023-05-15'), - createdAt: new Date('2023-05-16'), - updatedAt: new Date('2023-05-17') - }, - { - clubId: 3, - userId: null, - name: '준회원F', - gender: '남성', - memberType: '준회원', - status: 'inactive', - joinDate: new Date('2023-06-25'), - createdAt: new Date('2023-06-26'), - updatedAt: new Date('2023-06-27') - }, - { - clubId: 1, - userId: null, - name: '정회원G', - gender: '여성', - memberType: '정회원', - status: 'active', - joinDate: new Date('2023-07-30'), - createdAt: new Date('2023-07-31'), - updatedAt: new Date('2023-08-01') - }, - { - clubId: 2, - userId: null, - name: '준회원H', - gender: '남성', - memberType: '준회원', - status: 'suspended', - joinDate: new Date('2023-08-15'), - createdAt: new Date('2023-08-16'), - updatedAt: new Date('2023-08-17') - } - ]; - - await Member.bulkCreate(members); - console.log('샘플 클럽 회원 데이터가 생성되었습니다.'); - - // 샘플 클럽 기능 데이터 생성 - // 각 클럽의 기능을 개별적으로 생성 - // 클럽 1 (Enterprise) - await ClubFeature.create({ - clubId: 1, - featureId: 1, - enabled: true, - expiryDate: new Date(new Date().setMonth(new Date().getMonth() + 12)), - createdAt: new Date(), - updatedAt: new Date() - }); - - await ClubFeature.create({ - clubId: 1, - featureId: 2, - enabled: true, - expiryDate: new Date(new Date().setMonth(new Date().getMonth() + 12)), - createdAt: new Date(), - updatedAt: new Date() - }); - - // 클럽 2 (Premium) - await ClubFeature.create({ - clubId: 2, - featureId: 1, - enabled: true, - expiryDate: new Date(new Date().setMonth(new Date().getMonth() + 6)), - createdAt: new Date(), - updatedAt: new Date() - }); - - await ClubFeature.create({ - clubId: 2, - featureId: 2, - enabled: true, - expiryDate: new Date(new Date().setMonth(new Date().getMonth() + 6)), - createdAt: new Date(), - updatedAt: new Date() - }); - - // 클럽 3 (Basic) - await ClubFeature.create({ - clubId: 3, - featureId: 1, - enabled: true, - expiryDate: new Date(new Date().setMonth(new Date().getMonth() + 3)), - createdAt: new Date(), - updatedAt: new Date() - }); - console.log('샘플 클럽 기능 데이터가 생성되었습니다.'); - - // 샘플 메뉴 데이터 생성 - const menus = [ - // 클럽 관리 메뉴 - { - title: '클럽 관리', - icon: 'pi pi-building', - to: '/club', - role: 'user', - order: 1, - visible: true, - children: [ - { - title: '대시보드', - icon: 'pi pi-home', - to: '/club', - role: 'user', - order: 1, - visible: true - }, - { - title: '회원 관리', - icon: 'pi pi-users', - to: '/club/members', - role: 'user', - order: 2, - visible: true, - featureCode: 'member_management', - requiredSubscriptionTier: 'basic' - }, - { - title: '이벤트 관리', - icon: 'pi pi-calendar', - to: '/club/events', - role: 'user', - order: 3, - visible: true, - featureCode: 'event_management', - requiredSubscriptionTier: 'standard' - }, - { - title: '이벤트 캘린더', - icon: 'pi pi-calendar-plus', - to: '/club/events/calendar', - role: 'user', - order: 4, - visible: true, - featureCode: 'event_calendar', - requiredSubscriptionTier: 'standard' - }, - { - title: '통계', - icon: 'pi pi-chart-line', - to: '/club/statistics', - role: 'user', - order: 5, - visible: true, - featureCode: 'statistics', - requiredSubscriptionTier: 'premium' - } - ] - } - ]; - - // 부모 메뉴 먼저 생성 - for (const menuData of menus) { - const { children, ...parentData } = menuData; - const parentMenu = await Menu.create(parentData); - - // 자식 메뉴 생성 및 부모와 연결 - if (children) { - for (const childData of children) { - await Menu.create({ - ...childData, - parentId: parentMenu.id - }); - } - } - } - - console.log('샘플 메뉴 데이터가 생성되었습니다.'); - - // 샘플 클럽 생성 - const clubsCount = await Club.count(); - if (clubsCount === 0) { - // 3. 샘플 클럽 데이터 생성 - const clubs = await Club.bulkCreate([ - { - name: '서울 볼링 클럽', - description: '서울 지역 볼링 동호회입니다.', - ownerId: sampleUsers[0].id, - location: '서울시 강남구', - status: 'active', - memberCount: 3, - createdAt: new Date(), - updatedAt: new Date() - }, - { - name: '부산 볼링 클럽', - description: '부산 지역 볼링 동호회입니다.', - ownerId: sampleUsers[1].id, - location: '부산시 해운대구', - status: 'active', - memberCount: 3, - createdAt: new Date(), - updatedAt: new Date() - }, - { - name: '대전 볼링 클럽', - description: '대전 지역 볼링 동호회입니다.', - ownerId: sampleUsers[2].id, - location: '대전시 유성구', - status: 'active', - memberCount: 3, - createdAt: new Date(), - updatedAt: new Date() - } - ]); - console.log('샘플 클럽 데이터가 생성되었습니다:', clubs.map(club => club.id)); - } - - // 샘플 구독 플랜 생성 - const subscriptionPlansCount = await SubscriptionPlan.count(); - if (subscriptionPlansCount === 0) { - const subscriptionPlans = [ - { - name: 'Basic', - description: '기본적인 클럽 관리 기능을 제공합니다.', - price: 30000, - duration: 1, - maxMembers: 30, - features: ['member_management', 'event_basic'], - status: 'active' - }, - { - name: 'Premium', - description: '고급 분석 기능과 함께 더 많은 회원을 관리할 수 있습니다.', - price: 50000, - duration: 1, - maxMembers: 100, - features: ['member_management', 'event_basic', 'analytics_basic', 'lane_booking'], - status: 'active' - }, - { - name: 'Enterprise', - description: '무제한 회원 관리와 모든 프리미엄 기능을 제공합니다.', - price: 100000, - duration: 1, - maxMembers: 999999, // 무제한을 표현하기 위해 큰 숫자 사용 - features: ['member_management', 'event_basic', 'analytics_basic', 'lane_booking', 'tournament', 'analytics_advanced'], - status: 'active' - } - ]; - - await SubscriptionPlan.bulkCreate(subscriptionPlans); - console.log('샘플 구독 플랜 데이터가 생성되었습니다.'); - } - - // 샘플 클럽 구독 데이터 생성 - const clubSubscriptionsCount = await ClubSubscription.count(); - if (clubSubscriptionsCount === 0) { - const clubSubscriptions = [ - { - clubId: 1, - planId: 3, // Enterprise 플랜 - startDate: new Date(), - endDate: new Date(new Date().setMonth(new Date().getMonth() + 12)), - amount: 1200000, // 연간 구독 - status: 'active', - paymentStatus: 'paid' - }, - { - clubId: 2, - planId: 2, // Premium 플랜 - startDate: new Date(), - endDate: new Date(new Date().setMonth(new Date().getMonth() + 6)), - amount: 300000, // 6개월 구독 - status: 'active', - paymentStatus: 'paid' - }, - { - clubId: 3, - planId: 1, // Basic 플랜 - startDate: new Date(), - endDate: new Date(new Date().setMonth(new Date().getMonth() + 3)), - amount: 90000, // 3개월 구독 - status: 'active', - paymentStatus: 'paid' - } - ]; - - await ClubSubscription.bulkCreate(clubSubscriptions); - console.log('샘플 클럽 구독 데이터가 생성되었습니다.'); - } - - // 5. 샘플 기능 데이터 생성 - const createdFeatures = await Feature.bulkCreate([ - { - name: '토너먼트 관리', - description: '클럽 내 토너먼트 대회를 개최하고 관리할 수 있습니다.', - code: 'tournament', - purchaseType: 'both', - price: 50000, - status: 'active', - createdAt: new Date(), - updatedAt: new Date() - }, - { - name: '레인 예약', - description: '볼링장 레인을 미리 예약하고 관리할 수 있습니다.', - code: 'lane_booking', - purchaseType: 'subscription_only', - price: 30000, - status: 'active', - createdAt: new Date(), - updatedAt: new Date() - }, - { - name: '점수 분석', - description: '회원들의 경기 점수를 분석하고 통계를 제공합니다.', - code: 'score_analysis', - purchaseType: 'separate_only', - price: 40000, - status: 'active', - createdAt: new Date(), - updatedAt: new Date() - } - ]); - console.log('샘플 기능 데이터가 생성되었습니다:', createdFeatures.map(f => f.id)); - - // 6. 샘플 클럽 기능 데이터 생성 - const sampleClubs = await Club.findAll(); - - if (createdFeatures.length > 0 && sampleClubs.length > 0) { - const clubFeatures = await ClubFeature.bulkCreate([ - { - clubId: sampleClubs[0].id, - featureId: createdFeatures[0].id, - enabled: true, - expiryDate: new Date(new Date().setMonth(new Date().getMonth() + 6)), - amount: 50000, - duration: 6, - status: 'active', - createdAt: new Date(), - updatedAt: new Date() - }, - { - clubId: sampleClubs[1].id, - featureId: createdFeatures[1].id, - enabled: true, - expiryDate: new Date(new Date().setMonth(new Date().getMonth() + 3)), - amount: 30000, - duration: 3, - status: 'active', - createdAt: new Date(), - updatedAt: new Date() - }, - { - clubId: sampleClubs[2].id, - featureId: createdFeatures[2].id, - enabled: true, - expiryDate: new Date(new Date().setMonth(new Date().getMonth() + 12)), - amount: 40000, - duration: 12, - status: 'active', - createdAt: new Date(), - updatedAt: new Date() - } - ]); - console.log('샘플 클럽 기능 데이터가 생성되었습니다:', clubFeatures.map(cf => `${cf.clubId}-${cf.featureId}`)); - } - - } catch (error) { - console.error('샘플 데이터 생성 중 오류 발생:', error); - } -}; - -module.exports = { - sequelize, +// 모든 모델의 associate 호출 (관계 자동 연결) +const db = { User, Club, Event, + EventTeam, EventParticipant, EventScore, Member, @@ -850,7 +50,27 @@ module.exports = { SubscriptionPlanFeature, ClubSubscription, ActivityLog, - syncModels, - createAdminUser, - createSampleData +}; +Object.values(db).forEach(model => { + if (model.associate) model.associate(db); +}); + +module.exports = { + sequelize, + User, + Club, + Event, + EventTeam, + EventParticipant, + EventScore, + Member, + Notification, + Feature, + ClubFeature, + Menu, + SubscriptionPlan, + SubscriptionPlanFeature, + ClubSubscription, + ActivityLog, + syncModels }; diff --git a/backend/routes/adminRoutes.js b/backend/routes/adminRoutes.js index 9306ef9..1b9a431 100644 --- a/backend/routes/adminRoutes.js +++ b/backend/routes/adminRoutes.js @@ -1239,10 +1239,8 @@ router.get('/clubs/:id', async (req, res) => { }, { model: ClubFeature, - as: 'features', include: [{ model: Feature, - as: 'feature', attributes: ['id', 'name', 'code', 'description'] }] }, @@ -1280,12 +1278,13 @@ router.get('/clubs/:id', async (req, res) => { delete response.ClubSubscriptions; // 기능 정보 포맷팅 - const formattedFeatures = response.features.map(feature => ({ - ...feature.feature, + const formattedFeatures = response.ClubFeatures.map(feature => ({ + ...feature.Feature, enabled: feature.enabled, expiryDate: feature.expiryDate })); response.features = formattedFeatures; + delete response.ClubFeatures; res.json(response); await logActivity(req.user.id, 'VIEW_CLUB', `클럽 ${id}의 정보를 조회했습니다.`); @@ -1299,59 +1298,7 @@ router.get('/clubs/:id', async (req, res) => { router.post('/clubs', authenticateJWT, clubController.createClub); // 클럽 수정 -router.put('/clubs/:id', async (req, res) => { - try { - const { id } = req.params; - const { name, description, ownerEmail, location } = req.body; - - // 클럽 존재 여부 확인 - const club = await Club.findByPk(id); - if (!club) { - return res.status(404).json({ message: '클럽을 찾을 수 없습니다.' }); - } - - // 새 모임장 정보 확인 - const newOwner = await User.findOne({ where: { email: ownerEmail } }); - if (!newOwner) { - return res.status(404).json({ message: '모임장으로 지정할 사용자를 찾을 수 없습니다.' }); - } - - // 트랜잭션 시작 - const t = await sequelize.transaction(); - - try { - // 클럽 정보 업데이트 - await club.update({ - name, - description, - location, - ownerId: newOwner.id - }, { transaction: t }); - - // 활동 로그 기록 - await logActivity(req.user.id, 'UPDATE_CLUB', `클럽 "${name}"의 정보를 수정했습니다.`); - - await t.commit(); - - // 업데이트된 클럽 정보 조회 - const updatedClub = await Club.findByPk(id, { - include: [{ - model: User, - as: 'owner', - attributes: ['id', 'name', 'email'] - }] - }); - - res.json(updatedClub); - } catch (error) { - await t.rollback(); - throw error; - } - } catch (error) { - console.error('클럽 수정 중 오류:', error); - res.status(500).json({ message: '클럽 수정 중 오류가 발생했습니다.' }); - } -}); +router.put('/clubs/:id', authenticateJWT, clubController.updateClub); // 클럽 기능 관리 router.put('/clubs/:id/features', async (req, res) => { diff --git a/backend/routes/clubRoutes.js b/backend/routes/clubRoutes.js index 5544c12..390a3a5 100644 --- a/backend/routes/clubRoutes.js +++ b/backend/routes/clubRoutes.js @@ -3,6 +3,8 @@ const router = express.Router(); const jwt = require('jsonwebtoken'); const clubController = require('../controllers/clubController'); const { authenticateJWT } = require('../middleware/authMiddleware'); +const requireFeature = require('../middleware/requireFeature'); + // 모든 클럽 조회 router.get('/', clubController.getAllClubs); @@ -20,7 +22,7 @@ router.delete('/', authenticateJWT, clubController.deleteClub); router.post('/user', authenticateJWT, clubController.getUserClubs); // 클럽 회원 통계 조회 -router.post('/members/stats', authenticateJWT, clubController.getMemberStats); +router.post('/members/stats', authenticateJWT, requireFeature('statistics'), clubController.getMemberStats); // 클럽 회원 목록 조회 router.post('/members', authenticateJWT, clubController.getClubMembers); @@ -44,16 +46,16 @@ router.post('/meetings/last', authenticateJWT, clubController.getLastMeeting); router.post('/meetings/recent', authenticateJWT, clubController.getRecentMeetings); // 점수 통계 조회 -router.post('/scores/stats', authenticateJWT, clubController.getScoreStats); +router.post('/scores/stats', authenticateJWT, requireFeature('statistics'), clubController.getScoreStats); // 상위 회원 목록 조회 -router.post('/members/top', authenticateJWT, clubController.getTopMembers); +router.post('/members/top', authenticateJWT, requireFeature('statistics'), clubController.getTopMembers); // 새 클럽 생성 router.post('/create', authenticateJWT, clubController.createClub); // 클럽 통계 조회 -router.post('/statistics', authenticateJWT, clubController.getStatistics); +router.post('/statistics', authenticateJWT, requireFeature('statistics'), clubController.getStatistics); // 클럽 기능 관리 router.put('/features', authenticateJWT, clubController.manageClubFeatures); diff --git a/backend/routes/eventRoutes.js b/backend/routes/eventRoutes.js index fd2e496..c2430be 100644 --- a/backend/routes/eventRoutes.js +++ b/backend/routes/eventRoutes.js @@ -2,28 +2,29 @@ const express = require('express'); const router = express.Router(); const eventController = require('../controllers/eventController'); const { authenticateJWT } = require('../middleware/authMiddleware'); +const requireFeature = require('../middleware/requireFeature'); // 이벤트 기본 CRUD -router.post('/', authenticateJWT, eventController.getAllEvents); -router.get('/:id', authenticateJWT, eventController.getEventById); -router.post('/create', authenticateJWT, eventController.createEvent); -router.put('/:id', authenticateJWT, eventController.updateEvent); -router.delete('/:id', authenticateJWT, eventController.deleteEvent); +router.post('/', authenticateJWT, requireFeature('event_management'), eventController.getAllEvents); +router.get('/:id', authenticateJWT, requireFeature('event_management'), eventController.getEventById); +router.post('/create', authenticateJWT, requireFeature('event_management'), eventController.createEvent); +router.put('/:id', authenticateJWT, requireFeature('event_management'), eventController.updateEvent); +router.delete('/:id', authenticateJWT, requireFeature('event_management'), eventController.deleteEvent); // 이벤트 참가자 관리 -router.post('/:id/participants/list', authenticateJWT, eventController.getEventParticipants); -router.post('/:id/participants', authenticateJWT, eventController.addEventParticipant); -router.put('/:id/participants', authenticateJWT, eventController.updateEventParticipant); -router.delete('/:id/participants', authenticateJWT, eventController.deleteEventParticipant); +router.post('/:id/participants/list', authenticateJWT, requireFeature('event_management'), eventController.getEventParticipants); +router.post('/:id/participants', authenticateJWT, requireFeature('event_management'), eventController.addEventParticipant); +router.put('/:id/participants', authenticateJWT, requireFeature('event_management'), eventController.updateEventParticipant); +router.delete('/:id/participants', authenticateJWT, requireFeature('event_management'), eventController.deleteEventParticipant); // 이벤트 점수 관리 -router.get('/:id/scores', authenticateJWT, eventController.getEventScores); -router.post('/:id/scores', authenticateJWT, eventController.addEventScore); -router.put('/:id/scores/:scoreId', authenticateJWT, eventController.updateEventScore); -router.delete('/:id/scores/:scoreId', authenticateJWT, eventController.deleteEventScore); -router.delete('/:id/participants/:participantId/scores', authenticateJWT, eventController.deleteParticipantScores); +router.get('/:id/scores', authenticateJWT, requireFeature('event_management'), eventController.getEventScores); +router.post('/:id/scores', authenticateJWT, requireFeature('event_management'), eventController.addEventScore); +router.put('/:id/scores/:scoreId', authenticateJWT, requireFeature('event_management'), eventController.updateEventScore); +router.delete('/:id/scores/:scoreId', authenticateJWT, requireFeature('event_management'), eventController.deleteEventScore); +router.delete('/:id/participants/:participantId/scores', authenticateJWT, requireFeature('event_management'), eventController.deleteParticipantScores); // 사용자별 이벤트 관리 -router.get('/user/events', authenticateJWT, eventController.getUserEvents); +router.get('/user/events', authenticateJWT, requireFeature('event_management'), eventController.getUserEvents); module.exports = router; \ No newline at end of file diff --git a/backend/routes/featureRoutes.js b/backend/routes/featureRoutes.js index 2f5ab7d..b64c61a 100644 --- a/backend/routes/featureRoutes.js +++ b/backend/routes/featureRoutes.js @@ -3,6 +3,7 @@ const router = express.Router(); const featureController = require('../controllers/featureController'); const { authenticateJWT } = require('../middleware/authMiddleware'); + // 기본 기능 관리 router.get('/', authenticateJWT, featureController.getFeatures); diff --git a/backend/routes/menuRoutes.js b/backend/routes/menuRoutes.js index e9deef4..de0579e 100644 --- a/backend/routes/menuRoutes.js +++ b/backend/routes/menuRoutes.js @@ -5,6 +5,7 @@ const express = require('express'); const router = express.Router(); const { authenticateJWT } = require('../middleware/authMiddleware'); + const menuController = require('../controllers/menuController'); // 사용자의 접근 가능한 메뉴 목록 조회 diff --git a/backend/seeders/20250313-admin-user.js b/backend/seeders/20250313-admin-user.js deleted file mode 100644 index d93e8b5..0000000 --- a/backend/seeders/20250313-admin-user.js +++ /dev/null @@ -1,27 +0,0 @@ -'use strict'; -const bcrypt = require('bcrypt'); - -/** @type {import('sequelize-cli').Migration} */ -module.exports = { - async up (queryInterface, Sequelize) { - // 관리자 계정 비밀번호 해시 생성 - const salt = await bcrypt.genSalt(10); - const hashedPassword = await bcrypt.hash('Dlrwns0304#', salt); - - // 관리자 계정 생성 - await queryInterface.bulkInsert('users', [{ - name: '관리자', - email: 'master@jaybe.dev', - password: hashedPassword, - role: 'admin', - status: 'active', - createdAt: new Date(), - updatedAt: new Date() - }], {}); - }, - - async down (queryInterface, Sequelize) { - // 관리자 계정 삭제 - await queryInterface.bulkDelete('users', { email: 'master@jaybe.dev' }, {}); - } -}; diff --git a/backend/seeders/20250423-init-admin-user.js b/backend/seeders/20250423-init-admin-user.js new file mode 100644 index 0000000..973f2b6 --- /dev/null +++ b/backend/seeders/20250423-init-admin-user.js @@ -0,0 +1,45 @@ +'use strict'; +const bcrypt = require('bcrypt'); + +module.exports = { + async up (queryInterface, Sequelize) { + // 환경변수 기반으로 관리자 계정 생성 + const adminEmail = process.env.ADMIN_EMAIL; + const adminPassword = process.env.ADMIN_PASSWORD; + const adminName = process.env.ADMIN_NAME; + const adminUsername = process.env.ADMIN_USERNAME; + + // 이미 존재하는지 체크 + const [results] = await queryInterface.sequelize.query( + `SELECT id FROM Users WHERE email = :email LIMIT 1`, + { replacements: { email: adminEmail } } + ); + if (results.length > 0) return; + + const salt = await bcrypt.genSalt(10); + const hashedPassword = await bcrypt.hash(adminPassword, salt); + await queryInterface.bulkInsert('Users', [{ + username: adminUsername, + name: adminName, + email: adminEmail, + password: hashedPassword, + role: 'admin', + status: 'active', + createdAt: new Date(), + updatedAt: new Date() + }], {}); + }, + async down (queryInterface, Sequelize) { + const adminEmail = process.env.ADMIN_EMAIL; + // 먼저 ActivityLogs에서 admin 유저의 로그를 삭제 + const [results] = await queryInterface.sequelize.query( + `SELECT id FROM Users WHERE email = :email LIMIT 1`, + { replacements: { email: adminEmail } } + ); + if (results.length > 0) { + const adminId = results[0].id; + await queryInterface.bulkDelete('ActivityLogs', { userId: adminId }, {}); + } + await queryInterface.bulkDelete('Users', { email: adminEmail }, {}); + } +}; diff --git a/backend/seeders/20250423-init-features.js b/backend/seeders/20250423-init-features.js new file mode 100644 index 0000000..2858ac9 --- /dev/null +++ b/backend/seeders/20250423-init-features.js @@ -0,0 +1,49 @@ +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.bulkInsert('Features', [ + { + code: 'member_management', + name: '회원 관리', + description: '클럽 회원 관리 기능', + price: 0, + createdAt: new Date(), + updatedAt: new Date() + }, + { + code: 'event_management', + name: '이벤트 관리', + description: '클럽 이벤트 관리 기능', + price: 0, + createdAt: new Date(), + updatedAt: new Date() + }, + { + code: 'event_calendar', + name: '이벤트 캘린더', + description: '클럽 이벤트 캘린더 기능', + price: 1000, + createdAt: new Date(), + updatedAt: new Date() + }, + { + code: 'statistics', + name: '통계', + description: '클럽 통계 기능', + price: 1000, + createdAt: new Date(), + updatedAt: new Date() + }, + { + code: 'team_create', + name: '팀 생성', + description: '이벤트별 팀 생성 기능', + price: 1000, + createdAt: new Date(), + updatedAt: new Date() + } + ]); + }, + down: async (queryInterface, Sequelize) => { + await queryInterface.bulkDelete('Features', null, {}); + } +}; diff --git a/backend/seeders/20250423-init-subscription-01-plans.js b/backend/seeders/20250423-init-subscription-01-plans.js new file mode 100644 index 0000000..525a7d7 --- /dev/null +++ b/backend/seeders/20250423-init-subscription-01-plans.js @@ -0,0 +1,36 @@ +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.bulkInsert('SubscriptionPlan', [ + { + name: 'Basic', + description: '기본적인 클럽 관리 기능을 제공합니다.', + price: 10000, + maxMembers: 10, + status: 'active', + createdAt: new Date(), + updatedAt: new Date() + }, + { + name: 'Premium', + description: '고급 분석 기능과 함께 더 많은 회원을 관리할 수 있습니다.', + price: 25000, + maxMembers: 30, + status: 'active', + createdAt: new Date(), + updatedAt: new Date() + }, + { + name: 'Enterprise', + description: '무제한 회원 관리와 모든 프리미엄 기능을 제공합니다.', + price: 50000, + maxMembers: 0, + status: 'active', + createdAt: new Date(), + updatedAt: new Date() + } + ]); + }, + down: async (queryInterface, Sequelize) => { + await queryInterface.bulkDelete('SubscriptionPlan', null, {}); + } +}; diff --git a/backend/seeders/20250423-init-subscription-02-plan-features.js b/backend/seeders/20250423-init-subscription-02-plan-features.js new file mode 100644 index 0000000..28f29f3 --- /dev/null +++ b/backend/seeders/20250423-init-subscription-02-plan-features.js @@ -0,0 +1,23 @@ +module.exports = { + up: async (queryInterface, Sequelize) => { + // 플랜별로 3개의 기능을 연결 (플랜 1: 기능 1,2,3 / 플랜 2: 기능 2,3,4 / 플랜 3: 기능 1,3,4) + const planFeatures = [ + { planId: 1, featureId: 1, status: 'active', createdAt: new Date(2025, 0, 1), updatedAt: new Date(2025, 0, 1) }, + { planId: 1, featureId: 2, status: 'active', createdAt: new Date(2025, 0, 1), updatedAt: new Date(2025, 0, 1) }, + { planId: 2, featureId: 1, status: 'active', createdAt: new Date(2025, 0, 1), updatedAt: new Date(2025, 0, 1) }, + { planId: 2, featureId: 2, status: 'active', createdAt: new Date(2025, 0, 1), updatedAt: new Date(2025, 0, 1) }, + { planId: 2, featureId: 3, status: 'active', createdAt: new Date(2025, 0, 1), updatedAt: new Date(2025, 0, 1) }, + { planId: 2, featureId: 4, status: 'active', createdAt: new Date(2025, 0, 1), updatedAt: new Date(2025, 0, 1) }, + { planId: 2, featureId: 5, status: 'active', createdAt: new Date(2025, 0, 1), updatedAt: new Date(2025, 0, 1) }, + { planId: 3, featureId: 1, status: 'active', createdAt: new Date(2025, 0, 1), updatedAt: new Date(2025, 0, 1) }, + { planId: 3, featureId: 2, status: 'active', createdAt: new Date(2025, 0, 1), updatedAt: new Date(2025, 0, 1) }, + { planId: 3, featureId: 3, status: 'active', createdAt: new Date(2025, 0, 1), updatedAt: new Date(2025, 0, 1) }, + { planId: 3, featureId: 4, status: 'active', createdAt: new Date(2025, 0, 1), updatedAt: new Date(2025, 0, 1) }, + { planId: 3, featureId: 5, status: 'active', createdAt: new Date(2025, 0, 1), updatedAt: new Date(2025, 0, 1) } + ]; + await queryInterface.bulkInsert('SubscriptionPlanFeatures', planFeatures, {}); + }, + down: async (queryInterface, Sequelize) => { + await queryInterface.bulkDelete('SubscriptionPlanFeatures', null, {}); + } +}; diff --git a/backend/seeders/2025042300-sample-users.js b/backend/seeders/2025042300-sample-users.js new file mode 100644 index 0000000..c9b35ed --- /dev/null +++ b/backend/seeders/2025042300-sample-users.js @@ -0,0 +1,26 @@ +const bcrypt = require('bcrypt'); + +module.exports = { + up: async (queryInterface, Sequelize) => { + // 샘플 사용자 10명 생성 + const users = []; + for (let i = 1; i <= 10; i++) { + const salt = await bcrypt.genSalt(10); + const hashedPassword = await bcrypt.hash('1234', salt); + users.push({ + username: `user${i}`, + name: `샘플유저${i}`, + email: `user${i}@test.com`, + password: hashedPassword, + role: 'user', + status: 'active', + createdAt: new Date(), + updatedAt: new Date() + }); + } + await queryInterface.bulkInsert('Users', users, {}); + }, + down: async (queryInterface, Sequelize) => { + await queryInterface.bulkDelete('Users', { email: { [Sequelize.Op.like]: 'user%@test.com' } }, {}); + } +}; diff --git a/backend/seeders/2025042301-sample-clubs.js b/backend/seeders/2025042301-sample-clubs.js new file mode 100644 index 0000000..011af24 --- /dev/null +++ b/backend/seeders/2025042301-sample-clubs.js @@ -0,0 +1,22 @@ +module.exports = { + up: async (queryInterface, Sequelize) => { + // 샘플 클럽 5개 생성 (ownerId는 1~5) + const clubs = []; + for (let i = 1; i <= 5; i++) { + clubs.push({ + name: `샘플 볼링 클럽 ${i}`, + description: `샘플 볼링 클럽 ${i} 설명입니다.`, + location: `도시${i} 구${i}`, + ownerId: i+1, + status: 'active', + memberCount: 10, + createdAt: new Date(), + updatedAt: new Date() + }); + } + await queryInterface.bulkInsert('Clubs', clubs, {}); + }, + down: async (queryInterface, Sequelize) => { + await queryInterface.bulkDelete('Clubs', { name: { [Sequelize.Op.like]: '샘플 볼링 클럽 %' } }, {}); + } +}; diff --git a/backend/seeders/2025042302-sample-club-subscriptions.js b/backend/seeders/2025042302-sample-club-subscriptions.js new file mode 100644 index 0000000..f168c82 --- /dev/null +++ b/backend/seeders/2025042302-sample-club-subscriptions.js @@ -0,0 +1,24 @@ +module.exports = { + up: async (queryInterface, Sequelize) => { + // 각 클럽 5개에 대해 2개의 구독(플랜 1, 2)을 연결 + const clubSubscriptions = []; + for (let clubId = 1; clubId <= 5; clubId++) { + const planId = Math.floor(Math.random() * 2) + 1; // 랜덤으로 플랜 1, 2 중 하나 선택 + const startDate = new Date(2025, 3, 1 + clubId); + const endDate = new Date(startDate.getFullYear(), startDate.getMonth()+1, startDate.getDate()); + clubSubscriptions.push({ + clubId, + planId, + startDate, + endDate, + status: 'active', + createdAt: new Date(), + updatedAt: new Date() + }); + } + await queryInterface.bulkInsert('ClubSubscriptions', clubSubscriptions, {}); + }, + down: async (queryInterface, Sequelize) => { + await queryInterface.bulkDelete('ClubSubscriptions', { }, {}); + } +}; diff --git a/backend/seeders/2025042303-sample-events.js b/backend/seeders/2025042303-sample-events.js new file mode 100644 index 0000000..0751e37 --- /dev/null +++ b/backend/seeders/2025042303-sample-events.js @@ -0,0 +1,31 @@ +module.exports = { + up: async (queryInterface, Sequelize) => { + // 각 클럽마다 3개의 이벤트 생성 + const events = []; + for (let clubId = 1; clubId <= 5; clubId++) { + for (let i = 1; i <= 3; i++) { + events.push({ + clubId, + title: `클럽${clubId} 이벤트${i}`, + description: `클럽${clubId}의 이벤트${i} 설명입니다.`, + eventType: '정기전', // ENUM 값 중 하나 + startDate: new Date(2025, 3, 10 + i, 19, 0, 0), + endDate: new Date(2025, 3, 10 + i, 22, 0, 0), + location: `클럽${clubId} 이벤트장소${i}`, + gameCount: 3, + participantFee: 10000, + maxParticipants: 30, + registrationDeadline: new Date(2025, 3, 9 + i, 23, 59, 59), + participantCount: 0, + status: '활성', // ENUM 값 중 하나 + createdAt: new Date(), + updatedAt: new Date() + }); + } + } + await queryInterface.bulkInsert('Events', events, {}); + }, + down: async (queryInterface, Sequelize) => { + await queryInterface.bulkDelete('Events', { title: { [Sequelize.Op.like]: '클럽% 이벤트%' } }, {}); + } +}; diff --git a/backend/seeders/2025042304-sample-members.js b/backend/seeders/2025042304-sample-members.js new file mode 100644 index 0000000..2646945 --- /dev/null +++ b/backend/seeders/2025042304-sample-members.js @@ -0,0 +1,27 @@ +module.exports = { + up: async (queryInterface, Sequelize) => { + // 샘플 멤버 30명 생성 (각 클럽마다 6명씩) + const members = []; + let userId = 2; + for (let clubId = 1; clubId <= 5; clubId++) { + for (let i = 1; i <= 6; i++) { + members.push({ + clubId, + userId: userId <= 10 ? userId : null, + name: `클럽${clubId}멤버${i}`, + gender: i % 2 === 0 ? '여성' : '남성', + memberType: i === 1 ? '모임장' : (i % 2 === 0 ? '정회원' : '준회원'), + status: 'active', + joinDate: new Date(), + createdAt: new Date(), + updatedAt: new Date() + }); + userId++; + } + } + await queryInterface.bulkInsert('Members', members, {}); + }, + down: async (queryInterface, Sequelize) => { + await queryInterface.bulkDelete('Members', { name: { [Sequelize.Op.like]: '클럽%멤버%' } }, {}); + } +}; diff --git a/backend/seeders/20250424-init-menus.js b/backend/seeders/20250424-init-menus.js new file mode 100644 index 0000000..422b1af --- /dev/null +++ b/backend/seeders/20250424-init-menus.js @@ -0,0 +1,95 @@ +'use strict'; + +module.exports = { + up: async (queryInterface, Sequelize) => { + // 1. 부모 메뉴 생성 + await queryInterface.bulkInsert('Menus', [{ + title: '클럽 관리', + icon: 'pi pi-building', + to: '/club', + role: 'user', + order: 1, + visible: true, + parentId: null, + createdAt: new Date(), + updatedAt: new Date() + }]); + + // 2. 부모 메뉴 id select + const [results] = await queryInterface.sequelize.query( + "SELECT id FROM Menus WHERE title='클럽 관리' AND `to`='/club' ORDER BY id DESC LIMIT 1" + ); + const clubMenuId = results[0].id; + + // 3. 자식 메뉴들 생성 + await queryInterface.bulkInsert('Menus', [ + { + title: '대시보드', + icon: 'pi pi-home', + to: '/club', + role: 'user', + order: 1, + visible: true, + parentId: clubMenuId, + createdAt: new Date(), + updatedAt: new Date() + }, + { + title: '회원 관리', + icon: 'pi pi-users', + to: '/club/members', + role: 'user', + order: 2, + visible: true, + parentId: clubMenuId, + featureCode: 'member_management', + requiredSubscriptionTier: 'basic', + createdAt: new Date(), + updatedAt: new Date() + }, + { + title: '이벤트 관리', + icon: 'pi pi-calendar', + to: '/club/events', + role: 'user', + order: 3, + visible: true, + parentId: clubMenuId, + featureCode: 'event_management', + requiredSubscriptionTier: 'standard', + createdAt: new Date(), + updatedAt: new Date() + }, + { + title: '이벤트 캘린더', + icon: 'pi pi-calendar-plus', + to: '/club/events/calendar', + role: 'user', + order: 4, + visible: true, + parentId: clubMenuId, + featureCode: 'event_calendar', + requiredSubscriptionTier: 'standard', + createdAt: new Date(), + updatedAt: new Date() + }, + { + title: '통계', + icon: 'pi pi-chart-line', + to: '/club/statistics', + role: 'user', + order: 5, + visible: true, + parentId: clubMenuId, + featureCode: 'statistics', + requiredSubscriptionTier: 'premium', + createdAt: new Date(), + updatedAt: new Date() + } + ]); + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.bulkDelete('Menus', null, {}); + } +}; diff --git a/backend/utils/featureAccess.js b/backend/utils/featureAccess.js new file mode 100644 index 0000000..70855f1 --- /dev/null +++ b/backend/utils/featureAccess.js @@ -0,0 +1,40 @@ +const { ClubSubscription, SubscriptionPlan, ClubFeature, Feature } = require('../models'); +const { Op } = require('sequelize'); + +/** + * 클럽이 특정 featureCode를 사용할 수 있는지 여부 반환 + * @param {number} clubId + * @param {string} featureCode + * @returns {Promise} + */ +async function clubHasFeature(clubId, featureCode) { + // 활성 구독 플랜에서 해당 기능이 포함되어 있는지 체크 + const clubSub = await ClubSubscription.findOne({ + where: { clubId, status: 'active', endDate: { [Op.gt]: new Date() } }, + include: [{ + model: SubscriptionPlan, + include: [{ + model: Feature, + where: { code: featureCode, status: 'active' }, + through: { where: { status: 'active' } } + }] + }] + }); + if (clubSub) return true; + + // 별도 활성화된 ClubFeature가 있는지 체크 + const clubFeature = await ClubFeature.findOne({ + where: { + clubId, + enabled: true, + expiryDate: { [Op.or]: [null, { [Op.gt]: new Date() }] } + }, + include: [{ + model: Feature, + where: { code: featureCode, status: 'active' } + }] + }); + return !!clubFeature; +} + +module.exports = { clubHasFeature }; diff --git a/frontend/src/App.vue b/frontend/src/App.vue index fab6147..b7a8b1d 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -105,7 +105,7 @@ const singleMenuItems = ref([]); const expandedGroups = ref({}); const isMobileView = ref(window.innerWidth < 768); const isSidebarVisible = ref(!isMobileView.value); -const isLoggedIn = ref(!!localStorage.getItem('token')); +const isLoggedIn = computed(() => !!user.value && !!localStorage.getItem('token')); const clubs = ref([]); // 클럽 목록 const selectedClubId = ref(localStorage.getItem('clubId') || null); // 선택된 클럽 ID const selectedClub = ref(null); @@ -296,6 +296,16 @@ const loadUserData = async () => { } }; +// 로그인 성공 후 상태 갱신 핸들러 +const handleUserLoggedIn = async () => { + await loadUserData(); + if (isLoggedIn.value) { + await fetchClubs(); + await fetchClubInfo(); + loadMenuItems(); + } +}; + // 컴포넌트 마운트 시 데이터 로드 onMounted(async () => { await loadUserData(); @@ -308,6 +318,9 @@ onMounted(async () => { // 화면 크기 변경 감지 window.addEventListener('resize', handleResize); + // 로그인 성공 이벤트 감지 + window.addEventListener('user-logged-in', handleUserLoggedIn); + // 초기 expandedGroups 설정 initializeExpandedGroups(); }); @@ -315,6 +328,7 @@ onMounted(async () => { // 컴포넌트 언마운트 시 이벤트 리스너 제거 onBeforeUnmount(() => { window.removeEventListener('resize', handleResize); + window.removeEventListener('user-logged-in', handleUserLoggedIn); }); diff --git a/frontend/src/components/AppHeader.vue b/frontend/src/components/AppHeader.vue index c4665a6..0e20682 100644 --- a/frontend/src/components/AppHeader.vue +++ b/frontend/src/components/AppHeader.vue @@ -72,7 +72,7 @@ import Avatar from 'primevue/avatar'; const router = useRouter(); const userMenu = ref(null); const adminMenu = ref(null); -const isLoggedIn = computed(() => !!localStorage.getItem('token')); +const isLoggedIn = computed(() => !!user.value); const isAdmin = computed(() => { const role = localStorage.getItem('userRole'); return role === 'admin' || role === 'superadmin'; diff --git a/frontend/src/components/event/EventDetailsDialog.vue b/frontend/src/components/event/EventDetailsDialog.vue index 0741974..9300fd1 100644 --- a/frontend/src/components/event/EventDetailsDialog.vue +++ b/frontend/src/components/event/EventDetailsDialog.vue @@ -58,6 +58,52 @@ @score-updated="$emit('score-updated')" /> + + +
+
+ + + + +
+
+
+
+ 팀 {{ idx + 1 }} + (인원: {{ team.members.length }}명) + 팀 에버리지: {{ getTeamAverage(team).toFixed(1) }} + 핸디캡: + +
+
    +
  • + + ({{ getMemberAverage(member) }}) +
  • +
+
+ +
+
+
+
팀을 생성하려면 옵션을 선택 후 '팀 생성'을 눌러주세요.
+
+