diff --git a/backend/app.js b/backend/app.js index a947843..e084ba7 100644 --- a/backend/app.js +++ b/backend/app.js @@ -48,6 +48,15 @@ io.on('connection', (socket) => { // 전역 Socket.IO 인스턴스 설정 app.set('io', io); +// app.js 또는 서버 초기화 파일 +const session = require('express-session'); +app.use(session({ + secret: process.env.JWT_SECRET, + resave: false, + saveUninitialized: false, + cookie: { secure: process.env.NODE_ENV === 'production' } // https 환경이면 true, 개발환경은 false +})); + // 데이터베이스 연결 및 모델 동기화 실행 sequelize.authenticate() .then(() => { diff --git a/backend/controllers/clubController.js b/backend/controllers/clubController.js index 08d7521..d03b69c 100644 --- a/backend/controllers/clubController.js +++ b/backend/controllers/clubController.js @@ -16,6 +16,48 @@ const isAdmin = (user) => { return user && (user.role === 'admin' || user.role === 'superadmin'); }; +// 클럽 선택 +const allowedMemberTypes = ['owner', 'manager']; + +exports.selectClub = async (req, res) => { + try { + const { clubId } = req.body; + const userId = req.user && req.user.id; + + // 클럽 존재 여부 확인 + const club = await Club.findByPk(clubId); + if (!club) { + return res.status(404).json({ message: '클럽을 찾을 수 없습니다.' }); + } + + // 관리자 여부 체크 + const isAdmin = req.user && (req.user.role === 'admin' || req.user.role === 'superadmin'); + let isClubManager = false; + + if (!isAdmin) { + // Members 테이블에서 모임장/운영진 여부 확인 + const member = await Member.findOne({ + where: { + clubId, + userId, + memberType: ['모임장', '운영진'] + } + }); + isClubManager = !!member; + } + + if (!isAdmin && !isClubManager) { + return res.status(403).json({ message: '해당 클럽에 대한 권한이 없습니다.' }); + } + + req.session.clubId = clubId; + res.status(200).json({ message: '클럽을 선택했습니다.' }); + } catch (error) { + console.error('클럽 선택 오류:', error); + res.status(500).json({ message: '클럽을 선택하는 중 오류가 발생했습니다.' }); + } +}; + // 모든 클럽 조회 exports.getAllClubs = async (req, res) => { try { @@ -57,7 +99,7 @@ exports.getAllClubs = async (req, res) => { exports.getClubById = async (req, res) => { try { const { clubId } = req.body; - console.log(`clubId: ${clubId}`); + // console.log(`clubId: ${clubId}`); if (!clubId) { return res.status(400).json({ message: "유효하지 않은 클럽 ID입니다." }); } @@ -156,8 +198,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 +561,6 @@ exports.getNextMeeting = async (req, res) => { }, include: [{ model: EventParticipant, - as: 'participants', where: { status: { [Op.ne]: '취소' } }, @@ -563,7 +604,6 @@ exports.getLastMeeting = async (req, res) => { }, include: [{ model: EventParticipant, - as: 'participants', where: { status: { [Op.ne]: '취소' } }, @@ -611,10 +651,8 @@ exports.getScoreStats = async (req, res) => { }, include: [{ model: EventParticipant, - as: 'participants', include: [{ - model: EventScore, - as: 'scores' + model: EventScore }] }] }); @@ -690,7 +728,6 @@ exports.getRecentMeetings = async (req, res) => { }, include: [{ model: EventParticipant, - as: 'participants', where: { status: { [Op.ne]: '취소' } }, @@ -796,10 +833,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..f2f62df 100644 --- a/backend/controllers/eventController.js +++ b/backend/controllers/eventController.js @@ -2,7 +2,7 @@ * 이벤트 컨트롤러 * 이벤트 관련 기능을 처리하는 컨트롤러 */ -const { Event, EventParticipant, Member, Club, EventScore, ClubMember } = require('../models'); +const { Event, EventParticipant, Member, Club, EventScore, ClubMember, EventTeam, EventTeamMember } = require('../models'); const { Op } = require('sequelize'); const { sequelize } = require('../models'); @@ -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]: '취소' } - }, + 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, @@ -542,7 +538,6 @@ exports.updateEventScore = async (req, res) => { const newScore = await EventScore.create({ eventId, participantId: existingScore.participantId, - teamNumber: existingScore.teamNumber, gameNumber, score, handicap, @@ -618,7 +613,6 @@ exports.deleteEventScore = async (req, res) => { include: [ { model: EventParticipant, - as: 'participant', attributes: ['id', 'status'], include: [{ model: Member, @@ -702,7 +696,6 @@ exports.getEventStats = async (req, res) => { ], include: [{ model: EventParticipant, - as: 'participant', attributes: [], include: [{ model: Member, @@ -934,7 +927,6 @@ exports.getUserEvents = async (req, res) => { include: [ { model: EventParticipant, - as: 'participants', where: { userId }, attributes: [] }, @@ -983,7 +975,7 @@ exports.getEventParticipants = async (req, res) => { include: [ { model: Member, - attributes: ['id', 'name', 'email', 'phone', 'memberType'] + attributes: ['id', 'name', 'email', 'phone', 'memberType', 'average'] } ], order: [ @@ -1011,9 +1003,8 @@ exports.addEventParticipant = async (req, res) => { const { clubId, memberId, - teamNumber, - paymentStatus, - paymentAmount + status, + paymentStatus } = req.body; const eventId = req.params.id; if (!eventId || !clubId || !memberId) { @@ -1078,12 +1069,10 @@ exports.addEventParticipant = async (req, res) => { const participant = await EventParticipant.create({ eventId, memberId, - teamNumber: teamNumber || 1, - status: req.body.status || '참가예정', - attendance: false, - paymentStatus: req.body.paymentStatus || '미납', - paymentAmount: paymentAmount || event.entryFee || 0, - registeredAt: new Date() + status: status || '참가예정', + paymentStatus: paymentStatus || '미납', + createdAt: new Date(), + updatedAt: new Date() }); // 참가자 정보 조회 @@ -1173,6 +1162,96 @@ exports.updateEventParticipant = async (req, res) => { } }; +// [팀 생성 기능] 이벤트별 팀 목록 조회 +exports.getEventTeams = async (req, res) => { + try { + const eventId = req.params.id; + if (!eventId) { + return res.status(400).json({ message: '이벤트 ID가 필요합니다.' }); + } + const teams = await EventTeam.findAll({ + where: { eventId }, + include: [ + { + model: EventTeamMember, + include: [ + { + model: EventParticipant, + include: [{ model: Member }] + } + ] + } + ], + order: [['teamNumber', 'ASC']] + }); + const result = teams.map(team => ({ + id: team.id, + teamNumber: team.teamNumber, + handicap: team.handicap, + members: (team.EventTeamMembers || []) + .sort((a, b) => a.order - b.order) + .map(tm => ({ + id: tm.eventParticipantId, + name: tm.EventParticipant?.Member?.name || `참가자 ${tm.eventParticipantId}`, + order: tm.order + })) + })); + res.json({ teams: result }); + } catch (error) { + console.error('팀 목록 조회 오류:', error); + res.status(500).json({ message: '팀 목록을 가져오는 중 오류가 발생했습니다.' }); + } +}; + +// [팀 생성 기능] 이벤트별 팀 배정 저장 +exports.createOrUpdateEventTeams = async (req, res) => { + const eventId = req.params.id; + const { teams } = req.body; // [{teamNumber, handicap, members:[id]}] + + if (!eventId || !Array.isArray(teams)) { + return res.status(400).json({ message: '이벤트 ID와 팀 정보가 필요합니다.' }); + } + + try { + await sequelize.transaction(async (t) => { + // 기존 팀 및 팀멤버 데이터 삭제 + await EventTeam.destroy({ where: { eventId }, transaction: t }); + await EventTeamMember.destroy({ + where: {}, // eventId로만 삭제하려면, eventTeamId로 팀 id 목록을 먼저 조회해서 삭제 + transaction: t + }); + + // 새 팀 및 팀멤버 데이터 저장 + for (const team of teams) { + // 팀 생성 + const createdTeam = await EventTeam.create({ + eventId, + teamNumber: team.teamNumber, + handicap: team.handicap || 0 + }, { transaction: t }); + + // 팀 멤버(순서 포함) 저장 + if (Array.isArray(team.members) && team.members.length > 0) { + const teamMemberRows = team.members + .filter(m => m.id != null) + .map(m => ({ + eventTeamId: createdTeam.id, + eventParticipantId: m.id, + order: m.order ?? 1 + })); + if (teamMemberRows.length > 0) { + await EventTeamMember.bulkCreate(teamMemberRows, { transaction: t }); + } + } + } + }); + res.json({ message: '팀 배정이 저장되었습니다.' }); + } catch (error) { + console.error('팀 배정 저장 오류:', error); + res.status(500).json({ message: '팀 배정 저장 중 오류가 발생했습니다.' }); + } +}; + // 참가자 삭제 (소프트 삭제) exports.deleteEventParticipant = async (req, res) => { try { 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..7e300db --- /dev/null +++ b/backend/middleware/requireFeature.js @@ -0,0 +1,24 @@ +const { clubHasFeature } = require('../utils/featureAccess'); + +// featureCode: 예) 'event_management', 'team_create', 'statistics' 등 +function requireFeature(featureCode) { + return async function(req, res, next) { + try { + const clubId = req.session.clubId || req.clubId || req.body.clubId || req.params.clubId; + console.log('backendClubId:', 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..3b807d8 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,15 @@ 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' }); }; diff --git a/backend/models/EventParticipant.js b/backend/models/EventParticipant.js index 61ce63c..c9fbd2a 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,19 +49,27 @@ 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' + }); + EventParticipant.belongsTo(models.Member, { foreignKey: 'memberId' }); +}; + module.exports = EventParticipant; 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..85e4b0f --- /dev/null +++ b/backend/models/EventTeam.js @@ -0,0 +1,37 @@ +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' }); + EventTeam.hasMany(models.EventTeamMember, { foreignKey: 'eventTeamId' }); +}; + +module.exports = EventTeam; \ No newline at end of file diff --git a/backend/models/EventTeamMember.js b/backend/models/EventTeamMember.js new file mode 100644 index 0000000..b16fded --- /dev/null +++ b/backend/models/EventTeamMember.js @@ -0,0 +1,30 @@ +const { DataTypes } = require('sequelize'); +const { sequelize } = require('../config/database'); + +const EventTeamMember = sequelize.define('EventTeamMember', { + eventTeamId: { + type: DataTypes.INTEGER.UNSIGNED, + allowNull: false, + primaryKey: true + }, + eventParticipantId: { + type: DataTypes.INTEGER.UNSIGNED, + allowNull: false, + primaryKey: true + }, + order: { + type: DataTypes.INTEGER.UNSIGNED, + allowNull: false, + defaultValue: 1 + } +}, { + tableName: 'EventTeamMembers', + timestamps: false +}); + +EventTeamMember.associate = (models) => { + EventTeamMember.belongsTo(models.EventTeam, { foreignKey: 'eventTeamId' }); + EventTeamMember.belongsTo(models.EventParticipant, { foreignKey: 'eventParticipantId' }); +}; + +module.exports = EventTeamMember; 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..a637a9d 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', @@ -75,17 +75,14 @@ const Member = sequelize.define('Member', { tableName: 'Members', timestamps: true }); +Member.associate = (models) => { + // User와의 관계 설정 + Member.belongsTo(User, {foreignKey: 'userId'}); -// User와의 관계 설정 -Member.belongsTo(User, { - foreignKey: 'userId', - as: 'user' -}); - -// Club과의 관계 설정 -Member.belongsTo(Club, { - foreignKey: 'clubId', - as: 'club' -}); + // Club과의 관계 설정 + Member.belongsTo(Club, {foreignKey: 'clubId'}); + // EventParticipant와의 관계 설정 + Member.hasMany(models.EventParticipant, { foreignKey: 'memberId' }); +}; 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..717116f 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'); @@ -14,99 +15,7 @@ const SubscriptionPlan = require('./SubscriptionPlan'); 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 EventTeamMember = require('./EventTeamMember'); // 모델 동기화 함수 const syncModels = async () => { @@ -116,729 +25,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 +51,29 @@ module.exports = { SubscriptionPlanFeature, ClubSubscription, ActivityLog, - syncModels, - createAdminUser, - createSampleData + EventTeamMember, +}; +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, + EventTeamMember, + syncModels }; diff --git a/backend/package-lock.json b/backend/package-lock.json index d9308ed..4c9931c 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -15,6 +15,7 @@ "cors": "^2.8.5", "dotenv": "^16.4.7", "express": "^4.21.2", + "express-session": "^1.18.1", "express-validator": "^7.2.1", "helmet": "^8.0.0", "jsonwebtoken": "^9.0.2", @@ -1204,6 +1205,40 @@ "url": "https://opencollective.com/express" } }, + "node_modules/express-session": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.1.tgz", + "integrity": "sha512-a5mtTqEaZvBCL9A9aqkrtfz+3SMDhOVUnjafjo+s7A9Txkq+SVX2DLvSp1Zrv4uCXa3lMSK3viWnh9Gg07PBUA==", + "license": "MIT", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.7", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-headers": "~1.0.2", + "parseurl": "~1.3.3", + "safe-buffer": "5.2.1", + "uid-safe": "~2.1.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/express-session/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express-session/node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, "node_modules/express-validator": { "version": "7.2.1", "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.2.1.tgz", @@ -2721,6 +2756,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -3474,6 +3518,18 @@ "node": ">= 0.6" } }, + "node_modules/uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "license": "MIT", + "dependencies": { + "random-bytes": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/umzug": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/umzug/-/umzug-2.3.0.tgz", diff --git a/backend/package.json b/backend/package.json index 9773c68..07a8862 100644 --- a/backend/package.json +++ b/backend/package.json @@ -19,6 +19,7 @@ "cors": "^2.8.5", "dotenv": "^16.4.7", "express": "^4.21.2", + "express-session": "^1.18.1", "express-validator": "^7.2.1", "helmet": "^8.0.0", "jsonwebtoken": "^9.0.2", 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..3a5ac6e 100644 --- a/backend/routes/clubRoutes.js +++ b/backend/routes/clubRoutes.js @@ -3,6 +3,10 @@ const router = express.Router(); const jwt = require('jsonwebtoken'); const clubController = require('../controllers/clubController'); const { authenticateJWT } = require('../middleware/authMiddleware'); +const requireFeature = require('../middleware/requireFeature'); + +// 클럽 선택 +router.post('/select-club', authenticateJWT, clubController.selectClub); // 모든 클럽 조회 router.get('/', clubController.getAllClubs); @@ -53,7 +57,7 @@ router.post('/members/top', authenticateJWT, 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..9de3cfc 100644 --- a/backend/routes/eventRoutes.js +++ b/backend/routes/eventRoutes.js @@ -2,28 +2,33 @@ 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.post('/:id/teams', authenticateJWT, requireFeature('team_create'), eventController.createOrUpdateEventTeams); +router.get('/:id/teams', authenticateJWT, requireFeature('team_create'), eventController.getEventTeams); // 이벤트 점수 관리 -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/package-lock.json b/frontend/package-lock.json index ddb03c4..0da3eb8 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -28,6 +28,7 @@ "vue": "^3.5.13", "vue-chartjs": "^5.3.2", "vue-router": "^4.5.0", + "vuedraggable": "^4.1.0", "yup": "^1.3.3" }, "devDependencies": { @@ -3050,6 +3051,12 @@ } } }, + "node_modules/sortablejs": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.14.0.tgz", + "integrity": "sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w==", + "license": "MIT" + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -3435,6 +3442,18 @@ "vue": "^3.2.0" } }, + "node_modules/vuedraggable": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/vuedraggable/-/vuedraggable-4.1.0.tgz", + "integrity": "sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww==", + "license": "MIT", + "dependencies": { + "sortablejs": "1.14.0" + }, + "peerDependencies": { + "vue": "^3.0.1" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index 7516681..e1a9c91 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -30,6 +30,7 @@ "vue": "^3.5.13", "vue-chartjs": "^5.3.2", "vue-router": "^4.5.0", + "vuedraggable": "^4.1.0", "yup": "^1.3.3" }, "devDependencies": { diff --git a/frontend/src/App.vue b/frontend/src/App.vue index fab6147..ae5c245 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); @@ -171,8 +171,9 @@ const fetchClubs = async () => { })); // 첫 번째 클럽의 memberType을 localStorage에 저장 - if (clubs.value.length > 0) { - localStorage.setItem('userClubRole', clubs.value[0].memberType || ''); + if (clubs.value.length > 0 && !selectedClubId.value) { + selectedClubId.value = clubs.value[0].id; + await setSelectedClub(clubs.value[0]); } } catch (error) { console.error('클럽 목록 로드 실패:', error); @@ -198,15 +199,30 @@ const fetchClubInfo = async () => { } }; +// 클럽 선택 +const setSelectedClub = async (club) => { + // 1. localStorage에 저장 + localStorage.setItem('clubId', club.id); + localStorage.setItem('userClubRole', club.memberType || ''); + + // 2. 세션에도 저장 (백엔드에 clubId 전달) + try { + await apiClient.post('/api/club/select-club', { clubId: club.id }); + } catch (error) { + console.error('클럽 ID 세션 저장 실패:', error); + } + + // 3. 프론트엔드 상태도 변경 + selectedClubId.value = club.id; +}; + // 클럽 변경 시 const router = useRouter(); const onClubChange = async () => { try { - // 선택된 클럽의 memberType 찾기 const selectedClub = clubs.value.find(club => club.id === selectedClubId.value); if (selectedClub) { - localStorage.setItem('userClubRole', selectedClub.memberType || ''); - localStorage.setItem('clubId', selectedClub.id); + await setSelectedClub(selectedClub); } await fetchClubInfo(); @@ -296,9 +312,24 @@ const loadUserData = async () => { } }; +// 로그인 성공 후 상태 갱신 핸들러 +const handleUserLoggedIn = async () => { + await loadUserData(); + if (isLoggedIn.value) { + await fetchClubs(); + await fetchClubInfo(); + loadMenuItems(); + } +}; + // 컴포넌트 마운트 시 데이터 로드 onMounted(async () => { await loadUserData(); + const clubId = localStorage.getItem('clubId'); + if (clubId) { + // 서버 세션에 clubId를 동기화 + apiClient.post('/api/club/select-club', { clubId }); + } if (isLoggedIn.value) { await fetchClubs(); await fetchClubInfo(); @@ -308,6 +339,9 @@ onMounted(async () => { // 화면 크기 변경 감지 window.addEventListener('resize', handleResize); + // 로그인 성공 이벤트 감지 + window.addEventListener('user-logged-in', handleUserLoggedIn); + // 초기 expandedGroups 설정 initializeExpandedGroups(); }); @@ -315,6 +349,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..d044645 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'; @@ -91,6 +91,7 @@ const userAvatar = computed(() => user.value?.avatar || ''); // 로그아웃 const logout = () => { + user.value = null; localStorage.removeItem('token'); localStorage.removeItem('clubId'); localStorage.removeItem('userRole'); diff --git a/frontend/src/components/event/EventDetailsDialog.vue b/frontend/src/components/event/EventDetailsDialog.vue index 0741974..562a043 100644 --- a/frontend/src/components/event/EventDetailsDialog.vue +++ b/frontend/src/components/event/EventDetailsDialog.vue @@ -45,7 +45,7 @@ @@ -58,6 +58,10 @@ @score-updated="$emit('score-updated')" /> + + + + + + diff --git a/frontend/src/services/teamService.js b/frontend/src/services/teamService.js new file mode 100644 index 0000000..f0fcf0c --- /dev/null +++ b/frontend/src/services/teamService.js @@ -0,0 +1,30 @@ +import apiClient from './api'; + +const teamService = { + /** + * 이벤트별 팀 목록 조회 + * @param {number|string} eventId + * @returns {Promise} 팀 배열 + */ + async getTeams(eventId) { + const response = await apiClient.get(`/api/club/events/${eventId}/teams`); + return response.data.teams; + }, + /** + * 이벤트별 팀 저장 + * @param {number|string} eventId + * @param {Array} teams - [{ teamNumber, handicap, members: [id, ...] }] + */ + async saveTeams(eventId, teams) { + const payload = teams.map((team, idx) => ({ + teamNumber: team.teamNumber ?? idx + 1, + handicap: team.handicap || 0, + members: team.members + })); + return apiClient.post(`/api/club/events/${eventId}/teams`, { teams: payload }); + }, + + // 추후: 팀 불러오기, 수정, 삭제 등 추가 가능 +}; + +export default teamService; diff --git a/frontend/src/views/club/EventCalendar.vue b/frontend/src/views/club/EventCalendar.vue index 02cbd45..d722bd6 100644 --- a/frontend/src/views/club/EventCalendar.vue +++ b/frontend/src/views/club/EventCalendar.vue @@ -65,6 +65,7 @@ const fetchEvents = async () => { loading.value = true; try { const eventData = await eventService.getEvents(clubId.value); + console.log('eventData', eventData); events.value = eventData.map(event => ({ id: event.id, title: event.title, diff --git a/frontend/src/views/club/EventManagement.vue b/frontend/src/views/club/EventManagement.vue index 6f06211..8ff69b4 100644 --- a/frontend/src/views/club/EventManagement.vue +++ b/frontend/src/views/club/EventManagement.vue @@ -105,6 +105,7 @@