보안설정 수정, 모바일 최적화 변경

This commit is contained in:
2025-06-11 03:33:44 +09:00
parent d085e037a1
commit 48f1bf1c52
10 changed files with 339 additions and 141 deletions
+45 -5
View File
@@ -25,7 +25,29 @@ const io = socketIo(server, {
}
});
app.use(cors());
function getDomain(origin) {
try {
const url = new URL(origin);
return url.hostname; // 'lanebow.com', 'localhost'
} catch (e) {
return null;
}
}
const allowedDomains = ['lanebow.com', 'localhost'];
app.use(cors({
origin: function(origin, callback) {
if (!origin) return callback(null, true);
const domain = getDomain(origin);
if (allowedDomains.includes(domain)) {
return callback(null, true);
} else {
return callback(new Error('Not allowed by CORS'), false);
}
},
credentials: true
}));
app.use(bodyParser.json());
// Socket.IO 연결 처리
@@ -82,17 +104,35 @@ const options = {
// MySQL 세션 저장소 생성
const sessionStore = new MySQLStore(options);
app.use(session({
const baseSessionOptions = {
key: 'bowling_session',
secret: process.env.JWT_SECRET,
store: sessionStore,
resave: false,
saveUninitialized: false,
cookie: {
secure: process.env.NODE_ENV === 'production',
maxAge: 24 * 60 * 60 * 1000 // 1일
maxAge: 24 * 60 * 60 * 1000,
secure: false,
sameSite: 'lax'
}
}));
};
app.use(session(baseSessionOptions));
// 요청마다 secure/sameSite를 동적으로 할당
app.use((req, res, next) => {
const origin = req.headers.origin;
const domain = getDomain(origin);
if (req.session && req.session.cookie) {
if (domain === 'lanebow.com') {
req.session.cookie.secure = true;
req.session.cookie.sameSite = 'none';
} else {
req.session.cookie.secure = false;
req.session.cookie.sameSite = 'lax';
}
}
next();
});
// 데이터베이스 연결 및 모델 동기화 실행
sequelize.authenticate()
+7 -25
View File
@@ -54,6 +54,7 @@ exports.selectClub = async (req, res) => {
}
req.session.clubId = clubId;
console.log(`Session clubId: ${clubId}`);
res.status(200).json({ message: '클럽을 선택했습니다.' });
} catch (error) {
console.error('클럽 선택 오류:', error);
@@ -136,18 +137,18 @@ exports.getClubById = async (req, res) => {
// 새 클럽 생성
exports.createClub = async (req, res) => {
try {
const { name, location, description, ownerEmail, contact, features, sendInvite } = req.body;
const { name, location, description, averageCalculationPeriod } = req.body;
// 필수 필드 검증
if (!name || !ownerEmail) {
return res.status(400).json({ message: '클럽 이름과 모임장 이메일은 필수 항목입니다.' });
if (!name) {
return res.status(400).json({ message: '클럽 이름은 필수 항목입니다.' });
}
// 이메일로 사용자 조회
const owner = await User.findOne({ where: { email: ownerEmail } });
const owner = await User.findOne({ where: { id: req.user.id } });
if (!owner) {
return res.status(404).json({ message: '해당 이메일로 등록된 사용자를 찾을 수 없습니다.' });
return res.status(404).json({ message: '해당 사용자를 찾을 수 없습니다.' });
}
// 클럽 생성
@@ -155,7 +156,7 @@ exports.createClub = async (req, res) => {
name,
location,
description,
contact,
averageCalculationPeriod,
ownerId: owner.id,
memberCount: 1, // 모임장 포함
status: 'active'
@@ -172,25 +173,6 @@ exports.createClub = async (req, res) => {
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}에게 초대 이메일 발송 예정`);
}
// 베이직 1개월 플랜 자동 추가
try {
// 베이직 플랜 조회 (ID가 1이라고 가정, 실제 환경에 맞게 조정 필요)
+26 -23
View File
@@ -1,14 +1,15 @@
<template>
<div id="app">
<AppHeader @toggle-sidebar="sidebarVisible = !sidebarVisible" />
<Toast />
<template v-if="isRouterReady">
<AppPublicLayout v-if="isPublicLayout">
<router-view />
</AppPublicLayout>
<div v-else class="app">
<AppHeader />
<div class="app-content">
<div v-if="isSidebarVisible && isMobileView" class="sidebar-overlay" @click="closeSidebar"></div>
<aside v-if="isLoggedIn" class="sidebar" :class="{ 'mobile-visible': isSidebarVisible, 'mobile-hidden': !isSidebarVisible && isMobileView }">
<div v-if="sidebarVisible && isMobileView" class="sidebar-overlay" @click="closeSidebar"></div>
<aside v-if="isLoggedIn" class="sidebar" :class="{ 'mobile-visible': sidebarVisible, 'mobile-hidden': !sidebarVisible && isMobileView }">
<div class="sidebar-content">
<!-- 클럽 정보 -->
<div class="club-info" v-if="selectedClub">
@@ -43,7 +44,7 @@
</select>
</div>
<!-- 커스텀 사이드바 메뉴 -->
<div class="custom-sidebar-menu">
<div class="custom-sidebar-menu" :class="{ mobile: isMobileView }" v-show="sidebarVisible">
<template v-for="(menuGroup, groupIndex) in menuItems" :key="groupIndex">
<div class="menu-group">
<div class="menu-group-header" @click="toggleMenuGroup(groupIndex)">
@@ -68,7 +69,7 @@
</div>
</template>
<!-- 그룹이 없는 단일 메뉴 아이템 -->
<template v-for="(menuItem, itemIndex) in singleMenuItems" :key="'single-'+itemIndex">
<div v-for="(menuItem, itemIndex) in singleMenuItems" :key="'single-'+itemIndex">
<router-link
:to="menuItem.to"
class="menu-item single-menu-item"
@@ -79,7 +80,7 @@
<i :class="menuItem.icon"></i>
<span>{{ menuItem.label }}</span>
</router-link>
</template>
</div>
</div>
</div>
</aside>
@@ -92,6 +93,7 @@
</footer>
</div>
</template>
</div>
</template>
<script setup>
@@ -120,7 +122,7 @@ const menuItems = ref([]);
const singleMenuItems = ref([]);
const expandedGroups = ref({});
const isMobileView = ref(window.innerWidth < 768);
const isSidebarVisible = ref(!isMobileView.value);
const sidebarVisible = ref(window.innerWidth > 600);
const isLoggedIn = computed(() => !!user.value && !!localStorage.getItem('token'));
const clubs = ref([]); // 클럽 목록
const selectedClubId = ref(localStorage.getItem('clubId') || null); // 선택된 클럽 ID
@@ -135,20 +137,18 @@ const userClubRole = computed(() => localStorage.getItem('userClubRole') || '');
// 화면 크기 변경 감지
const handleResize = () => {
isMobileView.value = window.innerWidth < 768;
if (!isMobileView.value) {
isSidebarVisible.value = true;
}
sidebarVisible.value = window.innerWidth > 600;
};
// 사이드바 토글
const toggleSidebar = () => {
isSidebarVisible.value = !isSidebarVisible.value;
sidebarVisible.value = !sidebarVisible.value;
};
// 사이드바 닫기
const closeSidebar = () => {
if (isMobileView.value) {
isSidebarVisible.value = false;
sidebarVisible.value = false;
}
};
@@ -312,17 +312,7 @@ const loadMenuItems = async () => {
});
menuItems.value = groupMenus;
/*
// 구독관리 메뉴 하드코딩으로 추가
singleMenuItems.value = [
...singleMenus,
{
label: '구독관리',
icon: 'pi pi-credit-card',
to: '/club/subscription'
}
];
*/
singleMenuItems.value = singleMenus;
} else {
menuItems.value = [];
singleMenuItems.value = [];
@@ -534,6 +524,17 @@ body {
.custom-sidebar-menu {
margin-top: 20px;
}
.custom-sidebar-menu.mobile {
position: fixed;
left: 0;
width: 200px;
height: 100%;
z-index: 1200;
background: #fff;
box-shadow: 2px 0 8px rgba(0,0,0,0.1);
transition: left 0.2s;
}
.menu-group-header {
display: flex;
@@ -602,9 +603,11 @@ body {
/* 메인 컨텐츠 스타일 */
.main-content {
width: 100%;
flex: 1;
padding: 20px;
transition: margin-left 0.3s ease;
box-sizing: border-box;
}
.main-content.full-width {
+30 -1
View File
@@ -1,7 +1,10 @@
<template>
<header class="app-header">
<button class="sidebar-toggle" @click="$emit('toggle-sidebar')">
<i class="pi pi-bars"></i>
</button>
<div class="logo">
<img src="@/assets/LaneBowLogo.png" alt="LaneBow Logo" class="logo-img" />
<img src="@/assets/LaneBowLogo.png" alt="LaneBow Logo" class="logo-img header-logo" />
<div class="app-title">볼링 클럽 매니저</div>
</div>
@@ -524,4 +527,30 @@ onBeforeUnmount(() => {
margin: 0.5rem;
}
}
.header-logo {
height: 60px;
transition: height 0.2s;
}
.sidebar-toggle {
display: none;
background: none;
border: none;
font-size: 2rem;
margin-right: 10px;
cursor: pointer;
}
@media (max-width: 600px) {
.header-logo {
height: 36px;
}
.app-title {
font-size: .8rem;
margin-top: -10px;
}
.sidebar-toggle {
display: block;
}
}
</style>
+1
View File
@@ -10,6 +10,7 @@ const apiClient = axios.create({
headers: {
'Content-Type': 'application/json',
},
withCredentials: true,
timeout: 10000
});
@@ -170,4 +170,30 @@ const formatDate = (date) => {
:deep(.p-datatable-wrapper) {
overflow-x: auto;
}
@media (max-width: 600px) {
.activity-log-container, .header, .dashboard-card, .subscription-management-container, .user-management-container, .club-management {
padding: 0.5rem !important;
margin-bottom: 0.5rem !important;
}
.dashboard-card {
padding: 0.7rem !important;
font-size: 0.95rem;
}
.header h2, .dashboard-card h3 {
font-size: 1.1rem !important;
}
.p-datatable-wrapper {
overflow-x: auto !important;
}
/* Dialog 팝업 중앙 정렬 개선 */
:deep(.p-dialog) {
align-items: flex-start !important;
justify-content: center !important;
padding-top: 2.5rem !important;
}
:deep(.p-dialog .p-dialog-content) {
margin-top: 0 !important;
}
}
</style>
@@ -725,4 +725,35 @@ const saveMember = async () => {
color: #666;
line-height: 1.2;
}
@media (max-width: 600px) {
.club-management, .header, .dashboard-card, .activity-log-container, .subscription-management-container, .user-management-container {
padding: 0.5rem !important;
margin-bottom: 0.5rem !important;
}
.dashboard-card {
padding: 0.7rem !important;
font-size: 0.95rem;
}
.header h2, .dashboard-card h3 {
font-size: 1.1rem !important;
}
.features-grid, .plan-details {
grid-template-columns: 1fr !important;
flex-direction: column !important;
gap: 0.7rem !important;
}
.p-datatable-wrapper {
overflow-x: auto !important;
}
/* Dialog 팝업 중앙 정렬 개선 */
:deep(.p-dialog) {
align-items: flex-start !important;
justify-content: center !important;
padding-top: 2.5rem !important;
}
:deep(.p-dialog .p-dialog-content) {
margin-top: 0 !important;
}
}
</style>
+26
View File
@@ -303,4 +303,30 @@ const navigateTo = (path) => {
:deep(.p-datatable .p-datatable-tbody > tr) {
background-color: transparent;
}
@media (max-width: 600px) {
.admin-dashboard, .header, .dashboard-card, .activity-log-container, .subscription-management-container, .user-management-container, .club-management {
padding: 0.5rem !important;
margin-bottom: 0.5rem !important;
}
.dashboard-card {
padding: 0.7rem !important;
font-size: 0.95rem;
}
.header h2, .dashboard-card h3 {
font-size: 1.1rem !important;
}
.p-datatable-wrapper {
overflow-x: auto !important;
}
/* Dialog 팝업 중앙 정렬 개선 */
:deep(.p-dialog) {
align-items: flex-start !important;
justify-content: center !important;
padding-top: 2.5rem !important;
}
:deep(.p-dialog .p-dialog-content) {
margin-top: 0 !important;
}
}
</style>
@@ -16,10 +16,9 @@
dataKey="id"
@row-expand="onPlanExpand"
>
<Column :expander="true" style="width: 3%"/>
<Column :expander="true" style="width: 1%"/>
<Column field="id" header="ID" sortable style="width: 5%"></Column>
<Column field="name" header="플랜명" sortable style="width: 15%"></Column>
<Column field="description" header="설명" style="width: 25%"></Column>
<Column field="name" header="플랜명" sortable></Column>
<Column field="maxMembers" header="최대 회원수" sortable style="width: 10%">
<template #body="slotProps">
{{ slotProps.data.maxMembers }}
@@ -30,7 +29,7 @@
{{ formatCurrency(slotProps.data.price) }}
</template>
</Column>
<Column field="status" header="상태" sortable style="width: 10%">
<Column field="status" header="상태" sortable style="width: 15%">
<template #body="slotProps">
<Badge :value="getStatusLabel(slotProps.data.status)" :severity="getStatusSeverity(slotProps.data.status)" />
</template>
@@ -45,6 +44,10 @@
</Column>
<template #expansion="slotProps">
<div class="plan-details">
<div class="plan-description">
<h4>설명</h4>
<p>{{ slotProps.data.description }}</p>
</div>
<div class="plan-features">
<h4>포함된 기능</h4>
<div class="features-grid">
@@ -603,4 +606,35 @@ const formatDate = (date) => {
font-size: 0.8rem;
padding: 0.3rem 0.5rem;
}
@media (max-width: 600px) {
.subscription-management-container, .header, .dashboard-card, .activity-log-container, .user-management-container, .club-management {
padding: 0.5rem !important;
margin-bottom: 0.5rem !important;
}
.dashboard-card {
padding: 0.7rem !important;
font-size: 0.95rem;
}
.header h2, .dashboard-card h3 {
font-size: 1.1rem !important;
}
.features-grid, .plan-details {
grid-template-columns: 1fr !important;
flex-direction: column !important;
gap: 0.7rem !important;
}
.p-datatable-wrapper {
overflow-x: auto !important;
}
/* Dialog 팝업 중앙 정렬 개선 */
:deep(.p-dialog) {
align-items: flex-start !important;
justify-content: center !important;
padding-top: 2.5rem !important;
}
:deep(.p-dialog .p-dialog-content) {
margin-top: 0 !important;
}
}
</style>
@@ -294,4 +294,30 @@ const formatDate = (date) => {
:deep(.p-password-input) {
width: 100%;
}
@media (max-width: 600px) {
.user-management-container, .header, .dashboard-card, .activity-log-container, .subscription-management-container, .club-management {
padding: 0.5rem !important;
margin-bottom: 0.5rem !important;
}
.dashboard-card {
padding: 0.7rem !important;
font-size: 0.95rem;
}
.header h2, .dashboard-card h3 {
font-size: 1.1rem !important;
}
.p-datatable-wrapper {
overflow-x: auto !important;
}
/* Dialog 팝업 중앙 정렬 개선 */
:deep(.p-dialog) {
align-items: flex-start !important;
justify-content: center !important;
padding-top: 2.5rem !important;
}
:deep(.p-dialog .p-dialog-content) {
margin-top: 0 !important;
}
}
</style>