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
154 changes: 154 additions & 0 deletions frontend/src/AccountPage.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
.page {
display: flex;
justify-content: center;
padding: 2rem 1rem;
width: 100%;
}

.card {
background: white;
border-radius: 1rem;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.06);
padding: 2rem;
width: 100%;
max-width: 520px;
}

.title {
margin: 0 0 1.5rem;
font-size: 1.4rem;
color: #222;
}

.section {
margin-bottom: 2rem;
}

.section:last-child {
margin-bottom: 0;
}

.section-title {
font-size: 0.8rem;
font-weight: 600;
letter-spacing: 0.06em;
text-transform: uppercase;
color: #999;
margin: 0 0 0.75rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid #f0f0f0;
}

.field {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.5rem 0;
}

.label {
width: 110px;
flex-shrink: 0;
font-size: 0.875rem;
font-weight: 500;
color: #555;
}

.field-value {
display: flex;
align-items: center;
gap: 0.75rem;
flex: 1;
}

.field-text {
font-size: 0.95rem;
color: #333;
}

.inline-edit {
display: flex;
align-items: center;
gap: 0.5rem;
flex: 1;
}

.input {
flex: 1;
padding: 0.45rem 0.65rem;
border: 1px solid #ddd;
border-radius: 0.4rem;
color: #333;
outline: none;
transition: border-color 0.15s;
}

.input:focus {
border-color: #4facfe;
}

.btn-primary {
padding: 0.45rem 1rem;
background-color: #4facfe;
color: white;
border: none;
border-radius: 0.4rem;
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: background-color 0.15s;
}

.btn-primary:hover {
background-color: #0089f2;
}

.btn-secondary {
padding: 0.45rem 1rem;
background-color: white;
color: #555;
border: 1px solid #ddd;
border-radius: 0.4rem;
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: background-color 0.15s, border-color 0.15s;
}

.btn-secondary:hover {
background-color: #f5f5f5;
border-color: #bbb;
}

.btn-secondary:disabled {
opacity: 0.5;
cursor: default;
}

.btn-danger {
padding: 0.45rem 1rem;
background-color: white;
color: #e53935;
border: 1px solid #e53935;
border-radius: 0.4rem;
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: background-color 0.15s;
}

.btn-danger:hover {
background-color: #ffebee;
}

.error {
font-size: 0.875rem;
color: #e53935;
margin: 0.25rem 0;
}

.success {
font-size: 0.875rem;
color: #2e7d32;
margin: 0.25rem 0;
}
113 changes: 113 additions & 0 deletions frontend/src/AccountPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuth } from '@/useAuth.tsx';
import { useAxios } from '@/useAxios.tsx';
import { AccountDto } from '@/types.ts';
import styles from './AccountPage.module.css';

const AccountPage = () => {
const { user, reloadUser } = useAuth();
const api = useAxios();
const navigate = useNavigate();

const [accountData, setAccountData] = useState<AccountDto | null>(null);

const [editingName, setEditingName] = useState(false);
const [nameInput, setNameInput] = useState('');
const [nameMessage, setNameMessage] = useState<{ text: string; error: boolean } | null>(null);

const fetchAccountData = async () => {
if (!user) {
return;
}
try {
const res = await api.get<AccountDto>(`/users/${user.id}`);
setAccountData(res.data);
} catch {
// leave accountData null; page will show empty state
}
};

useEffect(() => {
(async () => {
await fetchAccountData();
})();
}, []);

const handleEditName = () => {
setNameInput(user?.name ?? '');
setNameMessage(null);
setEditingName(true);
};

const handleSaveName = async () => {
if (!user) {
return;
}
setNameMessage(null);
try {
await api.patch(`/users/${user.id}`, { name: nameInput });
await reloadUser();
setEditingName(false);
setNameMessage({ text: '별명이 변경되었습니다.', error: false });
} catch {
setNameMessage({ text: '별명 변경에 실패했습니다.', error: true });
}
};

return (
<div className={styles.page}>
<div className={styles.card}>
<h2 className={styles.title}>계정</h2>

<section className={styles.section}>
<h3 className={styles.sectionTitle}>기본 정보</h3>

<div className={styles.field}>
<span className={styles.label}>이름</span>
{editingName ? (
<div className={styles.inlineEdit}>
<input
name={'name-input'}
className={styles.input}
value={nameInput}
onChange={e => setNameInput(e.target.value)}
autoFocus
/>
<button className={styles.btnPrimary} onClick={handleSaveName}>저장</button>
<button className={styles.btnSecondary} onClick={() => setEditingName(false)}>취소</button>
</div>
) : (
<div className={styles.fieldValue}>
<span className={styles.fieldText}>{user?.name}</span>
<button className={styles.btnSecondary} onClick={handleEditName}>수정</button>
</div>
)}
</div>
{nameMessage && (
<p className={nameMessage.error ? styles.error : styles.success}>{nameMessage.text}</p>
)}

<div className={styles.field}>
<span className={styles.label}>이메일</span>
<span className={styles.fieldText}>{accountData?.user.email}</span>
</div>
</section>

{accountData?.user.hasPassword && (
<section className={styles.section}>
<h3 className={styles.sectionTitle}>보안</h3>
<button
className={styles.btnDanger}
onClick={() => navigate('/account/change-password')}
>
비밀번호 변경
</button>
</section>
)}
</div>
</div>
);
};

