Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions frontend/src/assets/right-arrow.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions frontend/src/assets/setting_icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -52,21 +52,6 @@ const SessionManagementCard = ({ styles: commonStyles }) => {
fetchRounds();
}, [selectedSessionId, roundsVersion]);

const handleAddRound = async (newRoundData) => {
if (!selectedSessionId) {
toast.error('세션을 먼저 선택해주세요.');
return;
}

try {
await handleAddRounds(selectedSessionId, [newRoundData]);

toast.success('라운드가 추가되었습니다.');
} catch (err) {
toast.error('라운드 추가에 실패했습니다.');
}
};

return (
<div className={styles.sessionManagementCardContainer}>
<div className={commonStyles.header}>
Expand Down Expand Up @@ -94,7 +79,7 @@ const SessionManagementCard = ({ styles: commonStyles }) => {
{/*세션 선택 드롭다운 */}
<div className={styles.selectGroup}>
<select
value={selectedSessionId}
value={selectedSessionId || ''}
onChange={(e) => setSelectedSessionId(e.target.value)}
>
<option value="" disabled>
Expand Down
31 changes: 20 additions & 11 deletions frontend/src/components/mypage/AccountSecurity.jsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,28 @@
import { useState } from 'react';
import styles from './AccountSecurity.module.css';
import lockIcon from '../../assets/lock.svg';
import settingIcon from '../../assets/setting_icon.svg';
import EditProfileModal from './EditProfileModal';

const AccountSecurity = () => {
const [isModalOpen, setIsModalOpen] = useState(false);

return (
<div className={styles.container}>
<h2 className={styles.header}>계정 보안</h2>
<p className={styles.description}>
계정 보안을 위해 비밀번호를 정기적으로 변경하세요.
</p>
<>
<div className={styles.container}>
<button
type="button"
className={styles.button}
onClick={() => setIsModalOpen(true)}
>
<img src={settingIcon} alt="설정 아이콘" className={styles.icon} />
<span className={styles.buttonName}>개인정보 수정하기</span>
</button>
</div>

<button type="button" className={styles.button}>
<img src={lockIcon} alt="잠금 아이콘" className={styles.icon} />
<span className={styles.buttonName}>비밀번호 수정</span>
</button>
</div>
{isModalOpen && (
<EditProfileModal onClose={() => setIsModalOpen(false)} />
)}
</>
);
};

Expand Down
11 changes: 7 additions & 4 deletions frontend/src/components/mypage/ActivityModal.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ import AttendanceSection from './AttendanceSection';
import ActivitySection from './ActivitySection';
import PointsSection from './PointsSection';

const ActivityModal = ({ isOpen, onClose, title, kind, data }) => {
const ActivityModal = ({ isOpen, onClose, title, kind }) => {
useEffect(() => {
if (!isOpen) return;

const onKeyDown = (e) => {
if (e.key === 'Escape') onClose();
};

document.addEventListener('keydown', onKeyDown);
return () => document.removeEventListener('keydown', onKeyDown);
}, [isOpen, onClose]);
Expand All @@ -26,6 +28,7 @@ const ActivityModal = ({ isOpen, onClose, title, kind, data }) => {
<div className={styles.modalContent} onClick={(e) => e.stopPropagation()}>
<div className={styles.modalHeader}>
<h2 className={styles.modalTitle}>{title}</h2>

<button
type="button"
className={styles.closeButton}
Expand All @@ -39,9 +42,9 @@ const ActivityModal = ({ isOpen, onClose, title, kind, data }) => {
<hr className={styles.modalDivider} />

<div className={styles.modalBody}>
{kind === 'attendance' && <AttendanceSection items={data.items} />}
{kind === 'activity' && <ActivitySection items={data.items} />}
{kind === 'points' && <PointsSection items={data.items} />}
{kind === 'attendance' && <AttendanceSection />}
{kind === 'activity' && <ActivitySection />}
{kind === 'points' && <PointsSection />}
</div>
</div>
</div>
Expand Down
66 changes: 66 additions & 0 deletions frontend/src/components/mypage/ActivityModal.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -116,3 +116,69 @@
font-weight: 700;
color: #c62828;
}
/* Pagination 스타일 */
.pagination {
display: flex;
justify-content: center;
gap: 8px;
margin-top: 16px;
flex-wrap: wrap;
}

.pageButton {
min-width: 32px;
height: 32px;
border: 1px solid #d4d4d4;
border-radius: 6px;
background-color: #fff;
cursor: pointer;
font-size: 14px;
font-weight: 500;
color: #171717;
}

.pageButton:hover {
background-color: #f0f0f0;
}

.activePage {
background-color: #e0e0e0; /* 현재 페이지 연한 그레이 */
font-weight: 700;
}

.pagination button:hover:not(:disabled) {
background-color: #f1f1f1;
border-color: #999;
}

.pagination button:disabled {
opacity: 0.5;
cursor: not-allowed;
}

.pagination .pageInfo {
font-size: 14px;
font-weight: 500;
color: #171717;
min-width: 50px;
text-align: center;
}

/* 모바일 대응 */
@media (max-width: 600px) {
.pagination {
gap: 8px;
}

.pagination button {
min-width: 32px;
height: 32px;
font-size: 13px;
padding: 0 8px;
}

.pagination .pageInfo {
font-size: 12px;
min-width: 40px;
}
}
73 changes: 62 additions & 11 deletions frontend/src/components/mypage/ActivitySection.jsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,68 @@
import { useState, useEffect } from 'react';
import styles from './ActivityModal.module.css';
import { getActivityLogs } from '../../utils/myPageMenu';

const ActivitySection = () => {
const [items, setItems] = useState([]);
const [page, setPage] = useState(0);
const [totalPages, setTotalPages] = useState(1);
const size = 5;

const fetchActivityLogs = async (page) => {
try {
const res = await getActivityLogs(page, size);

setItems(
(res?.content || []).map((it) => ({
id: it.id,
content: it.message,
time: new Date(it.createdAt).toLocaleString(),
}))
);

setTotalPages(res?.totalPages || 1);
} catch (error) {
console.error(error);
}
};

useEffect(() => {
fetchActivityLogs(page);
}, [page]);

const renderPagination = () => {
const pages = [];

for (let i = 0; i < totalPages; i++) {
pages.push(
<button
key={i}
className={`${styles.pageButton} ${i === page ? styles.activePage : ''}`}
onClick={() => setPage(i)}
>
{i + 1}
</button>
);
}

return <div className={styles.pagination}>{pages}</div>;
};

const ActivitySection = ({ items }) => {
return (
<ul className={styles.list}>
{items.map((it) => (
<li key={it.id} className={styles.row}>
<div className={styles.left}>
<div className={styles.title}>{it.content}</div>
<div className={styles.time}>{it.time}</div>
</div>
</li>
))}
</ul>
<div>
<ul className={styles.list}>
{items.map((it) => (
<li key={it.id} className={styles.row}>
<div className={styles.left}>
<div className={styles.title}>{it.content}</div>
<div className={styles.time}>{it.time}</div>
</div>
</li>
))}
</ul>

{renderPagination()}
</div>
);
};

Expand Down
77 changes: 77 additions & 0 deletions frontend/src/components/mypage/ChangeInfoForm.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { useState, useEffect } from 'react';
import styles from './EditProfileModal.module.css';

const passwordPolicy = [
{ label: '8~20자 이내', test: (pw) => pw.length >= 8 && pw.length <= 20 },
{ label: '최소 1개의 대문자 포함', test: (pw) => /[A-Z]/.test(pw) },
{ label: '최소 1개의 소문자 포함', test: (pw) => /[a-z]/.test(pw) },
{ label: '최소 1개의 숫자 포함', test: (pw) => /[0-9]/.test(pw) },
{ label: '최소 1개의 특수문자 포함', test: (pw) => /[\W_]/.test(pw) },
];

export default function ChangeInfoForm({ onValidChange }) {
const [currentPassword, setCurrentPassword] = useState('');
const [newPassword, setNewPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');

const passwordValid = passwordPolicy.every((p) => p.test(newPassword));

const isValid =
currentPassword && passwordValid && newPassword === confirmPassword;

useEffect(() => {
onValidChange(
isValid
? {
currentPassword,
newPassword,
}
: null
);
}, [isValid, currentPassword, newPassword, onValidChange]);

return (
<div className={styles.modalContent}>
<div className={styles.inputGroup}>
<label className={styles.label}>현재 비밀번호</label>

<input
className={styles.codeInput}
type="password"
value={currentPassword}
onChange={(e) => setCurrentPassword(e.target.value)}
/>
</div>

<div className={styles.inputGroup}>
<label className={styles.label}>새 비밀번호</label>

<input
className={styles.codeInput}
type="password"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
/>
</div>

<div className={styles.inputGroup}>
<label className={styles.label}>비밀번호 확인</label>

<input
className={styles.codeInput}
type="password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
/>
</div>

<div>
{passwordPolicy.map((rule, index) => (
<p key={index}>
{rule.test(newPassword) ? '✅' : '❌'} {rule.label}
</p>
))}
</div>
</div>
);
}
Loading