Skip to content

Commit bf425c8

Browse files
committed
2 parents c8641e1 + cf4bf90 commit bf425c8

83 files changed

Lines changed: 1314 additions & 349 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/ticket-admin/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"axios": "^1.10.0",
1717
"classnames": "^2.5.1",
1818
"next": "15.3.6",
19+
"query-string": "^9.3.1",
1920
"react": "^19.0.0",
2021
"react-dom": "^19.0.0",
2122
"react-error-boundary": "^6.0.0"
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { cookies } from "next/headers";
2+
import { NextResponse } from "next/server";
3+
4+
import { API_URL } from "@/data/constants";
5+
6+
export async function POST(req: Request) {
7+
const cookiesStore = await cookies();
8+
9+
const apiRes = await fetch(
10+
process.env.NEXT_PUBLIC_TICKET_API_BASE_URL + API_URL.USER.REISSUE_ACCESS_TOKEN,
11+
{
12+
method: "POST",
13+
headers: {
14+
"Content-Type": "application/json",
15+
Cookie: `accessToken=${cookiesStore.get("accessToken")?.value}; refreshToken=${cookiesStore.get("refreshToken")?.value}`,
16+
},
17+
credentials: "include",
18+
},
19+
);
20+
21+
const setCookies = apiRes.headers.getSetCookie();
22+
23+
const res = NextResponse.json(await apiRes.json(), {
24+
status: apiRes.status,
25+
});
26+
27+
// API 서버가 내려준 모든 Set-Cookie를 그대로 전달
28+
for (const cookie of setCookies) {
29+
// 기존 쿠키 그대로 전달
30+
// SSR 환경에서 브라우저 쿠키 사용할 수 있도록 하기 위함
31+
res.headers.append("Set-Cookie", cookie);
32+
33+
// Domain 추가한 쿠키 한 번 더 전달
34+
// 브라우저 단에서 자동으로 쿠키가 포함한 요청이 갈 수 있도록 하기 위함
35+
res.headers.append("Set-Cookie", `${cookie}; Domain=.permitseoul.com`);
36+
}
37+
38+
return res;
39+
}

apps/ticket-admin/src/app/events/[eventId]/edit/coupon/_clientBoundary/CouponManagementClient/index.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,16 @@ type Props = {
1818
};
1919