export default AccountPage;
4 changes: 4 additions & 0 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import SignupPage from '@/SignupPage.tsx';
import GoogleCallbackPage from '@/GoogleCallbackPage.tsx';
import InitNamePage from '@/InitNamePage.tsx';
import NavBar from "@/NavBar.tsx";
import AccountPage from '@/AccountPage.tsx';
import ChangePasswordPage from '@/ChangePasswordPage.tsx';
import HomePage from '@/HomePage/HomePage.tsx';
import RoomDetailPage from '@/RoomDetailPage.tsx';
import './App.css';
Expand Down Expand Up @@ -47,6 +49,8 @@ const AppContent = () => {
<Route element={<AuthenticatedLayout/>}>
<Route path="/" element={<HomePage/>}/>
<Route path="/rooms/:roomId" element={<RoomDetailPage/>}/>
<Route path="/account" element={<AccountPage/>}/>
<Route path="/account/change-password" element={<ChangePasswordPage/>}/>
{user?.role === 'ADMIN' && (
<Route path="/admin" element={<Suspense fallback={<div>Loading...</div>}>
<AdminPage/>
Expand Down
109 changes: 109 additions & 0 deletions frontend/src/ChangePasswordPage.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
.page {
display: flex;
justify-content: center;
padding: 2rem 1rem;
width: 100%;
}

.card {
background: white;
border-radius: 1rem;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.06);
padding: 2rem;
width: 100%;
max-width: 420px;
}

.title {
margin: 0 0 1.5rem;
font-size: 1.4rem;
color: #222;
}

.form {
display: flex;
flex-direction: column;
gap: 0.75rem;
}

.form-group {
display: flex;
flex-direction: column;
gap: 0.3rem;
}

.label {
font-size: 0.875rem;
font-weight: 500;
color: #555;
}

.input {
padding: 0.45rem 0.65rem;
border: 1px solid #ddd;
border-radius: 0.4rem;
color: #333;
outline: none;
transition: border-color 0.15s;
font-size: 1rem;
}

.input:focus {
border-color: #4facfe;
}

.actions {
display: flex;
gap: 0.5rem;
margin-top: 1rem;
}

.btn-primary {
padding: 0.45rem 1rem;
background-color: #4facfe;
color: white;
border: none;
border-radius: 0.4rem;
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: background-color 0.15s;
}

.btn-primary:hover:not(:disabled) {
background-color: #0089f2;
}

.btn-primary:disabled {
opacity: 0.6;
cursor: default;
}

.btn-secondary {
padding: 0.45rem 1rem;
background-color: white;
color: #555;
border: 1px solid #ddd;
border-radius: 0.4rem;
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: background-color 0.15s, border-color 0.15s;
}

.btn-secondary:hover {
background-color: #f5f5f5;
border-color: #bbb;
}

.error {
font-size: 0.875rem;
color: #e53935;
margin: 0.25rem 0;
}

.success {
font-size: 0.875rem;
color: #2e7d32;
margin: 0.25rem 0;
}
Loading