init
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
const express = require('express');
|
||||
const bodyParser = require('body-parser');
|
||||
const cors = require('cors');
|
||||
const http = require('http');
|
||||
const socketIo = require('socket.io');
|
||||
|
||||
const adminRoutes = require('./routes/adminRoutes');
|
||||
const authRoutes = require('./routes/authRoutes');
|
||||
const eventRoutes = require('./routes/eventRoutes');
|
||||
const clubRoutes = require('./routes/clubRoutes');
|
||||
const userRoutes = require('./routes/userRoutes');
|
||||
const notificationRoutes = require('./routes/notificationRoutes');
|
||||
const menuRoutes = require('./routes/menuRoutes');
|
||||
const subscriptionRoutes = require('./routes/subscriptionRoutes');
|
||||
const featureRoutes = require('./routes/featureRoutes');
|
||||
const { sequelize, syncModels } = require('./models');
|
||||
|
||||
const app = express();
|
||||
const server = http.createServer(app);
|
||||
const io = socketIo(server, {
|
||||
cors: {
|
||||
origin: '*',
|
||||
methods: ['GET', 'POST']
|
||||
}
|
||||
});
|
||||
|
||||
app.use(cors());
|
||||
app.use(bodyParser.json());
|
||||
|
||||
// Socket.IO 연결 처리
|
||||
io.on('connection', (socket) => {
|
||||
console.log('새로운 클라이언트 연결:', socket.id);
|
||||
|
||||
// 사용자 인증 및 알림 룸 참여
|
||||
socket.on('authenticate', (userData) => {
|
||||
if (userData && userData.id) {
|
||||
socket.join(`user:${userData.id}`);
|
||||
console.log(`사용자 ${userData.id}가 알림 룸에 참여했습니다.`);
|
||||
}
|
||||
});
|
||||
|
||||
// 연결 해제 처리
|
||||
socket.on('disconnect', () => {
|
||||
console.log('클라이언트 연결 해제:', socket.id);
|
||||
});
|
||||
});
|
||||
|
||||
// 전역 Socket.IO 인스턴스 설정
|
||||
app.set('io', io);
|
||||
|
||||
// 데이터베이스 연결 및 모델 동기화 실행
|
||||
sequelize.authenticate()
|
||||
.then(() => {
|
||||
console.log('데이터베이스에 연결되었습니다.');
|
||||
return syncModels();
|
||||
})
|
||||
.then(() => {
|
||||
console.log('데이터베이스 모델이 동기화되었습니다.');
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('데이터베이스 연결 또는 모델 동기화 오류:', err);
|
||||
});
|
||||
|
||||
// Routes 설정
|
||||
app.use('/api/auth', authRoutes);
|
||||
app.use('/api/admin', adminRoutes);
|
||||
app.use('/api/club', clubRoutes);
|
||||
app.use('/api/club/events', eventRoutes);
|
||||
app.use('/api/features', featureRoutes);
|
||||
app.use('/api/subscriptions', subscriptionRoutes);
|
||||
app.use('/api/users', userRoutes);
|
||||
app.use('/api/notifications', notificationRoutes);
|
||||
app.use('/api/menus', menuRoutes);
|
||||
|
||||
// 루트 경로 핸들러
|
||||
app.get('/', (req, res) => {
|
||||
res.json({ message: '볼링 클럽 매니저 API 서버에 오신 것을 환영합니다!' });
|
||||
});
|
||||
|
||||
// 서버 시작
|
||||
const PORT = process.env.PORT || 3030;
|
||||
server.listen(PORT, () => {
|
||||
console.log(`서버가 포트 ${PORT}에서 실행 중입니다.`);
|
||||
});
|
||||
|
||||
module.exports = app;
|
||||
@@ -0,0 +1,40 @@
|
||||
const { Sequelize } = require('sequelize');
|
||||
require('dotenv').config();
|
||||
|
||||
const sequelize = new Sequelize(
|
||||
process.env.DB_NAME,
|
||||
process.env.DB_USER,
|
||||
process.env.DB_PASSWORD,
|
||||
{
|
||||
host: process.env.DB_HOST,
|
||||
port: process.env.DB_PORT,
|
||||
dialect: 'mysql',
|
||||
logging: true, // 개발 중에는 true로 설정하여 SQL 쿼리 로그 확인 가능
|
||||
pool: {
|
||||
max: 5,
|
||||
min: 0,
|
||||
acquire: 30000,
|
||||
idle: 10000
|
||||
},
|
||||
define: {
|
||||
charset: 'utf8mb4',
|
||||
collate: 'utf8mb4_unicode_ci',
|
||||
timestamps: true
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 데이터베이스 연결 테스트
|
||||
const testConnection = async () => {
|
||||
try {
|
||||
await sequelize.authenticate();
|
||||
console.log('데이터베이스 연결 성공!');
|
||||
} catch (error) {
|
||||
console.error('데이터베이스 연결 실패:', error);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
sequelize,
|
||||
testConnection
|
||||
};
|
||||
@@ -0,0 +1,221 @@
|
||||
/**
|
||||
* 인증 컨트롤러
|
||||
* 사용자 인증 관련 기능을 처리하는 컨트롤러
|
||||
*/
|
||||
const jwt = require('jsonwebtoken');
|
||||
const { User, Member } = require('../models');
|
||||
const { Op } = require('sequelize');
|
||||
require('dotenv').config();
|
||||
|
||||
// 로그인
|
||||
exports.login = async (req, res) => {
|
||||
try {
|
||||
const { email, password } = req.body;
|
||||
|
||||
// 이메일 필수 확인
|
||||
if (!email) {
|
||||
return res.status(400).json({ message: '이메일은 필수 항목입니다.' });
|
||||
}
|
||||
|
||||
// 사용자 조회
|
||||
const user = await User.findOne({ where: { email } });
|
||||
|
||||
// 사용자가 존재하지 않는 경우
|
||||
if (!user) {
|
||||
return res.status(401).json({ message: '이메일 또는 비밀번호가 올바르지 않습니다.' });
|
||||
}
|
||||
|
||||
// 비밀번호 검증
|
||||
const isPasswordValid = await user.validatePassword(password);
|
||||
|
||||
if (!isPasswordValid) {
|
||||
return res.status(401).json({ message: '이메일 또는 비밀번호가 올바르지 않습니다.' });
|
||||
}
|
||||
|
||||
// 마지막 로그인 시간 업데이트
|
||||
await user.update({ lastLogin: new Date() });
|
||||
|
||||
// JWT 토큰 생성
|
||||
const token = jwt.sign(
|
||||
{
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
name: user.name
|
||||
},
|
||||
process.env.JWT_SECRET || 'your-secret-key',
|
||||
{ expiresIn: process.env.JWT_EXPIRES_IN || '1d' }
|
||||
);
|
||||
|
||||
// 모임장/운영진으로 참여 중인 클럽 ID 조회
|
||||
const clubMembers = await Member.findAll({
|
||||
where: {
|
||||
userId: user.id,
|
||||
memberType: {
|
||||
[Op.in]: ['모임장', '운영진']
|
||||
}
|
||||
},
|
||||
attributes: ['clubId']
|
||||
});
|
||||
// console.log(`clubMembers: ${JSON.stringify(clubMembers)}`);
|
||||
|
||||
// 모임장/운영진 클럽 ID 배열 생성
|
||||
const clubManagerIds = clubMembers.map(cm => cm.clubId);
|
||||
let clubId = null;
|
||||
if(clubManagerIds.length > 0) {
|
||||
clubId = clubManagerIds[0];
|
||||
}
|
||||
|
||||
// 사용자 정보에서 비밀번호 제외
|
||||
const userData = {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
status: user.status,
|
||||
clubId: clubId
|
||||
};
|
||||
// console.log(`userData: ${JSON.stringify(userData)}`);
|
||||
|
||||
res.json({
|
||||
token,
|
||||
user: userData
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('로그인 오류:', error);
|
||||
res.status(500).json({ message: '로그인 중 오류가 발생했습니다.' });
|
||||
}
|
||||
};
|
||||
|
||||
// 회원가입
|
||||
exports.register = async (req, res) => {
|
||||
try {
|
||||
const { name, email, password, role, status } = req.body;
|
||||
|
||||
// 필수 필드 검증
|
||||
if (!name || !email || !password) {
|
||||
return res.status(400).json({ message: '이름, 이메일, 비밀번호는 필수 항목입니다.' });
|
||||
}
|
||||
|
||||
// 이메일 중복 확인
|
||||
const existingUser = await User.findOne({ where: { email } });
|
||||
|
||||
if (existingUser) {
|
||||
return res.status(400).json({ message: '이미 등록된 이메일입니다.' });
|
||||
}
|
||||
|
||||
// 사용자 생성
|
||||
const newUser = await User.create({
|
||||
name,
|
||||
email,
|
||||
password,
|
||||
username: email,
|
||||
role: role || 'user',
|
||||
status: status || 'active'
|
||||
});
|
||||
|
||||
// 민감한 정보 제외
|
||||
const userResponse = {
|
||||
id: newUser.id,
|
||||
name: newUser.name,
|
||||
email: newUser.email,
|
||||
role: newUser.role,
|
||||
status: newUser.status,
|
||||
createdAt: newUser.createdAt
|
||||
};
|
||||
|
||||
res.status(201).json(userResponse);
|
||||
} catch (error) {
|
||||
console.error('회원가입 오류:', error);
|
||||
res.status(500).json({ message: '회원가입 중 오류가 발생했습니다.' });
|
||||
}
|
||||
};
|
||||
|
||||
// 현재 사용자 정보 조회
|
||||
exports.getCurrentUser = async (req, res) => {
|
||||
try {
|
||||
const userId = req.user.id;
|
||||
|
||||
const user = await User.findByPk(userId, {
|
||||
attributes: { exclude: ['password'] }
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return res.status(404).json({ message: '사용자를 찾을 수 없습니다.' });
|
||||
}
|
||||
|
||||
res.json(user);
|
||||
} catch (error) {
|
||||
console.error('사용자 정보 조회 오류:', error);
|
||||
res.status(500).json({ message: '사용자 정보를 조회하는 중 오류가 발생했습니다.' });
|
||||
}
|
||||
};
|
||||
|
||||
// 프로필 업데이트
|
||||
exports.updateProfile = async (req, res) => {
|
||||
try {
|
||||
const userId = req.user.id;
|
||||
const { name, email, password, currentPassword } = req.body;
|
||||
|
||||
// 사용자 조회
|
||||
const user = await User.findByPk(userId);
|
||||
|
||||
if (!user) {
|
||||
return res.status(404).json({ message: '사용자를 찾을 수 없습니다.' });
|
||||
}
|
||||
|
||||
// 비밀번호 변경이 요청된 경우
|
||||
if (password && currentPassword) {
|
||||
// 현재 비밀번호 검증
|
||||
const isPasswordValid = await user.validatePassword(currentPassword);
|
||||
if (!isPasswordValid) {
|
||||
return res.status(401).json({ message: '현재 비밀번호가 올바르지 않습니다.' });
|
||||
}
|
||||
}
|
||||
|
||||
// 이메일이 변경되는 경우
|
||||
if (email && email !== user.email) {
|
||||
// Users 테이블에서 이메일 중복 확인
|
||||
const existingUser = await User.findOne({ where: { email } });
|
||||
if (existingUser) {
|
||||
return res.status(400).json({ message: '이미 사용 중인 이메일입니다.' });
|
||||
}
|
||||
|
||||
// Members 테이블에서 이메일 중복 확인
|
||||
const existingMember = await Member.findOne({ where: { email } });
|
||||
if (existingMember) {
|
||||
return res.status(400).json({ message: '이미 클럽 회원이 사용 중인 이메일입니다.' });
|
||||
}
|
||||
|
||||
// 해당 사용자의 Members 테이블 레코드도 함께 업데이트
|
||||
await Member.update(
|
||||
{ email },
|
||||
{ where: { userId } }
|
||||
);
|
||||
}
|
||||
|
||||
// 프로필 업데이트
|
||||
const updateData = {
|
||||
name: name || user.name,
|
||||
email: email || user.email,
|
||||
username: email || user.email // username도 이메일과 동일하게 업데이트
|
||||
};
|
||||
|
||||
// 비밀번호가 제공된 경우에만 업데이트
|
||||
if (password && currentPassword) {
|
||||
updateData.password = password;
|
||||
}
|
||||
|
||||
await user.update(updateData);
|
||||
|
||||
// 업데이트된 사용자 정보 반환 (비밀번호 제외)
|
||||
const updatedUser = await User.findByPk(userId, {
|
||||
attributes: { exclude: ['password'] }
|
||||
});
|
||||
|
||||
res.json(updatedUser);
|
||||
} catch (error) {
|
||||
console.error('프로필 업데이트 오류:', error);
|
||||
res.status(500).json({ message: '프로필 업데이트 중 오류가 발생했습니다.' });
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,925 @@
|
||||
/**
|
||||
* 클럽 컨트롤러
|
||||
* 클럽 관련 기능을 처리하는 컨트롤러
|
||||
*/
|
||||
const Club = require('../models/Club');
|
||||
const User = require('../models/User');
|
||||
const Member = require('../models/Member');
|
||||
const ClubFeature = require('../models/ClubFeature');
|
||||
const Event = require('../models/Event');
|
||||
const EventParticipant = require('../models/EventParticipant');
|
||||
const EventScore = require('../models/EventScore');
|
||||
const { Op } = require('sequelize');
|
||||
|
||||
// 관리자 권한 체크 함수
|
||||
const isAdmin = (user) => {
|
||||
return user && (user.role === 'admin' || user.role === 'superadmin');
|
||||
};
|
||||
|
||||
// 모든 클럽 조회
|
||||
exports.getAllClubs = async (req, res) => {
|
||||
try {
|
||||
const clubs = await Club.findAll({
|
||||
where: { status: 'active' },
|
||||
include: [
|
||||
{
|
||||
model: Member,
|
||||
where: { memberType: '모임장' },
|
||||
required: false,
|
||||
attributes: ['id', 'name', 'email', 'phone', 'memberType']
|
||||
}
|
||||
],
|
||||
order: [['createdAt', 'DESC']]
|
||||
});
|
||||
|
||||
const formattedClubs = clubs.map(club => {
|
||||
const clubData = club.toJSON();
|
||||
// 모임장 정보 추가
|
||||
const ownerMember = clubData.Members && clubData.Members.length > 0 ? clubData.Members[0] : null;
|
||||
|
||||
return {
|
||||
...clubData,
|
||||
ownerName: ownerMember ? ownerMember.name : '알 수 없음',
|
||||
ownerEmail: ownerMember ? ownerMember.email : '',
|
||||
ownerPhone: ownerMember ? ownerMember.phone : '',
|
||||
ownerMemberType: ownerMember ? ownerMember.memberType : ''
|
||||
};
|
||||
});
|
||||
|
||||
res.json(formattedClubs);
|
||||
} catch (error) {
|
||||
console.error('클럽 목록 조회 오류:', error);
|
||||
res.status(500).json({ message: '클럽 목록을 가져오는 중 오류가 발생했습니다.' });
|
||||
}
|
||||
};
|
||||
|
||||
// 특정 클럽 조회
|
||||
exports.getClubById = async (req, res) => {
|
||||
try {
|
||||
const { clubId } = req.body;
|
||||
console.log(`clubId: ${clubId}`);
|
||||
if (!clubId) {
|
||||
return res.status(400).json({ message: "유효하지 않은 클럽 ID입니다." });
|
||||
}
|
||||
|
||||
const club = await Club.findByPk(clubId);
|
||||
// console.log(`club: ${club}`);
|
||||
if (club) {
|
||||
// 클럽 소유자 정보 조회
|
||||
const owner = await Member.findOne({where: {userId: club.ownerId}}, {
|
||||
attributes: ['name', 'email', 'memberType']
|
||||
});
|
||||
|
||||
const clubData = club.toJSON();
|
||||
const formattedClub = {
|
||||
...clubData,
|
||||
ownerEmail: owner ? owner.email : '',
|
||||
ownerName: owner ? owner.name : '',
|
||||
ownerMemberType: owner ? owner.memberType : ''
|
||||
};
|
||||
|
||||
res.json(formattedClub);
|
||||
} else {
|
||||
res.status(404).json({ message: '클럽을 찾을 수 없습니다.' });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('클럽 조회 오류:', error);
|
||||
res.status(500).json({ message: '클럽 정보를 가져오는 중 오류가 발생했습니다.' });
|
||||
}
|
||||
};
|
||||
|
||||
// 새 클럽 생성
|
||||
exports.createClub = async (req, res) => {
|
||||
try {
|
||||
const { name, location, description, ownerEmail, contact, features, sendInvite } = req.body;
|
||||
|
||||
// 필수 필드 검증
|
||||
if (!name || !ownerEmail) {
|
||||
return res.status(400).json({ message: '클럽 이름과 모임장 이메일은 필수 항목입니다.' });
|
||||
}
|
||||
|
||||
// 이메일로 사용자 조회
|
||||
const owner = await User.findOne({ where: { email: ownerEmail } });
|
||||
|
||||
if (!owner) {
|
||||
return res.status(404).json({ message: '해당 이메일로 등록된 사용자를 찾을 수 없습니다.' });
|
||||
}
|
||||
|
||||
// 클럽 생성
|
||||
const newClub = await Club.create({
|
||||
name,
|
||||
location,
|
||||
description,
|
||||
contact,
|
||||
ownerId: owner.id,
|
||||
memberCount: 1, // 모임장 포함
|
||||
status: 'active'
|
||||
});
|
||||
|
||||
// 클럽 모임장을 클럽 멤버로 추가
|
||||
await Member.create({
|
||||
name: owner.name,
|
||||
gender: '남성',
|
||||
userId: owner.id,
|
||||
clubId: newClub.id,
|
||||
memberType: '모임장',
|
||||
joinDate: new Date(),
|
||||
status: 'active'
|
||||
});
|
||||
|
||||
// 선택된 기능 추가
|
||||
if (features && features.length > 0) {
|
||||
const featurePromises = features.map(featureId =>
|
||||
ClubFeature.create({
|
||||
clubId: newClub.id,
|
||||
featureId,
|
||||
enabled: true
|
||||
})
|
||||
);
|
||||
|
||||
await Promise.all(featurePromises);
|
||||
}
|
||||
|
||||
// 초대 이메일 발송 로직 (sendInvite가 true인 경우)
|
||||
if (sendInvite) {
|
||||
// TODO: 초대 이메일 발송 로직 구현
|
||||
console.log(`${ownerEmail}에게 초대 이메일 발송 예정`);
|
||||
}
|
||||
|
||||
res.status(201).json(newClub);
|
||||
} catch (error) {
|
||||
console.error('클럽 생성 오류:', error);
|
||||
res.status(500).json({ message: '클럽을 생성하는 중 오류가 발생했습니다.' });
|
||||
}
|
||||
};
|
||||
|
||||
// 클럽 정보 업데이트
|
||||
exports.updateClub = async (req, res) => {
|
||||
try {
|
||||
const { clubId, name, location, description, contact, ownerEmail, status } = req.body;
|
||||
|
||||
const club = await Club.findByPk(clubId);
|
||||
|
||||
if (!club) {
|
||||
return res.status(404).json({ message: '클럽을 찾을 수 없습니다.' });
|
||||
}
|
||||
|
||||
// 이메일이 변경된 경우 새 모임장 확인
|
||||
let newOwnerId = club.ownerId;
|
||||
let ownerChanged = false;
|
||||
|
||||
if (ownerEmail && ownerEmail !== club.ownerEmail) {
|
||||
const newOwner = await User.findOne({ where: { email: ownerEmail } });
|
||||
if (!newOwner) {
|
||||
return res.status(404).json({ message: '해당 이메일로 등록된 사용자를 찾을 수 없습니다.' });
|
||||
}
|
||||
newOwnerId = newOwner.id;
|
||||
ownerChanged = true;
|
||||
}
|
||||
|
||||
// 클럽 정보 업데이트
|
||||
await club.update({
|
||||
name: name || club.name,
|
||||
location: location || club.location,
|
||||
description: description !== undefined ? description : club.description,
|
||||
contact: contact !== undefined ? contact : club.contact,
|
||||
status: status || club.status,
|
||||
ownerId: newOwnerId
|
||||
});
|
||||
|
||||
// 모임장이 변경된 경우 Members 테이블 업데이트
|
||||
if (ownerChanged) {
|
||||
// 기존 모임장을 정회원으로 변경
|
||||
await Member.update(
|
||||
{ memberType: '정회원' },
|
||||
{
|
||||
where: {
|
||||
clubId: clubId,
|
||||
userId: club.ownerId,
|
||||
memberType: '모임장'
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 새 모임장 확인 및 업데이트
|
||||
const newOwnerMember = await Member.findOne({
|
||||
where: {
|
||||
clubId: clubId,
|
||||
userId: newOwnerId
|
||||
}
|
||||
});
|
||||
|
||||
if (newOwnerMember) {
|
||||
// 기존 회원인 경우 모임장으로 변경
|
||||
await newOwnerMember.update({ memberType: '모임장' });
|
||||
} else {
|
||||
// 새로운 사용자 정보 가져오기
|
||||
const newOwnerUser = await User.findByPk(newOwnerId);
|
||||
if (!newOwnerUser) {
|
||||
return res.status(404).json({ message: '새 모임장 사용자 정보를 찾을 수 없습니다.' });
|
||||
}
|
||||
|
||||
// 새 회원으로 모임장 추가
|
||||
await Member.create({
|
||||
userId: newOwnerId,
|
||||
clubId: clubId,
|
||||
name: newOwnerUser.name || ownerEmail.split('@')[0], // 이름이 없는 경우 이메일 아이디 사용
|
||||
gender: '남성', // 성별 정보가 없는 경우 기본값
|
||||
memberType: '모임장',
|
||||
joinDate: new Date(),
|
||||
status: 'active'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
res.json(club);
|
||||
} catch (error) {
|
||||
console.error('클럽 정보 업데이트 오류:', error);
|
||||
res.status(500).json({ message: '클럽 정보를 업데이트하는 중 오류가 발생했습니다.' });
|
||||
}
|
||||
};
|
||||
|
||||
// 클럽 삭제
|
||||
exports.deleteClub = async (req, res) => {
|
||||
try {
|
||||
const { clubId } = req.body;
|
||||
|
||||
// 클럽 존재 여부 확인
|
||||
const club = await Club.findByPk(clubId);
|
||||
|
||||
if (!club) {
|
||||
return res.status(404).json({ message: '클럽을 찾을 수 없습니다.' });
|
||||
}
|
||||
|
||||
// 소프트 삭제 (상태만 변경)
|
||||
await club.update({ status: 'inactive' });
|
||||
|
||||
res.status(204).send();
|
||||
} catch (error) {
|
||||
console.error('클럽 삭제 오류:', error);
|
||||
res.status(500).json({ message: '클럽을 삭제하는 중 오류가 발생했습니다.' });
|
||||
}
|
||||
};
|
||||
|
||||
// 클럽 기능 관리
|
||||
exports.manageClubFeatures = async (req, res) => {
|
||||
try {
|
||||
const { clubId, features } = req.body;
|
||||
|
||||
// 클럽 존재 여부 확인
|
||||
const club = await Club.findByPk(clubId);
|
||||
|
||||
if (!club) {
|
||||
return res.status(404).json({ message: '클럽을 찾을 수 없습니다.' });
|
||||
}
|
||||
|
||||
// 클럽 기능 업데이트
|
||||
await club.update({ features });
|
||||
|
||||
res.json({
|
||||
id: club.id,
|
||||
features: club.features,
|
||||
updatedAt: club.updatedAt
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('클럽 기능 관리 오류:', error);
|
||||
res.status(500).json({ message: '클럽 기능을 업데이트하는 중 오류가 발생했습니다.' });
|
||||
}
|
||||
};
|
||||
|
||||
// 클럽 회원 목록 조회
|
||||
exports.getClubMembers = async (req, res) => {
|
||||
try {
|
||||
const { clubId } = req.body;
|
||||
|
||||
if (!clubId) {
|
||||
return res.status(400).json({ message: '클럽 ID가 필요합니다.' });
|
||||
}
|
||||
|
||||
// 클럽 존재 여부 확인
|
||||
const club = await Club.findByPk(clubId);
|
||||
if (!club) {
|
||||
return res.status(404).json({ message: '클럽을 찾을 수 없습니다.' });
|
||||
}
|
||||
|
||||
const members = await Member.findAll({
|
||||
where: {
|
||||
clubId,
|
||||
status: {
|
||||
[Op.ne]: 'deleted'
|
||||
}
|
||||
},
|
||||
order: [
|
||||
['memberType', 'ASC'],
|
||||
['name', 'ASC']
|
||||
]
|
||||
});
|
||||
|
||||
// 응답 데이터 형식화
|
||||
const formattedMembers = members.map(member => {
|
||||
const memberData = member.toJSON();
|
||||
return {
|
||||
id: memberData.id,
|
||||
name: memberData.name,
|
||||
gender: memberData.gender,
|
||||
memberType: memberData.memberType,
|
||||
handicap: memberData.handicap,
|
||||
average: memberData.average,
|
||||
games: memberData.games,
|
||||
phone: memberData.phone,
|
||||
email: memberData.user ? memberData.user.email : memberData.email,
|
||||
joinDate: memberData.joinDate,
|
||||
status: memberData.status
|
||||
};
|
||||
});
|
||||
|
||||
res.json(formattedMembers);
|
||||
} catch (error) {
|
||||
console.error('클럽 회원 목록 조회 오류:', error);
|
||||
res.status(500).json({ message: '클럽 회원 목록을 가져오는 중 오류가 발생했습니다.' });
|
||||
}
|
||||
};
|
||||
|
||||
// 클럽 회원 추가
|
||||
exports.addClubMember = async (req, res) => {
|
||||
try {
|
||||
const { clubId, userId, name, gender, memberType, email, phone } = req.body;
|
||||
|
||||
if (!clubId || !name || !gender) {
|
||||
return res.status(400).json({ message: '필수 정보가 누락되었습니다.' });
|
||||
}
|
||||
|
||||
const club = await Club.findByPk(clubId);
|
||||
if (!club) {
|
||||
return res.status(404).json({ message: '클럽을 찾을 수 없습니다.' });
|
||||
}
|
||||
|
||||
// 회원 생성
|
||||
const newMember = await Member.create({
|
||||
clubId,
|
||||
userId,
|
||||
name,
|
||||
gender,
|
||||
memberType: memberType || '준회원',
|
||||
email,
|
||||
phone
|
||||
});
|
||||
|
||||
// 클럽의 회원 수 업데이트
|
||||
const memberCount = await Member.count({ where: { clubId: clubId, status: 'active' } });
|
||||
await club.update({ memberCount });
|
||||
|
||||
res.status(201).json(newMember);
|
||||
} catch (error) {
|
||||
console.error('클럽 회원 추가 오류:', error);
|
||||
res.status(500).json({ message: '회원을 추가하는 중 오류가 발생했습니다.' });
|
||||
}
|
||||
};
|
||||
|
||||
// 클럽 회원 정보 수정
|
||||
exports.updateClubMember = async (req, res) => {
|
||||
try {
|
||||
const { clubId, memberId, name, gender, memberType, email, phone, status } = req.body;
|
||||
|
||||
if (!clubId || !memberId) {
|
||||
return res.status(400).json({ message: '잘못된 클럽 또는 회원 ID입니다.' });
|
||||
}
|
||||
|
||||
const member = await Member.findOne({
|
||||
where: {
|
||||
id: memberId,
|
||||
clubId: clubId
|
||||
}
|
||||
});
|
||||
|
||||
if (!member) {
|
||||
return res.status(404).json({ message: '회원을 찾을 수 없습니다.' });
|
||||
}
|
||||
|
||||
// 모임장은 한 명만 존재할 수 있음
|
||||
if (memberType === '모임장') {
|
||||
const existingOwner = await Member.findOne({
|
||||
where: {
|
||||
clubId: clubId,
|
||||
memberType: '모임장',
|
||||
status: 'active',
|
||||
id: { [Op.ne]: memberId }
|
||||
}
|
||||
});
|
||||
|
||||
if (existingOwner) {
|
||||
return res.status(400).json({ message: '이미 다른 모임장이 존재합니다.' });
|
||||
}
|
||||
}
|
||||
|
||||
// 회원 정보 업데이트
|
||||
await member.update({
|
||||
name: name || member.name,
|
||||
gender: gender || member.gender,
|
||||
memberType: memberType || member.memberType,
|
||||
email: email || member.email,
|
||||
phone: phone || member.phone,
|
||||
status: status || member.status
|
||||
});
|
||||
|
||||
// 클럽의 회원 수 업데이트
|
||||
const club = await Club.findByPk(clubId);
|
||||
if (club) {
|
||||
const memberCount = await Member.count({ where: { clubId: clubId, status: 'active' } });
|
||||
await club.update({ memberCount });
|
||||
}
|
||||
|
||||
res.json(member);
|
||||
} catch (error) {
|
||||
console.error('클럽 회원 정보 수정 오류:', error);
|
||||
res.status(500).json({ message: '회원 정보를 수정하는 중 오류가 발생했습니다.' });
|
||||
}
|
||||
};
|
||||
|
||||
// 클럽 회원 삭제
|
||||
exports.deleteClubMember = async (req, res) => {
|
||||
try {
|
||||
const { clubId, memberId } = req.body;
|
||||
|
||||
if (!clubId || !memberId) {
|
||||
return res.status(400).json({ message: '잘못된 클럽 또는 회원 ID입니다.' });
|
||||
}
|
||||
|
||||
const member = await Member.findOne({
|
||||
where: {
|
||||
id: memberId,
|
||||
clubId: clubId
|
||||
}
|
||||
});
|
||||
|
||||
if (!member) {
|
||||
return res.status(404).json({ message: '회원을 찾을 수 없습니다.' });
|
||||
}
|
||||
|
||||
// 회원 상태를 deleted로 변경
|
||||
await member.update({ status: 'deleted' });
|
||||
|
||||
// 클럽의 회원 수 감소
|
||||
const club = await Club.findByPk(clubId);
|
||||
if (club) {
|
||||
const memberCount = await Member.count({ where: { clubId: clubId, status: 'active' } });
|
||||
await club.update({ memberCount });
|
||||
}
|
||||
|
||||
res.json({ message: '회원이 성공적으로 삭제되었습니다.' });
|
||||
} catch (error) {
|
||||
console.error('클럽 회원 삭제 오류:', error);
|
||||
res.status(500).json({ message: '회원을 삭제하는 중 오류가 발생했습니다.' });
|
||||
}
|
||||
};
|
||||
|
||||
// 회원 통계 조회
|
||||
exports.getMemberStats = async (req, res) => {
|
||||
try {
|
||||
const { clubId } = req.body;
|
||||
|
||||
if (!clubId) {
|
||||
return res.status(400).json({ message: '클럽 ID가 필요합니다.' });
|
||||
}
|
||||
|
||||
const members = await Member.findAll({
|
||||
where: {
|
||||
clubId,
|
||||
status: 'active'
|
||||
}
|
||||
});
|
||||
|
||||
const stats = {
|
||||
totalCount: members.length,
|
||||
regularCount: members.filter(m => m.memberType === '정회원').length,
|
||||
associateCount: members.filter(m => m.memberType === '준회원').length
|
||||
};
|
||||
|
||||
res.json(stats);
|
||||
} catch (error) {
|
||||
console.error('회원 통계 조회 오류:', error);
|
||||
res.status(500).json({ message: '회원 통계를 가져오는 중 오류가 발생했습니다.' });
|
||||
}
|
||||
};
|
||||
|
||||
// 다음 모임 정보 조회
|
||||
exports.getNextMeeting = async (req, res) => {
|
||||
try {
|
||||
const { clubId } = req.body;
|
||||
|
||||
if (!clubId) {
|
||||
return res.status(400).json({ message: '클럽 ID가 필요합니다.' });
|
||||
}
|
||||
|
||||
const nextMeeting = await Event.findOne({
|
||||
where: {
|
||||
clubId,
|
||||
startDate: { [Op.gt]: new Date() },
|
||||
status: 'active'
|
||||
},
|
||||
include: [{
|
||||
model: EventParticipant,
|
||||
as: 'participants',
|
||||
where: {
|
||||
status: { [Op.ne]: '취소' }
|
||||
},
|
||||
required: false
|
||||
}],
|
||||
order: [['startDate', 'ASC']]
|
||||
});
|
||||
|
||||
if (!nextMeeting) {
|
||||
return res.json(null);
|
||||
}
|
||||
|
||||
const meetingData = {
|
||||
date: nextMeeting.startDate,
|
||||
participantsCount: nextMeeting.participants ? nextMeeting.participants.length : 0,
|
||||
title: nextMeeting.title,
|
||||
location: nextMeeting.location
|
||||
};
|
||||
|
||||
res.json(meetingData);
|
||||
} catch (error) {
|
||||
console.error('다음 모임 정보 조회 오류:', error);
|
||||
res.status(500).json({ message: '다음 모임 정보를 가져오는 중 오류가 발생했습니다.' });
|
||||
}
|
||||
};
|
||||
|
||||
// 최근 모임 정보 조회
|
||||
exports.getLastMeeting = async (req, res) => {
|
||||
try {
|
||||
const { clubId } = req.body;
|
||||
|
||||
if (!clubId) {
|
||||
return res.status(400).json({ message: '클럽 ID가 필요합니다.' });
|
||||
}
|
||||
|
||||
const lastMeeting = await Event.findOne({
|
||||
where: {
|
||||
clubId,
|
||||
startDate: { [Op.lt]: new Date() },
|
||||
status: 'active'
|
||||
},
|
||||
include: [{
|
||||
model: EventParticipant,
|
||||
as: 'participants',
|
||||
where: {
|
||||
status: { [Op.ne]: '취소' }
|
||||
},
|
||||
required: false
|
||||
}],
|
||||
order: [['startDate', 'DESC']]
|
||||
});
|
||||
|
||||
if (!lastMeeting) {
|
||||
return res.json(null);
|
||||
}
|
||||
|
||||
const meetingData = {
|
||||
date: lastMeeting.startDate,
|
||||
participantsCount: lastMeeting.participants ? lastMeeting.participants.length : 0,
|
||||
title: lastMeeting.title,
|
||||
games: lastMeeting.gameCount || 0
|
||||
};
|
||||
|
||||
res.json(meetingData);
|
||||
} catch (error) {
|
||||
console.error('최근 모임 정보 조회 오류:', error);
|
||||
res.status(500).json({ message: '최근 모임 정보를 가져오는 중 오류가 발생했습니다.' });
|
||||
}
|
||||
};
|
||||
|
||||
// 점수 통계 조회
|
||||
exports.getScoreStats = async (req, res) => {
|
||||
try {
|
||||
const { clubId } = req.body;
|
||||
|
||||
if (!clubId) {
|
||||
return res.status(400).json({ message: '클럽 ID가 필요합니다.' });
|
||||
}
|
||||
|
||||
// 최근 3개월 내의 이벤트 점수만 집계
|
||||
const threeMonthsAgo = new Date();
|
||||
threeMonthsAgo.setMonth(threeMonthsAgo.getMonth() - 3);
|
||||
|
||||
const events = await Event.findAll({
|
||||
where: {
|
||||
clubId,
|
||||
startDate: { [Op.gte]: threeMonthsAgo },
|
||||
status: 'active'
|
||||
},
|
||||
include: [{
|
||||
model: EventParticipant,
|
||||
as: 'participants',
|
||||
include: [{
|
||||
model: EventScore,
|
||||
as: 'scores'
|
||||
}]
|
||||
}]
|
||||
});
|
||||
|
||||
let allScores = [];
|
||||
let totalGames = 0;
|
||||
|
||||
events.forEach(event => {
|
||||
event.participants.forEach(participant => {
|
||||
if (participant.scores && participant.scores.length > 0) {
|
||||
allScores = allScores.concat(participant.scores.map(score => score.score));
|
||||
totalGames += participant.scores.length;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const stats = {
|
||||
topGame: allScores.length > 0 ? Math.max(...allScores) : 0,
|
||||
averageScore: allScores.length > 0 ? Math.round(allScores.reduce((a, b) => a + b, 0) / allScores.length) : 0,
|
||||
totalGames: totalGames
|
||||
};
|
||||
|
||||
res.json(stats);
|
||||
} catch (error) {
|
||||
console.error('점수 통계 조회 오류:', error);
|
||||
res.status(500).json({ message: '점수 통계를 가져오는 중 오류가 발생했습니다.' });
|
||||
}
|
||||
};
|
||||
|
||||
// 상위 회원 목록 조회
|
||||
exports.getTopMembers = async (req, res) => {
|
||||
try {
|
||||
const { clubId } = req.body;
|
||||
|
||||
if (!clubId) {
|
||||
return res.status(400).json({ message: '클럽 ID가 필요합니다.' });
|
||||
}
|
||||
|
||||
const topMembers = await Member.findAll({
|
||||
where: {
|
||||
clubId,
|
||||
status: 'active',
|
||||
games: { [Op.gt]: 0 } // 최소 1게임 이상 플레이한 회원
|
||||
},
|
||||
order: [
|
||||
['average', 'DESC']
|
||||
],
|
||||
limit: 5,
|
||||
attributes: ['id', 'name', 'average', 'games', 'handicap', 'memberType']
|
||||
});
|
||||
|
||||
res.json(topMembers);
|
||||
} catch (error) {
|
||||
console.error('상위 회원 목록 조회 오류:', error);
|
||||
res.status(500).json({ message: '상위 회원 목록을 가져오는 중 오류가 발생했습니다.' });
|
||||
}
|
||||
};
|
||||
|
||||
// 최근 모임 목록 조회
|
||||
exports.getRecentMeetings = async (req, res) => {
|
||||
try {
|
||||
const { clubId } = req.body;
|
||||
|
||||
if (!clubId) {
|
||||
return res.status(400).json({ message: '클럽 ID가 필요합니다.' });
|
||||
}
|
||||
|
||||
const recentMeetings = await Event.findAll({
|
||||
where: {
|
||||
clubId,
|
||||
status: 'active',
|
||||
startDate: { [Op.lt]: new Date() }
|
||||
},
|
||||
include: [{
|
||||
model: EventParticipant,
|
||||
as: 'participants',
|
||||
where: {
|
||||
status: { [Op.ne]: '취소' }
|
||||
},
|
||||
required: false
|
||||
}],
|
||||
order: [['startDate', 'DESC']],
|
||||
limit: 5
|
||||
});
|
||||
|
||||
const formattedMeetings = recentMeetings.map(meeting => ({
|
||||
date: meeting.startDate,
|
||||
title: meeting.title,
|
||||
participants: meeting.participants ? meeting.participants.length : 0,
|
||||
games: meeting.gameCount || 0
|
||||
}));
|
||||
|
||||
res.json(formattedMeetings);
|
||||
} catch (error) {
|
||||
console.error('최근 모임 목록 조회 오류:', error);
|
||||
res.status(500).json({ message: '최근 모임 목록을 가져오는 중 오류가 발생했습니다.' });
|
||||
}
|
||||
};
|
||||
|
||||
// 사용자 클럽 목록 조회
|
||||
exports.getUserClubs = async (req, res) => {
|
||||
try {
|
||||
// 사용자가 속한 클럽 목록 조회 (Member 테이블 조인)
|
||||
// admin이면 모든 클럽을, 아니면 자신의 클럽만 조회
|
||||
const hasAdminAccess = isAdmin(req.user);
|
||||
|
||||
const queryOptions = {
|
||||
where: { status: 'active' }
|
||||
};
|
||||
|
||||
// admin이 아닌 경우에만 Member 조건 추가
|
||||
if (!hasAdminAccess) {
|
||||
queryOptions.include = [{
|
||||
model: Member,
|
||||
where: {
|
||||
userId: req.user.id
|
||||
},
|
||||
attributes: ['memberType']
|
||||
}];
|
||||
}
|
||||
|
||||
// order 옵션 추가
|
||||
queryOptions.order = [['createdAt', 'DESC']];
|
||||
const clubs = await Club.findAll(queryOptions);
|
||||
|
||||
// 응답 데이터 가공
|
||||
const formattedClubs = clubs.map(club => {
|
||||
const clubData = club.toJSON();
|
||||
let memberType = '';
|
||||
|
||||
// admin이 아닌 경우에만 Members가 있음
|
||||
if (!hasAdminAccess && clubData.Members && clubData.Members.length > 0) {
|
||||
memberType = clubData.Members[0].memberType;
|
||||
} else if (hasAdminAccess) {
|
||||
memberType = 'admin';
|
||||
}
|
||||
|
||||
return {
|
||||
id: clubData.id,
|
||||
name: clubData.name,
|
||||
location: clubData.location,
|
||||
memberType
|
||||
};
|
||||
});
|
||||
|
||||
res.json(formattedClubs);
|
||||
} catch (error) {
|
||||
console.error('클럽 목록 조회 오류:', error);
|
||||
res.status(500).json({ message: '클럽 목록을 가져오는 중 오류가 발생했습니다.' });
|
||||
}
|
||||
};
|
||||
|
||||
// 클럽 통계 조회
|
||||
exports.getStatistics = async (req, res) => {
|
||||
try {
|
||||
const { clubId, days } = req.body;
|
||||
|
||||
if (!clubId) {
|
||||
return res.status(400).json({ message: '클럽 ID가 필요합니다.' });
|
||||
}
|
||||
|
||||
const startDate = new Date();
|
||||
startDate.setDate(startDate.getDate() - (days || 90)); // 기본값: 90일
|
||||
|
||||
// 1. 회원 통계
|
||||
const members = await Member.findAll({
|
||||
where: {
|
||||
clubId,
|
||||
status: 'active'
|
||||
}
|
||||
});
|
||||
|
||||
// 2. 이벤트 및 참가자 통계
|
||||
const events = await Event.findAll({
|
||||
where: {
|
||||
clubId,
|
||||
startDate: { [Op.gte]: startDate },
|
||||
status: 'active'
|
||||
},
|
||||
include: [{
|
||||
model: EventParticipant,
|
||||
as: 'participants',
|
||||
include: [{
|
||||
model: EventScore,
|
||||
as: 'scores'
|
||||
}]
|
||||
}]
|
||||
});
|
||||
|
||||
// 3. 점수 데이터 수집
|
||||
let allScores = [];
|
||||
let totalParticipants = 0;
|
||||
let totalGames = 0;
|
||||
|
||||
events.forEach(event => {
|
||||
if (event.participants) {
|
||||
totalParticipants += event.participants.length;
|
||||
event.participants.forEach(participant => {
|
||||
if (participant.scores) {
|
||||
allScores = allScores.concat(participant.scores.map(score => score.score));
|
||||
totalGames += participant.scores.length;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 4. 점수 분포 계산
|
||||
const scoreDistribution = Array(10).fill(0);
|
||||
allScores.forEach(score => {
|
||||
const index = Math.floor(score / 30);
|
||||
if (index >= 0 && index < 10) {
|
||||
scoreDistribution[index]++;
|
||||
}
|
||||
});
|
||||
|
||||
// 5. 참석률 추이 계산
|
||||
const attendanceTrend = events.map(event => ({
|
||||
date: event.startDate,
|
||||
attendance: Math.round((event.participants ? event.participants.length : 0) / members.length * 100)
|
||||
}));
|
||||
|
||||
// 6. 회원 순위 계산
|
||||
const memberRankings = await Member.findAll({
|
||||
where: {
|
||||
clubId,
|
||||
status: 'active',
|
||||
games: { [Op.gt]: 0 }
|
||||
},
|
||||
order: [['average', 'DESC']],
|
||||
limit: 10,
|
||||
attributes: ['id', 'name', 'average', 'games', 'handicap', 'memberType']
|
||||
});
|
||||
|
||||
// 각 회원의 참석률 계산
|
||||
const memberAttendance = {};
|
||||
events.forEach(event => {
|
||||
event.participants.forEach(participant => {
|
||||
memberAttendance[participant.memberId] = memberAttendance[participant.memberId] || { attended: 0, total: 0 };
|
||||
memberAttendance[participant.memberId].attended++;
|
||||
memberAttendance[participant.memberId].total = events.length;
|
||||
});
|
||||
});
|
||||
|
||||
const rankingsWithAttendance = memberRankings.map(member => ({
|
||||
...member.toJSON(),
|
||||
attendance: memberAttendance[member.id]
|
||||
? Math.round(memberAttendance[member.id].attended / memberAttendance[member.id].total * 100)
|
||||
: 0
|
||||
}));
|
||||
|
||||
// 7. 모임별 통계
|
||||
const meetingStats = events.map(event => ({
|
||||
date: event.startDate,
|
||||
title: event.title,
|
||||
participants: event.participants ? event.participants.length : 0,
|
||||
games: event.gameCount || 0,
|
||||
averageScore: event.participants && event.participants.length > 0
|
||||
? Math.round(event.participants.reduce((sum, p) =>
|
||||
sum + (p.scores ? p.scores.reduce((s, score) => s + score.score, 0) / p.scores.length : 0),
|
||||
0) / event.participants.length)
|
||||
: 0
|
||||
}));
|
||||
|
||||
// 8. 응답 데이터 구성
|
||||
const response = {
|
||||
summary: {
|
||||
totalMembers: members.length,
|
||||
activeMembers: members.filter(m => m.games > 0).length,
|
||||
averageAttendance: events.length > 0
|
||||
? Math.round(totalParticipants / (events.length * members.length) * 100)
|
||||
: 0,
|
||||
highestGame: allScores.length > 0 ? Math.max(...allScores) : 0,
|
||||
averageScore: allScores.length > 0
|
||||
? Math.round(allScores.reduce((a, b) => a + b, 0) / allScores.length)
|
||||
: 0,
|
||||
totalGames: totalGames,
|
||||
totalMeetings: events.length,
|
||||
averageParticipants: events.length > 0
|
||||
? Math.round(totalParticipants / events.length)
|
||||
: 0,
|
||||
averageGamesPerMeeting: events.length > 0
|
||||
? Math.round(totalGames / events.length)
|
||||
: 0,
|
||||
averageHandicap: members.length > 0
|
||||
? Math.round(members.reduce((sum, m) => sum + m.handicap, 0) / members.length)
|
||||
: 0,
|
||||
highestHandicap: members.length > 0
|
||||
? Math.max(...members.map(m => m.handicap))
|
||||
: 0,
|
||||
lowestHandicap: members.length > 0
|
||||
? Math.min(...members.map(m => m.handicap))
|
||||
: 0
|
||||
},
|
||||
scoreDistribution: scoreDistribution.map((count, index) => ({
|
||||
range: `${index * 30}-${(index + 1) * 30 - 1}`,
|
||||
count
|
||||
})),
|
||||
attendanceTrend,
|
||||
memberRankings: rankingsWithAttendance,
|
||||
meetingStats
|
||||
};
|
||||
|
||||
res.json(response);
|
||||
} catch (error) {
|
||||
console.error('통계 데이터 조회 오류:', error);
|
||||
res.status(500).json({ message: '통계 데이터를 가져오는 중 오류가 발생했습니다.' });
|
||||
}
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,203 @@
|
||||
const { Feature, ClubFeature, Club } = require('../models');
|
||||
const { Op } = require('sequelize');
|
||||
|
||||
// 전체 기능 목록 조회
|
||||
exports.getFeatures = async (req, res) => {
|
||||
try {
|
||||
const features = await Feature.findAll({
|
||||
where: {
|
||||
status: 'active'
|
||||
},
|
||||
attributes: ['id', 'name', 'description', 'price']
|
||||
});
|
||||
res.json(features);
|
||||
} catch (error) {
|
||||
console.error('기능 목록 조회 오류:', error);
|
||||
res.status(500).json({ message: '기능 목록을 가져오는 중 오류가 발생했습니다.' });
|
||||
}
|
||||
};
|
||||
|
||||
// 사용 가능한 기능 목록 조회
|
||||
exports.getAvailableFeatures = async (req, res) => {
|
||||
try {
|
||||
const { clubId } = req.query;
|
||||
|
||||
if (!clubId) {
|
||||
return res.status(400).json({ message: '클럽 ID가 필요합니다.' });
|
||||
}
|
||||
|
||||
// 모든 기능 목록 조회
|
||||
const features = await Feature.findAll({
|
||||
where: {
|
||||
status: 'active'
|
||||
},
|
||||
attributes: ['id', 'name', 'description', 'price']
|
||||
});
|
||||
|
||||
// 클럽의 활성화된 기능 조회
|
||||
const activeFeatures = await ClubFeature.findAll({
|
||||
where: {
|
||||
clubId,
|
||||
expiryDate: {
|
||||
[Op.gt]: new Date()
|
||||
}
|
||||
},
|
||||
attributes: ['featureId']
|
||||
});
|
||||
|
||||
const activeFeatureIds = activeFeatures.map(af => af.featureId);
|
||||
|
||||
// 기능 목록에 활성화 상태 추가
|
||||
const formattedFeatures = features.map(feature => ({
|
||||
...feature.toJSON(),
|
||||
isActive: activeFeatureIds.includes(feature.id)
|
||||
}));
|
||||
|
||||
res.json(formattedFeatures);
|
||||
} catch (error) {
|
||||
console.error('사용 가능한 기능 목록 조회 오류:', error);
|
||||
res.status(500).json({ message: '기능 목록을 가져오는 중 오류가 발생했습니다.' });
|
||||
}
|
||||
};
|
||||
|
||||
// 활성화된 기능 목록 조회
|
||||
exports.getActiveFeatures = async (req, res) => {
|
||||
try {
|
||||
const { clubId } = req.query;
|
||||
|
||||
if (!clubId) {
|
||||
return res.status(400).json({ message: '클럽 ID가 필요합니다.' });
|
||||
}
|
||||
|
||||
const activeFeatures = await ClubFeature.findAll({
|
||||
where: {
|
||||
clubId,
|
||||
expiryDate: {
|
||||
[Op.gt]: new Date()
|
||||
}
|
||||
},
|
||||
include: [{
|
||||
model: Feature,
|
||||
as: 'feature',
|
||||
attributes: ['name', 'description']
|
||||
}],
|
||||
attributes: ['id', 'featureId', 'purchaseDate', 'expiryDate']
|
||||
});
|
||||
|
||||
res.json(activeFeatures);
|
||||
} catch (error) {
|
||||
console.error('활성화된 기능 목록 조회 오류:', error);
|
||||
res.status(500).json({ message: '활성화된 기능 목록을 가져오는 중 오류가 발생했습니다.' });
|
||||
}
|
||||
};
|
||||
|
||||
// 기능 구매 내역 조회
|
||||
exports.getFeaturePayments = async (req, res) => {
|
||||
try {
|
||||
const { clubId } = req.query;
|
||||
|
||||
if (!clubId) {
|
||||
return res.status(400).json({ message: '클럽 ID가 필요합니다.' });
|
||||
}
|
||||
|
||||
const payments = await ClubFeature.findAll({
|
||||
where: {
|
||||
clubId
|
||||
},
|
||||
include: [{
|
||||
model: Feature,
|
||||
as: 'feature',
|
||||
attributes: ['name']
|
||||
}],
|
||||
attributes: [
|
||||
'id',
|
||||
'featureId',
|
||||
'purchaseDate',
|
||||
'expiryDate',
|
||||
'amount',
|
||||
'duration',
|
||||
'status'
|
||||
],
|
||||
order: [['purchaseDate', 'DESC']]
|
||||
});
|
||||
|
||||
const formattedPayments = payments.map(payment => ({
|
||||
id: payment.id,
|
||||
featureName: payment.feature.name,
|
||||
purchaseDate: payment.purchaseDate,
|
||||
expiryDate: payment.expiryDate,
|
||||
amount: payment.amount,
|
||||
duration: payment.duration,
|
||||
status: payment.status
|
||||
}));
|
||||
|
||||
res.json(formattedPayments);
|
||||
} catch (error) {
|
||||
console.error('기능 구매 내역 조회 오류:', error);
|
||||
res.status(500).json({ message: '구매 내역을 가져오는 중 오류가 발생했습니다.' });
|
||||
}
|
||||
};
|
||||
|
||||
// 기능 구매
|
||||
exports.purchaseFeature = async (req, res) => {
|
||||
try {
|
||||
const { clubId, featureId, duration } = req.body;
|
||||
|
||||
if (!clubId) {
|
||||
return res.status(400).json({ message: '클럽 ID가 필요합니다.' });
|
||||
}
|
||||
|
||||
// 기능 정보 조회
|
||||
const feature = await Feature.findByPk(featureId);
|
||||
if (!feature) {
|
||||
return res.status(404).json({ message: '존재하지 않는 기능입니다.' });
|
||||
}
|
||||
|
||||
// 클럽 정보 조회
|
||||
const club = await Club.findByPk(clubId);
|
||||
if (!club) {
|
||||
return res.status(404).json({ message: '존재하지 않는 클럽입니다.' });
|
||||
}
|
||||
|
||||
// 이미 활성화된 기능인지 확인
|
||||
const existingFeature = await ClubFeature.findOne({
|
||||
where: {
|
||||
clubId,
|
||||
featureId,
|
||||
expiryDate: {
|
||||
[Op.gt]: new Date()
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (existingFeature) {
|
||||
return res.status(400).json({ message: '이미 활성화된 기능입니다.' });
|
||||
}
|
||||
|
||||
// 구매 금액 계산 (duration에 따른 할인 적용 가능)
|
||||
const amount = feature.price * duration;
|
||||
|
||||
// 기능 구매 기록 생성
|
||||
const purchaseDate = new Date();
|
||||
const expiryDate = new Date();
|
||||
expiryDate.setMonth(expiryDate.getMonth() + duration);
|
||||
|
||||
const clubFeature = await ClubFeature.create({
|
||||
clubId,
|
||||
featureId,
|
||||
purchaseDate,
|
||||
expiryDate,
|
||||
amount,
|
||||
duration,
|
||||
status: 'active'
|
||||
});
|
||||
|
||||
res.json({
|
||||
message: '기능 구매가 완료되었습니다.',
|
||||
purchaseInfo: clubFeature
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('기능 구매 오류:', error);
|
||||
res.status(500).json({ message: '기능 구매 중 오류가 발생했습니다.' });
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,166 @@
|
||||
const { Menu, Feature, ClubFeature, ClubSubscription, SubscriptionPlan, Member } = require('../models');
|
||||
const { Op } = require('sequelize');
|
||||
|
||||
/**
|
||||
* 사용자의 접근 가능한 메뉴 목록을 반환합니다.
|
||||
*/
|
||||
exports.getUserMenus = async (req, res) => {
|
||||
try {
|
||||
const { clubId, role = 'user' } = req.body;
|
||||
const userId = req.user.id; // JWT 토큰에서 사용자 ID 가져오기
|
||||
|
||||
if (!clubId) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '클럽 ID가 필요합니다.'
|
||||
});
|
||||
}
|
||||
|
||||
// 멤버 정보 조회
|
||||
const member = await Member.findOne({
|
||||
where: {
|
||||
clubId,
|
||||
userId
|
||||
}
|
||||
});
|
||||
|
||||
const menus = await getAccessibleMenus(clubId, role, member);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: menus
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('메뉴 조회 중 오류 발생:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '메뉴 조회 중 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자 권한과 클럽의 구독/기능 상태에 따라 접근 가능한 메뉴 목록을 조회합니다.
|
||||
*/
|
||||
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']
|
||||
}]
|
||||
});
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
/**
|
||||
* 알림 컨트롤러
|
||||
*
|
||||
* 볼링 클럽 매니저에서 지원하는 알림 유형:
|
||||
* 1. 시스템 알림
|
||||
* - 시스템 업데이트, 유지보수 공지 등
|
||||
* - 아이콘: pi pi-info-circle
|
||||
* - 타입: info
|
||||
*
|
||||
* 2. 클럽 관련 알림
|
||||
* - 새 회원 가입, 클럽 정보 변경 등
|
||||
* - 아이콘: pi pi-users
|
||||
* - 타입: info 또는 success
|
||||
*
|
||||
* 3. 모임/대회 알림
|
||||
* - 모임 생성, 대회 일정, 참가 신청 등
|
||||
* - 아이콘: pi pi-calendar 또는 pi pi-flag
|
||||
* - 타입: info 또는 warning
|
||||
*
|
||||
* 4. 점수 관련 알림
|
||||
* - 새 점수 등록, 평균 점수 갱신 등
|
||||
* - 아이콘: pi pi-chart-bar
|
||||
* - 타입: success
|
||||
*
|
||||
* 5. 결제/구독 알림
|
||||
* - 결제 완료, 구독 만료 예정 등
|
||||
* - 아이콘: pi pi-credit-card 또는 pi pi-clock
|
||||
* - 타입: success 또는 warning
|
||||
*
|
||||
* 6. 오류/경고 알림
|
||||
* - 시스템 오류, 보안 경고 등
|
||||
* - 아이콘: pi pi-exclamation-triangle
|
||||
* - 타입: error 또는 warning
|
||||
*
|
||||
* 알림 생성 방법:
|
||||
* 1. API 엔드포인트를 통한 생성: POST /api/notifications
|
||||
* 2. 내부 함수를 통한 생성: createSystemNotification()
|
||||
*
|
||||
* 알림 필드:
|
||||
* - user: 알림을 받을 사용자 ID (필수)
|
||||
* - title: 알림 제목 (필수)
|
||||
* - message: 알림 내용 (필수)
|
||||
* - icon: 알림 아이콘 (PrimeIcons 사용)
|
||||
* - type: 알림 타입 (info, success, warning, error)
|
||||
* - link: 알림 클릭 시 이동할 링크 (옵션)
|
||||
* - read: 읽음 상태 (기본값: false)
|
||||
*/
|
||||
|
||||
const User = require('../models/User');
|
||||
const Notification = require('../models/Notification');
|
||||
const { validationResult } = require('express-validator');
|
||||
|
||||
// 사용자의 알림 목록 조회
|
||||
exports.getUserNotifications = async (req, res) => {
|
||||
try {
|
||||
const userId = req.user.id;
|
||||
|
||||
// 사용자의 알림 목록 조회 (최신순으로 정렬)
|
||||
const notifications = await Notification.findAll({
|
||||
where: { userId },
|
||||
order: [['createdAt', 'DESC']],
|
||||
limit: 10
|
||||
});
|
||||
|
||||
res.status(200).json(notifications);
|
||||
} catch (error) {
|
||||
console.error('알림 조회 오류:', error);
|
||||
res.status(500).json({ message: '서버 오류가 발생했습니다.' });
|
||||
}
|
||||
};
|
||||
|
||||
// 알림 읽음 표시
|
||||
exports.markAsRead = async (req, res) => {
|
||||
try {
|
||||
const { notificationId } = req.params;
|
||||
const userId = req.user.id;
|
||||
|
||||
const notification = await Notification.findOne({
|
||||
where: {
|
||||
id: notificationId,
|
||||
userId
|
||||
}
|
||||
});
|
||||
|
||||
if (!notification) {
|
||||
return res.status(404).json({ message: '알림을 찾을 수 없습니다.' });
|
||||
}
|
||||
|
||||
notification.read = true;
|
||||
await notification.save();
|
||||
|
||||
res.status(200).json({ message: '알림이 읽음 처리되었습니다.' });
|
||||
} catch (error) {
|
||||
console.error('알림 읽음 표시 오류:', error);
|
||||
res.status(500).json({ message: '서버 오류가 발생했습니다.' });
|
||||
}
|
||||
};
|
||||
|
||||
// 모든 알림 읽음 표시
|
||||
exports.markAllAsRead = async (req, res) => {
|
||||
try {
|
||||
const userId = req.user.id;
|
||||
|
||||
await Notification.update(
|
||||
{ read: true },
|
||||
{ where: { userId, read: false } }
|
||||
);
|
||||
|
||||
res.status(200).json({ message: '모든 알림이 읽음 처리되었습니다.' });
|
||||
} catch (error) {
|
||||
console.error('모든 알림 읽음 표시 오류:', error);
|
||||
res.status(500).json({ message: '서버 오류가 발생했습니다.' });
|
||||
}
|
||||
};
|
||||
|
||||
// 알림 생성 (관리자용)
|
||||
exports.createNotification = async (req, res) => {
|
||||
try {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({ errors: errors.array() });
|
||||
}
|
||||
|
||||
const { userId, title, message, icon, type, link } = req.body;
|
||||
|
||||
const newNotification = await Notification.create({
|
||||
userId,
|
||||
title,
|
||||
message,
|
||||
icon: icon || 'pi pi-info-circle',
|
||||
type: type || 'info',
|
||||
link,
|
||||
read: false
|
||||
});
|
||||
|
||||
// Socket.IO를 통해 실시간 알림 전송
|
||||
const io = req.app.get('io');
|
||||
if (io) {
|
||||
io.to(`user:${userId}`).emit('notification', {
|
||||
id: newNotification.id,
|
||||
title: newNotification.title,
|
||||
message: newNotification.message,
|
||||
icon: newNotification.icon,
|
||||
type: newNotification.type,
|
||||
link: newNotification.link,
|
||||
createdAt: newNotification.createdAt
|
||||
});
|
||||
}
|
||||
|
||||
res.status(201).json(newNotification);
|
||||
} catch (error) {
|
||||
console.error('알림 생성 오류:', error);
|
||||
res.status(500).json({ message: '서버 오류가 발생했습니다.' });
|
||||
}
|
||||
};
|
||||
|
||||
// 알림 삭제
|
||||
exports.deleteNotification = async (req, res) => {
|
||||
try {
|
||||
const { notificationId } = req.params;
|
||||
const userId = req.user.id;
|
||||
|
||||
const notification = await Notification.findOne({
|
||||
where: {
|
||||
id: notificationId,
|
||||
userId
|
||||
}
|
||||
});
|
||||
|
||||
if (!notification) {
|
||||
return res.status(404).json({ message: '알림을 찾을 수 없습니다.' });
|
||||
}
|
||||
|
||||
await notification.destroy();
|
||||
|
||||
res.status(200).json({ message: '알림이 삭제되었습니다.' });
|
||||
} catch (error) {
|
||||
console.error('알림 삭제 오류:', error);
|
||||
res.status(500).json({ message: '서버 오류가 발생했습니다.' });
|
||||
}
|
||||
};
|
||||
|
||||
// 시스템 알림 생성 (내부 함수)
|
||||
exports.createSystemNotification = async (userId, title, message, options = {}) => {
|
||||
try {
|
||||
const { icon, type, link } = options;
|
||||
|
||||
const newNotification = await Notification.create({
|
||||
userId,
|
||||
title,
|
||||
message,
|
||||
icon: icon || 'pi pi-info-circle',
|
||||
type: type || 'info',
|
||||
link,
|
||||
read: false
|
||||
});
|
||||
|
||||
// Socket.IO를 통해 실시간 알림 전송
|
||||
const io = global.io;
|
||||
if (io) {
|
||||
io.to(`user:${userId}`).emit('notification', {
|
||||
id: newNotification.id,
|
||||
title: newNotification.title,
|
||||
message: newNotification.message,
|
||||
icon: newNotification.icon,
|
||||
type: newNotification.type,
|
||||
link: newNotification.link,
|
||||
createdAt: newNotification.createdAt
|
||||
});
|
||||
}
|
||||
|
||||
return newNotification;
|
||||
} catch (error) {
|
||||
console.error('시스템 알림 생성 오류:', error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,237 @@
|
||||
const { SubscriptionPlan, SubscriptionPlanFeature, ClubSubscription, Feature, Club, User } = require('../models');
|
||||
|
||||
// 전체 구독 목록 조회
|
||||
exports.getClubSubscriptions = async (req, res) => {
|
||||
try {
|
||||
const subscriptions = await ClubSubscription.findAll({
|
||||
include: [
|
||||
{
|
||||
model: SubscriptionPlan,
|
||||
include: [{
|
||||
model: Feature,
|
||||
through: { attributes: ['status'] }
|
||||
}]
|
||||
},
|
||||
{
|
||||
model: Club,
|
||||
include: [{
|
||||
model: User,
|
||||
as: 'owner',
|
||||
attributes: ['id', 'name', 'email']
|
||||
}]
|
||||
}
|
||||
]
|
||||
});
|
||||
res.json(subscriptions);
|
||||
} catch (error) {
|
||||
console.error('Error in getClubSubscriptions:', error);
|
||||
res.status(500).json({ message: '구독 목록 조회 중 오류가 발생했습니다.' });
|
||||
}
|
||||
};
|
||||
|
||||
// 구독 플랜 목록 조회
|
||||
exports.getSubscriptionPlans = async (req, res) => {
|
||||
try {
|
||||
const plans = await SubscriptionPlan.findAll({
|
||||
include: [{
|
||||
model: Feature,
|
||||
through: { attributes: ['status'] }
|
||||
}]
|
||||
});
|
||||
res.json(plans);
|
||||
} catch (error) {
|
||||
console.error('Error in getSubscriptionPlans:', error);
|
||||
res.status(500).json({ message: '구독 플랜 목록 조회 중 오류가 발생했습니다.' });
|
||||
}
|
||||
};
|
||||
|
||||
// 구독 플랜 생성
|
||||
exports.createSubscriptionPlan = async (req, res) => {
|
||||
try {
|
||||
const { name, description, maxMembers, price, features } = req.body;
|
||||
|
||||
const plan = await SubscriptionPlan.create({
|
||||
name,
|
||||
description,
|
||||
maxMembers,
|
||||
price
|
||||
});
|
||||
|
||||
if (features && features.length > 0) {
|
||||
const featureAssociations = features.map(featureId => ({
|
||||
planId: plan.id,
|
||||
featureId,
|
||||
status: 'active'
|
||||
}));
|
||||
await SubscriptionPlanFeature.bulkCreate(featureAssociations);
|
||||
}
|
||||
|
||||
const createdPlan = await SubscriptionPlan.findByPk(plan.id, {
|
||||
include: [{
|
||||
model: Feature,
|
||||
through: { attributes: ['status'] }
|
||||
}]
|
||||
});
|
||||
|
||||
res.status(201).json(createdPlan);
|
||||
} catch (error) {
|
||||
console.error('Error in createSubscriptionPlan:', error);
|
||||
res.status(500).json({ message: '구독 플랜 생성 중 오류가 발생했습니다.' });
|
||||
}
|
||||
};
|
||||
|
||||
// 구독 플랜 수정
|
||||
exports.updateSubscriptionPlan = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { name, description, maxMembers, price, features } = req.body;
|
||||
|
||||
const plan = await SubscriptionPlan.findByPk(id);
|
||||
if (!plan) {
|
||||
return res.status(404).json({ message: '구독 플랜을 찾을 수 없습니다.' });
|
||||
}
|
||||
|
||||
await plan.update({
|
||||
name,
|
||||
description,
|
||||
maxMembers,
|
||||
price
|
||||
});
|
||||
|
||||
if (features) {
|
||||
// 기존 기능 연결 삭제
|
||||
await SubscriptionPlanFeature.destroy({
|
||||
where: { planId: id }
|
||||
});
|
||||
|
||||
// 새로운 기능 연결 생성
|
||||
if (features.length > 0) {
|
||||
const featureAssociations = features.map(featureId => ({
|
||||
planId: id,
|
||||
featureId,
|
||||
status: 'active'
|
||||
}));
|
||||
await SubscriptionPlanFeature.bulkCreate(featureAssociations);
|
||||
}
|
||||
}
|
||||
|
||||
const updatedPlan = await SubscriptionPlan.findByPk(id, {
|
||||
include: [{
|
||||
model: Feature,
|
||||
through: { attributes: ['status'] }
|
||||
}]
|
||||
});
|
||||
|
||||
res.json(updatedPlan);
|
||||
} catch (error) {
|
||||
console.error('Error in updateSubscriptionPlan:', error);
|
||||
res.status(500).json({ message: '구독 플랜 수정 중 오류가 발생했습니다.' });
|
||||
}
|
||||
};
|
||||
|
||||
// 구독 플랜 삭제
|
||||
exports.deleteSubscriptionPlan = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
// 플랜이 존재하는지 확인
|
||||
const plan = await SubscriptionPlan.findByPk(id);
|
||||
if (!plan) {
|
||||
return res.status(404).json({ message: '구독 플랜을 찾을 수 없습니다.' });
|
||||
}
|
||||
|
||||
// 플랜과 연결된 기능 관계 삭제
|
||||
await SubscriptionPlanFeature.destroy({
|
||||
where: { planId: id }
|
||||
});
|
||||
|
||||
// 플랜 삭제
|
||||
await plan.destroy();
|
||||
|
||||
res.json({ message: '구독 플랜이 삭제되었습니다.' });
|
||||
} catch (error) {
|
||||
console.error('Error in deleteSubscriptionPlan:', error);
|
||||
res.status(500).json({ message: '구독 플랜 삭제 중 오류가 발생했습니다.' });
|
||||
}
|
||||
};
|
||||
|
||||
// 클럽 구독 생성/수정
|
||||
exports.subscribeClub = async (req, res) => {
|
||||
try {
|
||||
const { clubId, planId, startDate, expiryDate } = req.body;
|
||||
|
||||
// 기존 구독 확인
|
||||
let subscription = await ClubSubscription.findOne({
|
||||
where: { clubId, status: 'active' }
|
||||
});
|
||||
|
||||
if (subscription) {
|
||||
// 기존 구독 만료 처리
|
||||
await subscription.update({
|
||||
status: 'expired',
|
||||
updatedAt: new Date()
|
||||
});
|
||||
}
|
||||
|
||||
// 새로운 구독 생성
|
||||
subscription = await ClubSubscription.create({
|
||||
clubId,
|
||||
planId,
|
||||
startDate: new Date(startDate),
|
||||
expiryDate: new Date(expiryDate),
|
||||
status: 'active'
|
||||
});
|
||||
|
||||
res.status(201).json(subscription);
|
||||
} catch (error) {
|
||||
console.error('Error in subscribeClub:', error);
|
||||
res.status(500).json({ message: '클럽 구독 처리 중 오류가 발생했습니다.' });
|
||||
}
|
||||
};
|
||||
|
||||
// 클럽의 현재 구독 정보 조회
|
||||
exports.getClubSubscription = async (req, res) => {
|
||||
try {
|
||||
const { clubId } = req.params;
|
||||
|
||||
const subscription = await ClubSubscription.findOne({
|
||||
where: { clubId, status: 'active' },
|
||||
include: [{
|
||||
model: SubscriptionPlan,
|
||||
include: [{
|
||||
model: Feature,
|
||||
through: { attributes: ['status'] }
|
||||
}]
|
||||
}]
|
||||
});
|
||||
|
||||
if (!subscription) {
|
||||
return res.status(404).json({ message: '활성화된 구독 정보가 없습니다.' });
|
||||
}
|
||||
|
||||
res.json(subscription);
|
||||
} catch (error) {
|
||||
console.error('Error in getClubSubscription:', error);
|
||||
res.status(500).json({ message: '구독 정보 조회 중 오류가 발생했습니다.' });
|
||||
}
|
||||
};
|
||||
|
||||
// 플랜별 구독 목록 조회
|
||||
exports.getPlanSubscriptions = async (req, res) => {
|
||||
try {
|
||||
const { planId } = req.params;
|
||||
|
||||
const subscriptions = await ClubSubscription.findAll({
|
||||
where: { planId },
|
||||
include: [{
|
||||
model: Club,
|
||||
attributes: ['id', 'name']
|
||||
}]
|
||||
});
|
||||
|
||||
res.json(subscriptions);
|
||||
} catch (error) {
|
||||
console.error('Error in getPlanSubscriptions:', error);
|
||||
res.status(500).json({ message: '플랜별 구독 목록 조회 중 오류가 발생했습니다.' });
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,91 @@
|
||||
const User = require('../models/User');
|
||||
|
||||
/**
|
||||
* 전체 사용자 목록 조회
|
||||
*/
|
||||
const getAllUsers = async (req, res) => {
|
||||
try {
|
||||
const users = await User.findAll({
|
||||
attributes: {
|
||||
exclude: ['password']
|
||||
},
|
||||
order: [['createdAt', 'DESC']]
|
||||
});
|
||||
res.json(users);
|
||||
} catch (error) {
|
||||
console.error('사용자 목록 조회 중 오류 발생:', error);
|
||||
res.status(500).json({ message: '사용자 목록을 가져오는 중 오류가 발생했습니다.' });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 단일 사용자 정보 조회
|
||||
*/
|
||||
const getUser = async (req, res) => {
|
||||
try {
|
||||
const userId = req.params.id;
|
||||
const user = await User.findByPk(userId, {
|
||||
attributes: { exclude: ['password'] }
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return res.status(404).json({ message: '사용자를 찾을 수 없습니다.' });
|
||||
}
|
||||
|
||||
res.json(user);
|
||||
} catch (error) {
|
||||
console.error('사용자 정보 조회 중 오류 발생:', error);
|
||||
res.status(500).json({ message: '사용자 정보를 가져오는 중 오류가 발생했습니다.' });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 사용자 정보 수정
|
||||
*/
|
||||
const updateUser = async (req, res) => {
|
||||
try {
|
||||
const userId = req.params.id;
|
||||
const { name, email, role, status } = req.body;
|
||||
|
||||
const user = await User.findByPk(userId);
|
||||
if (!user) {
|
||||
return res.status(404).json({ message: '사용자를 찾을 수 없습니다.' });
|
||||
}
|
||||
|
||||
// 이메일 중복 확인 (이메일이 변경된 경우에만)
|
||||
if (email !== user.email) {
|
||||
const existingUser = await User.findOne({ where: { email } });
|
||||
if (existingUser) {
|
||||
return res.status(400).json({ message: '이미 사용 중인 이메일입니다.' });
|
||||
}
|
||||
}
|
||||
|
||||
// 필수 필드 확인
|
||||
if (!name || !email || !role || !status) {
|
||||
return res.status(400).json({ message: '필수 정보가 누락되었습니다.' });
|
||||
}
|
||||
|
||||
await user.update({
|
||||
name,
|
||||
email,
|
||||
role,
|
||||
status,
|
||||
updatedAt: new Date()
|
||||
});
|
||||
|
||||
const updatedUser = await User.findByPk(userId, {
|
||||
attributes: { exclude: ['password'] }
|
||||
});
|
||||
|
||||
res.json(updatedUser);
|
||||
} catch (error) {
|
||||
console.error('사용자 정보 수정 중 오류 발생:', error);
|
||||
res.status(500).json({ message: '사용자 정보를 수정하는 중 오류가 발생했습니다.' });
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
getAllUsers,
|
||||
getUser,
|
||||
updateUser
|
||||
};
|
||||
@@ -0,0 +1,209 @@
|
||||
-- 볼링 매니저 데이터베이스 스키마
|
||||
-- 데이터베이스 생성
|
||||
CREATE DATABASE IF NOT EXISTS bowlingManager;
|
||||
USE bowlingManager;
|
||||
|
||||
-- 사용자 테이블
|
||||
CREATE TABLE IF NOT EXISTS Users (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
username VARCHAR(255) NOT NULL UNIQUE,
|
||||
password VARCHAR(255) NOT NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
email VARCHAR(255) NOT NULL UNIQUE,
|
||||
role ENUM('admin', 'clubadmin', 'user') NOT NULL DEFAULT 'user',
|
||||
status ENUM('active', 'inactive', 'suspended') NOT NULL DEFAULT 'active',
|
||||
lastLoginAt DATETIME,
|
||||
createdAt DATETIME NOT NULL,
|
||||
updatedAt DATETIME NOT NULL
|
||||
);
|
||||
|
||||
-- 클럽 테이블
|
||||
CREATE TABLE IF NOT EXISTS Clubs (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
ownerId INT NOT NULL,
|
||||
location VARCHAR(255),
|
||||
contact VARCHAR(255),
|
||||
status ENUM('active', 'inactive', 'suspended') NOT NULL DEFAULT 'active',
|
||||
memberCount INT NOT NULL DEFAULT 0,
|
||||
createdAt DATETIME NOT NULL,
|
||||
updatedAt DATETIME NOT NULL,
|
||||
FOREIGN KEY (ownerId) REFERENCES Users(id)
|
||||
);
|
||||
|
||||
-- 기능 테이블
|
||||
CREATE TABLE IF NOT EXISTS Features (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
code VARCHAR(255) NOT NULL UNIQUE,
|
||||
price INT NOT NULL DEFAULT 0,
|
||||
status ENUM('active', 'inactive', 'suspended') NOT NULL DEFAULT 'active',
|
||||
createdAt DATETIME NOT NULL,
|
||||
updatedAt DATETIME NOT NULL
|
||||
);
|
||||
|
||||
-- 구독 플랜 테이블
|
||||
CREATE TABLE IF NOT EXISTS SubscriptionPlans (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
maxMembers INT NOT NULL,
|
||||
price INT NOT NULL DEFAULT 0,
|
||||
status ENUM('active', 'inactive') NOT NULL DEFAULT 'active',
|
||||
createdAt DATETIME NOT NULL,
|
||||
updatedAt DATETIME NOT NULL
|
||||
);
|
||||
|
||||
-- 클럽 구독 테이블
|
||||
CREATE TABLE IF NOT EXISTS ClubSubscriptions (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
clubId INT NOT NULL,
|
||||
planId INT NOT NULL,
|
||||
status ENUM('active', 'expired', 'suspended') NOT NULL DEFAULT 'active',
|
||||
startDate DATETIME NOT NULL,
|
||||
endDate DATETIME NOT NULL,
|
||||
price INT NOT NULL DEFAULT 0,
|
||||
createdAt DATETIME NOT NULL,
|
||||
updatedAt DATETIME NOT NULL,
|
||||
FOREIGN KEY (clubId) REFERENCES Clubs(id),
|
||||
FOREIGN KEY (planId) REFERENCES SubscriptionPlans(id)
|
||||
);
|
||||
|
||||
-- 구독 플랜 기능 테이블
|
||||
CREATE TABLE IF NOT EXISTS SubscriptionPlanFeatures (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
planId INT NOT NULL,
|
||||
featureId INT NOT NULL,
|
||||
status ENUM('active', 'inactive') NOT NULL DEFAULT 'active',
|
||||
createdAt DATETIME NOT NULL,
|
||||
updatedAt DATETIME NOT NULL,
|
||||
FOREIGN KEY (planId) REFERENCES SubscriptionPlans(id),
|
||||
FOREIGN KEY (featureId) REFERENCES Features(id)
|
||||
);
|
||||
|
||||
-- 클럽 기능 테이블 (N:M 관계)
|
||||
CREATE TABLE IF NOT EXISTS ClubFeatures (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
clubId INT NOT NULL,
|
||||
featureId INT NOT NULL,
|
||||
enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
expiryDate DATETIME,
|
||||
createdAt DATETIME NOT NULL,
|
||||
updatedAt DATETIME NOT NULL,
|
||||
FOREIGN KEY (clubId) REFERENCES Clubs(id),
|
||||
FOREIGN KEY (featureId) REFERENCES Features(id)
|
||||
);
|
||||
|
||||
-- 회원 테이블
|
||||
CREATE TABLE IF NOT EXISTS Members (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
clubId INT NOT NULL,
|
||||
userId INT,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
gender ENUM('남성', '여성') NOT NULL,
|
||||
memberType ENUM('정회원', '준회원', '운영진', '모임장') NOT NULL DEFAULT '준회원',
|
||||
handicap INT NOT NULL DEFAULT 0,
|
||||
average FLOAT NOT NULL DEFAULT 0,
|
||||
games INT NOT NULL DEFAULT 0,
|
||||
phone VARCHAR(255),
|
||||
email VARCHAR(255),
|
||||
joinDate DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
status ENUM('active', 'inactive', 'suspended') NOT NULL DEFAULT 'active',
|
||||
createdAt DATETIME NOT NULL,
|
||||
updatedAt DATETIME NOT NULL,
|
||||
FOREIGN KEY (clubId) REFERENCES Clubs(id),
|
||||
FOREIGN KEY (userId) REFERENCES Users(id)
|
||||
);
|
||||
|
||||
-- 이벤트 테이블
|
||||
CREATE TABLE IF NOT EXISTS `Events` (
|
||||
`id` int NOT NULL AUTO_INCREMENT,
|
||||
`clubId` int NOT NULL,
|
||||
`title` varchar(255) NOT NULL,
|
||||
`description` text,
|
||||
`eventType` enum('정기전', '번개', '연습', '교류전', '이벤트', '기타') NOT NULL DEFAULT '정기전',
|
||||
`startDate` datetime NOT NULL,
|
||||
`endDate` datetime,
|
||||
`location` varchar(255),
|
||||
`gameCount` int NOT NULL DEFAULT 3,
|
||||
`entryFee` int NOT NULL DEFAULT 0,
|
||||
`maxParticipants` int NOT NULL DEFAULT 0,
|
||||
`registrationDeadline` datetime,
|
||||
`status` enum('active', 'inactive', 'deleted') NOT NULL DEFAULT 'active',
|
||||
`createdAt` datetime NOT NULL,
|
||||
`updatedAt` datetime NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `clubId` (`clubId`),
|
||||
CONSTRAINT `events_ibfk_1` FOREIGN KEY (`clubId`) REFERENCES `Clubs` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
-- 이벤트 참가자 테이블
|
||||
CREATE TABLE IF NOT EXISTS EventParticipants (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
eventId INT NOT NULL,
|
||||
memberId INT NOT NULL,
|
||||
status ENUM('참가예정', '참가완료', '취소') NOT NULL DEFAULT '참가예정',
|
||||
paymentStatus ENUM('미납', '납부완료') NOT NULL DEFAULT '미납',
|
||||
createdAt DATETIME NOT NULL,
|
||||
updatedAt DATETIME NOT NULL,
|
||||
FOREIGN KEY (eventId) REFERENCES Events(id),
|
||||
FOREIGN KEY (memberId) REFERENCES Members(id)
|
||||
);
|
||||
|
||||
-- 이벤트 점수 테이블
|
||||
CREATE TABLE IF NOT EXISTS EventScores (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
eventId INT NOT NULL,
|
||||
participantId INT NOT NULL,
|
||||
gameNumber INT NOT NULL,
|
||||
score INT NOT NULL,
|
||||
handicap INT NOT NULL DEFAULT 0,
|
||||
totalScore INT NOT NULL,
|
||||
strikes INT NOT NULL DEFAULT 0,
|
||||
spares INT NOT NULL DEFAULT 0,
|
||||
openFrames INT NOT NULL DEFAULT 0,
|
||||
createdAt DATETIME NOT NULL,
|
||||
updatedAt DATETIME NOT NULL,
|
||||
FOREIGN KEY (eventId) REFERENCES Events(id),
|
||||
FOREIGN KEY (participantId) REFERENCES EventParticipants(id)
|
||||
);
|
||||
|
||||
-- 메뉴 테이블
|
||||
CREATE TABLE IF NOT EXISTS menus (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
icon VARCHAR(255),
|
||||
`to` VARCHAR(255) NOT NULL,
|
||||
role VARCHAR(255) NOT NULL DEFAULT 'user',
|
||||
`order` INT NOT NULL DEFAULT 0,
|
||||
visible BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
createdAt DATETIME NOT NULL,
|
||||
updatedAt DATETIME NOT NULL
|
||||
);
|
||||
|
||||
-- 알림 테이블
|
||||
CREATE TABLE IF NOT EXISTS Notifications (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
userId INT NOT NULL,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
message TEXT NOT NULL,
|
||||
icon VARCHAR(255) DEFAULT 'pi pi-info-circle',
|
||||
type ENUM('info', 'success', 'warning', 'error') NOT NULL DEFAULT 'info',
|
||||
read BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
link VARCHAR(255) DEFAULT NULL,
|
||||
createdAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updatedAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (userId) REFERENCES Users(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- 관리자 활동 로그 테이블
|
||||
CREATE TABLE IF NOT EXISTS ActivityLogs (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
userId INT NOT NULL,
|
||||
type VARCHAR(50) NOT NULL,
|
||||
description TEXT,
|
||||
createdAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (userId) REFERENCES Users(id) ON DELETE CASCADE
|
||||
);
|
||||
@@ -0,0 +1,26 @@
|
||||
const jwt = require('jsonwebtoken');
|
||||
require('dotenv').config();
|
||||
|
||||
module.exports = (req, res, next) => {
|
||||
try {
|
||||
// 헤더에서 토큰 가져오기
|
||||
const authHeader = req.headers.authorization;
|
||||
|
||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||
return res.status(401).json({ message: '인증 토큰이 필요합니다.' });
|
||||
}
|
||||
|
||||
const token = authHeader.split(' ')[1];
|
||||
|
||||
// 토큰 검증
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET || 'your-secret-key');
|
||||
|
||||
// 요청 객체에 사용자 정보 추가
|
||||
req.user = decoded;
|
||||
|
||||
next();
|
||||
} catch (error) {
|
||||
console.error('인증 오류:', error);
|
||||
res.status(401).json({ message: '유효하지 않은 토큰입니다.' });
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
const jwt = require('jsonwebtoken');
|
||||
|
||||
/**
|
||||
* JWT 토큰 인증 미들웨어
|
||||
* 요청 헤더에서 토큰을 추출하고 검증
|
||||
*/
|
||||
exports.authenticateJWT = (req, res, next) => {
|
||||
try {
|
||||
// 헤더에서 토큰 가져오기
|
||||
const authHeader = req.headers.authorization;
|
||||
|
||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||
return res.status(401).json({ message: '인증 토큰이 필요합니다.' });
|
||||
}
|
||||
|
||||
const token = authHeader.split(' ')[1];
|
||||
|
||||
// 토큰 검증
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
||||
|
||||
// 요청 객체에 사용자 정보 추가
|
||||
req.user = decoded;
|
||||
|
||||
next();
|
||||
} catch (error) {
|
||||
console.error('인증 오류:', error);
|
||||
res.status(401).json({ message: '유효하지 않은 토큰입니다.' });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 역할 기반 권한 부여 미들웨어
|
||||
* 특정 역할을 가진 사용자만 접근 허용
|
||||
* @param {string[]} roles - 허용된 역할 목록
|
||||
*/
|
||||
exports.authorizeRoles = (...roles) => {
|
||||
return (req, res, next) => {
|
||||
if (!req.user) {
|
||||
return res.status(401).json({ message: '인증이 필요합니다.' });
|
||||
}
|
||||
|
||||
if (!roles.includes(req.user.role)) {
|
||||
return res.status(403).json({ message: '이 작업을 수행할 권한이 없습니다.' });
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const { sequelize } = require('../config/database');
|
||||
|
||||
const Club = sequelize.define('Club', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true
|
||||
},
|
||||
name: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
description: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true
|
||||
},
|
||||
ownerId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'Users',
|
||||
key: 'id'
|
||||
}
|
||||
},
|
||||
location: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true
|
||||
},
|
||||
contact: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.ENUM('active', 'inactive', 'suspended'),
|
||||
allowNull: false,
|
||||
defaultValue: 'active'
|
||||
},
|
||||
memberCount: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0
|
||||
},
|
||||
createdAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false
|
||||
},
|
||||
updatedAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false
|
||||
}
|
||||
}, {
|
||||
tableName: 'Clubs',
|
||||
timestamps: true
|
||||
});
|
||||
|
||||
module.exports = Club;
|
||||
@@ -0,0 +1,48 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const { sequelize } = require('../config/database');
|
||||
|
||||
const ClubFeature = sequelize.define('ClubFeature', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true
|
||||
},
|
||||
clubId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'Clubs',
|
||||
key: 'id'
|
||||
}
|
||||
},
|
||||
featureId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'Features',
|
||||
key: 'id'
|
||||
}
|
||||
},
|
||||
enabled: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: true
|
||||
},
|
||||
expiryDate: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true
|
||||
},
|
||||
createdAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false
|
||||
},
|
||||
updatedAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false
|
||||
}
|
||||
}, {
|
||||
tableName: 'ClubFeatures',
|
||||
timestamps: true
|
||||
});
|
||||
|
||||
module.exports = ClubFeature;
|
||||
@@ -0,0 +1,49 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const { sequelize } = require('../config/database');
|
||||
|
||||
const ClubSubscription = sequelize.define('ClubSubscription', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true
|
||||
},
|
||||
clubId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'Clubs',
|
||||
key: 'id'
|
||||
}
|
||||
},
|
||||
planId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'SubscriptionPlans',
|
||||
key: 'id'
|
||||
}
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.ENUM('active', 'expired', 'suspended'),
|
||||
defaultValue: 'active',
|
||||
allowNull: false
|
||||
},
|
||||
startDate: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false
|
||||
},
|
||||
endDate: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false
|
||||
},
|
||||
price: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0
|
||||
}
|
||||
}, {
|
||||
tableName: 'ClubSubscriptions',
|
||||
timestamps: true
|
||||
});
|
||||
|
||||
module.exports = ClubSubscription;
|
||||
@@ -0,0 +1,103 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const { sequelize } = require('../config/database');
|
||||
|
||||
const Event = sequelize.define('Event', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true
|
||||
},
|
||||
clubId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'Clubs',
|
||||
key: 'id'
|
||||
}
|
||||
},
|
||||
title: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
description: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true
|
||||
},
|
||||
eventType: {
|
||||
type: DataTypes.ENUM('정기전', '번개', '연습', '교류전', '이벤트', '기타'),
|
||||
allowNull: false,
|
||||
defaultValue: '정기전'
|
||||
},
|
||||
startDate: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false
|
||||
},
|
||||
endDate: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true
|
||||
},
|
||||
location: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true
|
||||
},
|
||||
gameCount: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 3
|
||||
},
|
||||
participantFee: {
|
||||
type: DataTypes.DECIMAL(10, 2),
|
||||
allowNull: false,
|
||||
defaultValue: 0
|
||||
},
|
||||
maxParticipants: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0
|
||||
},
|
||||
registrationDeadline: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true
|
||||
},
|
||||
participantCount: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.ENUM('준비', '활성', '완료', '취소', '삭제'),
|
||||
allowNull: false,
|
||||
defaultValue: '활성'
|
||||
},
|
||||
createdAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false
|
||||
},
|
||||
updatedAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false
|
||||
}
|
||||
}, {
|
||||
tableName: 'Events',
|
||||
timestamps: true
|
||||
});
|
||||
|
||||
// 관계 설정
|
||||
Event.associate = (models) => {
|
||||
Event.belongsTo(models.Club, {
|
||||
foreignKey: 'clubId',
|
||||
as: 'club'
|
||||
});
|
||||
|
||||
Event.hasMany(models.EventParticipant, {
|
||||
foreignKey: 'eventId',
|
||||
as: 'participants'
|
||||
});
|
||||
|
||||
Event.hasMany(models.EventScore, {
|
||||
foreignKey: 'eventId',
|
||||
as: 'scores'
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = Event;
|
||||
@@ -0,0 +1,59 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const { sequelize } = require('../config/database');
|
||||
|
||||
const EventParticipant = sequelize.define('EventParticipant', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true
|
||||
},
|
||||
eventId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'Events',
|
||||
key: 'id'
|
||||
}
|
||||
},
|
||||
memberId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'Members',
|
||||
key: 'id'
|
||||
}
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.ENUM('참가예정', '참가확정', '취소'),
|
||||
allowNull: false,
|
||||
defaultValue: '참가예정'
|
||||
},
|
||||
paymentStatus: {
|
||||
type: DataTypes.ENUM('미납', '납부완료'),
|
||||
allowNull: false,
|
||||
defaultValue: '미납'
|
||||
}
|
||||
}, {
|
||||
tableName: 'EventParticipants',
|
||||
timestamps: true
|
||||
});
|
||||
|
||||
// 관계 설정
|
||||
EventParticipant.associate = (models) => {
|
||||
EventParticipant.belongsTo(models.Event, {
|
||||
foreignKey: 'eventId',
|
||||
as: 'event'
|
||||
});
|
||||
|
||||
EventParticipant.belongsTo(models.Member, {
|
||||
foreignKey: 'memberId',
|
||||
as: 'member'
|
||||
});
|
||||
|
||||
EventParticipant.hasMany(models.EventScore, {
|
||||
foreignKey: 'participantId',
|
||||
as: 'scores'
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = EventParticipant;
|
||||
@@ -0,0 +1,52 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const { sequelize } = require('../config/database');
|
||||
|
||||
const EventScore = sequelize.define('EventScore', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true
|
||||
},
|
||||
eventId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'Events',
|
||||
key: 'id'
|
||||
}
|
||||
},
|
||||
participantId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'EventParticipants',
|
||||
key: 'id'
|
||||
}
|
||||
},
|
||||
teamNumber: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
},
|
||||
gameNumber: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false
|
||||
},
|
||||
score: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false
|
||||
},
|
||||
handicap: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0
|
||||
},
|
||||
totalScore: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false
|
||||
}
|
||||
}, {
|
||||
tableName: 'EventScores',
|
||||
timestamps: true
|
||||
});
|
||||
|
||||
module.exports = EventScore;
|
||||
@@ -0,0 +1,59 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const { sequelize } = require('../config/database');
|
||||
|
||||
const Feature = sequelize.define('Feature', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true
|
||||
},
|
||||
name: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
description: {
|
||||
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,
|
||||
defaultValue: 'active'
|
||||
},
|
||||
createdAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false
|
||||
},
|
||||
updatedAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false
|
||||
}
|
||||
}, {
|
||||
tableName: 'Features',
|
||||
timestamps: true
|
||||
});
|
||||
|
||||
module.exports = Feature;
|
||||
@@ -0,0 +1,91 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const { sequelize } = require('../config/database');
|
||||
const User = require('./User');
|
||||
const Club = require('./Club');
|
||||
|
||||
const Member = sequelize.define('Member', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true
|
||||
},
|
||||
clubId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'Clubs',
|
||||
key: 'id'
|
||||
}
|
||||
},
|
||||
userId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
references: {
|
||||
model: 'Users',
|
||||
key: 'id'
|
||||
}
|
||||
},
|
||||
name: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
gender: {
|
||||
type: DataTypes.ENUM('남성', '여성'),
|
||||
allowNull: false
|
||||
},
|
||||
memberType: {
|
||||
type: DataTypes.ENUM('정회원', '준회원', '운영진', '모임장'),
|
||||
allowNull: false,
|
||||
defaultValue: '준회원'
|
||||
},
|
||||
handicap: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0
|
||||
},
|
||||
average: {
|
||||
type: DataTypes.FLOAT,
|
||||
allowNull: false,
|
||||
defaultValue: 0
|
||||
},
|
||||
games: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0
|
||||
},
|
||||
phone: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true
|
||||
},
|
||||
email: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true
|
||||
},
|
||||
joinDate: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.ENUM('active', 'inactive', 'suspended', 'deleted'),
|
||||
allowNull: false,
|
||||
defaultValue: 'active'
|
||||
}
|
||||
}, {
|
||||
tableName: 'Members',
|
||||
timestamps: true
|
||||
});
|
||||
|
||||
// User와의 관계 설정
|
||||
Member.belongsTo(User, {
|
||||
foreignKey: 'userId',
|
||||
as: 'user'
|
||||
});
|
||||
|
||||
// Club과의 관계 설정
|
||||
Member.belongsTo(Club, {
|
||||
foreignKey: 'clubId',
|
||||
as: 'club'
|
||||
});
|
||||
|
||||
module.exports = Member;
|
||||
@@ -0,0 +1,70 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const { sequelize } = require('../config/database');
|
||||
|
||||
const Menu = sequelize.define('Menu', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true
|
||||
},
|
||||
parentId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
references: {
|
||||
model: 'Menus', // 변경
|
||||
key: 'id'
|
||||
}
|
||||
},
|
||||
title: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
icon: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true
|
||||
},
|
||||
to: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
featureCode: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
references: {
|
||||
model: 'Features',
|
||||
key: 'code'
|
||||
}
|
||||
},
|
||||
requiredSubscriptionTier: {
|
||||
type: DataTypes.ENUM('basic', 'standard', 'premium'),
|
||||
allowNull: true
|
||||
},
|
||||
role: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
defaultValue: 'user'
|
||||
},
|
||||
order: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0
|
||||
},
|
||||
visible: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: true
|
||||
},
|
||||
createdAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false
|
||||
},
|
||||
updatedAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false
|
||||
}
|
||||
}, {
|
||||
tableName: 'Menus',
|
||||
timestamps: true
|
||||
});
|
||||
|
||||
module.exports = Menu;
|
||||
@@ -0,0 +1,59 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const { sequelize } = require('../config/database');
|
||||
|
||||
const Notification = sequelize.define('Notification', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true
|
||||
},
|
||||
userId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'Users',
|
||||
key: 'id'
|
||||
}
|
||||
},
|
||||
title: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
message: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: false
|
||||
},
|
||||
icon: {
|
||||
type: DataTypes.STRING,
|
||||
defaultValue: 'pi pi-info-circle'
|
||||
},
|
||||
type: {
|
||||
type: DataTypes.ENUM('info', 'success', 'warning', 'error'),
|
||||
defaultValue: 'info'
|
||||
},
|
||||
read: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false
|
||||
},
|
||||
link: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
defaultValue: null
|
||||
},
|
||||
createdAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW
|
||||
},
|
||||
updatedAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW
|
||||
}
|
||||
}, {
|
||||
timestamps: true,
|
||||
tableName: 'Notifications'
|
||||
});
|
||||
|
||||
module.exports = Notification;
|
||||
@@ -0,0 +1,63 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const { sequelize } = require('../config/database');
|
||||
|
||||
const Participant = sequelize.define('Participant', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true
|
||||
},
|
||||
meetingId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'Meetings',
|
||||
key: 'id'
|
||||
}
|
||||
},
|
||||
memberId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'Members',
|
||||
key: 'id'
|
||||
}
|
||||
},
|
||||
teamNumber: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 1
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.ENUM('참가예정', '참가완료', '취소'),
|
||||
allowNull: false,
|
||||
defaultValue: '참가예정'
|
||||
},
|
||||
attendance: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: false
|
||||
},
|
||||
paymentStatus: {
|
||||
type: DataTypes.ENUM('미납', '납부완료'),
|
||||
allowNull: false,
|
||||
defaultValue: '미납'
|
||||
},
|
||||
paymentAmount: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0
|
||||
},
|
||||
createdAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false
|
||||
},
|
||||
updatedAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false
|
||||
}
|
||||
}, {
|
||||
tableName: 'EventParticipants',
|
||||
timestamps: true
|
||||
});
|
||||
|
||||
module.exports = Participant;
|
||||
@@ -0,0 +1,36 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const { sequelize } = require('../config/database');
|
||||
|
||||
const SubscriptionPlan = sequelize.define('SubscriptionPlan', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true
|
||||
},
|
||||
name: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
description: {
|
||||
type: DataTypes.TEXT
|
||||
},
|
||||
maxMembers: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false
|
||||
},
|
||||
price: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.ENUM('active', 'inactive'),
|
||||
defaultValue: 'active',
|
||||
allowNull: false
|
||||
}
|
||||
}, {
|
||||
tableName: 'SubscriptionPlan',
|
||||
timestamps: true
|
||||
});
|
||||
|
||||
module.exports = SubscriptionPlan;
|
||||
@@ -0,0 +1,36 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const { sequelize } = require('../config/database');
|
||||
|
||||
const SubscriptionPlanFeature = sequelize.define('SubscriptionPlanFeature', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true
|
||||
},
|
||||
planId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'SubscriptionPlans',
|
||||
key: 'id'
|
||||
}
|
||||
},
|
||||
featureId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'Features',
|
||||
key: 'id'
|
||||
}
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.ENUM('active', 'inactive'),
|
||||
defaultValue: 'active',
|
||||
allowNull: false
|
||||
}
|
||||
}, {
|
||||
tableName: 'SubscriptionPlanFeatures',
|
||||
timestamps: true
|
||||
});
|
||||
|
||||
module.exports = SubscriptionPlanFeature;
|
||||
@@ -0,0 +1,78 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const { sequelize } = require('../config/database');
|
||||
const bcrypt = require('bcrypt');
|
||||
|
||||
const User = sequelize.define('User', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true
|
||||
},
|
||||
username: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
unique: true
|
||||
},
|
||||
password: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
name: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
email: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
validate: {
|
||||
isEmail: true
|
||||
}
|
||||
},
|
||||
role: {
|
||||
type: DataTypes.ENUM('admin', 'clubadmin', 'user'),
|
||||
allowNull: false,
|
||||
defaultValue: 'user'
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.ENUM('active', 'inactive', 'suspended'),
|
||||
allowNull: false,
|
||||
defaultValue: 'active'
|
||||
},
|
||||
lastLoginAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true
|
||||
},
|
||||
createdAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false
|
||||
},
|
||||
updatedAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false
|
||||
}
|
||||
}, {
|
||||
tableName: 'Users',
|
||||
timestamps: true,
|
||||
hooks: {
|
||||
beforeCreate: async (user) => {
|
||||
if (user.password) {
|
||||
const salt = await bcrypt.genSalt(10);
|
||||
user.password = await bcrypt.hash(user.password, salt);
|
||||
}
|
||||
},
|
||||
beforeUpdate: async (user) => {
|
||||
if (user.changed('password')) {
|
||||
const salt = await bcrypt.genSalt(10);
|
||||
user.password = await bcrypt.hash(user.password, salt);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 비밀번호 검증 메소드
|
||||
User.prototype.validatePassword = async function(password) {
|
||||
return await bcrypt.compare(password, this.password);
|
||||
};
|
||||
|
||||
module.exports = User;
|
||||
@@ -0,0 +1,40 @@
|
||||
const { Model, DataTypes } = require('sequelize');
|
||||
const { sequelize } = require('../config/database');
|
||||
|
||||
class ActivityLog extends Model {}
|
||||
|
||||
ActivityLog.init({
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true
|
||||
},
|
||||
userId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'Users',
|
||||
key: 'id'
|
||||
}
|
||||
},
|
||||
type: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: false
|
||||
},
|
||||
description: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true
|
||||
},
|
||||
createdAt: {
|
||||
type: DataTypes.DATE,
|
||||
defaultValue: DataTypes.NOW
|
||||
}
|
||||
}, {
|
||||
sequelize,
|
||||
modelName: 'ActivityLog',
|
||||
tableName: 'ActivityLogs',
|
||||
timestamps: true,
|
||||
updatedAt: false
|
||||
});
|
||||
|
||||
module.exports = ActivityLog;
|
||||
@@ -0,0 +1,855 @@
|
||||
const { sequelize } = require('../config/database');
|
||||
const User = require('./User');
|
||||
const Club = require('./Club');
|
||||
const Event = require('./Event');
|
||||
const EventParticipant = require('./EventParticipant');
|
||||
const EventScore = require('./EventScore');
|
||||
const Member = require('./Member');
|
||||
const Notification = require('./Notification');
|
||||
const Feature = require('./Feature');
|
||||
const ClubFeature = require('./ClubFeature');
|
||||
const Menu = require('./Menu');
|
||||
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 syncModels = async () => {
|
||||
try {
|
||||
// 외래 키 제약 조건을 비활성화하고 테이블 재생성
|
||||
// 개발
|
||||
// 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) {
|
||||
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: {} });
|
||||
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,
|
||||
User,
|
||||
Club,
|
||||
Event,
|
||||
EventParticipant,
|
||||
EventScore,
|
||||
Member,
|
||||
Notification,
|
||||
Feature,
|
||||
ClubFeature,
|
||||
Menu,
|
||||
SubscriptionPlan,
|
||||
SubscriptionPlanFeature,
|
||||
ClubSubscription,
|
||||
ActivityLog,
|
||||
syncModels,
|
||||
createAdminUser,
|
||||
createSampleData
|
||||
};
|
||||
Generated
+3753
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "backend",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"dependencies": {
|
||||
"bcrypt": "^5.1.1",
|
||||
"body-parser": "^1.20.3",
|
||||
"cookie-parser": "^1.4.7",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.4.7",
|
||||
"express": "^4.21.2",
|
||||
"express-validator": "^7.2.1",
|
||||
"helmet": "^8.0.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"mariadb": "^3.4.0",
|
||||
"mongoose": "^8.12.1",
|
||||
"morgan": "^1.10.0",
|
||||
"mysql2": "^3.13.0",
|
||||
"sequelize": "^6.37.6",
|
||||
"socket.io": "^4.8.1",
|
||||
"winston": "^3.17.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"sequelize-cli": "^6.6.2"
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,20 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const authController = require('../controllers/authController');
|
||||
|
||||
// 인증 미들웨어
|
||||
const authMiddleware = require('../middleware/auth');
|
||||
|
||||
// 로그인
|
||||
router.post('/login', authController.login);
|
||||
|
||||
// 현재 사용자 정보 가져오기
|
||||
router.get('/me', authMiddleware, authController.getCurrentUser);
|
||||
|
||||
// 회원가입
|
||||
router.post('/register', authController.register);
|
||||
|
||||
// 프로필 업데이트
|
||||
router.put('/profile', authMiddleware, authController.updateProfile);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,61 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const jwt = require('jsonwebtoken');
|
||||
const clubController = require('../controllers/clubController');
|
||||
const { authenticateJWT } = require('../middleware/authMiddleware');
|
||||
|
||||
// 모든 클럽 조회
|
||||
router.get('/', clubController.getAllClubs);
|
||||
|
||||
// 특정 클럽 조회
|
||||
router.post('/', authenticateJWT, clubController.getClubById);
|
||||
|
||||
// 클럽 정보 업데이트
|
||||
router.put('/', authenticateJWT, clubController.updateClub);
|
||||
|
||||
// 클럽 삭제
|
||||
router.delete('/', authenticateJWT, clubController.deleteClub);
|
||||
|
||||
// 사용자 클럽 목록 조회
|
||||
router.post('/user', authenticateJWT, clubController.getUserClubs);
|
||||
|
||||
// 클럽 회원 통계 조회
|
||||
router.post('/members/stats', authenticateJWT, clubController.getMemberStats);
|
||||
|
||||
// 클럽 회원 목록 조회
|
||||
router.post('/members', authenticateJWT, clubController.getClubMembers);
|
||||
|
||||
// 클럽 회원 추가
|
||||
router.post('/members/add', authenticateJWT, clubController.addClubMember);
|
||||
|
||||
// 클럽 회원 수정
|
||||
router.post('/members/update', authenticateJWT, clubController.updateClubMember);
|
||||
|
||||
// 클럽 회원 삭제
|
||||
router.post('/members/delete', authenticateJWT, clubController.deleteClubMember);
|
||||
|
||||
// 다음 모임 정보 조회
|
||||
router.post('/meetings/next', authenticateJWT, clubController.getNextMeeting);
|
||||
|
||||
// 최근 모임 정보 조회
|
||||
router.post('/meetings/last', authenticateJWT, clubController.getLastMeeting);
|
||||
|
||||
// 최근 정기 모임 목록 조회
|
||||
router.post('/meetings/recent', authenticateJWT, clubController.getRecentMeetings);
|
||||
|
||||
// 점수 통계 조회
|
||||
router.post('/scores/stats', authenticateJWT, clubController.getScoreStats);
|
||||
|
||||
// 상위 회원 목록 조회
|
||||
router.post('/members/top', authenticateJWT, clubController.getTopMembers);
|
||||
|
||||
// 새 클럽 생성
|
||||
router.post('/create', authenticateJWT, clubController.createClub);
|
||||
|
||||
// 클럽 통계 조회
|
||||
router.post('/statistics', authenticateJWT, clubController.getStatistics);
|
||||
|
||||
// 클럽 기능 관리
|
||||
router.put('/features', authenticateJWT, clubController.manageClubFeatures);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,29 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const eventController = require('../controllers/eventController');
|
||||
const { authenticateJWT } = require('../middleware/authMiddleware');
|
||||
|
||||
// 이벤트 기본 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('/: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.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('/user/events', authenticateJWT, eventController.getUserEvents);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,15 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const featureController = require('../controllers/featureController');
|
||||
const { authenticateJWT } = require('../middleware/authMiddleware');
|
||||
|
||||
// 기본 기능 관리
|
||||
router.get('/', authenticateJWT, featureController.getFeatures);
|
||||
|
||||
// 기능 구매 관련 API
|
||||
router.get('/available', authenticateJWT, featureController.getAvailableFeatures);
|
||||
router.get('/active', authenticateJWT, featureController.getActiveFeatures);
|
||||
router.get('/payments', authenticateJWT, featureController.getFeaturePayments);
|
||||
router.post('/purchase', authenticateJWT, featureController.purchaseFeature);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* 메뉴 라우트
|
||||
* 사용자 역할과 클럽의 구독/기능 상태에 따른 메뉴 아이템을 제공하는 API
|
||||
*/
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { authenticateJWT } = require('../middleware/authMiddleware');
|
||||
const menuController = require('../controllers/menuController');
|
||||
|
||||
// 사용자의 접근 가능한 메뉴 목록 조회
|
||||
router.post('/', authenticateJWT, menuController.getUserMenus);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* 알림 라우트
|
||||
* 사용자 알림 관련 API
|
||||
*/
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const notificationController = require('../controllers/notificationController');
|
||||
const { check } = require('express-validator');
|
||||
const { authenticateJWT } = require('../middleware/authMiddleware');
|
||||
|
||||
// 사용자 알림 목록 가져오기
|
||||
router.get('/', authenticateJWT, notificationController.getUserNotifications);
|
||||
|
||||
// 알림 읽음 표시
|
||||
router.put('/:notificationId/read', authenticateJWT, notificationController.markAsRead);
|
||||
|
||||
// 모든 알림 읽음 표시
|
||||
router.put('/read-all', authenticateJWT, notificationController.markAllAsRead);
|
||||
|
||||
// 알림 삭제
|
||||
router.delete('/:notificationId', authenticateJWT, notificationController.deleteNotification);
|
||||
|
||||
// 알림 생성 (관리자 전용)
|
||||
router.post('/',
|
||||
authenticateJWT,
|
||||
[
|
||||
check('userId', '사용자 ID가 필요합니다.').not().isEmpty(),
|
||||
check('title', '알림 제목이 필요합니다.').not().isEmpty(),
|
||||
check('message', '알림 내용이 필요합니다.').not().isEmpty()
|
||||
],
|
||||
notificationController.createNotification
|
||||
);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,19 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const subscriptionController = require('../controllers/subscriptionController');
|
||||
const authMiddleware = require('../middleware/auth');
|
||||
// 구독 목록 조회
|
||||
router.get('/', authMiddleware, subscriptionController.getClubSubscriptions);
|
||||
|
||||
// 구독 플랜 관리 (관리자 전용)
|
||||
router.get('/plans', authMiddleware, subscriptionController.getSubscriptionPlans);
|
||||
router.post('/plans', authMiddleware, subscriptionController.createSubscriptionPlan);
|
||||
router.put('/plans/:id', authMiddleware, subscriptionController.updateSubscriptionPlan);
|
||||
router.delete('/plans/:id', authMiddleware, subscriptionController.deleteSubscriptionPlan);
|
||||
router.get('/plans/:planId/subscriptions', authMiddleware, subscriptionController.getPlanSubscriptions);
|
||||
|
||||
// 클럽 구독 관리
|
||||
router.post('/subscribe', authMiddleware, subscriptionController.subscribeClub);
|
||||
router.get('/clubs/:clubId', authMiddleware, subscriptionController.getClubSubscription);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* 사용자 라우트
|
||||
* 사용자 관련 API 엔드포인트 정의
|
||||
*/
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { getCurrentUser } = require('../controllers/authController');
|
||||
const { getAllUsers, getUser, updateUser } = require('../controllers/userController');
|
||||
const { authenticateJWT } = require('../middleware/authMiddleware');
|
||||
|
||||
// 전체 사용자 목록 조회
|
||||
router.get('/all', authenticateJWT, getAllUsers);
|
||||
|
||||
// 사용자 프로필 조회
|
||||
router.get('/profile', authenticateJWT, getCurrentUser);
|
||||
|
||||
// 단일 사용자 정보 조회
|
||||
router.get('/:id', authenticateJWT, getUser);
|
||||
|
||||
// 사용자 정보 수정
|
||||
router.put('/:id', authenticateJWT, updateUser);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,38 @@
|
||||
const { sequelize } = require('../config/database');
|
||||
|
||||
async function checkDatabaseTables() {
|
||||
try {
|
||||
// 데이터베이스 연결
|
||||
await sequelize.authenticate();
|
||||
console.log('데이터베이스 연결 성공');
|
||||
|
||||
// 모든 테이블 목록 조회
|
||||
const [tables] = await sequelize.query('SHOW TABLES');
|
||||
|
||||
console.log('데이터베이스 테이블 목록:');
|
||||
tables.forEach(table => {
|
||||
const tableName = Object.values(table)[0];
|
||||
console.log(`- ${tableName}`);
|
||||
});
|
||||
|
||||
// 각 테이블의 구조 확인
|
||||
for (const table of tables) {
|
||||
const tableName = Object.values(table)[0];
|
||||
const [columns] = await sequelize.query(`DESCRIBE \`${tableName}\``);
|
||||
|
||||
console.log(`\n테이블: ${tableName}`);
|
||||
columns.forEach(column => {
|
||||
console.log(` - ${column.Field}: ${column.Type} ${column.Null === 'NO' ? 'NOT NULL' : 'NULL'} ${column.Key === 'PRI' ? 'PRIMARY KEY' : ''}`);
|
||||
});
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('오류 발생:', error);
|
||||
} finally {
|
||||
// 연결 종료
|
||||
await sequelize.close();
|
||||
}
|
||||
}
|
||||
|
||||
// 스크립트 실행
|
||||
checkDatabaseTables();
|
||||
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* 기본 클럽 생성 스크립트
|
||||
* 데이터베이스에 기본 클럽을 생성하고 사용자에게 할당합니다.
|
||||
*/
|
||||
const { sequelize } = require('../src/config/database');
|
||||
const Club = require('../src/models/Club');
|
||||
const User = require('../src/models/User');
|
||||
const UserClub = require('../src/models/UserClub');
|
||||
require('dotenv').config();
|
||||
|
||||
async function createDefaultClub() {
|
||||
try {
|
||||
// 데이터베이스 연결 확인
|
||||
await sequelize.authenticate();
|
||||
console.log('데이터베이스 연결 성공');
|
||||
|
||||
// 관리자 사용자 찾기
|
||||
const adminUser = await User.findOne({
|
||||
where: {
|
||||
role: 'admin'
|
||||
}
|
||||
});
|
||||
|
||||
if (!adminUser) {
|
||||
console.log('관리자 사용자를 찾을 수 없습니다. 기본 클럽을 생성할 수 없습니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('관리자 사용자 찾음:', adminUser.email);
|
||||
|
||||
// 기존 클럽 확인
|
||||
const existingClubs = await Club.findAll();
|
||||
|
||||
let defaultClub;
|
||||
|
||||
if (existingClubs.length === 0) {
|
||||
// 기본 클럽 생성
|
||||
defaultClub = await Club.create({
|
||||
name: '볼링 마스터 클럽',
|
||||
description: '볼링 클럽 매니저 기본 클럽입니다.',
|
||||
ownerId: adminUser.id,
|
||||
active: true,
|
||||
subscriptionStatus: '활성',
|
||||
subscriptionType: '기본 구독',
|
||||
subscriptionExpiry: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000), // 1년 후
|
||||
memberCount: 1
|
||||
});
|
||||
console.log('기본 클럽 생성 완료:', defaultClub.name);
|
||||
} else {
|
||||
defaultClub = existingClubs[0];
|
||||
console.log('기존 클럽 사용:', defaultClub.name);
|
||||
}
|
||||
|
||||
// 모든 사용자 가져오기
|
||||
const users = await User.findAll();
|
||||
|
||||
// 각 사용자에게 클럽 할당
|
||||
for (const user of users) {
|
||||
// 이미 클럽에 속해 있는지 확인
|
||||
const existingUserClub = await UserClub.findOne({
|
||||
where: {
|
||||
userId: user.id,
|
||||
clubId: defaultClub.id
|
||||
}
|
||||
});
|
||||
|
||||
if (!existingUserClub) {
|
||||
// 사용자-클럽 관계 생성
|
||||
await UserClub.create({
|
||||
userId: user.id,
|
||||
clubId: defaultClub.id,
|
||||
role: user.role === 'admin' || user.role === 'superadmin' ? 'admin' : 'member',
|
||||
joinDate: new Date(),
|
||||
status: 'active'
|
||||
});
|
||||
console.log(`사용자 ${user.email}에게 클럽 할당 완료`);
|
||||
} else {
|
||||
console.log(`사용자 ${user.email}는 이미 클럽에 속해 있습니다.`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('기본 클럽 생성 및 사용자 할당이 완료되었습니다.');
|
||||
console.log('기본 클럽 ID:', defaultClub.id);
|
||||
|
||||
return defaultClub.id;
|
||||
} catch (error) {
|
||||
console.error('기본 클럽 생성 중 오류 발생:', error);
|
||||
} finally {
|
||||
process.exit();
|
||||
}
|
||||
}
|
||||
|
||||
// 스크립트 실행
|
||||
createDefaultClub();
|
||||
@@ -0,0 +1,153 @@
|
||||
/**
|
||||
* 메뉴 생성 스크립트
|
||||
* 데이터베이스에 기본 메뉴 항목을 생성합니다.
|
||||
*/
|
||||
const { Menu } = require('../src/models');
|
||||
const { sequelize } = require('../src/config/database');
|
||||
require('dotenv').config();
|
||||
|
||||
// 관리자용 메뉴 항목
|
||||
const adminMenus = [
|
||||
{
|
||||
title: '대시보드',
|
||||
icon: 'pi pi-home',
|
||||
to: '/admin',
|
||||
role: 'admin',
|
||||
order: 1,
|
||||
visible: true
|
||||
},
|
||||
{
|
||||
title: '클럽 관리',
|
||||
icon: 'pi pi-users',
|
||||
to: '/admin/clubs',
|
||||
role: 'admin',
|
||||
order: 2,
|
||||
visible: true
|
||||
},
|
||||
{
|
||||
title: '사용자 관리',
|
||||
icon: 'pi pi-user',
|
||||
to: '/admin/users',
|
||||
role: 'admin',
|
||||
order: 3,
|
||||
visible: true
|
||||
},
|
||||
{
|
||||
title: '구독 관리',
|
||||
icon: 'pi pi-credit-card',
|
||||
to: '/admin/subscriptions',
|
||||
role: 'admin',
|
||||
order: 4,
|
||||
visible: true
|
||||
}
|
||||
];
|
||||
|
||||
// 일반 사용자용 메뉴 항목
|
||||
const userMenus = [
|
||||
{
|
||||
title: '대시보드',
|
||||
icon: 'pi pi-home',
|
||||
to: '/clubs',
|
||||
role: 'user',
|
||||
order: 1,
|
||||
visible: true
|
||||
},
|
||||
{
|
||||
title: '회원 관리',
|
||||
icon: 'pi pi-users',
|
||||
to: '/clubs/members',
|
||||
role: 'user',
|
||||
order: 2,
|
||||
visible: true
|
||||
},
|
||||
{
|
||||
title: '모임 관리',
|
||||
icon: 'pi pi-calendar',
|
||||
to: '/clubs/meetings',
|
||||
role: 'user',
|
||||
order: 3,
|
||||
visible: true
|
||||
},
|
||||
{
|
||||
title: '점수 관리',
|
||||
icon: 'pi pi-chart-bar',
|
||||
to: '/clubs/meetings/scores',
|
||||
role: 'user',
|
||||
order: 4,
|
||||
visible: true
|
||||
},
|
||||
{
|
||||
title: '통계',
|
||||
icon: 'pi pi-chart-line',
|
||||
to: '/statistics',
|
||||
role: 'user',
|
||||
order: 5,
|
||||
visible: true
|
||||
},
|
||||
{
|
||||
title: '예약',
|
||||
icon: 'pi pi-bookmark',
|
||||
to: '/clubs/reservations',
|
||||
role: 'user',
|
||||
order: 6,
|
||||
visible: true
|
||||
},
|
||||
{
|
||||
title: '이벤트 관리',
|
||||
icon: 'pi pi-list',
|
||||
to: '/clubs/events',
|
||||
role: 'user',
|
||||
order: 7,
|
||||
visible: true
|
||||
},
|
||||
{
|
||||
title: '이벤트 캘린더',
|
||||
icon: 'pi pi-calendar',
|
||||
to: '/clubs/events/calendar',
|
||||
role: 'user',
|
||||
order: 8,
|
||||
visible: true
|
||||
}
|
||||
];
|
||||
|
||||
// 메뉴 생성 함수
|
||||
async function createMenus() {
|
||||
try {
|
||||
// 데이터베이스 연결 확인
|
||||
await sequelize.authenticate();
|
||||
console.log('데이터베이스 연결 성공');
|
||||
|
||||
// 메뉴 테이블 동기화
|
||||
await Menu.sync({ alter: true });
|
||||
console.log('메뉴 테이블 동기화 완료');
|
||||
|
||||
// 기존 메뉴 항목 확인
|
||||
const existingMenus = await Menu.findAll();
|
||||
|
||||
if (existingMenus.length > 0) {
|
||||
console.log('메뉴 항목이 이미 존재합니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
// 관리자용 메뉴 생성
|
||||
for (const menu of adminMenus) {
|
||||
await Menu.create(menu);
|
||||
}
|
||||
console.log('관리자용 메뉴 생성 완료');
|
||||
|
||||
// 일반 사용자용 메뉴 생성
|
||||
for (const menu of userMenus) {
|
||||
await Menu.create(menu);
|
||||
}
|
||||
console.log('일반 사용자용 메뉴 생성 완료');
|
||||
|
||||
console.log('모든 메뉴 항목이 성공적으로 생성되었습니다.');
|
||||
} catch (error) {
|
||||
console.error('메뉴 생성 중 오류 발생:', error);
|
||||
} finally {
|
||||
process.exit();
|
||||
}
|
||||
}
|
||||
|
||||
// 스크립트 실행
|
||||
createMenus();
|
||||
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* 초기 알림 데이터 생성 스크립트
|
||||
*/
|
||||
require('dotenv').config();
|
||||
const mongoose = require('mongoose');
|
||||
const User = require('../models/User');
|
||||
const Notification = require('../models/Notification');
|
||||
|
||||
// MongoDB 연결
|
||||
mongoose.connect(process.env.MONGODB_URI || 'mongodb://localhost:27017/bowlingManager', {
|
||||
useNewUrlParser: true,
|
||||
useUnifiedTopology: true
|
||||
})
|
||||
.then(() => console.log('MongoDB 연결 성공'))
|
||||
.catch(err => {
|
||||
console.error('MongoDB 연결 오류:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
// 초기 알림 데이터 생성
|
||||
const createNotifications = async () => {
|
||||
try {
|
||||
// 모든 사용자 가져오기
|
||||
const users = await User.find();
|
||||
|
||||
if (users.length === 0) {
|
||||
console.log('사용자가 없습니다. 먼저 사용자를 생성해주세요.');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// 기존 알림 삭제
|
||||
await Notification.deleteMany({});
|
||||
console.log('기존 알림 데이터 삭제 완료');
|
||||
|
||||
// 각 사용자에 대한 알림 생성
|
||||
const notificationPromises = users.map(async (user) => {
|
||||
// 알림 타입 및 메시지 배열
|
||||
const notificationTemplates = [
|
||||
{
|
||||
title: '시스템 알림',
|
||||
message: '볼링 클럽 매니저 시스템에 오신 것을 환영합니다.',
|
||||
icon: 'pi pi-info-circle',
|
||||
type: 'info',
|
||||
read: false
|
||||
},
|
||||
{
|
||||
title: '클럽 알림',
|
||||
message: '새로운 클럽 회원이 가입했습니다.',
|
||||
icon: 'pi pi-users',
|
||||
type: 'success',
|
||||
read: false
|
||||
},
|
||||
{
|
||||
title: '예약 알림',
|
||||
message: '레인 예약이 확정되었습니다.',
|
||||
icon: 'pi pi-calendar',
|
||||
type: 'info',
|
||||
read: true
|
||||
},
|
||||
{
|
||||
title: '대회 알림',
|
||||
message: '다음 주 토요일 클럽 대회가 예정되어 있습니다.',
|
||||
icon: 'pi pi-flag',
|
||||
type: 'warning',
|
||||
read: false
|
||||
},
|
||||
{
|
||||
title: '시스템 업데이트',
|
||||
message: '시스템이 업데이트 되었습니다. 새로운 기능을 확인해보세요.',
|
||||
icon: 'pi pi-refresh',
|
||||
type: 'info',
|
||||
read: true
|
||||
}
|
||||
];
|
||||
|
||||
// 사용자별 알림 생성 (시간 차이를 두고)
|
||||
const notifications = notificationTemplates.map((template, index) => {
|
||||
const hoursAgo = (index + 1) * 12; // 12시간, 24시간, 36시간... 간격
|
||||
const createdAt = new Date(Date.now() - (hoursAgo * 3600000));
|
||||
|
||||
return new Notification({
|
||||
user: user._id,
|
||||
title: template.title,
|
||||
message: template.message,
|
||||
icon: template.icon,
|
||||
type: template.type,
|
||||
read: template.read,
|
||||
createdAt
|
||||
});
|
||||
});
|
||||
|
||||
return Notification.insertMany(notifications);
|
||||
});
|
||||
|
||||
await Promise.all(notificationPromises);
|
||||
console.log('알림 데이터 생성 완료');
|
||||
|
||||
// 연결 종료
|
||||
mongoose.connection.close();
|
||||
console.log('MongoDB 연결 종료');
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error('알림 데이터 생성 오류:', error);
|
||||
mongoose.connection.close();
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
createNotifications();
|
||||
@@ -0,0 +1,46 @@
|
||||
require('dotenv').config();
|
||||
const { User, syncModels, testConnection } = require('../src/models');
|
||||
|
||||
async function createSuperAdmin() {
|
||||
try {
|
||||
// 데이터베이스 연결 테스트
|
||||
console.log('데이터베이스 연결 테스트 중...');
|
||||
await testConnection();
|
||||
|
||||
// 모델 동기화 (필요한 경우)
|
||||
// console.log('데이터베이스 모델 동기화 중...');
|
||||
// await syncModels();
|
||||
|
||||
// 기존 superadmin 계정 확인
|
||||
const existingAdmin = await User.findOne({
|
||||
where: { email: process.env.ADMIN_EMAIL }
|
||||
});
|
||||
|
||||
if (existingAdmin) {
|
||||
console.log('이미 superadmin 계정이 존재합니다:', existingAdmin.email);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// superadmin 계정 생성
|
||||
const superAdmin = await User.create({
|
||||
username: 'master',
|
||||
password: process.env.ADMIN_PASSWORD,
|
||||
name: '최고관리자',
|
||||
email: process.env.ADMIN_EMAIL,
|
||||
role: 'superadmin',
|
||||
active: true
|
||||
});
|
||||
|
||||
console.log('superadmin 계정이 성공적으로 생성되었습니다:');
|
||||
console.log('- 이메일:', superAdmin.email);
|
||||
console.log('- 역할:', superAdmin.role);
|
||||
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error('superadmin 계정 생성 중 오류 발생:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// 스크립트 실행
|
||||
createSuperAdmin();
|
||||
@@ -0,0 +1,27 @@
|
||||
'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,23 @@
|
||||
// 데이터베이스 모델 동기화 스크립트
|
||||
const { sequelize, syncModels, testConnection } = require('./src/models');
|
||||
|
||||
async function initializeDatabase() {
|
||||
try {
|
||||
// 데이터베이스 연결 테스트
|
||||
console.log('데이터베이스 연결 테스트 중...');
|
||||
await testConnection();
|
||||
|
||||
// 모델 동기화 (테이블 생성)
|
||||
console.log('데이터베이스 모델 동기화 중...');
|
||||
await syncModels();
|
||||
|
||||
console.log('데이터베이스 초기화가 완료되었습니다.');
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error('데이터베이스 초기화 중 오류 발생:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// 스크립트 실행
|
||||
initializeDatabase();
|
||||
@@ -0,0 +1,160 @@
|
||||
/**
|
||||
* 알림 생성 예제 유틸리티
|
||||
* 다양한 상황에서 알림을 생성하는 예제 함수들을 제공합니다.
|
||||
*/
|
||||
|
||||
const notificationController = require('../controllers/notificationController');
|
||||
|
||||
/**
|
||||
* 새 회원 가입 알림 생성
|
||||
* @param {Object} clubOwner 클럽 소유자 정보
|
||||
* @param {Object} newMember 새로 가입한 회원 정보
|
||||
*/
|
||||
exports.newMemberJoinNotification = async (clubOwner, newMember) => {
|
||||
await notificationController.createSystemNotification(
|
||||
clubOwner.id,
|
||||
'새 회원 가입',
|
||||
`${newMember.name}님이 클럽에 가입했습니다.`,
|
||||
{
|
||||
icon: 'pi pi-users',
|
||||
type: 'success',
|
||||
link: `/club/members/${newMember.id}`
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 모임 생성 알림 생성
|
||||
* @param {Array} members 알림을 받을 회원 목록
|
||||
* @param {Object} meeting 생성된 모임 정보
|
||||
*/
|
||||
exports.newMeetingNotification = async (members, meeting) => {
|
||||
for (const member of members) {
|
||||
await notificationController.createSystemNotification(
|
||||
member.id,
|
||||
'새 모임 생성',
|
||||
`새로운 모임 "${meeting.title}"이(가) 생성되었습니다. (${meeting.date})`,
|
||||
{
|
||||
icon: 'pi pi-calendar',
|
||||
type: 'info',
|
||||
link: `/meetings/${meeting.id}`
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 점수 등록 알림 생성
|
||||
* @param {Object} member 점수를 등록한 회원
|
||||
* @param {Object} score 등록된 점수 정보
|
||||
*/
|
||||
exports.scoreRegisteredNotification = async (member, score) => {
|
||||
await notificationController.createSystemNotification(
|
||||
member.id,
|
||||
'점수 등록 완료',
|
||||
`게임 점수 ${score.score}점이 등록되었습니다.`,
|
||||
{
|
||||
icon: 'pi pi-chart-bar',
|
||||
type: 'success',
|
||||
link: `/scores/${score.id}`
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 구독 만료 예정 알림 생성
|
||||
* @param {Object} clubOwner 클럽 소유자 정보
|
||||
* @param {Object} subscription 구독 정보
|
||||
* @param {Number} daysLeft 만료까지 남은 일수
|
||||
*/
|
||||
exports.subscriptionExpiryNotification = async (clubOwner, subscription, daysLeft) => {
|
||||
await notificationController.createSystemNotification(
|
||||
clubOwner.id,
|
||||
'구독 만료 예정',
|
||||
`클럽 구독이 ${daysLeft}일 후에 만료됩니다. 갱신하시려면 결제를 진행해주세요.`,
|
||||
{
|
||||
icon: 'pi pi-clock',
|
||||
type: 'warning',
|
||||
link: '/subscription'
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 시스템 유지보수 알림 생성
|
||||
* @param {Array} users 알림을 받을 사용자 목록
|
||||
* @param {String} maintenanceDate 유지보수 일자
|
||||
* @param {String} maintenanceTime 유지보수 시간
|
||||
*/
|
||||
exports.systemMaintenanceNotification = async (users, maintenanceDate, maintenanceTime) => {
|
||||
for (const user of users) {
|
||||
await notificationController.createSystemNotification(
|
||||
user.id,
|
||||
'시스템 유지보수 안내',
|
||||
`${maintenanceDate} ${maintenanceTime}에 시스템 유지보수가 예정되어 있습니다. 서비스 이용에 참고해주세요.`,
|
||||
{
|
||||
icon: 'pi pi-info-circle',
|
||||
type: 'info'
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 결제 완료 알림 생성
|
||||
* @param {Object} user 결제한 사용자 정보
|
||||
* @param {Object} payment 결제 정보
|
||||
*/
|
||||
exports.paymentCompletedNotification = async (user, payment) => {
|
||||
await notificationController.createSystemNotification(
|
||||
user.id,
|
||||
'결제 완료',
|
||||
`${payment.amount.toLocaleString()}원 결제가 완료되었습니다.`,
|
||||
{
|
||||
icon: 'pi pi-credit-card',
|
||||
type: 'success',
|
||||
link: '/payments/history'
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 대회 일정 알림 생성
|
||||
* @param {Array} participants 대회 참가자 목록
|
||||
* @param {Object} tournament 대회 정보
|
||||
*/
|
||||
exports.tournamentReminderNotification = async (participants, tournament) => {
|
||||
for (const participant of participants) {
|
||||
await notificationController.createSystemNotification(
|
||||
participant.id,
|
||||
'대회 일정 안내',
|
||||
`내일 "${tournament.title}" 대회가 ${tournament.location}에서 개최됩니다.`,
|
||||
{
|
||||
icon: 'pi pi-flag',
|
||||
type: 'warning',
|
||||
link: `/tournaments/${tournament.id}`
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 오류 발생 알림 생성 (관리자용)
|
||||
* @param {Array} admins 알림을 받을 관리자 목록
|
||||
* @param {String} errorMessage 오류 메시지
|
||||
* @param {String} errorLocation 오류 발생 위치
|
||||
*/
|
||||
exports.systemErrorNotification = async (admins, errorMessage, errorLocation) => {
|
||||
for (const admin of admins) {
|
||||
await notificationController.createSystemNotification(
|
||||
admin.id,
|
||||
'시스템 오류 발생',
|
||||
`위치: ${errorLocation}\n오류: ${errorMessage}`,
|
||||
{
|
||||
icon: 'pi pi-exclamation-triangle',
|
||||
type: 'error',
|
||||
link: '/admin/logs'
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user