Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[modify] 로그아웃 기능 추가, 자잘한 버그 수정, 접근성 개선 #123

Merged
merged 26 commits into from
Aug 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
8d258bc
[modify] 선착순 이벤트 id 고정
lybell-art Aug 20, 2024
c7baa45
[feat] openModal 함수에 닫기 동작을 정의할 수 있도록 Promise화 (resolve #117)
lybell-art Aug 20, 2024
aeddac3
[modify] 이벤트 등록 완료 모달 후 페이지를 바꾸도록 변경
lybell-art Aug 20, 2024
7e0a504
[refactor] 알러트 모달 스타일을 공통 컴포넌트로 분리
lybell-art Aug 20, 2024
4f16a5a
[refactor] 공통 양식을 가진 모달 전부 공통 컴포넌트 적용
lybell-art Aug 20, 2024
ad5c10f
[feat] 로그아웃 모달 추가
lybell-art Aug 20, 2024
0a2a71a
[feat] 로그아웃 알러트 모달 추가
lybell-art Aug 20, 2024
48b7c46
[feat] 로그아웃 확인 모달에서 확인을 누르면 로그아웃 알러트 모달이 뜨도록 변경
lybell-art Aug 20, 2024
fb7eba3
[feat] 로그아웃 기능 통합 (resolve #78)
lybell-art Aug 20, 2024
26a9721
[design] 로그아웃 버튼 호버/포커스 시 문구가 뜨도록 수정
lybell-art Aug 20, 2024
e5d5d7f
[fix] 로그아웃 후 로그인시 발생하는 데이터 불일치 문제 수정
lybell-art Aug 20, 2024
749be5e
[fix] 로그인 된 상태에서 로그아웃하고, 다시 로그인한 뒤, 로그아웃하면 상태가 갱신되지 않는 버그 수정
lybell-art Aug 20, 2024
2815c00
[fix] zustand-getData 로직이 무한으로 요청을 보내는 버그 수정
lybell-art Aug 20, 2024
cb7173e
[fix] 선착순이벤트 zustand 데이터 페처도 적절하게 변경
lybell-art Aug 20, 2024
e230b86
[modify] GNB 탭키로 조작할 수 있도록 수정
lybell-art Aug 20, 2024
5ddc3b5
[modify] QnA 탭키로 조작할 수 있도록 수정
lybell-art Aug 20, 2024
ec64faa
[feat] 모달 포커스 트랩 추가
lybell-art Aug 20, 2024
3fd96b9
[modify] 상세 스와이퍼 접근성 개선
lybell-art Aug 20, 2024
7bce1e9
[modify] ESC 키를 눌렀을 때 모달이 닫히도록 수정
lybell-art Aug 20, 2024
89d7a6c
[modify] 카드 아이템, 기대평 캐러셀 aria-label 수정
lybell-art Aug 20, 2024
f5068c6
[fix] 인터랙션 모달 정답을 열었을 때 탭 키가 모달 바깥으로 튀는 버그 수정
lybell-art Aug 20, 2024
d8c7060
[modify] 보조금 이벤트 키보드 접근성 추가
lybell-art Aug 20, 2024
45a2150
[modify] V2L 인터랙션 button으로 변경, 적절한 라벨 추가
lybell-art Aug 20, 2024
53d27ce
[modify] 기대평이 width보다 적게 보여질 때 후처리 추가
lybell-art Aug 20, 2024
ef57bfd
[modify] 기대평 없을 때 폴백 UI 추가 (resolve #84)
lybell-art Aug 20, 2024
b063d9f
[chore] 린트 수정 및 프리티어 적용
lybell-art Aug 20, 2024
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: 1 addition & 2 deletions src/adminPage/features/eventEdit/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,7 @@ function EventEditor({ initialData = null } = {}) {
title={`${mode === "create" ? "등록" : "수정"} 완료`}
description={`이벤트가 성공적으로 ${mode === "create" ? "등록" : "수정"}되었습니다!`}
/>,
);
navigate(mode === "create" ? "/events" : `/events/${state.eventId}`);
).then(() => navigate(mode === "create" ? "/events" : `/events/${state.eventId}`));
},
onError: (e) => {
openModal(<AlertModal title="등록 실패" description={e.message} />);
Expand Down
2 changes: 1 addition & 1 deletion src/common/constants.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export const EVENT_FCFS_ID = 1;
export const EVENT_FCFS_ID = "HD_240808_001";
export const EVENT_DRAW_ID = "HD-19700101-01";
export const EVENT_ID = "the-new-ioniq5";
export const EVENT_START_DATE = new Date(2024, 8, 9);
Expand Down
24 changes: 13 additions & 11 deletions src/common/dataFetch/getQuery.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,18 +103,20 @@ export function useQuery(key, promiseFn, config = {}) {
*
* @return Function : 호출 시, 실제로 post 요청을 발송하는 함수를 반환합니다.
*/
export async function mutate(key, promiseFn, { onSuccess, onError } = {}) {
try {
const value = await promiseFn();
updateSubscribedQuery(key);
onSuccess?.(value);
return value;
} catch (e) {
onError?.(e);
if (onError === undefined) throw e;
}
}

export function useMutation(key, promiseFn, { onSuccess, onError } = {}) {
return async () => {
try {
const value = await promiseFn();
updateSubscribedQuery(key);
onSuccess?.(value);
return value;
} catch (e) {
onError?.(e);
if (onError === undefined) throw e;
}
};
return () => mutate(key, promiseFn, { onSuccess, onError });
}

export function getQuerySuspense(key, promiseFn, dependencyArray = []) {
Expand Down
16 changes: 16 additions & 0 deletions src/common/modal/modal.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createContext, useCallback, useEffect, useState, useRef } from "react";
import useModalStore, { closeModal } from "./store.js";
import useFocusTrap from "./useFocusTrap.js";

export const ModalCloseContext = createContext(() => {
console.log("모달이 닫힙니다.");
Expand Down Expand Up @@ -29,11 +30,26 @@ function Modal({ layer }) {
}
}, [child]);

useEffect(() => {
if (child === null) return;

function escHatch(e) {
if (e.key !== "Escape") return;
close();
e.preventDefault();
}
document.addEventListener("keydown", escHatch);
return () => document.removeEventListener("keydown", escHatch);
}, [child, close]);

const focusTrapRef = useFocusTrap(child !== null);

return (
<ModalCloseContext.Provider value={close}>
{child !== null ? (
<div
className={`fixed z-[100] top-0 left-0 w-full h-dvh flex justify-center items-center transition-opacity ${opacity === 0 ? "opacity-0" : "opacity-100"}`}
ref={focusTrapRef}
>
{child}
<div
Expand Down
9 changes: 9 additions & 0 deletions src/common/modal/openModal.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,13 @@ import { modalStore } from "./store.js";

export default function openModal(component, layer = "alert") {
modalStore.changeModal(component, layer);
return new Promise((resolve) => {
function observe() {
if (modalStore.getSnapshot(layer) !== component) {
resolve();
clear();
}
}
const clear = modalStore.subscribe(observe);
});
}
64 changes: 64 additions & 0 deletions src/common/modal/useFocusTrap.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { useEffect, useRef } from "react";

function getEndPointChild(element) {
const focusableElements = [
...element.querySelectorAll(
"a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), iframe, object, embed, *[tabindex], *[contenteditable]",
),
].filter((elem) => elem.tabIndex >= 0);
if (focusableElements.length === 0) return [null, null];
return [focusableElements[0], focusableElements[focusableElements.length - 1]];
}

function useFocusTrap(active) {
const prevRef = useRef(null);
const ref = useRef(null);
const endPointChild = useRef([null, null]);
useEffect(() => {
if (!active || ref.current === null) return;

function renewEndPointChild() {
if (ref.current === null) return;
endPointChild.current = getEndPointChild(ref.current);
}

function handleTabKey(e) {
if (e.key !== "Tab") return;

const [first, last] = endPointChild.current;

if (document.activeElement === prevRef.current) {
if (e.shiftKey) last?.focus();
else first?.focus();
e.preventDefault();
return;
}

if (first === null || last === null) return;
if (document.activeElement === last && !e.shiftKey) {
first.focus();
e.preventDefault();
} else if (document.activeElement === first && e.shiftKey) {
last.focus();
e.preventDefault();
}
}

renewEndPointChild();
prevRef.current = document.activeElement;
document.addEventListener("keydown", handleTabKey);
const config = { subtree: true, childList: true, attributeFilter: ["disabled", "tabindex"] };
const observer = new MutationObserver(renewEndPointChild);
observer.observe(ref.current, config);

return () => {
document.removeEventListener("keydown", handleTabKey);
observer.disconnect();
prevRef.current.focus();
};
}, [active]);

return ref;
}

export default useFocusTrap;
13 changes: 9 additions & 4 deletions src/mainPage/features/comment/autoScrollCarousel/index.jsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import useAutoCarousel from "./useAutoCarousel.js";

function AutoScrollCarousel({ speed = 1, gap = 0, children }) {
const { position, ref, eventListener } = useAutoCarousel(speed);
const { position, ref, eventListener } = useAutoCarousel(speed, gap);

const flexStyle = "flex [&>div]:flex-shrink-0 gap-[var(--gap,0)] items-center absolute";
const flexStyle =
"min-w-full flex [&>div]:flex-shrink-0 gap-[var(--gap,0)] justify-around items-center absolute";
return (
<div className="w-full h-full overflow-hidden" {...eventListener}>
<div
Expand All @@ -13,11 +14,15 @@ function AutoScrollCarousel({ speed = 1, gap = 0, children }) {
}}
className="relative h-max touch-pan-y"
>
<div className={`${flexStyle} -translate-x-[calc(100%+var(--gap,0px))]`}>{children}</div>
<div className={`${flexStyle} -translate-x-[calc(100%+var(--gap,0px))]`} aria-hidden="true">
{children}
</div>
<div className={flexStyle} ref={ref}>
{children}
</div>
<div className={`${flexStyle} translate-x-[calc(100%+var(--gap,0px))]`}>{children}</div>
<div className={`${flexStyle} translate-x-[calc(100%+var(--gap,0px))]`} aria-hidden="true">
{children}
</div>
</div>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const FRICTION_RATE = 0.1;
const MOMENTUM_THRESHOLD = 0.6;
const MOMENTUM_RATE = 0.3;

function useAutoCarousel(speed = 1) {
function useAutoCarousel(speed = 1, gap = 0) {
const childRef = useRef(null);
const [position, setPosition] = useState(0);
const [isControlled, setIsControlled] = useState(false);
Expand All @@ -19,7 +19,7 @@ function useAutoCarousel(speed = 1) {
(time) => {
if (childRef.current === null) return;

const width = childRef.current.clientWidth;
const width = childRef.current.clientWidth + gap;

// 마우스 뗐을 때 관성 재계산
const baseSpeed = isHovered ? 0 : speed;
Expand All @@ -38,7 +38,7 @@ function useAutoCarousel(speed = 1) {
// 타임스탬프 저장
timestamp.current = time;
},
[isHovered, speed],
[isHovered, speed, gap],
);

useEffect(() => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useQuery } from "@common/dataFetch/getQuery.js";
import { fetchServer } from "@common/dataFetch/fetchServer.js";
import CommentCarouselNoData from "./CommentCarouselNoData.jsx";
import AutoScrollCarousel from "../autoScrollCarousel";
import { formatDate } from "@common/utils.js";
import { EVENT_ID } from "@common/constants.js";
Expand All @@ -14,6 +15,8 @@ function mask(string) {
function CommentCarousel() {
const { comments } = useQuery("comment-data", () => fetchServer(`/api/v1/comment/${EVENT_ID}`));

if (comments.length === 0) return <CommentCarouselNoData />;

return (
<div className="w-full h-[29rem]">
<AutoScrollCarousel speed={0.1} gap={28}>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
function CommentCarouselNoData() {
return (
<div className="w-full h-[29rem] flex justify-center items-center px-6">
<div className="w-full max-w-[1200px] h-96 bg-neutral-50 flex flex-col justify-center items-center gap-4">
<img src="/icons/error.svg" alt="기대평 없음" width="120" height="120" />
<p className="text-body-l text-red-500 font-bold">기대평이 없어요!</p>
</div>
</div>
);
}

export default CommentCarouselNoData;
14 changes: 6 additions & 8 deletions src/mainPage/features/comment/modals/CommentNegativeModal.jsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,20 @@
import { useContext } from "react";
import { ModalCloseContext } from "@common/modal/modal.jsx";
import AlertModalContainer from "@main/components/AlertModalContainer.jsx";
import Button from "@common/components/Button.jsx";

function CommentNegativeModal() {
const close = useContext(ModalCloseContext);

return (
<div className="w-[calc(100%-1rem)] max-w-[31.25rem] h-[calc(100svh-2rem)] max-h-[15.625rem] p-10 shadow bg-white relative flex flex-col justify-between items-center">
<div className="flex flex-col gap-2 items-center">
<p className="text-body-l font-bold text-neutral-700">해당 기대평을 등록할 수 없습니다</p>
<p className="w-full max-w-80 text-body-s font-medium text-neutral-400 text-center">
비속어, 혐오표현 등 타인에게 불쾌감을 줄 수 있는 표현이 포함된 기대평은 작성이 불가합니다
</p>
</div>
<AlertModalContainer
title="해당 기대평을 등록할 수 없습니다"
description="비속어, 혐오표현 등 타인에게 불쾌감을 줄 수 있는 표현이 포함된 기대평은 작성이 불가합니다"
>
<Button styleType="filled" onClick={close}>
확인
</Button>
</div>
</AlertModalContainer>
);
}

Expand Down
18 changes: 8 additions & 10 deletions src/mainPage/features/comment/modals/CommentNoUserModal.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useContext } from "react";
import { ModalCloseContext } from "@common/modal/modal.jsx";
import AlertModalContainer from "@main/components/AlertModalContainer.jsx";
import Button from "@common/components/Button.jsx";
import scrollTo from "@main/scroll/scrollTo.js";
import { INTERACTION_SECTION } from "@main/scroll/constants.js";
Expand All @@ -13,22 +14,19 @@ function CommentNoUserModal() {
}

return (
<div className="w-[calc(100%-1rem)] max-w-[31.25rem] h-[calc(100svh-2rem)] max-h-[31.25rem] p-10 shadow bg-white relative flex flex-col justify-between items-center">
<div className="flex flex-col gap-2 items-center">
<p className="text-body-l font-bold text-neutral-700">아직 기대평을 작성할 수 없습니다.</p>
<p className="w-full max-w-80 text-body-s font-medium text-neutral-400 text-center">
오늘의 추첨 이벤트에 참여하고 기대평을 작성하세요
</p>
</div>
<div className="absolute top-0 left-0 w-full h-full flex justify-center items-center pointer-events-none">
<AlertModalContainer
title="아직 기대평을 작성할 수 없습니다."
description="오늘의 추첨 이벤트에 참여하고 기대평을 작성하세요"
image={
<img
src="/icons/[email protected]"
srcSet="/icons/[email protected] 1x, /icons/[email protected] 2x"
alt="추첨 이벤트 참여 바랍니다"
width="208"
height="40"
/>
</div>
}
>
<div className="w-full flex flex-wrap justify-center gap-5">
<Button styleType="filled" onClick={toMoveInteraction}>
추첨 이벤트 참여하기
Expand All @@ -37,7 +35,7 @@ function CommentNoUserModal() {
닫기
</Button>
</div>
</div>
</AlertModalContainer>
);
}

Expand Down
12 changes: 7 additions & 5 deletions src/mainPage/features/comment/modals/CommentSuccessModal.jsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,28 @@
import { useContext } from "react";
import { ModalCloseContext } from "@common/modal/modal.jsx";
import AlertModalContainer from "@main/components/AlertModalContainer.jsx";
import Button from "@common/components/Button.jsx";

function CommentSuccessModal() {
const close = useContext(ModalCloseContext);

return (
<div className="w-[calc(100%-1rem)] max-w-[31.25rem] h-[calc(100svh-2rem)] max-h-[31.25rem] p-10 shadow bg-white relative flex flex-col justify-between items-center">
<p className="text-body-l font-bold text-neutral-700">기대평이 등록되었습니다!</p>
<div className="absolute top-0 left-0 w-full h-full flex justify-center items-center pointer-events-none">
<AlertModalContainer
title="기대평이 등록되었습니다!"
image={
<img
src="/icons/[email protected]"
srcSet="/icons/[email protected] 1x, /icons/[email protected] 2x"
alt="기대평 등록 완료"
width="173"
height="182"
/>
</div>
}
>
<Button styleType="ghost" onClick={close}>
확인
</Button>
</div>
</AlertModalContainer>
);
}

Expand Down
Loading