Merge pull request '팀-생성-기능' (#1) from 팀-생성-기능 into main
Reviewed-on: #1
This commit was merged in pull request #1.
This commit is contained in:
@@ -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(() => {
|
||||
|
||||
@@ -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
|
||||
}]
|
||||
}]
|
||||
});
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
+22
-2
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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'
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
+25
-19
@@ -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;
|
||||
|
||||
+11
-14
@@ -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;
|
||||
|
||||
+24
-3
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
},
|
||||
|
||||
@@ -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,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',
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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',
|
||||
|
||||
+33
-810
@@ -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
|
||||
};
|
||||
|
||||
Generated
+56
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
@@ -3,6 +3,7 @@ const router = express.Router();
|
||||
const featureController = require('../controllers/featureController');
|
||||
const { authenticateJWT } = require('../middleware/authMiddleware');
|
||||
|
||||
|
||||
// 기본 기능 관리
|
||||
router.get('/', authenticateJWT, featureController.getFeatures);
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { authenticateJWT } = require('../middleware/authMiddleware');
|
||||
|
||||
const menuController = require('../controllers/menuController');
|
||||
|
||||
// 사용자의 접근 가능한 메뉴 목록 조회
|
||||
|
||||
@@ -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 }, {});
|
||||
}
|
||||
};
|
||||
@@ -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]: '클럽%멤버%' } }, {});
|
||||
}
|
||||
};
|
||||
@@ -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, {});
|
||||
}
|
||||
};
|
||||
@@ -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 };
|
||||
Generated
+19
@@ -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",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
+41
-6
@@ -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);
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
<ParticipantManagement
|
||||
:eventId="eventModel.id"
|
||||
:availableMembers="availableMembers"
|
||||
@participant-updated="$emit('participant-updated')"
|
||||
@participant-updated="handleParticipantUpdated"
|
||||
/>
|
||||
</TabPanel>
|
||||
|
||||
@@ -58,6 +58,10 @@
|
||||
@score-updated="$emit('score-updated')"
|
||||
/>
|
||||
</TabPanel>
|
||||
<!-- 팀 편성 탭 -->
|
||||
<TabPanel header="팀 편성">
|
||||
<TeamGeneration :participants="confirmedParticipants" :teams="teams" @update:teams="teams = $event" @teams-generated="teams = $event" @save-teams="handleSaveTeams" />
|
||||
</TabPanel>
|
||||
</TabView>
|
||||
|
||||
<template #footer>
|
||||
@@ -69,60 +73,110 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import TeamGeneration from './TeamGeneration.vue';
|
||||
import Dialog from 'primevue/dialog';
|
||||
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');
|
||||
};
|
||||
// 팀 생성 결과 저장
|
||||
const teams = ref([]);
|
||||
|
||||
// 이벤트 상세 다이얼로그가 열릴 때마다 팀 목록 불러오기
|
||||
watch(() => eventModel.value.id, async (eventId) => {
|
||||
if (eventId) {
|
||||
const latestTeams = await teamService.getTeams(eventId);
|
||||
teams.value = latestTeams;
|
||||
}
|
||||
}, { immediate: true });
|
||||
|
||||
// 참가확정 참가자 목록을 계산하는 computed 속성
|
||||
const confirmedParticipants = computed(() => {
|
||||
const arr = eventModel.value.EventParticipants?.filter(p => p.status === '참가확정') || [];
|
||||
console.log('[상위] 참가확정:', arr);
|
||||
return arr;
|
||||
});
|
||||
|
||||
// 참가확정 인원 수
|
||||
const confirmedCount = computed(() => confirmedParticipants.value.length);
|
||||
|
||||
|
||||
// 팀 배정 저장 핸들러
|
||||
import teamService from '@/services/teamService';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
const toast = useToast();
|
||||
|
||||
async function handleSaveTeams(savedTeams) {
|
||||
const eventId = eventModel.value.id;
|
||||
try {
|
||||
await teamService.saveTeams(eventId, savedTeams);
|
||||
toast.add({ severity: 'success', summary: '저장 완료', detail: '팀 배정이 저장되었습니다.', life: 2000 });
|
||||
// 저장 후 최신 팀 목록 불러오기
|
||||
const latestTeams = await teamService.getTeams(eventId);
|
||||
teams.value = latestTeams;
|
||||
} catch (e) {
|
||||
toast.add({ severity: 'error', summary: '저장 실패', detail: '팀 저장 중 오류가 발생했습니다.', life: 3000 });
|
||||
}
|
||||
}
|
||||
|
||||
// 참가자 상태 변경 처리 함수
|
||||
function handleParticipantUpdated(data) {
|
||||
// 부모 컴포넌트에 이벤트 전달
|
||||
emit('participant-updated', data);
|
||||
|
||||
// 전체 참가자 목록이 있는 경우 직접 업데이트
|
||||
if (data.allParticipants) {
|
||||
// EventParticipants 속성을 완전히 대체
|
||||
eventModel.value.EventParticipants = [...data.allParticipants];
|
||||
}
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
:deep(.mini-inputnumber .p-inputnumber-input) {
|
||||
width: 60px !important;
|
||||
min-width: 0 !important;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<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;
|
||||
}
|
||||
|
||||
@@ -238,7 +238,15 @@ const saveParticipant = async () => {
|
||||
});
|
||||
}
|
||||
|
||||
// 참가자 데이터 다시 가져오기
|
||||
await fetchParticipants();
|
||||
|
||||
// 참가자 변경 이벤트 발생 - 변경된 참가자와 전체 참가자 목록 함께 전달
|
||||
emit('participant-updated', {
|
||||
updatedParticipant: editingParticipant.value,
|
||||
allParticipants: participants.value
|
||||
});
|
||||
|
||||
hideDialog();
|
||||
} catch (error) {
|
||||
console.error('참가자 저장 오류:', error.message);
|
||||
|
||||
@@ -14,11 +14,6 @@
|
||||
{{ slotProps.data.participant?.name }}
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="teamNumber" header="팀" sortable style="width: 80px">
|
||||
<template #body="slotProps">
|
||||
{{ slotProps.data.teamNumber }}
|
||||
</template>
|
||||
</Column>
|
||||
<template v-for="i in maxGames" :key="i">
|
||||
<Column :field="'games.' + (i-1) + '.score'" :header="i + '게임'" sortable>
|
||||
<template #body="slotProps">
|
||||
@@ -65,17 +60,6 @@
|
||||
<small v-if="submitted && !editingScore.participantId" class="p-error">참가자를 선택해주세요.</small>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="teamNumber">팀</label>
|
||||
<InputNumber
|
||||
id="teamNumber"
|
||||
v-model="editingScore.teamNumber"
|
||||
:min="1"
|
||||
:max="10"
|
||||
placeholder="팀 번호"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
<template v-for="game in editingScore.games" :key="game.gameNumber">
|
||||
<div class="col-6">
|
||||
|
||||
@@ -0,0 +1,343 @@
|
||||
<template>
|
||||
<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="participants.length" placeholder="팀 인원수" class="mini-inputnumber" />
|
||||
<label class="ml-4">
|
||||
<input type="radio" value="byTeamCount" v-model="teamOption" /> 팀 개수로 나누기
|
||||
</label>
|
||||
<InputNumber v-if="teamOption==='byTeamCount'" v-model="teamOptionValue" :min="2" :max="participants.length" placeholder="팀 개수" class="mini-inputnumber" />
|
||||
<Button label="팀 생성" icon="pi pi-users" class="ml-3" :disabled="!canGenerateTeams" @click="generateTeams" />
|
||||
<Button label="팀 배정 저장" icon="pi pi-save" :disabled="teams.length === 0" @click="saveTeams" />
|
||||
</div>
|
||||
<!-- 미배정 인원 표시: 팀 리스트 위에 항상 노출 -->
|
||||
<div class="mt-3 mb-4">
|
||||
<strong>미배정 인원:</strong>
|
||||
<span v-if="unassignedMembers.length === 0">없음</span>
|
||||
<span v-else>
|
||||
<span v-for="member in unassignedMembers" :key="member.id" class="badge bg-gray-200 text-gray-800 mr-2">{{ member.name }}</span>
|
||||
</span>
|
||||
</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" style="width:80px;" />
|
||||
</div>
|
||||
<!-- 팀 멤버 드래그&드롭 정렬: ul로 감싸고, 각 아이템은 li로 시멘틱하게 구성 -->
|
||||
<ul class="pl-3">
|
||||
<draggable
|
||||
v-model="team.members"
|
||||
:group="'team-members-' + idx"
|
||||
item-key="id"
|
||||
handle=".drag-handle"
|
||||
@end="updateMemberOrder(idx)"
|
||||
>
|
||||
<template #item="{ element: member, index: mIdx }">
|
||||
<li class="flex align-items-center gap-2 mb-1 draggable-item">
|
||||
<span class="drag-handle" style="cursor:grab; color:#888"><i class="pi pi-bars"></i></span>
|
||||
<span class="badge bg-primary text-white mr-1">{{ mIdx + 1 }}</span>
|
||||
<span>{{ member.name }}</span>
|
||||
<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="removeMemberFromTeam(idx, mIdx)" v-if="team.members.length > 1" />
|
||||
</li>
|
||||
</template>
|
||||
</draggable>
|
||||
</ul>
|
||||
<div class="flex align-items-center gap-2 mt-2">
|
||||
<Dropdown
|
||||
v-model="team._selectedMember"
|
||||
:options="availableMembersForTeam(idx)"
|
||||
optionLabel="name"
|
||||
placeholder="팀원 선택"
|
||||
style="width: 180px;"
|
||||
>
|
||||
<template #option="slotProps">
|
||||
<span>{{ slotProps.option.name }}</span>
|
||||
<span v-if="getMemberAverage(slotProps.option) !== null" class="text-xs text-gray-700 ml-1">
|
||||
({{ getMemberAverage(slotProps.option) }})
|
||||
</span>
|
||||
</template>
|
||||
<template #value="slotProps">
|
||||
<span v-if="slotProps.value">
|
||||
{{ slotProps.value.name }}
|
||||
<span v-if="getMemberAverage(slotProps.value) !== null" class="text-xs text-gray-700 ml-1">
|
||||
({{ getMemberAverage(slotProps.value) }})
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
</Dropdown>
|
||||
<Button icon="pi pi-plus" class="p-button-text p-0" :disabled="!team._selectedMember" @click="addMemberToTeam(idx)" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<strong>미배정 인원:</strong>
|
||||
<span v-if="unassignedMembers.length === 0">없음</span>
|
||||
<span v-else>
|
||||
<span v-for="member in unassignedMembers" :key="member.id" class="badge bg-gray-200 text-gray-800 mr-2">{{ member.name }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="text-gray-500 mt-3">팀을 생성하려면 옵션을 선택 후 '팀 생성'을 눌러주세요.</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import draggable from 'vuedraggable'; // npm install vuedraggable@next 필요 (vue3)
|
||||
|
||||
import { ref, computed, watch, unref } from 'vue';
|
||||
import InputNumber from 'primevue/inputnumber';
|
||||
import Button from 'primevue/button';
|
||||
import Dropdown from 'primevue/dropdown';
|
||||
|
||||
// 참가자 에버리지 구하기
|
||||
function getMemberAverage(member) {
|
||||
if (!member.id) return null;
|
||||
// 원본 참가자 객체에서 id로 찾아서 평균값 추출
|
||||
const arr = unref(props.participants);
|
||||
const p = arr.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;
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
participants: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
teams: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['teams-generated', 'save-teams', 'update:teams']);
|
||||
|
||||
const teamOption = ref('byMemberCount');
|
||||
const teamOptionValue = ref(2);
|
||||
const teams = ref(Array.isArray(props.teams) ? JSON.parse(JSON.stringify(props.teams)) : []);
|
||||
|
||||
// props.teams가 바뀌면 내부 상태 동기화
|
||||
watch(() => props.teams, (newVal) => {
|
||||
teams.value = Array.isArray(newVal) ? JSON.parse(JSON.stringify(newVal)) : [];
|
||||
}, { immediate: true, deep: true });
|
||||
|
||||
// teams가 내부에서 바뀌면 부모로 emit
|
||||
watch(teams, (val) => {
|
||||
emit('update:teams', val);
|
||||
}, { deep: true });
|
||||
|
||||
// 팀별로 아직 배정되지 않은 참가자만 선택지로 제공
|
||||
function availableMembersForTeam(teamIdx) {
|
||||
const arr = unref(props.participants);
|
||||
// 이미 다른 팀에 배정된 인원은 제외
|
||||
const assignedIds = new Set();
|
||||
teams.value.forEach((team, idx) => {
|
||||
if (idx === teamIdx) return;
|
||||
team.members.forEach(m => assignedIds.add(m.id));
|
||||
});
|
||||
// 현재 팀 내 중복도 막기 위해 현재 팀 내 id도 추가
|
||||
teams.value[teamIdx]?.members.forEach((m, i, arr) => {
|
||||
if (arr.findIndex(x => x.id === m.id) !== i) assignedIds.add(m.id);
|
||||
});
|
||||
return arr.filter(p => !assignedIds.has(p.id) && !teams.value[teamIdx].members.some(m => m.id === p.id)).map(p => ({ id: p.id, name: p.Member?.name || p.name || `참가자 ${p.id}` }));
|
||||
}
|
||||
|
||||
function addMemberToTeam(teamIdx) {
|
||||
const team = teams.value[teamIdx];
|
||||
const member = team._selectedMember;
|
||||
if (!member) return;
|
||||
// 구조 통일: 항상 {id, name}만 저장
|
||||
if (!team.members.some(m => m.id === member.id)) {
|
||||
team.members.push({ id: member.id, name: member.name });
|
||||
}
|
||||
team._selectedMember = null;
|
||||
}
|
||||
|
||||
function removeMemberFromTeam(teamIdx, memberIdx) {
|
||||
teams.value[teamIdx].members.splice(memberIdx, 1);
|
||||
}
|
||||
|
||||
// 미배정 인원 계산
|
||||
const unassignedMembers = computed(() => {
|
||||
const arr = unref(props.participants);
|
||||
const assignedIds = new Set();
|
||||
teams.value.forEach(team => {
|
||||
team.members.forEach(m => assignedIds.add(String(m.id)));
|
||||
});
|
||||
// name 보정: 항상 name 필드가 있도록!
|
||||
return arr
|
||||
.filter(p => !assignedIds.has(String(p.id)))
|
||||
.map(p => ({
|
||||
id: p.id,
|
||||
name: p.Member?.name || p.name || `참가자 ${p.id}`
|
||||
}));
|
||||
});
|
||||
|
||||
// 팀 생성/편집 다이얼로그 상태
|
||||
const editingTeam = ref({
|
||||
teamNumber: null,
|
||||
handicap: 0,
|
||||
members: []
|
||||
});
|
||||
const teamDialog = ref(false);
|
||||
const submitted = ref(false);
|
||||
const loading = ref(false);
|
||||
|
||||
// 신규 팀 생성 다이얼로그 열기
|
||||
function openNewTeamDialog() {
|
||||
editingTeam.value = {
|
||||
teamNumber: null,
|
||||
handicap: 0,
|
||||
members: []
|
||||
};
|
||||
submitted.value = false;
|
||||
teamDialog.value = true;
|
||||
}
|
||||
|
||||
// 기존 팀 편집 다이얼로그 열기
|
||||
function editTeam(team) {
|
||||
editingTeam.value = {
|
||||
teamNumber: team.teamNumber,
|
||||
handicap: team.handicap,
|
||||
members: [...team.members]
|
||||
};
|
||||
submitted.value = false;
|
||||
teamDialog.value = true;
|
||||
}
|
||||
|
||||
// 팀 저장(생성/수정)
|
||||
async function saveTeam() {
|
||||
submitted.value = true;
|
||||
if (!editingTeam.value.teamNumber || editingTeam.value.members.length === 0) return;
|
||||
loading.value = true;
|
||||
try {
|
||||
// 신규 생성 또는 기존 수정 구분(여기서는 teamNumber 기준)
|
||||
const idx = teams.value.findIndex(t => t.teamNumber === editingTeam.value.teamNumber);
|
||||
if (idx >= 0) {
|
||||
teams.value[idx] = { ...editingTeam.value };
|
||||
} else {
|
||||
teams.value.push({ ...editingTeam.value });
|
||||
}
|
||||
teamDialog.value = false;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 여러 팀 저장(상위로 emit)
|
||||
function saveTeams() {
|
||||
// 저장 시 members: [{id, order}], teamNumber: 1부터 순서대로 부여
|
||||
const payload = teams.value.map((team, idx) => ({
|
||||
...team,
|
||||
teamNumber: idx + 1,
|
||||
members: team.members.map((m, mIdx) => ({ id: m.id, order: mIdx + 1 }))
|
||||
}));
|
||||
emit('save-teams', payload);
|
||||
}
|
||||
|
||||
// 드래그 후 멤버 순서(order) 갱신
|
||||
function updateMemberOrder(teamIdx) {
|
||||
teams.value[teamIdx].members.forEach((m, i) => {
|
||||
m.order = i + 1;
|
||||
});
|
||||
}
|
||||
|
||||
const canGenerateTeams = computed(() => {
|
||||
const arr = unref(props.participants);
|
||||
const n = arr.length;
|
||||
if (n < 2) return false;
|
||||
return teamOptionValue.value >= 2 && teamOptionValue.value <= n;
|
||||
});
|
||||
|
||||
function shuffle(array) {
|
||||
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 arr = unref(props.participants);
|
||||
const participants = arr.map(p => ({
|
||||
id: p.id,
|
||||
name: p.Member?.name || p.name || `참가자 ${p.id}`
|
||||
}));
|
||||
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: [] });
|
||||
}
|
||||
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: [] });
|
||||
}
|
||||
shuffled.forEach((member, idx) => {
|
||||
result[idx % teamCount].members.push(member);
|
||||
});
|
||||
}
|
||||
teams.value = result;
|
||||
emit('teams-generated', result);
|
||||
}
|
||||
|
||||
watch(() => props.participants, () => {
|
||||
teams.value = [];
|
||||
}, { deep: true });
|
||||
</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);
|
||||
}
|
||||
.draggable-item {
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.draggable-item.dragging {
|
||||
background: #e3f2fd;
|
||||
}
|
||||
.drag-handle {
|
||||
cursor: grab;
|
||||
font-size: 1.1em;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
.badge {
|
||||
display: inline-block;
|
||||
min-width: 1.8em;
|
||||
padding: 0.2em 0.6em;
|
||||
font-size: 90%;
|
||||
font-weight: 600;
|
||||
border-radius: 0.7em;
|
||||
background: #1976d2;
|
||||
color: #fff;
|
||||
margin-right: 0.25em;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,30 @@
|
||||
import apiClient from './api';
|
||||
|
||||
const teamService = {
|
||||
/**
|
||||
* 이벤트별 팀 목록 조회
|
||||
* @param {number|string} eventId
|
||||
* @returns {Promise<Array>} 팀 배열
|
||||
*/
|
||||
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;
|
||||
@@ -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,
|
||||
|
||||
@@ -105,6 +105,7 @@
|
||||
<Button label="예" icon="pi pi-check" class="p-button-danger" @click="deleteEvent" />
|
||||
</template>
|
||||
</Dialog>
|
||||
<Toast />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user