2020
export const CouponManagementClient = ({ eventId }: Props) => {
21-
const { data: couponsData, isLoading, refetch } = useCouponsQuery({ eventId });
21+
const {
22+
data: couponsData,
23+
isLoading,
24+
refetch,
25+
} = useCouponsQuery({
26+
eventId,
27+
options: {
28+
refetchOnWindowFocus: true,
29+
},
30+
});
2231
const [isEditMode, setIsEditMode] = useState(false);
2332
const [isSubmitting, setIsSubmitting] = useState(false);
2433

apps/ticket-admin/src/app/events/[eventId]/edit/rounds/add/_clientBoundary/AddRoundFormClient/index.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ export function AddRoundFormClient({ eventId }: Props) {
3636

3737
const { data: eventDetailData } = useEventDetailQuery({
3838
eventId,
39+
options: {
40+
refetchOnWindowFocus: true,
41+
},
3942
});
4043

4144
// 티켓 라운드 이름 필드

apps/ticket-admin/src/app/events/[eventId]/edit/ticket-detail/_clientBoundary/EditTicketDetailFormClient/index.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,16 @@ export function EditTicketDetailFormClient({ eventId, ticketRoundId }: Props) {
2525

2626
const { data: eventDetailData } = useEventDetailSuspenseQuery({
2727
eventId,
28+
options: {
29+
refetchOnWindowFocus: true,
30+
},
2831
});
2932

3033
const { data: ticketsDetail } = useTicketsDetailSuspenseQuery({
3134
ticketRoundId,
35+
options: {
36+
refetchOnWindowFocus: true,
37+
},
3238
});
3339

3440
const { mutateAsync: updateTickets } = usePatchTicketsMutation({});

apps/ticket-admin/src/app/events/create/_clientBoundary/EventFormClient/index.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -243,8 +243,6 @@ export function EventFormClient() {
243243
const venueField = useTextField({
244244
initialValue: "",
245245
validate: (value: string) => {
246-
if (!value.trim()) return "장소를 입력해주세요.";
247-
248246
return undefined;
249247
},
250248
onChange: (value: string) => {
@@ -624,8 +622,9 @@ export function EventFormClient() {
624622
console.log(error);
625623

626624
if (isAxiosErrorResponse(error)) {
627-
alert("이벤트 생성 중 오류가 발생했습니다. 다시 시도해주세요.\n" + error.message);
628-
console.error("Error creating event:", error);
625+
alert(
626+
"이벤트 생성 중 오류가 발생했습니다. 다시 시도해주세요.\n" + error.response?.data.message,
627+
);
629628
}
630629
} finally {
631630
setIsSubmitting(false);

apps/ticket-admin/src/app/events/create/_components/EventFormLayout/index.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,6 @@ export function EventFormLayout({
304304
<Typography type="body16" weight="bold">
305305
Venue
306306
</Typography>
307-
<div className={cx("required")}>*</div>
308307
</Flex>
309308
<TextField
310309
readOnly={isReadOnlyMode}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { useState } from "react";
2+
3+
import { Button, Dialog, Flex } from "@permit/design-system";
4+
import { usePutUserRoleMutation } from "@/data/admin/putUserRole/mutation";
5+
import { ModalComponentProps } from "@/shared/hooks/useModal/types";
6+
import { isAxiosErrorResponse } from "@/shared/types/axioxError";
7+
8+
type Props = { userId: number } & ModalComponentProps<{ result: boolean }>;
9+
10+
export const ChangeRoleModal = ({ userId, isOpen, close }: Props) => {
11+
const [isSubmitting, setIsSubmitting] = useState(false);
12+
13+
const { mutateAsync } = usePutUserRoleMutation({ userId });
14+
15+
const changeUserRole = async (role: "ADMIN" | "STAFF" | "USER") => {
16+
setIsSubmitting(true);
17+
18+
try {
19+
await mutateAsync({ role });
20+
alert(`${role}으로 권한 변경에 성공하였습니다.`);
21+
} catch (error) {
22+
if (isAxiosErrorResponse(error)) {
23+
alert(error.response?.data.message || "Failed to change user role.");
24+
25+
return;
26+
}
27+
} finally {
28+
setIsSubmitting(false);
29+
}
30+
};
31+
32+
const handleClose = () => {
33+
if (!isSubmitting) {
34+
close();
35+
}
36+
};
37+
38+
return (
39+
<Dialog open={isOpen} onClose={handleClose} title="Change User Role">
40+
<Dialog.Content>
41+
<Flex gap={12}>
42+
<Button
43+
variant="primary"
44+
size="md"
45+
onClick={() => changeUserRole("ADMIN")}
46+
disabled={isSubmitting}
47+
>
48+
ADMIN
49+
</Button>
50+
<Button
51+
variant="primary"
52+
size="md"
53+
onClick={() => changeUserRole("STAFF")}
54+
disabled={isSubmitting}
55+
>
56+
STAFF
57+
</Button>
58+
<Button
59+
variant="primary"
60+
size="md"
61+
onClick={() => changeUserRole("USER")}
62+
disabled={isSubmitting}
63+
>
64+
USER
65+
</Button>
66+
</Flex>
67+
</Dialog.Content>
68+
</Dialog>
69+
);
70+
};
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
.container {
2+
width: 1200px;
3+
margin: 0 auto;
4+
}
5+
6+
.header {
7+
margin-bottom: 16px;
8+
}
9+
10+
.input {
11+
width: 400px;
12+
}
13+
14+
.serach_area {
15+
margin-top: 40px;
16+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
"use client";
2+
3+
import { useState } from "react";
4+
import { useQueryClient } from "@tanstack/react-query";
5+
import classNames from "classnames/bind";
6+
7+
import { Button, Flex, TextField, Typography } from "@permit/design-system";
8+
import { useTextField } from "@permit/design-system/hooks";
9+
import { userOptions } from "@/data/admin/getUser/queries";
10+
import { UserResponse } from "@/data/admin/getUser/types";
11+
import { useModal } from "@/shared/hooks/useModal";
12+
import { isAxiosErrorResponse } from "@/shared/types/axioxError";
13+
14+
import { ChangeRoleModal } from "../ChangeRoleModal";
15+
import styles from "./index.module.scss";
16+
17+
const cx = classNames.bind(styles);
18+
19+
export const UserManagementClient = () => {
20+
const qc = useQueryClient();
21+
const { show: changeUserRoleModal } = useModal(ChangeRoleModal);
22+
23+
const [userInfo, setUserInfo] = useState<UserResponse | null>(null);
24+
25+
const userEmail = useTextField({
26+
initialValue: "",
27+
validate: (value: string) => {
28+
if (!value.trim()) return "유저 이메일을 입력해주세요.";
29+
30+
return undefined;
31+
},
32+
});
33+
34+
const searchUser = async () => {
35+
const userEmailValid = userEmail.validateValue();
36+
37+
if (!userEmailValid) {
38+
return;
39+
}
40+
41+
try {
42+
const userInfoData = await qc.fetchQuery(userOptions({ email: userEmail.value }));
43+
44+
setUserInfo(userInfoData);
45+
} catch (error) {
46+
if (isAxiosErrorResponse(error)) {
47+
setUserInfo(null);
48+
49+
alert(error.response?.data.message || "유저를 찾을 수 없습니다.");
50+
}
51+
}
52+
};
53+
54+
return (
55+
<div className={cx("container")}>
56+
<header className={cx("header")}>
57+
<Typography type="title32">User Management</Typography>
58+
<Typography type="body14" color="gray300">
59+
* 권한을 바꾼 후 적용하려면 로그인을 다시 해주세요.
60+
</Typography>
61+
</header>
62+
<main>
63+
<Flex gap={20}>
64+
<TextField
65+
className={cx("input")}
66+
placeholder="이메일을 입력해주세요"
67+
value={userEmail.value}
68+
onChange={userEmail.handleChange}
69+
error={userEmail.error}
70+
/>
71+
<Button variant="cta" onClick={searchUser}>
72+
Search
73+
</Button>
74+
</Flex>
75+
76+
{userInfo && (
77+
<div className={cx("serach_area")}>
78+
<Flex justify="space-between">
79+
<Flex direction="column">
80+
<Typography type="title20">userName: {userInfo.userName}</Typography>
81+
<Typography type="title20">current Role: {userInfo.currentUserRole}</Typography>
82+
</Flex>
83+
<Button
84+
variant="primary"
85+
onClick={() => changeUserRoleModal({ userId: userInfo.userId })}
86+
>
87+
Change Role
88+
</Button>
89+
</Flex>
90+
</div>
91+
)}
92+
</main>
93+
</div>
94+
);
95+
};

0 commit comments

Comments
 (0)