팀-생성-기능 #1

Merged
jaybe merged 3 commits from 팀-생성-기능 into main 2025-05-08 16:23:38 +00:00
42 changed files with 938 additions and 1170 deletions
Showing only changes of commit 7fa99e6988 - Show all commits
+4 -11
View File
@@ -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
}]
}]
});
+9 -16
View File
@@ -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: [
-2
View File
@@ -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: [
+26 -120
View File
@@ -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);
}
}
// 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;
+23
View File
@@ -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;
+22 -2
View File
@@ -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;
+8 -3
View File
@@ -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;
+8 -3
View File
@@ -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;
+17 -8
View File
@@ -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'
});
};
+24 -9
View File
@@ -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'
});
};
+14 -5
View File
@@ -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;
+36
View File
@@ -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;
+25 -19
View File
@@ -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;
+5 -11
View File
@@ -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;
+24 -3
View File
@@ -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;
+6 -2
View File
@@ -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;
+4 -4
View File
@@ -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
},
+17 -1
View File
@@ -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;
+3 -3
View File
@@ -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',
+5 -1
View File
@@ -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;
+2 -2
View File
@@ -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',
+30 -810
View File
@@ -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
};
+4 -57
View File
@@ -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) => {
+6 -4
View File
@@ -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);
+16 -15
View File
@@ -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;
+1
View File
@@ -3,6 +3,7 @@ const router = express.Router();
const featureController = require('../controllers/featureController');
const { authenticateJWT } = require('../middleware/authMiddleware');
// 기본 기능 관리
router.get('/', authenticateJWT, featureController.getFeatures);
+1
View File
@@ -5,6 +5,7 @@
const express = require('express');
const router = express.Router();
const { authenticateJWT } = require('../middleware/authMiddleware');
const menuController = require('../controllers/menuController');
// 사용자의 접근 가능한 메뉴 목록 조회
-27
View File
@@ -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' }, {});
}
};
@@ -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 }, {});
}
};
+49
View File
@@ -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, {});
}
};
@@ -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, {});
}
};
@@ -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, {});
}
};
@@ -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' } }, {});
}
};
@@ -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]: '샘플 볼링 클럽 %' } }, {});
}
};
@@ -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', { }, {});
}
};
@@ -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]: '클럽% 이벤트%' } }, {});
}
};
@@ -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]: '클럽%멤버%' } }, {});
}
};
+95
View File
@@ -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, {});
}
};
+40
View File
@@ -0,0 +1,40 @@
const { ClubSubscription, SubscriptionPlan, ClubFeature, Feature } = require('../models');
const { Op } = require('sequelize');
/**
* 클럽이 특정 featureCode를 사용할 있는지 여부 반환
* @param {number} clubId
* @param {string} featureCode
* @returns {Promise<boolean>}
*/
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 };
+15 -1
View File
@@ -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);
});
</script>
+1 -1
View File
@@ -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';
@@ -58,6 +58,52 @@
@score-updated="$emit('score-updated')"
/>
</TabPanel>
<!-- 편성 -->
<TabPanel header="팀 편성">
<div class="team-generation-section">
<div class="flex align-items-center gap-2 mb-3">
<label>
<input type="radio" value="byMemberCount" v-model="teamOption" /> 인원수로 나누기
</label>
<InputNumber v-if="teamOption==='byMemberCount'" v-model="teamOptionValue" :min="2" :max="eventModel.participants?.length || 2" placeholder="팀 인원수" style="width:120px;" />
<label class="ml-4">
<input type="radio" value="byTeamCount" v-model="teamOption" /> 개수로 나누기
</label>
<InputNumber v-if="teamOption==='byTeamCount'" v-model="teamOptionValue" :min="2" :max="eventModel.participants?.length || 2" placeholder="팀 개수" style="width:120px;" />
<Button label="팀 생성" icon="pi pi-users" class="ml-3" :disabled="!canGenerateTeams" @click="generateTeams" />
</div>
<div v-if="teams.length > 0">
<div v-for="(team, idx) in teams" :key="idx" class="team-box mb-3 p-3">
<div class="flex align-items-center gap-2 mb-2">
<strong> {{ idx + 1 }}</strong>
<span class="ml-2">(인원: {{ team.members.length }})</span>
<span class="ml-3"> 에버리지: <span class="text-primary">{{ getTeamAverage(team).toFixed(1) }}</span></span>
<span class="ml-3">핸디캡:</span>
<InputNumber v-model="team.handicap" :min="-100" :max="100" style="width:80px;" />
</div>
<ul class="pl-3">
<li v-for="(member, mIdx) in team.members" :key="member.id || mIdx" class="flex align-items-center gap-2 mb-1">
<InputText v-model="member.name" style="width: 140px;" placeholder="이름 입력" />
<span v-if="getMemberAverage(member) !== null" class="text-xs text-gray-700 ml-1">({{ getMemberAverage(member) }})</span>
<Button icon="pi pi-times" class="p-button-text p-button-danger p-0" @click="team.members.splice(mIdx,1)" v-if="team.members.length > 1" />
</li>
</ul>
<div class="flex align-items-center gap-2 mt-2">
<Dropdown
v-model="team._selectedMemberId"
:options="availableMembersForTeam(idx)"
optionLabel="name"
optionValue="id"
placeholder="팀원 선택"
style="width: 140px;"
/>
<Button icon="pi pi-plus" class="p-button-text p-0" :disabled="!team._selectedMemberId" @click="addMemberToTeam(idx)" />
</div>
</div>
</div>
<div v-else class="text-gray-500 mt-3">팀을 생성하려면 옵션을 선택 '팀 생성' 눌러주세요.</div>
</div>
</TabPanel>
</TabView>
<template #footer>
@@ -75,54 +121,142 @@ import TabView from 'primevue/tabview';
import TabPanel from 'primevue/tabpanel';
import Badge from 'primevue/badge';
import Button from 'primevue/button';
import InputNumber from 'primevue/inputnumber';
import Dropdown from 'primevue/dropdown';
import InputText from 'primevue/inputtext';
import ParticipantManagement from './ParticipantManagement.vue';
import ScoreManagement from './ScoreManagement.vue';
import dayjs from 'dayjs';
const props = defineProps({
visible: {
type: Boolean,
required: true
},
event: {
type: Object,
required: true
},
getEventTypeName: {
type: Function,
required: true
},
getStatusName: {
type: Function,
required: true
},
getStatusSeverity: {
type: Function,
required: true
},
formatDate: {
type: Function,
required: true
},
availableMembers: {
type: Array,
default: () => []
}
visible: { type: Boolean, required: true },
event: { type: Object, default: () => ({}) },
getEventTypeName: { type: Function, required: true },
getStatusName: { type: Function, required: true },
getStatusSeverity: { type: Function, required: true },
formatDate: { type: Function, required: true },
availableMembers: { type: Array, default: () => [] }
});
const emit = defineEmits(['update:visible', 'hide', 'edit', 'participant-updated', 'score-updated']);
const eventModel = computed(() => props.event);
const eventModel = computed(() => props.event || {});
const formattedStartDate = computed(() => props.formatDate(eventModel.value.startDate));
const formattedEndDate = computed(() => props.formatDate(eventModel.value.endDate));
const hideDialog = () => {
emit('hide');
};
console.log("props", props);
console.log("eventModel", eventModel.value);
console.log("참가자", JSON.stringify(eventModel.value.participants, null, 2));
//
const teamOption = ref('byMemberCount');
const teamOptionValue = ref(2);
const teams = ref([]);
const canGenerateTeams = computed(() => {
const n = eventModel.value.participants?.length || 0;
if (n < 2) return false;
if (teamOption.value === 'byMemberCount') {
return teamOptionValue.value >= 2 && teamOptionValue.value <= n;
} else {
return teamOptionValue.value >= 2 && teamOptionValue.value <= n;
}
});
function shuffle(array) {
// Fisher-Yates shuffle
let arr = array.slice();
for (let i = arr.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[arr[i], arr[j]] = [arr[j], arr[i]];
}
return arr;
}
function generateTeams() {
const participants = (eventModel.value.participants || []).map(p => ({ id: p.id, name: p.name }));
const shuffled = shuffle(participants);
let result = [];
if (teamOption.value === 'byMemberCount') {
const size = teamOptionValue.value;
const teamCount = Math.ceil(shuffled.length / size);
for (let i = 0; i < teamCount; i++) {
result.push({ members: [], handicap: 0, _selectedMemberId: null });
}
shuffled.forEach((member, idx) => {
result[Math.floor(idx / size)].members.push(member);
});
} else {
const teamCount = teamOptionValue.value;
for (let i = 0; i < teamCount; i++) {
result.push({ members: [], handicap: 0, _selectedMemberId: null });
}
shuffled.forEach((member, idx) => {
result[idx % teamCount].members.push(member);
});
}
teams.value = result;
}
//
function availableMembersForTeam(teamIdx) {
const usedIds = new Set();
teams.value.forEach((team, idx) => {
if (idx === teamIdx) return;
team.members.forEach(m => usedIds.add(m.id));
});
// id
teams.value[teamIdx]?.members.forEach((m, i, arr) => {
if (arr.findIndex(x => x.id === m.id) !== i) usedIds.add(m.id);
});
return (eventModel.value.participants || [])
.filter(p => p.id && p.Member && p.Member.name &&
!usedIds.has(p.id) &&
!teams.value[teamIdx].members.some(m => m.id === p.id)
)
.map(p => ({ id: p.id, name: p.Member.name }));
}
function addMemberToTeam(teamIdx) {
const team = teams.value[teamIdx];
const memberId = team._selectedMemberId;
if (!memberId) return;
const participant = (eventModel.value.participants || []).find(p => p.id === memberId);
if (participant && !team.members.some(m => m.id === participant.id)) {
team.members.push({ id: participant.id, name: participant.Member?.name });
}
team._selectedMemberId = null;
}
function getMemberAverage(member) {
// Member.average
if (!member.id) return null;
const p = (eventModel.value.participants || []).find(x => x.id === member.id);
if (!p) return null;
if (p.Member && typeof p.Member.average === 'number') return p.Member.average;
if (typeof p.average === 'number') return p.average; //
if (typeof p.averageScore === 'number') return p.averageScore;
return null;
}
function getTeamAverage(team) {
const avgs = team.members.map(getMemberAverage).filter(v => typeof v === 'number');
if (!avgs.length) return 0;
return avgs.reduce((a, b) => a + b, 0) / avgs.length;
}
</script>
<style scoped>
.team-generation-section {
margin-top: 1rem;
}
.team-box {
background: #f8f9fa;
border-radius: 8px;
box-shadow: 0 1px 4px rgba(0,0,0,0.04);
}
.event-details {
margin-top: 1rem;
}