This commit is contained in:
2025-04-17 23:32:39 +09:00
commit 5b719b6766
105 changed files with 25653 additions and 0 deletions
+86
View File
@@ -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;
+40
View File
@@ -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
};
+221
View File
@@ -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: '프로필 업데이트 중 오류가 발생했습니다.' });
}
};
+925
View File
@@ -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
+203
View File
@@ -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: '기능 구매 중 오류가 발생했습니다.' });
}
};
+166
View File
@@ -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: '플랜별 구독 목록 조회 중 오류가 발생했습니다.' });
}
};
+91
View File
@@ -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
};
+209
View File
@@ -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
);
+26
View File
@@ -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: '유효하지 않은 토큰입니다.' });
}
};
+48
View File
@@ -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();
};
};
+57
View File
@@ -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;
+48
View File
@@ -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;
+49
View File
@@ -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;
+103
View File
@@ -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;
+59
View File
@@ -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;
+52
View File
@@ -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;
+59
View File
@@ -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;
+91
View File
@@ -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;
+70
View File
@@ -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;
+59
View File
@@ -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;
+63
View File
@@ -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;
+36
View File
@@ -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;
+36
View File
@@ -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;
+78
View File
@@ -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;
+40
View File
@@ -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;
+855
View File
@@ -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
};
+3753
View File
File diff suppressed because it is too large Load Diff
+33
View File
@@ -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
+20
View File
@@ -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;
+61
View File
@@ -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;
+29
View File
@@ -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;
+15
View File
@@ -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;
+13
View File
@@ -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;
+34
View File
@@ -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;
+19
View File
@@ -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;
+23
View File
@@ -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;
+38
View File
@@ -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();
+94
View File
@@ -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();
+153
View File
@@ -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();
+109
View File
@@ -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();
+46
View File
@@ -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();
+27
View File
@@ -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' }, {});
}
};
+23
View File
@@ -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();
+160
View File
@@ -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'
}
);
}
};