Skip to content

Commit 89e72f3

Browse files
authored
Merge pull request #82 from wafflestudio/haram
QA 기타 사항 수정
2 parents 5ff9f8b + 9a08e87 commit 89e72f3

17 files changed

Lines changed: 687 additions & 265 deletions

File tree

src/api/auth.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,11 @@ export const logout = async () => {
5555
TokenService.clearTokens();
5656
};
5757

58+
export const deleteAccount = async () => {
59+
await api.delete("/users/me");
60+
TokenService.clearTokens();
61+
};
62+
5863
export const refresh = async () => {
5964
const res = await api.post<AuthTokens>("/auth/refresh");
6065
TokenService.setToken(res.data.accessToken);

src/api/bugReport.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import api from "./axios";
2+
3+
export interface CreateBugReportRequest {
4+
title: string;
5+
content: string;
6+
}
7+
8+
export const createBugReport = async (body: CreateBugReportRequest) => {
9+
await api.post("/api/v1/bug-reports", body);
10+
};

src/contexts/AuthProvider.tsx

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {
22
createContext,
33
type ReactNode,
4+
useCallback,
45
useContext,
56
useEffect,
67
useState,
@@ -22,6 +23,7 @@ interface AuthContextType {
2223
signup: (email: string, password: string) => Promise<void>;
2324
completeSocialLogin: (accessToken: string) => Promise<void>;
2425
logout: () => Promise<void>;
26+
deleteAccount: () => Promise<void>;
2527
updateUsername: (username: string) => Promise<void>;
2628
clearProfileImg: () => Promise<void>;
2729
setProfileImg: (file: File) => Promise<void>;
@@ -96,18 +98,18 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
9698
}
9799
};
98100

99-
const completeSocialLogin = async (accessToken: string) => {
100-
try {
101-
TokenService.setToken(accessToken);
102-
const userData = await auth.getUser();
103-
setUser(userData);
104-
setIsAuthenticated(true);
105-
} catch (err) {
106-
TokenService.clearTokens();
107-
console.error("Completing social login failed:", err);
108-
throw err;
109-
}
110-
};
101+
const completeSocialLogin = useCallback(async (accessToken: string) => {
102+
try {
103+
TokenService.setToken(accessToken);
104+
const userData = await auth.getUser();
105+
setUser(userData);
106+
setIsAuthenticated(true);
107+
} catch (err) {
108+
TokenService.clearTokens();
109+
console.error("Completing social login failed:", err);
110+
throw err;
111+
}
112+
}, []);
111113

112114
/**
113115
* Signup Function
@@ -139,6 +141,17 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
139141
}
140142
};
141143

144+
const deleteAccount = async () => {
145+
try {
146+
await auth.deleteAccount();
147+
setUser(null);
148+
setIsAuthenticated(false);
149+
} catch (error) {
150+
console.error("Server error at account deletion", error);
151+
throw error;
152+
}
153+
};
154+
142155
/**
143156
* Update username
144157
*/
@@ -201,6 +214,7 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
201214
signup,
202215
completeSocialLogin,
203216
logout,
217+
deleteAccount,
204218
updateUsername,
205219
clearProfileImg,
206220
setProfileImg,

src/pages/MyPage.tsx

Lines changed: 140 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { useAuth } from "@/contexts/AuthProvider";
2+
import { createBugReport } from "@/api/bugReport";
23
import Navigationbar from "@/widgets/Navigationbar";
34
import styles from "@styles/MyPage.module.css";
45
import { BookmarkWidget } from "./bookmark/Bookmark";
@@ -7,7 +8,7 @@ import { useNavigate } from "react-router-dom";
78
import { useTimetable } from "@/contexts/TimetableContext";
89
import { useEffect, useState } from "react";
910
import { RiPencilFill } from "react-icons/ri";
10-
import { FaCamera, FaStar } from "react-icons/fa6";
11+
import { FaBug, FaCamera, FaStar, FaTrashCan } from "react-icons/fa6";
1112
import { IoMdDoneAll } from "react-icons/io";
1213
import { useUserData } from "@/contexts/UserDataContext";
1314
import Onboarding from "./auth/OnBoarding/Onboarding";
@@ -147,16 +148,16 @@ const ProfileCard = ({ onClickInterest }: { onClickInterest: () => void }) => {
147148
<span>행사 보기 우선순위</span>
148149
</div>
149150
{interestCategories && interestCategories.length > 0 ? (
150-
<ul className={styles.preferenceChips}>
151-
{interestCategories.map((cat, idx) => (
152-
<li
153-
className={`${styles.preferenceChip} ${cat.groupId === 3 && styles.category} ${cat.groupId === 2 && styles.organization}`}
154-
key={cat.id}
155-
>
156-
{`${idx + 1}순위: ${cat.name}`}
157-
</li>
158-
))}
159-
</ul>
151+
<ul className={styles.preferenceChips}>
152+
{interestCategories.map((cat, idx) => (
153+
<li
154+
className={`${styles.preferenceChip} ${cat.groupId === 3 && styles.category} ${cat.groupId === 2 && styles.organization}`}
155+
key={cat.id}
156+
>
157+
{`${idx + 1}순위: ${cat.name}`}
158+
</li>
159+
))}
160+
</ul>
160161
) : (
161162
<span className={styles.notYetText}>
162163
클릭해서 우선순위로 확인할 행사를 설정해보세요!
@@ -187,6 +188,132 @@ const ProfileCard = ({ onClickInterest }: { onClickInterest: () => void }) => {
187188
);
188189
};
189190

191+
const BugReportSection = () => {
192+
const [title, setTitle] = useState("");
193+
const [content, setContent] = useState("");
194+
const [isSubmitting, setIsSubmitting] = useState(false);
195+
196+
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
197+
event.preventDefault();
198+
199+
const trimmedTitle = title.trim();
200+
const trimmedContent = content.trim();
201+
202+
if (!trimmedTitle || !trimmedContent) {
203+
alert("제목과 내용을 모두 입력해주세요.");
204+
return;
205+
}
206+
207+
if (isSubmitting) return;
208+
209+
try {
210+
setIsSubmitting(true);
211+
await createBugReport({
212+
title: trimmedTitle,
213+
content: trimmedContent,
214+
});
215+
setTitle("");
216+
setContent("");
217+
alert("버그 신고가 접수되었습니다.");
218+
} catch (error) {
219+
console.error("Bug report submission failed:", error);
220+
alert("버그 신고 접수에 실패했습니다. 잠시 후 다시 시도해주세요.");
221+
} finally {
222+
setIsSubmitting(false);
223+
}
224+
};
225+
226+
return (
227+
<section className={styles.bugReportSection}>
228+
<div className={styles.bugReportHeader}>
229+
<div className={styles.bugReportTitle}>
230+
<FaBug size={18} />
231+
<strong>버그 신고</strong>
232+
</div>
233+
<span>이용 중 발견한 문제를 알려주세요.</span>
234+
</div>
235+
<form className={styles.bugReportForm} onSubmit={handleSubmit}>
236+
<input
237+
className={styles.bugReportInput}
238+
type="text"
239+
value={title}
240+
placeholder="제목"
241+
maxLength={100}
242+
onChange={(event) => setTitle(event.currentTarget.value)}
243+
disabled={isSubmitting}
244+
/>
245+
<textarea
246+
className={styles.bugReportTextarea}
247+
value={content}
248+
placeholder="문제가 발생한 상황을 자세히 적어주세요."
249+
rows={5}
250+
maxLength={1000}
251+
onChange={(event) => setContent(event.currentTarget.value)}
252+
disabled={isSubmitting}
253+
/>
254+
<button
255+
className={styles.bugReportSubmitButton}
256+
type="submit"
257+
disabled={isSubmitting}
258+
>
259+
{isSubmitting ? "접수 중" : "신고하기"}
260+
</button>
261+
</form>
262+
</section>
263+
);
264+
};
265+
266+
const AccountDeletionSection = () => {
267+
const { deleteAccount } = useAuth();
268+
const [isConfirmOpen, setIsConfirmOpen] = useState(false);
269+
const [isDeleting, setIsDeleting] = useState(false);
270+
const navigate = useNavigate();
271+
272+
const handleDeleteAccount = async () => {
273+
if (isDeleting) return;
274+
275+
try {
276+
setIsDeleting(true);
277+
await deleteAccount();
278+
navigate("/", { replace: true });
279+
} catch (error) {
280+
console.error("Account deletion failed:", error);
281+
alert("회원탈퇴에 실패했습니다. 잠시 후 다시 시도해주세요.");
282+
} finally {
283+
setIsDeleting(false);
284+
setIsConfirmOpen(false);
285+
}
286+
};
287+
288+
return (
289+
<section className={styles.accountDeletionSection}>
290+
<div className={styles.accountDeletionText}>
291+
<strong>회원탈퇴</strong>
292+
<span>계정을 삭제하면 저장된 정보가 복구되지 않습니다.</span>
293+
</div>
294+
<button
295+
className={styles.deleteAccountButton}
296+
type="button"
297+
onClick={() => setIsConfirmOpen(true)}
298+
disabled={isDeleting}
299+
>
300+
<FaTrashCan size={14} />
301+
<span>{isDeleting ? "탈퇴 처리 중" : "회원탈퇴"}</span>
302+
</button>
303+
{isConfirmOpen && (
304+
<Modal
305+
content="정말 회원탈퇴를 진행하시겠어요? 삭제된 계정은 복구할 수 없습니다."
306+
leftText={isDeleting ? "처리 중" : "탈퇴하기"}
307+
rightText="취소"
308+
onLeftClick={handleDeleteAccount}
309+
onRightClick={() => setIsConfirmOpen(false)}
310+
onClose={() => setIsConfirmOpen(false)}
311+
/>
312+
)}
313+
</section>
314+
);
315+
};
316+
190317
const MyPage = () => {
191318
const { user, isLoading } = useAuth();
192319
const [isEditingInterest, setIsEditingInterest] = useState<boolean>(false);
@@ -211,6 +338,8 @@ const MyPage = () => {
211338
<BookmarkWidget />
212339
<MemoWidget />
213340
</div>
341+
<BugReportSection />
342+
<AccountDeletionSection />
214343
</div>
215344
) : (
216345
<div className={styles.notFound}>

src/pages/timetable/TimeTableToolbar.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import type { Semester } from "../../util/types";
2-
import { IoIosSearch } from "react-icons/io";
32
import { useAuth } from "../../contexts/AuthProvider";
43
import styles from "../../styles/Toolbar.module.css";
54
import { useNavigate } from "react-router-dom";
5+
import SearchButton from "../../widgets/SearchButton";
66

77
interface TimeTableToolbarProps {
88
timetableName: string;
@@ -95,7 +95,7 @@ const TimeTableToolbar = ({
9595
</div>
9696

9797
<div className={styles.profileRow}>
98-
<IoIosSearch size={20} color="rgba(130, 130, 130, 1)" />
98+
<SearchButton />
9999
<button
100100
type="button"
101101
className={styles.profileButton}

src/pages/timetable/TimetableGrid.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ export function TimetableGrid({
7474
const hourMarks = useMemo(() => {
7575
const list: { hour: number; top: number; label: string }[] = [];
7676
for (let h = config.startHour; h <= config.endHour; h++) {
77-
const top = (h * 60 - config.startHour * 60) * config.ppm;
77+
const top = (h * 60 - config.startHour * 60) * config.ppm + config.ppm*15;
7878
const labelHour = isMobile
7979
? String(h % 12 || 12)
8080
: formatAmPmFromMinutes(h * 60);

src/router/AppRoutes.tsx

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Route, Routes } from "react-router-dom";
1+
import { Navigate, Route, Routes } from "react-router-dom";
22
import Home from "../pages/auth/Home";
33
import Login from "../pages/auth/Login/Login";
44
import LoginHandler from "../pages/auth/Login/SocialLoginHandler";
@@ -28,20 +28,20 @@ export default function AppRoutes() {
2828
{/* OAuth Redirect */}
2929
<Route path="/auth/callback" element={<LoginHandler />} />
3030

31-
{/* Main Feature page */}
32-
<Route path="/main" element={<CalendarView />} />
33-
<Route path="/main/day" element={<MainDay />} />
31+
{/* Main Feature page */}
32+
<Route path="/main" element={<CalendarView />} />
33+
<Route path="/main/day" element={<MainDay />} />
3434

35-
{/* Timetable page */}
36-
<Route path="/timetable" element={<TimetablePage />} />
35+
{/* Timetable page */}
36+
<Route path="/timetable" element={<TimetablePage />} />
3737

38-
{/* Search page */}
39-
<Route path="/search" element={<SearchView />} />
38+
{/* Search page */}
39+
<Route path="/search" element={<SearchView />} />
4040

41-
{/* Mypage & bookmark & memo */}
42-
<Route path="/my" element={<MyPage />} />
43-
<Route path="/bookmark" element={<BookmarksPage />} />
44-
<Route path="/memo" element={<MemoPage />} />
41+
{/* Mypage & bookmark & memo */}
42+
<Route path="/my" element={<MyPage />} />
43+
<Route path="/bookmark" element={<BookmarksPage />} />
44+
<Route path="/memo" element={<MemoPage />} />
4545

4646
{/* Admin page */}
4747
<Route
@@ -52,6 +52,8 @@ export default function AppRoutes() {
5252
</AdminRoute>
5353
}
5454
/>
55+
56+
<Route path="*" element={<Navigate to="/" replace />} />
5557
</Routes>
5658
</>
5759
);

src/styles/DetailMemo.module.css

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,12 @@
4242
background: #f6f6f6;
4343
}
4444

45+
.memoIconBtn:disabled,
46+
.memoTriggerText:disabled {
47+
cursor: default;
48+
opacity: 0.6;
49+
}
50+
4551
.memoTriggerText {
4652
background: transparent;
4753
border: none;
@@ -167,6 +173,11 @@ textarea.memoInput {
167173
opacity: 0.8;
168174
}
169175

176+
.tagInput:disabled {
177+
background: #fafafa;
178+
cursor: not-allowed;
179+
}
180+
170181
/* chips */
171182
.tagChips {
172183
display: flex;

0 commit comments

Comments
 (0)