-
-
{betResult} !
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+ navigate(-1)} css={S.backButton}>
+
- >
+
+
+
+
+ {nameList && (
+
+ )}
+
+ {isButtonShown && (
+
+ )}
+
+
+
);
}
-const containerStyle = css`
- display: flex;
- align-items: center;
- justify-content: center;
- height: 100vh;
-`;
-
-const starContainerStyle = css`
- position: relative;
-
- display: flex;
- align-items: center;
- justify-content: center;
-
- width: 150px;
- height: 50px;
-`;
-
-const starOneStyle = css`
- position: absolute;
- top: -30px;
- left: -40px;
-`;
-
-const starTwoStyle = css`
- position: absolute;
- top: -20px;
- right: -30px;
-`;
-
-const starThreeStyle = css`
- position: absolute;
- bottom: -20px;
- left: -20px;
-`;
-
-const starFourStyle = css`
- position: absolute;
- right: -20px;
- bottom: -30px;
-`;
-
-const rotate = keyframes`
- 0% { transform: rotate(0deg); }
- 50% { transform: rotate(15deg); }
- 100% { transform: rotate(0deg); }
-`;
-
-const scale = keyframes`
- 0%, 100% { transform: scale(1); }
- 50% { transform: scale(1.2); }
-`;
-
-const rotateAnimation = css`
- animation: ${rotate} 2s infinite ease-in-out;
-`;
-
-const scaleAnimation = css`
- animation: ${scale} 2s infinite ease-in-out;
-`;
+// ๋ณ์ด ์์ง์ด๋ ์ปดํฌ๋ํธ
+//
+//
+//
{betResult} !
+
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+// const containerStyle = css`
+// display: flex;
+// align-items: center;
+// justify-content: center;
+// height: 100vh;
+// `;
+
+// const starContainerStyle = css`
+// position: relative;
+
+// display: flex;
+// align-items: center;
+// justify-content: center;
+
+// width: 150px;
+// height: 50px;
+// `;
+
+// const starOneStyle = css`
+// position: absolute;
+// top: -30px;
+// left: -40px;
+// `;
+
+// const starTwoStyle = css`
+// position: absolute;
+// top: -20px;
+// right: -30px;
+// `;
+
+// const starThreeStyle = css`
+// position: absolute;
+// bottom: -20px;
+// left: -20px;
+// `;
+
+// const starFourStyle = css`
+// position: absolute;
+// right: -20px;
+// bottom: -30px;
+// `;
+
+// const rotate = keyframes`
+// 0% { transform: rotate(0deg); }
+// 50% { transform: rotate(15deg); }
+// 100% { transform: rotate(0deg); }
+// `;
+
+// const scale = keyframes`
+// 0%, 100% { transform: scale(1); }
+// 50% { transform: scale(1.2); }
+// `;
+
+// const rotateAnimation = css`
+// animation: ${rotate} 2s infinite ease-in-out;
+// `;
+
+// const scaleAnimation = css`
+// animation: ${scale} 2s infinite ease-in-out;
+// `;
diff --git a/frontend/src/pages/Bet/components/Roulette/DrawRoulette.ts b/frontend/src/pages/Bet/components/Roulette/DrawRoulette.ts
new file mode 100644
index 000000000..dcbcb93af
--- /dev/null
+++ b/frontend/src/pages/Bet/components/Roulette/DrawRoulette.ts
@@ -0,0 +1,362 @@
+const getCanvasUtil = (canvas: HTMLCanvasElement) => {
+ return {
+ /**
+ * getPercentToWidth
+ * @param percent 1~100
+ * @param canvas
+ * @returns percentNumber
+ */
+
+ gpw: (percent: number) => {
+ const width = canvas.width;
+ percent = Math.min(percent, width);
+
+ return (percent / 100) * width;
+ },
+
+ /**
+ * getPercentToHeight
+ * @param percent 1~100
+ * @param canvas
+ * @returns percentNumber
+ */
+ gph: (percent: number) => {
+ const height = canvas.height;
+ percent = Math.min(percent, height);
+
+ return (percent / 100) * height;
+ },
+ };
+};
+
+interface SlotItem {
+ name: string;
+ drawStartHeightPercent: number;
+}
+
+// ํ์ด์ฆ : ๋์๊ฐ๊ธฐ(SPIN_PHASE)->์ก๊ธฐ(CATCH_PHASE)->ํจ๋ฐฐ์์ ๋ง๊ฒ ์กฐ์ (ADJUST_PHASE)
+const SPIN_PHASE = Symbol('SPIN_PHASE');
+const CATCH_PHASE = Symbol('CATCH_PHASE');
+const ADJUST_PHASE = Symbol('ADJUST_PHASE');
+const FINISH_PHASE = Symbol('FINISH_PHASE');
+
+type SlotPhase =
+ | typeof SPIN_PHASE
+ | typeof CATCH_PHASE
+ | typeof ADJUST_PHASE
+ | typeof FINISH_PHASE;
+
+class SlotItemMover {
+ nowList: SlotItem[];
+ #slotSize;
+ #itemHeightPercent;
+
+ constructor(nameList: string[], itemHeightPercent: number = 33) {
+ this.nowList = nameList.map((name, index) => ({
+ name,
+ drawStartHeightPercent: 50 + itemHeightPercent * index,
+ }));
+ this.#slotSize = itemHeightPercent * this.nowList.length;
+ this.#itemHeightPercent = itemHeightPercent;
+ }
+
+ moveUp(movePercent: number = 1) {
+ movePercent = Math.max(movePercent, 0);
+
+ this.nowList.forEach((item) => {
+ item.drawStartHeightPercent = this.#move(
+ item.drawStartHeightPercent,
+ -movePercent,
+ );
+ });
+ }
+
+ moveDown(movePercent: number = 1) {
+ movePercent = Math.max(movePercent, 0);
+ this.nowList.forEach((item) => {
+ item.drawStartHeightPercent = this.#move(
+ item.drawStartHeightPercent,
+ movePercent,
+ );
+ });
+ }
+
+ #move(position: number, percent: number) {
+ const nextPosition = (position + percent + this.#slotSize) % this.#slotSize;
+ if (nextPosition >= this.#slotSize - this.#itemHeightPercent)
+ return (nextPosition % this.#itemHeightPercent) - this.#itemHeightPercent;
+ return nextPosition;
+ }
+}
+
+class SlotPhaser {
+ #accCount = 0;
+ #catchingStartCount: number;
+ #catchingIndexDiff: number = 0;
+ #itemHeightPercent;
+ nowPhase: SlotPhase = SPIN_PHASE;
+
+ constructor(
+ catchingStartCount: number,
+ catchingIndexDiff: number = 0,
+ itemHeightPercent: number,
+ ) {
+ this.#catchingStartCount = catchingStartCount;
+ this.#catchingIndexDiff = catchingIndexDiff;
+ this.#itemHeightPercent = itemHeightPercent;
+ }
+
+ getNextPhase(slotItemList: SlotItem[], loserIndex: number) {
+ this.#updateAccTime();
+
+ if (this.nowPhase === FINISH_PHASE) {
+ return (this.nowPhase = FINISH_PHASE);
+ }
+
+ if (this.#accCount < this.#catchingStartCount) {
+ return (this.nowPhase = SPIN_PHASE);
+ }
+
+ if (this.nowPhase === SPIN_PHASE) {
+ return (this.nowPhase = CATCH_PHASE);
+ }
+ if (this.nowPhase === CATCH_PHASE) {
+ const targetIndex =
+ (loserIndex + this.#catchingIndexDiff + slotItemList.length) %
+ slotItemList.length;
+ const isCaught = slotItemList.some(
+ (item, index) =>
+ index === targetIndex &&
+ this.#checkIsCaught(item.drawStartHeightPercent),
+ );
+ if (isCaught) return (this.nowPhase = ADJUST_PHASE);
+ return (this.nowPhase = CATCH_PHASE);
+ }
+
+ if (this.nowPhase === ADJUST_PHASE) {
+ const isFinished = slotItemList.some(
+ (item, index) =>
+ index === loserIndex && item.drawStartHeightPercent === 50,
+ );
+ if (isFinished) return (this.nowPhase = FINISH_PHASE);
+ return (this.nowPhase = ADJUST_PHASE);
+ }
+
+ return this.nowPhase;
+ }
+
+ #updateAccTime() {
+ this.#accCount++;
+ }
+
+ #checkIsCaught(percent: number) {
+ return (
+ 50 - this.#itemHeightPercent < percent &&
+ percent <= 50 + this.#itemHeightPercent
+ );
+ }
+}
+
+class SlotItemSpeeder {
+ slotNowSpeed = 10;
+ #speedChangeTryCount = 0;
+ #speedFunc = (cnt: number, nowSpeed: number) => {
+ if (cnt % 100 === 0 && nowSpeed > 1) {
+ return nowSpeed - 1;
+ }
+ return nowSpeed;
+ };
+ constructor(
+ slotDefaultSpeed = 10,
+ speedFunc?: (cnt: number, nowSpeed?: number) => number,
+ ) {
+ this.slotNowSpeed = slotDefaultSpeed;
+ if (speedFunc) this.#speedFunc = speedFunc;
+ }
+
+ tryChangeSpeed() {
+ this.slotNowSpeed = this.#speedFunc(
+ this.#speedChangeTryCount,
+ this.slotNowSpeed,
+ );
+ this.#speedChangeTryCount++;
+ }
+}
+
+class SlotController {
+ #slotItemMover: SlotItemMover;
+ #slotPhaser: SlotPhaser;
+ #slotItemSpeeder: SlotItemSpeeder;
+ #loserIndex: number;
+
+ constructor(
+ slotItemMover: SlotItemMover,
+ slotPhaser: SlotPhaser,
+ slotItemSpeeder: SlotItemSpeeder,
+ loserIndex: number,
+ ) {
+ this.#slotItemMover = slotItemMover;
+ this.#slotPhaser = slotPhaser;
+ this.#slotItemSpeeder = slotItemSpeeder;
+ this.#loserIndex = loserIndex;
+ }
+
+ get phase() {
+ return this.#slotPhaser.nowPhase;
+ }
+
+ getNextSlotItem() {
+ this.#moveItem();
+ this.#updatePhase();
+ return this.#slotItemMover.nowList;
+ }
+
+ #moveItem() {
+ this.#slotItemSpeeder.tryChangeSpeed();
+ switch (this.#slotPhaser.nowPhase) {
+ case FINISH_PHASE:
+ break;
+ case SPIN_PHASE:
+ this.#moveDown();
+ break;
+ case CATCH_PHASE:
+ this.#moveDown();
+ break;
+ case ADJUST_PHASE: {
+ const nowSlotItem = this.#slotItemMover.nowList;
+ const loser = nowSlotItem.find(
+ (_, index) => index === this.#loserIndex,
+ );
+ if (!loser) {
+ this.#moveDown();
+ break;
+ }
+ if (loser.drawStartHeightPercent < 50) {
+ this.#moveDown();
+ break;
+ }
+ this.#moveUp();
+ break;
+ }
+ default:
+ this.#moveDown();
+ }
+ }
+
+ #updatePhase() {
+ this.#slotPhaser.getNextPhase(
+ this.#slotItemMover.nowList,
+ this.#loserIndex,
+ );
+ }
+
+ #moveDown() {
+ this.#slotItemMover.moveDown(this.#slotItemSpeeder.slotNowSpeed);
+ }
+
+ #moveUp() {
+ this.#slotItemMover.moveUp(8);
+ const nowSlotItem = this.#slotItemMover.nowList;
+ const loser = nowSlotItem.find((_, index) => index === this.#loserIndex);
+ if (!loser) return;
+ if (loser.drawStartHeightPercent < 50) loser.drawStartHeightPercent = 50;
+ }
+}
+
+interface drawRouletteProps {
+ canvas: HTMLCanvasElement;
+ nameList: string[];
+ loser?: string;
+ minMs?: number;
+ startSpeed?: number;
+ backgroundColor?: string;
+ font?: string;
+ stopSpeed?: number;
+ fontColor?: string;
+ itemPercent?: number;
+ onEnd?: () => void;
+}
+
+export default function drawRoulette(props: drawRouletteProps) {
+ const {
+ canvas,
+ nameList,
+ loser,
+ minMs = Infinity,
+ startSpeed = 10,
+ backgroundColor = 'white',
+ font = "700 normal 5rem 'Pretendard'",
+ stopSpeed = 3,
+ fontColor = 'grey',
+ itemPercent = 100,
+ onEnd,
+ } = props;
+ const ctx = canvas.getContext('2d');
+ if (!ctx) return { clearCanvas: () => {} };
+
+ const { gpw, gph } = getCanvasUtil(canvas);
+
+ const FRAME_SECOND = 20;
+ const slotItemMover = new SlotItemMover(
+ nameList.length === 1 ? nameList.concat(nameList) : nameList,
+ itemPercent,
+ );
+
+ const catchingIndexDiff = -1;
+ const slotPhaser = new SlotPhaser(
+ minMs / FRAME_SECOND,
+ catchingIndexDiff,
+ nameList.length === 1 ? 2 : nameList.length,
+ );
+
+ const moveCount = (cnt: number, speed?: number) => {
+ if (!speed) return startSpeed;
+ if (
+ cnt >= minMs / FRAME_SECOND / 2 &&
+ cnt % 10 === 0 &&
+ speed > stopSpeed
+ ) {
+ return speed - 1;
+ }
+ return speed;
+ };
+ const slotItemSlower = new SlotItemSpeeder(startSpeed, moveCount);
+
+ const loserIndex = nameList.findIndex((name) => name === loser);
+ const slotController = new SlotController(
+ slotItemMover,
+ slotPhaser,
+ slotItemSlower,
+ loserIndex,
+ );
+
+ ctx.font = font;
+ ctx.textAlign = 'center';
+ ctx.textBaseline = 'middle';
+ const drawBackground = () => {
+ ctx.fillStyle = backgroundColor;
+ ctx.fillRect(gpw(0), gph(0), gpw(100), gph(100));
+ };
+
+ const drawName = () => {
+ ctx.fillStyle = fontColor;
+ const nextSlotItem = slotController.getNextSlotItem();
+ nextSlotItem.forEach(({ name, drawStartHeightPercent }) => {
+ if (drawStartHeightPercent > 100 + itemPercent) return;
+ ctx.fillText(name, gpw(50), gph(drawStartHeightPercent));
+ });
+ };
+
+ const intervalId = setInterval(() => {
+ drawBackground();
+ drawName();
+ if (slotController.phase === FINISH_PHASE) {
+ clearInterval(intervalId);
+ if (onEnd) onEnd();
+ }
+ }, FRAME_SECOND);
+
+ const clearCanvas = () => clearInterval(intervalId);
+
+ return { clearCanvas };
+}
diff --git a/frontend/src/pages/Bet/components/Roulette/Roulette.stories.tsx b/frontend/src/pages/Bet/components/Roulette/Roulette.stories.tsx
new file mode 100644
index 000000000..5d5799976
--- /dev/null
+++ b/frontend/src/pages/Bet/components/Roulette/Roulette.stories.tsx
@@ -0,0 +1,21 @@
+import type { Meta, StoryObj } from '@storybook/react';
+
+import Roulette from './Roulette';
+
+const meta: Meta
= {
+ component: Roulette,
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Default: Story = {
+ args: {
+ loser: "ํ๋",
+ nameList: ["์ํ", "ํ๋", "์น", "ํ", "๊ฐ", "๋"],
+ startSpeed: 2,
+ minMs: 3000,
+ itemPercent: 66,
+ stopSpeed: 1
+ },
+};
diff --git a/frontend/src/pages/Bet/components/Roulette/Roulette.styles.ts b/frontend/src/pages/Bet/components/Roulette/Roulette.styles.ts
new file mode 100644
index 000000000..b93a88053
--- /dev/null
+++ b/frontend/src/pages/Bet/components/Roulette/Roulette.styles.ts
@@ -0,0 +1,7 @@
+import { css } from '@emotion/react';
+
+export const canvas = css`
+ width: 25rem;
+ height: 13rem;
+ border-radius: 8px;
+`;
diff --git a/frontend/src/pages/Bet/components/Roulette/Roulette.tsx b/frontend/src/pages/Bet/components/Roulette/Roulette.tsx
new file mode 100644
index 000000000..f35904ab3
--- /dev/null
+++ b/frontend/src/pages/Bet/components/Roulette/Roulette.tsx
@@ -0,0 +1,75 @@
+import * as S from './Roulette.styles';
+
+import { HTMLAttributes, useEffect, useRef } from 'react';
+
+import drawRoulette from './DrawRoulette';
+
+interface RouletteProps extends HTMLAttributes {
+ nameList: string[];
+ loser?: string;
+ minMs?: number;
+ startSpeed?: number;
+ backgroundColor?: string;
+ font?: string;
+ fontSize?: number;
+ stopSpeed?: number;
+ fontColor?: string;
+ itemPercent?: number;
+ onEnd?: () => void;
+}
+
+export default function Roulette(props: RouletteProps) {
+ const {
+ nameList,
+ loser,
+ minMs = Infinity,
+ startSpeed = 500,
+ backgroundColor = '#D2D5DB',
+ font = "500 normal 5rem 'bitbit'",
+ fontSize = 32,
+ stopSpeed = 3,
+ fontColor = '#6D717F',
+ itemPercent = 100,
+ onEnd,
+ ...otherProps
+ } = props;
+ const canvasRef = useRef(null);
+ const isEnded = useRef(false);
+
+ useEffect(() => {
+ if (!canvasRef.current) return;
+ if (isEnded.current) return;
+ const { clearCanvas } = drawRoulette({
+ canvas: canvasRef.current,
+ nameList,
+ loser,
+ minMs,
+ startSpeed,
+ backgroundColor,
+ font,
+ stopSpeed,
+ fontColor,
+ itemPercent,
+ onEnd: () => {
+ onEnd && onEnd();
+ isEnded.current = true;
+ },
+ });
+
+ return clearCanvas;
+ }, [
+ nameList,
+ loser,
+ minMs,
+ startSpeed,
+ backgroundColor,
+ font,
+ fontSize,
+ stopSpeed,
+ fontColor,
+ itemPercent,
+ onEnd,
+ ]);
+
+ return ;
+}
diff --git a/frontend/src/pages/Bet/components/RouletteWrapper/RouletteWrapper.stories.tsx b/frontend/src/pages/Bet/components/RouletteWrapper/RouletteWrapper.stories.tsx
new file mode 100644
index 000000000..131b28661
--- /dev/null
+++ b/frontend/src/pages/Bet/components/RouletteWrapper/RouletteWrapper.stories.tsx
@@ -0,0 +1,41 @@
+import type { Meta, StoryObj } from '@storybook/react';
+
+import Roulette from '@_pages/Bet/components/Roulette/Roulette';
+import RouletteWrapper from './RouletteWrapper';
+
+const meta: Meta = {
+ component: RouletteWrapper,
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Default: Story = {
+ args: {
+ mainDescription: '00:00',
+ description: '๋๋ง ์ค๋ฉด*100%*',
+ title: '๋๋ง ์ค๋ฉด๊ณ ',
+ children: (
+
+ ),
+ },
+};
diff --git a/frontend/src/pages/Bet/components/RouletteWrapper/RouletteWrapper.style.ts b/frontend/src/pages/Bet/components/RouletteWrapper/RouletteWrapper.style.ts
new file mode 100644
index 000000000..9659b421b
--- /dev/null
+++ b/frontend/src/pages/Bet/components/RouletteWrapper/RouletteWrapper.style.ts
@@ -0,0 +1,58 @@
+import { Theme, css } from '@emotion/react';
+
+const bitbit = 'bitbit';
+
+export const container = ({ theme }: { theme: Theme }) => css`
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+
+ padding: 3.5rem;
+
+ background-color: ${theme.colorPalette.yellow[200]};
+ border-radius: 24px;
+ box-shadow: 0 5px 15px rgb(0 0 0 / 20%);
+`;
+
+export const title = ({ theme }: { theme: Theme }) => css`
+ ${theme.typography.h2}
+ width: 30rem;
+
+ font: 300 normal 5rem ${bitbit};
+ text-align: center;
+ word-wrap: break-word;
+ white-space: pre-line;
+`;
+
+export const descriptionBox = ({ theme }: { theme: Theme }) => css`
+ display: flex;
+ align-items: center;
+ justify-content: space-evenly;
+
+ height: 4rem;
+ padding: 0.5rem 2rem;
+
+ color: ${theme.colorPalette.white[100]};
+
+ background-color: ${theme.colorPalette.orange[300]};
+ border-radius: 1rem;
+`;
+
+export const descriptionWrapper = css`
+ display: flex;
+ gap: 0.4rem;
+ align-items: center;
+`;
+
+export const time = ({ theme }: { theme: Theme }) => css`
+ ${theme.typography.h2}
+ height: 48px;
+ margin: 3rem 0;
+ font: 500 normal 4rem ${bitbit};
+`;
+
+export const rouletteContainer = ({ theme }: { theme: Theme }) => css`
+ padding: 2rem;
+ background-color: ${theme.colorPalette.grey[400]};
+ border-radius: 28px;
+`;
diff --git a/frontend/src/pages/Bet/components/RouletteWrapper/RouletteWrapper.tsx b/frontend/src/pages/Bet/components/RouletteWrapper/RouletteWrapper.tsx
new file mode 100644
index 000000000..eea52f0e6
--- /dev/null
+++ b/frontend/src/pages/Bet/components/RouletteWrapper/RouletteWrapper.tsx
@@ -0,0 +1,47 @@
+/* stylelint-disable font-family-no-missing-generic-family-keyword */
+
+import * as S from './RouletteWrapper.style';
+
+import { css, useTheme } from '@emotion/react';
+
+import { PropsWithChildren } from 'react';
+
+interface RouletteWrapperProps extends PropsWithChildren {
+ title: string;
+ description: string;
+ mainDescription: string;
+}
+const bitbit = 'bitbit';
+
+export default function RouletteWrapper(props: RouletteWrapperProps) {
+ const theme = useTheme();
+ const { title, description, mainDescription, children } = props;
+
+ return (
+
+
{title}
+
+
+ {description.split('*').map((str, index) => (
+
+ {str}
+
+ ))}
+
+
+
{mainDescription}
+
{children}
+
+ );
+}
diff --git a/frontend/src/pages/Bet/components/Tag/Tag.tsx b/frontend/src/pages/Bet/components/Tag/Tag.tsx
index afea633fc..67042461e 100644
--- a/frontend/src/pages/Bet/components/Tag/Tag.tsx
+++ b/frontend/src/pages/Bet/components/Tag/Tag.tsx
@@ -2,7 +2,7 @@ import * as S from './Tag.style';
import { HTMLProps } from 'react';
import { useTheme } from '@emotion/react';
import { BetDetail } from '@_types/index';
-import calculateMinutesUntilDeadline from '@_utils/calculateMinutesUntilDeadline';
+import calculateMinutesUntilDeadline from '@_utils/calculateLeftMinutesUntilDeadline';
interface TagProps extends HTMLProps {
isAnnounced: BetDetail['isAnnounced'];
diff --git a/frontend/src/pages/Chatting/ChatPage/ChatPage.tsx b/frontend/src/pages/Chatting/ChatPage/ChatPage.tsx
index 6a3fd3b05..c2506639f 100644
--- a/frontend/src/pages/Chatting/ChatPage/ChatPage.tsx
+++ b/frontend/src/pages/Chatting/ChatPage/ChatPage.tsx
@@ -9,7 +9,7 @@ import ChattingPreview from './components/ChattingPreview/ChattingPreview';
import ChattingPreviewLayout from '@_layouts/ChattingPreviewLayout/ChattingPreviewLayout';
import DarakbangNameWrapper from '@_components/DarakbangNameWrapper/DarakbangNameWrapper';
import GET_ROUTES from '@_common/getRoutes';
-import MissingFallback from '@_components/MissingFallback/MissingFallback';
+import MissingFallback from '@_components/Fallback/MissingFallback/MissingFallback';
import NavigationBar from '@_components/NavigationBar/NavigationBar';
import NavigationBarWrapper from '@_layouts/components/NavigationBarWrapper/NavigationBarWrapper';
import { common } from '@_common/common.style';
@@ -43,7 +43,7 @@ export default function ChatPage() {
onClick={() => setNowChatRoomType('MOIM')}
/>
setNowChatRoomType('BET')}
/>
diff --git a/frontend/src/pages/Chatting/ChattingRoomPage/ChattingRoomPage.tsx b/frontend/src/pages/Chatting/ChattingRoomPage/ChattingRoomPage.tsx
index f7bd96027..8d008f986 100644
--- a/frontend/src/pages/Chatting/ChattingRoomPage/ChattingRoomPage.tsx
+++ b/frontend/src/pages/Chatting/ChattingRoomPage/ChattingRoomPage.tsx
@@ -1,6 +1,6 @@
import { css, useTheme } from '@emotion/react';
import { isBetChatRoomDetail, isMoimChatRoomDetail } from '@_types/typeGuards';
-import { useMemo, useState } from 'react';
+import { Fragment, useMemo, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import CalenderClock from '@_components/Icons/CalenderClock';
@@ -15,7 +15,7 @@ import DarakbangNameWrapper from '@_components/DarakbangNameWrapper/DarakbangNam
import DateTimeModalContent from './components/DateTimeModalContent/DateTimeModalContent';
import GET_ROUTES from '@_common/getRoutes';
import Hamburger from '@_components/Icons/Hamburger';
-import MissingFallback from '@_components/MissingFallback/MissingFallback';
+import MissingFallback from '@_components/Fallback/MissingFallback/MissingFallback';
import Modal from '@_components/Modal/Modal';
import Picker from '@_components/Icons/Picker';
import PlaceModalContent from './components/PlaceModalContent/PlaceModalContent';
@@ -26,6 +26,7 @@ import useChats from '@_hooks/queries/useChats';
import useConfirmDateTime from '@_hooks/mutaions/useConfirmDatetime';
import useConfirmPlace from '@_hooks/mutaions/useConfirmPlace';
import useSendMessage from '@_hooks/mutaions/useSendMessage';
+import LooserBanner from './components/LooserBanner/LooserBanner';
type ModalContent = 'place' | 'datetime';
@@ -137,7 +138,7 @@ export default function ChattingRoomPage() {
if (isBetChatRoomDetail(chatRoomDetail)) {
return (
);
@@ -202,18 +203,20 @@ export default function ChattingRoomPage() {
/>
)}
{chatRoomDetail && isBetChatRoomDetail(chatRoomDetail) && (
-
+
+
+
+
)}
diff --git a/frontend/src/pages/Chatting/ChattingRoomPage/components/ChattingFooter/ChattingFooter.style.ts b/frontend/src/pages/Chatting/ChattingRoomPage/components/ChattingFooter/ChattingFooter.style.ts
index 0d9fef630..ea6a20bed 100644
--- a/frontend/src/pages/Chatting/ChattingRoomPage/components/ChattingFooter/ChattingFooter.style.ts
+++ b/frontend/src/pages/Chatting/ChattingRoomPage/components/ChattingFooter/ChattingFooter.style.ts
@@ -35,6 +35,7 @@ export const messageForm = ({ theme }: { theme: Theme }) => css`
export const messageTextArea = ({ theme }: { theme: Theme }) => css`
${theme.typography.s2};
+ /* stylelint-disable-next-line plugin/no-unsupported-browser-features */
resize: none;
width: 100%;
diff --git a/frontend/src/pages/Chatting/ChattingRoomPage/components/LooserBanner/LooserBanner.style.ts b/frontend/src/pages/Chatting/ChattingRoomPage/components/LooserBanner/LooserBanner.style.ts
new file mode 100644
index 000000000..838a2e6e7
--- /dev/null
+++ b/frontend/src/pages/Chatting/ChattingRoomPage/components/LooserBanner/LooserBanner.style.ts
@@ -0,0 +1,36 @@
+import { css, Theme } from '@emotion/react';
+
+export const bannerContainer = ({ theme }: { theme: Theme }) => css`
+ overflow: hidden; /* ํ
์คํธ๊ฐ ๋ฒ์ด๋๋ฉด ์จ๊น */
+ display: flex;
+ align-items: center;
+
+ width: 100%;
+ height: 4.4rem;
+ padding-bottom: 0.4rem;
+
+ background-color: ${theme.colorPalette.black[100]};
+`;
+
+export const bannerTextContainer = css`
+ display: flex; /* ํ
์คํธ๊ฐ ๊ฐ๋ก๋ก ๋๋ํ ๋ฐฐ์น๋๋๋ก ์ค์ */
+ white-space: nowrap; /* ํ
์คํธ๊ฐ ํ ์ค๋ก ์ ์ง๋๋๋ก ์ค์ */
+`;
+
+export const bannerText = ({ theme }: { theme: Theme }) => css`
+ display: inline-block;
+ font-size: 2rem;
+ color: ${theme.colorPalette.white[100]};
+ ${theme.typography.partialSansKR}
+ animation: marquee 3s linear infinite; /* ์ ๋๋ฉ์ด์
์ค์ */
+
+ @keyframes marquee {
+ 0% {
+ transform: translateX(0); /* ์ฒ์ ์ํ */
+ }
+
+ 100% {
+ transform: translateX(-100%); /* ์ผ์ชฝ ๋์ผ๋ก ์ด๋ */
+ }
+ }
+`;
diff --git a/frontend/src/pages/Chatting/ChattingRoomPage/components/LooserBanner/LooserBanner.tsx b/frontend/src/pages/Chatting/ChattingRoomPage/components/LooserBanner/LooserBanner.tsx
new file mode 100644
index 000000000..2dd5be442
--- /dev/null
+++ b/frontend/src/pages/Chatting/ChattingRoomPage/components/LooserBanner/LooserBanner.tsx
@@ -0,0 +1,32 @@
+import { BetChatRoomDetail } from '@_types/index';
+import { useTheme } from '@emotion/react';
+import * as S from './LooserBanner.style';
+
+interface LooserBannerProps {
+ chatRoomDetail: BetChatRoomDetail;
+}
+
+export default function LooserBanner(props: LooserBannerProps) {
+ const { chatRoomDetail } = props;
+
+ const theme = useTheme();
+
+ return (
+
+
+
+ ์ถํ๋๋ฆฝ๋๋ค {chatRoomDetail.attributes.loser.nickname}๋!
+
+
+ ์ถํ๋๋ฆฝ๋๋ค {chatRoomDetail.attributes.loser.nickname}๋!
+
+
+ ์ถํ๋๋ฆฝ๋๋ค {chatRoomDetail.attributes.loser.nickname}๋!
+
+
+ ์ถํ๋๋ฆฝ๋๋ค {chatRoomDetail.attributes.loser.nickname}๋!
+
+
+
+ );
+}
diff --git a/frontend/src/pages/Darakbang/DarakbangCreationPage/DarakbangCreationModalContent/DarakbangCreationModalContent.tsx b/frontend/src/pages/Darakbang/DarakbangCreationPage/DarakbangCreationModalContent/DarakbangCreationModalContent.tsx
index 7d3868505..aae5367f0 100644
--- a/frontend/src/pages/Darakbang/DarakbangCreationPage/DarakbangCreationModalContent/DarakbangCreationModalContent.tsx
+++ b/frontend/src/pages/Darakbang/DarakbangCreationPage/DarakbangCreationModalContent/DarakbangCreationModalContent.tsx
@@ -19,13 +19,16 @@ export default function DarakbangCreationModalContent(
const theme = useTheme();
return (
-
+
{'๋ค๋ฝ๋ฐฉ ์ด๋ฆ์ผ๋ก '}
{darakbangName}
{',\n๋๋ค์์ผ๋ก '}
{nickname}
{
- '์(๋ฅผ) ์ ํํ์
จ์ต๋๋ค.\n\nํ ๋ฒ ์ ํํ ๋ค๋ฝ๋ฐฉ ์ด๋ฆ๊ณผ ๋๋ค์์ ๋ฐ๊ฟ ์ ์์ต๋๋ค.\n์งํํ์๊ฒ ์ต๋๊น?'
+ '์(๋ฅผ) ์ ํํ์
จ์ต๋๋ค.\n\nํ ๋ฒ ์ ํํ ๋ค๋ฝ๋ฐฉ ์ด๋ฆ์ ๋ฐ๊ฟ ์ ์์ต๋๋ค. ์งํํ์๊ฒ ์ต๋๊น?'
}
diff --git a/frontend/src/pages/Darakbang/DarakbangCreationPage/DarakbangCreationPage.tsx b/frontend/src/pages/Darakbang/DarakbangCreationPage/DarakbangCreationPage.tsx
index 71d87d66f..ab358a873 100644
--- a/frontend/src/pages/Darakbang/DarakbangCreationPage/DarakbangCreationPage.tsx
+++ b/frontend/src/pages/Darakbang/DarakbangCreationPage/DarakbangCreationPage.tsx
@@ -10,11 +10,11 @@ import HighlightSpan from '@_components/HighlightSpan/HighlightSpan';
import Modal from '@_components/Modal/Modal';
import POLICES from '@_constants/poclies';
import SelectLayout from '@_layouts/SelectLayout/SelectLayout';
-import SolidArrow from '@_components/Icons/SolidArrow';
import { setLastDarakbangId } from '@_common/lastDarakbangManager';
import useCreateDarakbang from '@_hooks/mutaions/useCreateDarakbang';
import { useNavigate } from 'react-router-dom';
import { useTheme } from '@emotion/react';
+import BackArrowButton from '@_components/Button/BackArrowButton/BackArrowButton';
export default function DarakbangCreationPage() {
const theme = useTheme();
@@ -34,8 +34,7 @@ export default function DarakbangCreationPage() {
- {
navigate(-1);
}}
@@ -47,15 +46,14 @@ export default function DarakbangCreationPage() {
-
- {'๋ค๋ฝ๋ฐฉ์ '}
- ์ด๋ฆ์
+
+ ๋ค๋ฝ๋ฐฉ์ ์ด๋ฆ์
๋ฌด์์ธ๊ฐ์?
) => {
setDarakbangName(e.target.value);
}}
@@ -63,7 +61,7 @@ export default function DarakbangCreationPage() {
/>
-
+
๋๋ค์์
์
๋ ฅํด์ฃผ์ธ์{'\n์ต๋ '}
{`${POLICES.maxNicknameLength}๊ธ์`}
diff --git a/frontend/src/pages/Darakbang/DarakbangEntrancePage/DarakbangEntrancePage.tsx b/frontend/src/pages/Darakbang/DarakbangEntrancePage/DarakbangEntrancePage.tsx
index 7da481172..5a1748965 100644
--- a/frontend/src/pages/Darakbang/DarakbangEntrancePage/DarakbangEntrancePage.tsx
+++ b/frontend/src/pages/Darakbang/DarakbangEntrancePage/DarakbangEntrancePage.tsx
@@ -5,9 +5,9 @@ import ErrorControlledInput from '@_components/ErrorControlledInput/ErrorControl
import POLICES from '@_constants/poclies';
import ROUTES from '@_constants/routes';
import SelectLayout from '@_layouts/SelectLayout/SelectLayout';
-import SolidArrow from '@_components/Icons/SolidArrow';
import { useNavigate } from 'react-router-dom';
import { useTheme } from '@emotion/react';
+import BackArrowButton from '@_components/Button/BackArrowButton/BackArrowButton';
export default function DarakbangEntrancePage() {
const theme = useTheme();
@@ -17,8 +17,7 @@ export default function DarakbangEntrancePage() {
- {
navigate(-1);
}}
diff --git a/frontend/src/pages/Darakbang/DarakbangLandingPage/DarakbangLandingPage.tsx b/frontend/src/pages/Darakbang/DarakbangLandingPage/DarakbangLandingPage.tsx
index d46f0b297..8aaa33416 100644
--- a/frontend/src/pages/Darakbang/DarakbangLandingPage/DarakbangLandingPage.tsx
+++ b/frontend/src/pages/Darakbang/DarakbangLandingPage/DarakbangLandingPage.tsx
@@ -24,12 +24,15 @@ export default function DarakbangLandingPage() {
-
+
{myInfo?.nickname}
{`๋ ๋ฐ๊ฐ์์~!\n์ด์ `}
๋ชจ์์
ํ์ธํด๋ณผ๊น์?
- {' '}
+
-
+
๋ค๋ฝ๋ฐฉ์
์ฐธ์ฌํด๋ณด์ธ์
diff --git a/frontend/src/pages/Darakbang/DarakbangSelectPage/DarakbangSelectPage.tsx b/frontend/src/pages/Darakbang/DarakbangSelectPage/DarakbangSelectPage.tsx
index 3d1cfb5a2..231ac319a 100644
--- a/frontend/src/pages/Darakbang/DarakbangSelectPage/DarakbangSelectPage.tsx
+++ b/frontend/src/pages/Darakbang/DarakbangSelectPage/DarakbangSelectPage.tsx
@@ -2,16 +2,16 @@ import * as S from './DarakbangSelectPage.style';
import GET_ROUTES from '@_common/getRoutes';
import HighlightSpan from '@_components/HighlightSpan/HighlightSpan';
-import MissingFallback from '@_components/MissingFallback/MissingFallback';
+import MissingFallback from '@_components/Fallback/MissingFallback/MissingFallback';
import ROUTES from '@_constants/routes';
import SelectLayout from '@_layouts/SelectLayout/SelectLayout';
-import SolidArrow from '@_components/Icons/SolidArrow';
import { common } from '@_common/common.style';
import { setLastDarakbangId } from '@_common/lastDarakbangManager';
import useMyDarakbangs from '@_hooks/queries/useMyDarakbang';
import { useNavigate } from 'react-router-dom';
import { useTheme } from '@emotion/react';
import SelectBar from '../components/SelectBar/SelectBar';
+import BackArrowButton from '@_components/Button/BackArrowButton/BackArrowButton';
export default function DarakbangSelectPage() {
const theme = useTheme();
@@ -22,8 +22,7 @@ export default function DarakbangSelectPage() {
- {
navigate(-1);
}}
@@ -34,9 +33,8 @@ export default function DarakbangSelectPage() {
-
- {'์ด๋ '}
- ๋ค๋ฝ๋ฐฉ์
+
+ ์ด๋ ๋ค๋ฝ๋ฐฉ์
๋ค์ด๊ฐ๊น์?
{isLoading && <>loading...>}
diff --git a/frontend/src/pages/Fallback/NotFoundPage/NotFoundPage.tsx b/frontend/src/pages/Fallback/NotFoundPage/NotFoundPage.tsx
index ee2f0f43f..e763eaead 100644
--- a/frontend/src/pages/Fallback/NotFoundPage/NotFoundPage.tsx
+++ b/frontend/src/pages/Fallback/NotFoundPage/NotFoundPage.tsx
@@ -2,7 +2,7 @@ import CompleteLayout from '@_layouts/CompleteLayout/CompleteLayout';
import { useNavigate } from 'react-router-dom';
import BackLogo from '@_common/assets/back.svg';
-import MissingFallback from '@_components/MissingFallback/MissingFallback';
+import MissingFallback from '@_components/Fallback/MissingFallback/MissingFallback';
import Button from '@_components/Button/Button';
import ROUTES from '@_constants/routes';
diff --git a/frontend/src/pages/Login/DataMigrationPage/Explanation/DataMigrationExplanationPage.style.ts b/frontend/src/pages/Login/DataMigrationPage/Explanation/DataMigrationExplanationPage.style.ts
deleted file mode 100644
index a3f274a0a..000000000
--- a/frontend/src/pages/Login/DataMigrationPage/Explanation/DataMigrationExplanationPage.style.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import { css, Theme } from '@emotion/react';
-
-export const explanationSection = ({ theme }: { theme: Theme }) => css`
- margin: 0rem 5rem;
- ${theme.typography.s1}
-`;
diff --git a/frontend/src/pages/Login/DataMigrationPage/Explanation/DataMigrationExplanationPage.tsx b/frontend/src/pages/Login/DataMigrationPage/Explanation/DataMigrationExplanationPage.tsx
deleted file mode 100644
index f6e02ff93..000000000
--- a/frontend/src/pages/Login/DataMigrationPage/Explanation/DataMigrationExplanationPage.tsx
+++ /dev/null
@@ -1,76 +0,0 @@
-import LoginLayout from '@_layouts/LoginLayout/LoginLayout';
-import KakaoOAuthLoginIcon from '@_components/Icons/KakaoOAuthIcon';
-import { useTheme } from '@emotion/react';
-import { explanationSection } from './DataMigrationExplanationPage.style';
-import MissingFallback from '@_components/MissingFallback/MissingFallback';
-import SolidArrow from '@_components/Icons/SolidArrow';
-import { useNavigate } from 'react-router-dom';
-import ROUTES from '@_constants/routes';
-
-export default function DataMigrationExplanationPage() {
- const theme = useTheme();
- const navigate = useNavigate();
- const kakaoAuthLogin = () => {
- if (
- !process.env.KAKAO_O_AUTH_CLIENT_ID ||
- !process.env.KAKAO_OAUTH_REDIRECT_URI
- ) {
- throw new Error('Google OAuth ์ ๋ณด๊ฐ ์์ต๋๋ค.');
- }
- const params = {
- client_id: process.env.KAKAO_O_AUTH_CLIENT_ID,
- redirect_uri: process.env.KAKAO_OAUTH_REDIRECT_URI,
- response_type: 'code',
- scope: 'openid',
- };
- const queryString = new URLSearchParams(params).toString();
- const kakaoOAuthUrl = `${process.env.KAKAO_REQUEST_URL}?${queryString}`;
- if (process.env.MSW == 'true') {
- window.location.href = 'http://localhost:8081/kakao-o-auth?code=1';
- } else {
- window.location.href = kakaoOAuthUrl;
- }
- };
-
- return (
-
-
-
- {
- navigate(ROUTES.home);
- }}
- />
-
-
-
-
-
-
- ๋ชจ์ฐ๋ค๋ ์ด์ฉ์ ์ฌ๋ฌ๋ถ์ ๋ ์ข์ ์๋น์ค ์ ๊ณต์ ์ํ์ฌ ์นด์นด์คํก ์์
- ๋ก๊ทธ์ธ์์ ๊ตฌ๊ธ๊ณผ ์ ํ์ ์์
๋ก๊ทธ์ธ์ผ๋ก ์ ํ์ ์ ํํ์ต๋๋ค.
-
2024๋
10์ 5์ผ ์ด์ ์นด์นด์คํก์ ์ด์ฉํ์ฌ ๋ก๊ทธ์ธ ํ์
จ๋
- ๊ธฐ์กด ์ด์ฉ์๋ค ์ค์์ ๊ธฐ์กด ๋ฐ์ดํฐ๋ฅผ ์ด์ ์ ์ํ์๋ ๋ถ๋ค์๊ฒ๋ ๋ฐ์ดํฐ
- ์ ํ ์ ์ฐจ๋ฅผ ์๋ดํด ๋๋ฆฌ๊ณ ์์ต๋๋ค. ์ ์ฐจ๋ ์๋์ ๊ฐ์ต๋๋ค.
-
-
1. ์นด์นด์คํก ๋ก๊ทธ์ธ์ ์งํํ๋ค.
2. ๊ตฌ๊ธ๊ณผ ์ ํ์ค
- ๋ฐ์ดํฐ์ด์ ์ ์ํ์๋ ์์
์ ํํ๊ณ ๋ก๊ทธ์ธ์ ์งํํ๋ค.
-
-
-
-
-
-
-
- );
-}
diff --git a/frontend/src/pages/Login/DataMigrationPage/Explanation/KakaoSelct.style.ts b/frontend/src/pages/Login/DataMigrationPage/Explanation/KakaoSelct.style.ts
new file mode 100644
index 000000000..95b5fd943
--- /dev/null
+++ b/frontend/src/pages/Login/DataMigrationPage/Explanation/KakaoSelct.style.ts
@@ -0,0 +1,21 @@
+import { css, Theme } from '@emotion/react';
+
+export const titleWrapper = () => css`
+ width: 90%;
+ margin-top: 20%;
+`;
+export const title = ({ theme }: { theme: Theme }) => css`
+ ${theme.typography.h3}
+`;
+
+export const subtitle = ({ theme }: { theme: Theme }) => css`
+ ${theme.typography.b1}
+`;
+
+export const explain = ({ theme }: { theme: Theme }) => css`
+ ${theme.typography.b2}
+ padding: 1rem;
+ color: ${theme.colorPalette.grey[300]};
+ text-decoration: underline;
+ background-color: ${theme.colorPalette.white[100]};
+`;
diff --git a/frontend/src/pages/Login/DataMigrationPage/Explanation/KakaoSelect.tsx b/frontend/src/pages/Login/DataMigrationPage/Explanation/KakaoSelect.tsx
new file mode 100644
index 000000000..fc38b1af1
--- /dev/null
+++ b/frontend/src/pages/Login/DataMigrationPage/Explanation/KakaoSelect.tsx
@@ -0,0 +1,70 @@
+import LoginLayout from '@_layouts/LoginLayout/LoginLayout';
+import MissingFallback from '@_components/Fallback/MissingFallback/MissingFallback';
+import * as S from './KakaoSelct.style';
+import { useTheme } from '@emotion/react';
+import Button from '@_components/Button/Button';
+import { useNavigate } from 'react-router-dom';
+import ROUTES from '@_constants/routes';
+import { getInviteCode } from '@_common/inviteCodeManager';
+import GET_ROUTES from '@_common/getRoutes';
+
+export default function KakaoSelectPage() {
+ const theme = useTheme();
+ const navigate = useNavigate();
+
+ return (
+
+
+
+
+
+ ๊ธฐ์กด ์นด์นด์ค ๊ณ์ ์ผ๋ก
๋ก๊ทธ์ธํ์ ์ ์ด ์๋์?
+
+
+
+
+
+
+
+
+
+ navigate(`${GET_ROUTES.default.oAuthSelection}/unknown`)
+ }
+ >
+ ์ ๊ธฐ์ต๋์ง ์์์
+
+
+
+
+ );
+}
diff --git a/frontend/src/pages/Login/DataMigrationPage/Select/OAuthSelectPage.stories.tsx b/frontend/src/pages/Login/DataMigrationPage/KakaoLogin/KakaoLoginPage.stories.tsx
similarity index 55%
rename from frontend/src/pages/Login/DataMigrationPage/Select/OAuthSelectPage.stories.tsx
rename to frontend/src/pages/Login/DataMigrationPage/KakaoLogin/KakaoLoginPage.stories.tsx
index 4d6cf54a8..c8fab78db 100644
--- a/frontend/src/pages/Login/DataMigrationPage/Select/OAuthSelectPage.stories.tsx
+++ b/frontend/src/pages/Login/DataMigrationPage/KakaoLogin/KakaoLoginPage.stories.tsx
@@ -1,15 +1,15 @@
import { Meta, StoryObj } from '@storybook/react/*';
-import OAuthSelectPage from './OAuthSelectPage';
+import KakaoLoginPage from './KakaoLoginPage';
const meta = {
title: 'pages/OAuthSelectPage',
- component: OAuthSelectPage,
-} satisfies Meta;
+ component: KakaoLoginPage,
+} satisfies Meta;
export default meta;
type Story = StoryObj;
export const Default: Story = {
- render: () => ,
+ render: () => ,
};
diff --git a/frontend/src/pages/Login/DataMigrationPage/KakaoLogin/KakaoLoginPage.style.ts b/frontend/src/pages/Login/DataMigrationPage/KakaoLogin/KakaoLoginPage.style.ts
new file mode 100644
index 000000000..1c679e6b3
--- /dev/null
+++ b/frontend/src/pages/Login/DataMigrationPage/KakaoLogin/KakaoLoginPage.style.ts
@@ -0,0 +1,13 @@
+import { css, Theme } from '@emotion/react';
+
+export const titleWrapper = () => css`
+ width: 90%;
+ margin-top: 20%;
+`;
+export const title = ({ theme }: { theme: Theme }) => css`
+ ${theme.typography.h3}
+`;
+
+export const subtitle = ({ theme }: { theme: Theme }) => css`
+ ${theme.typography.b1}
+`;
diff --git a/frontend/src/pages/Login/DataMigrationPage/KakaoLogin/KakaoLoginPage.tsx b/frontend/src/pages/Login/DataMigrationPage/KakaoLogin/KakaoLoginPage.tsx
new file mode 100644
index 000000000..e445c937f
--- /dev/null
+++ b/frontend/src/pages/Login/DataMigrationPage/KakaoLogin/KakaoLoginPage.tsx
@@ -0,0 +1,84 @@
+import LoginLayout from '@_layouts/LoginLayout/LoginLayout';
+import MissingFallback from '@_components/Fallback/MissingFallback/MissingFallback';
+import * as S from './KakaoLoginPage.style';
+import { useTheme } from '@emotion/react';
+import KakaoOAuthLoginIcon from '@_components/Icons/KakaoOAuthIcon';
+import { useNavigate, useParams } from 'react-router-dom';
+import { useEffect } from 'react';
+import ROUTES from '@_constants/routes';
+
+export default function KakaoLoginPage() {
+ const theme = useTheme();
+ const { type } = useParams();
+ const navigate = useNavigate();
+
+ useEffect(() => {
+ if (!type || (type !== 'known' && type !== 'unknown')) {
+ navigate(ROUTES.notFound);
+ }
+ }, [type, navigate]);
+ const kakaoAuthLogin = () => {
+ if (
+ !process.env.KAKAO_O_AUTH_CLIENT_ID ||
+ !process.env.KAKAO_OAUTH_REDIRECT_URI
+ ) {
+ throw new Error('kakao OAuth ์ ๋ณด๊ฐ ์์ต๋๋ค.');
+ }
+ const params = {
+ client_id: process.env.KAKAO_O_AUTH_CLIENT_ID,
+ redirect_uri: process.env.KAKAO_OAUTH_REDIRECT_URI,
+ response_type: 'code',
+ scope: 'openid',
+ };
+ const queryString = new URLSearchParams(params).toString();
+ const kakaoOAuthUrl = `${process.env.KAKAO_REQUEST_URL}?${queryString}`;
+ if (process.env.MSW == 'true') {
+ window.location.href = 'http://localhost:8081/kakao-o-auth?code=1';
+ } else {
+ window.location.href = kakaoOAuthUrl;
+ }
+ };
+
+ return (
+
+
+
+ {type === 'known' && (
+
+
+ ์นด์นด์ค ๋ก๊ทธ์ธ์
๋ ์ด์ ์ง์ํ์ง ์์์
+
+
+
+ ์ด์ ๊ธฐ๋ก์ ์ ์งํ๊ธฐ ์ํด ์นด์นด์ค ๋ก๊ทธ์ธ์ ์งํํด์ฃผ์ธ์.
+
+
+ )}
+ {type === 'unknown' && (
+
+
+ ์นด์นด์ค ๋ก๊ทธ์ธ์
๋ ์ด์ ์ง์ํ์ง ์์์
+
+
+
+ ์ด์ ์ ์นด์นด์ค ๊ณ์ ์ ์ฌ์ฉํ๋์ง ํ์ธํ๊ณ , ๊ธฐ๋ก์ ์ด์ ํด ๋๋ฆด๊ฒ์!
+
+
+ )}
+
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/pages/Login/DataMigrationPage/OAuthMigrationResult/OAuthMigrationResultPage.style.ts b/frontend/src/pages/Login/DataMigrationPage/OAuthMigrationResult/OAuthMigrationResultPage.style.ts
new file mode 100644
index 000000000..1c679e6b3
--- /dev/null
+++ b/frontend/src/pages/Login/DataMigrationPage/OAuthMigrationResult/OAuthMigrationResultPage.style.ts
@@ -0,0 +1,13 @@
+import { css, Theme } from '@emotion/react';
+
+export const titleWrapper = () => css`
+ width: 90%;
+ margin-top: 20%;
+`;
+export const title = ({ theme }: { theme: Theme }) => css`
+ ${theme.typography.h3}
+`;
+
+export const subtitle = ({ theme }: { theme: Theme }) => css`
+ ${theme.typography.b1}
+`;
diff --git a/frontend/src/pages/Login/DataMigrationPage/OAuthMigrationResult/OAuthMigrationResultPage.tsx b/frontend/src/pages/Login/DataMigrationPage/OAuthMigrationResult/OAuthMigrationResultPage.tsx
new file mode 100644
index 000000000..a3600c6ed
--- /dev/null
+++ b/frontend/src/pages/Login/DataMigrationPage/OAuthMigrationResult/OAuthMigrationResultPage.tsx
@@ -0,0 +1,86 @@
+import LoginLayout from '@_layouts/LoginLayout/LoginLayout';
+import MissingFallback from '@_components/Fallback/MissingFallback/MissingFallback';
+import * as S from './OAuthMigrationResultPage.style';
+import { useTheme } from '@emotion/react';
+
+import { useNavigate, useParams } from 'react-router-dom';
+import Button from '@_components/Button/Button';
+import ROUTES from '@_constants/routes';
+import { getInviteCode } from '@_common/inviteCodeManager';
+import { useEffect } from 'react';
+import GET_ROUTES from '@_common/getRoutes';
+import HappyFallback from '@_components/Fallback/HappyFallback/HappyFallback';
+
+export default function OAuthMigrationResultPage() {
+ const theme = useTheme();
+ const { result } = useParams();
+ const navigate = useNavigate();
+
+ useEffect(() => {
+ if (!result || (result !== 'success' && result !== 'fail')) {
+ navigate(GET_ROUTES.default.notFound);
+ }
+ }, [result, navigate]);
+
+ if (result === 'success') {
+ return (
+
+
+
+
+
+ ๋ชจ์ฐ๋ค๋ฅผ ์ฌ์ฉํ ์ค๋น๊ฐ ๋์์ด์
+
+
+
+ ์ด์ ๊ธฐ๋ก์ ์๋ก์ด ๊ณ์ ์ผ๋ก ์ฎ๊ฒผ์ด์!
+
+
+
+
+
+
+
+
+ );
+ }
+ if (result === 'fail') {
+ return (
+
+
+
+
+ ๊ธฐ๋ก์ ์ด์ ํ๋๋ฐ ์คํจํ์ด์!
+
+
+ ํน์ ์ด์ ์นด์นด์ค ๊ณ์ ๊ฐ์
์๊ฐ ์๋๊ฐ์?
+
+
+
+
+
+
+
+
+ );
+ }
+}
diff --git a/frontend/src/pages/Login/DataMigrationPage/Select/OAuthSelectPage.tsx b/frontend/src/pages/Login/DataMigrationPage/Select/OAuthSelectPage.tsx
deleted file mode 100644
index 13d66bd93..000000000
--- a/frontend/src/pages/Login/DataMigrationPage/Select/OAuthSelectPage.tsx
+++ /dev/null
@@ -1,75 +0,0 @@
-import AppleOAuthIcon from '@_components/Icons/AppleOAuthIcon';
-import GoogleLoginButton from '@_components/GoogleLoginButton/GoogleLoginButton';
-import LoginLayout from '@_layouts/LoginLayout/LoginLayout';
-import MissingFallback from '@_components/MissingFallback/MissingFallback';
-import ROUTES from '@_constants/routes';
-import { getMemberToken } from '@_utils/tokenManager';
-import { useEffect } from 'react';
-import { useNavigate } from 'react-router-dom';
-
-export default function OAuthSelectPage() {
- const navigate = useNavigate();
-
- useEffect(() => {
- if (!getMemberToken()) {
- alert('์๋ชป๋ ์ ๊ทผ์
๋๋ค.');
- navigate(ROUTES.home);
- }
- }, [navigate]);
- const appleAuthLogin = () => {
- if (
- !process.env.APPLE_O_AUTH_CLIENT_ID ||
- !process.env.APPLE_OAUTH_REDIRECT_URI
- ) {
- throw new Error('Apple OAuth ์ ๋ณด๊ฐ ์์ต๋๋ค.');
- }
-
- const params = {
- client_id: process.env.APPLE_O_AUTH_CLIENT_ID,
- redirect_uri: process.env.APPLE_OAUTH_REDIRECT_URI,
- response_type: 'code id_token',
- response_mode: 'form_post',
- scope: 'name email',
- };
- const queryString = new URLSearchParams(params).toString();
- const appleOAuthUrl = `${process.env.APPLE_REQUEST_URL}?${queryString}`;
- if (process.env.MSW === 'true') {
- window.location.href = 'http://localhost:8081/kakao-o-auth?code=1';
- } else {
- window.location.href = appleOAuthUrl;
- }
- };
-
- return (
-
-
-
-
-
-
-
-
-
- );
-}
diff --git a/frontend/src/pages/Login/HomePage/HomePage.style.ts b/frontend/src/pages/Login/HomePage/HomePage.style.ts
new file mode 100644
index 000000000..9a4a80198
--- /dev/null
+++ b/frontend/src/pages/Login/HomePage/HomePage.style.ts
@@ -0,0 +1,17 @@
+import { css, Theme } from '@emotion/react';
+
+export const kakaoButton = () => css`
+ background: none;
+ border: none;
+`;
+export const explain = ({ theme }: { theme: Theme }) => css`
+ ${theme.typography.b3}
+ padding: 1rem;
+ color: ${theme.colorPalette.grey[300]};
+ background-color: ${theme.colorPalette.white[100]};
+`;
+
+export const boundary = ({ theme }: { theme: Theme }) => css`
+ height: 4rem;
+ border-right: 1px solid ${theme.colorPalette.grey[300]};
+`;
diff --git a/frontend/src/pages/Login/HomePage/HomePage.tsx b/frontend/src/pages/Login/HomePage/HomePage.tsx
index 9233e7a64..a907615a6 100644
--- a/frontend/src/pages/Login/HomePage/HomePage.tsx
+++ b/frontend/src/pages/Login/HomePage/HomePage.tsx
@@ -1,17 +1,17 @@
import { css, useTheme } from '@emotion/react';
-import { Navigate, useNavigate } from 'react-router-dom';
+import { Navigate } from 'react-router-dom';
import GET_ROUTES from '@_common/getRoutes';
import LoginLayout from '@_layouts/LoginLayout/LoginLayout';
import MainLogoIcon from '@_components/Icons/MainLogoIcon';
import AppleOAuthIcon from '@_components/Icons/AppleOAuthIcon';
-import ROUTES from '@_constants/routes';
import { getLastDarakbangId } from '@_common/lastDarakbangManager';
import { getAccessToken } from '@_utils/tokenManager';
import GoogleLoginButton from '@_components/GoogleLoginButton/GoogleLoginButton';
+import * as S from './HomePage.style';
+import KakaoOAuthLoginIcon from '@_components/Icons/KakaoOAuthIcon';
export default function HomePage() {
const theme = useTheme();
const nowToken = getAccessToken();
- const navigate = useNavigate();
if (nowToken) {
const lastDarakbangId = getLastDarakbangId();
@@ -19,7 +19,7 @@ export default function HomePage() {
return ;
}
if (!lastDarakbangId) {
- return ;
+ return ;
}
}
@@ -47,10 +47,6 @@ export default function HomePage() {
}
};
- const handleDataMigraionLink = () => {
- navigate(ROUTES.oAuthMigration);
- };
-
return (
@@ -59,58 +55,64 @@ export default function HomePage() {
css={css`
display: flex;
flex-direction: column;
- gap: 28px;
- height: 70vh;
- justify-content: center;
+ margin-top: 30%;
+ gap: 2rem;
align-items: center;
+ justify-content: center;
`}
>
-
-
๋ชจ์ฌ๋ด์ ์ฐ๋ฆฌ์
- ๋ค๋ฝ๋ฐฉ
-
-
-
-
-
+
+ ์นด์นด์คํก ๋ก๊ทธ์ธ์ ๋ ์ด์ ์ง์ํ์ง ์์์!
+
+ ๋ค๋ฅธ ๋ก๊ทธ์ธ์ ์ด์ฉํด์ฃผ์ธ์
+
+
);
diff --git a/frontend/src/pages/Login/OAuthLoginPage/OAuthLoginPage.tsx b/frontend/src/pages/Login/OAuthLoginPage/OAuthLoginPage.tsx
index 1915812f1..20e75a2c6 100644
--- a/frontend/src/pages/Login/OAuthLoginPage/OAuthLoginPage.tsx
+++ b/frontend/src/pages/Login/OAuthLoginPage/OAuthLoginPage.tsx
@@ -1,14 +1,11 @@
import ROUTES from '@_constants/routes';
-import { getInviteCode } from '@_common/inviteCodeManager';
-import { kakaoOAuth, googleOAuth } from '@_apis/auth';
-import {
- getMemberToken,
- removeMemberToken,
- setAccessToken,
- setMemberToken,
-} from '@_utils/tokenManager';
+import { googleOAuth } from '@_apis/auth';
+import { setAccessToken } from '@_utils/tokenManager';
import { useEffect } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
+import { ApiError } from '@_utils/customError/ApiError';
+import useMigrationOAuth from '@_hooks/mutaions/useMigrationOAuth';
+import GET_ROUTES from '@_common/getRoutes';
type Provider = 'apple' | 'google' | 'kakao';
@@ -16,6 +13,10 @@ export default function OAuthLoginPage() {
const navigate = useNavigate();
const params = useParams<'provider'>();
const provider = params.provider as Provider | undefined;
+ const { mutate: kakaoMigration } = useMigrationOAuth(
+ () => navigate(`${GET_ROUTES.default.resultMigration}/sucess`),
+ () => navigate(`${GET_ROUTES.default.resultMigration}/fail`),
+ );
useEffect(() => {
const loginOAuth = async () => {
@@ -42,36 +43,27 @@ export default function OAuthLoginPage() {
const oauthHandlers: Record Promise> = {
apple: async () => {
setAccessToken(codeOrToken);
+ navigate(ROUTES.kakaoSelection);
},
google: async () => {
- const response = await googleOAuth(codeOrToken, getMemberToken());
+ const response = await googleOAuth(codeOrToken);
setAccessToken(response.data.accessToken);
+ navigate(ROUTES.kakaoSelection);
},
kakao: async () => {
- const response = await kakaoOAuth(codeOrToken);
- setAccessToken(response.data.accessToken);
- setMemberToken(response.data.memberId);
- navigate(ROUTES.oAuthSelection);
- return true; // ์กฐ๊ธฐ ๋ฐํ
+ kakaoMigration(codeOrToken);
},
};
const handler = oauthHandlers[provider];
- const shouldReturn = await handler();
- if (shouldReturn) return;
-
- removeMemberToken();
-
- const inviteCode = getInviteCode();
- if (inviteCode) {
- navigate(`${ROUTES.darakbangInvitationRoute}?code=${inviteCode}`);
+ await handler();
+ } catch (error) {
+ if (error instanceof ApiError) {
+ alert(error.message);
} else {
- navigate(ROUTES.darakbangSelectOption);
+ alert('์ ์ ์๋ ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.');
}
- } catch (error) {
- console.error('OAuth ์ฒ๋ฆฌ ์ค ์ค๋ฅ ๋ฐ์:', error);
- alert('๋ก๊ทธ์ธ ์ฒ๋ฆฌ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.');
navigate(ROUTES.home);
}
};
diff --git a/frontend/src/pages/Moim/MainPage/MainPage.style.ts b/frontend/src/pages/Moim/MainPage/MainPage.style.ts
index 9b054ccbd..9b9b8442f 100644
--- a/frontend/src/pages/Moim/MainPage/MainPage.style.ts
+++ b/frontend/src/pages/Moim/MainPage/MainPage.style.ts
@@ -20,3 +20,20 @@ export const ModalContent = (props: { theme: Theme }) => css`
margin-bottom: 4rem;
padding-right: 60px;
`;
+
+export const skipLink = () => css`
+ position: absolute;
+ z-index: 100;
+ top: -40px;
+ left: 0;
+
+ padding: 8px;
+
+ color: #fff;
+
+ background: #000;
+
+ &:focus {
+ top: 0;
+ }
+`;
diff --git a/frontend/src/pages/Moim/MainPage/MainPage.tsx b/frontend/src/pages/Moim/MainPage/MainPage.tsx
index ff5166c56..b86650189 100644
--- a/frontend/src/pages/Moim/MainPage/MainPage.tsx
+++ b/frontend/src/pages/Moim/MainPage/MainPage.tsx
@@ -148,16 +148,28 @@ export default function MainPage() {
}, [darakbangMenuOption]);
return (
+
+ ํ๋จ ๋ฉ๋ด ๋ฐ๋ก๊ฐ๊ธฐ
+
- {
e.stopPropagation();
setIsDarakbangMenuOpened(!isDarakbangMenuOpened);
}}
+ aria-label="๋ค๋ฝ๋ฐฉ"
>
{darakbangName}
-
+
-
+
@@ -178,16 +194,14 @@ export default function MainPage() {
{isDarakbangMenuOpened && darakbangMenu}
-
-
-
-
-
navigate(GET_ROUTES.nowDarakbang.addMoim())}
/>
+
+
+
diff --git a/frontend/src/pages/Moim/MainPage/components/HomeMainContent/MoimCardList/MoimCard/MoimCard.tsx b/frontend/src/pages/Moim/MainPage/components/HomeMainContent/MoimCardList/MoimCard/MoimCard.tsx
index ff22569ce..11f5b7b69 100644
--- a/frontend/src/pages/Moim/MainPage/components/HomeMainContent/MoimCardList/MoimCard/MoimCard.tsx
+++ b/frontend/src/pages/Moim/MainPage/components/HomeMainContent/MoimCardList/MoimCard/MoimCard.tsx
@@ -1,14 +1,12 @@
import * as S from './MoimCard.style';
-
import { formatHhmmToKorean, formatYyyymmddToKorean } from '@_utils/formatters';
-
import { HTMLProps } from 'react';
import HeartIcon from '@_components/Icons/HeartIcon';
import { MoimInfo } from '@_types/index';
import useChangeZzim from '@_hooks/mutaions/useChangeZzim';
import { useTheme } from '@emotion/react';
-interface MoimCardProps extends HTMLProps {
+interface MoimCardProps extends HTMLProps {
moimInfo: MoimInfo;
}
@@ -28,29 +26,59 @@ export default function MoimCard(props: MoimCardProps) {
} = props;
const theme = useTheme();
-
const { mutate: changeZzim } = useChangeZzim();
+ const { onClick, ...restArgs } = args;
- const handleHeartButtonClick = (e: React.MouseEvent) => {
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (e.currentTarget !== e.target) {
+ return;
+ }
+ if (onClick && (e.key === 'Enter' || e.key === ' ')) {
+ e.preventDefault();
+ onClick(e as unknown as React.MouseEvent);
+ }
+ };
+ const handleZzimButtonClick = (e: React.MouseEvent) => {
e.stopPropagation();
changeZzim(moimId);
};
+ // ์นด๋ ์ ์ฒด ์ ๋ณด๋ฅผ aria-describedby๋ก ์ ๊ณต
+ const cardInfoId = `moim-info-${moimId}`;
+ const titleId = `moim-title-${moimId}`;
+
return (
-
+
-
{title}
-
+ {/* ์ ๋ชฉ์ ์คํฌ๋ฆฐ ๋ฆฌ๋์ ์ฝํ๋๋ก id๋ฅผ ๋ถ์ฌ */}
+
+ {title}
+
+
-
+
{(date || time) && (
- {`${date ? '๋ ์ง' : ''}${date && time ? ' ๋ฐ ' : ''}${time ? '์๊ฐ' : ''}`}
+
+ {`${date ? '๋ ์ง' : ''}${date && time ? ' ๋ฐ ' : ''}${time ? '์๊ฐ' : ''}`}
+
{`${date ? formatYyyymmddToKorean(date) + ' ' : ''}${time ? formatHhmmToKorean(time) : ''}`}
@@ -71,6 +99,6 @@ export default function MoimCard(props: MoimCardProps) {
-
+
);
}
diff --git a/frontend/src/pages/Moim/MainPage/components/HomeMainContent/MoimList/MoimList.tsx b/frontend/src/pages/Moim/MainPage/components/HomeMainContent/MoimList/MoimList.tsx
index 6a9b573ee..0cae8fe4e 100644
--- a/frontend/src/pages/Moim/MainPage/components/HomeMainContent/MoimList/MoimList.tsx
+++ b/frontend/src/pages/Moim/MainPage/components/HomeMainContent/MoimList/MoimList.tsx
@@ -1,4 +1,4 @@
-import MissingFallback from '@_components/MissingFallback/MissingFallback';
+import MissingFallback from '@_components/Fallback/MissingFallback/MissingFallback';
import useMoims from '@_hooks/queries/useMoims';
import MoimCardListSkeleton from './MoimCardListSkeleton/MoimCardListSkeleton';
import MoimCardList from '../MoimCardList/MoimCardList';
diff --git a/frontend/src/pages/Moim/MainPage/components/HomeMainContent/MyMoim/MyMoim.tsx b/frontend/src/pages/Moim/MainPage/components/HomeMainContent/MyMoim/MyMoim.tsx
index 6af195238..ad40a0fa8 100644
--- a/frontend/src/pages/Moim/MainPage/components/HomeMainContent/MyMoim/MyMoim.tsx
+++ b/frontend/src/pages/Moim/MainPage/components/HomeMainContent/MyMoim/MyMoim.tsx
@@ -5,7 +5,7 @@ import MyMoimListFilters, {
Filter,
} from '@_pages/Moim/MainPage/components/HomeMainContent/MyMoim/MyMoimListFilters/MyMoimListFilters';
-import MissingFallback from '@_components/MissingFallback/MissingFallback';
+import MissingFallback from '@_components/Fallback/MissingFallback/MissingFallback';
import useMyMoims from '@_hooks/queries/useMyMoims';
import MoimCardList from '../MoimCardList/MoimCardList';
diff --git a/frontend/src/pages/Moim/MainPage/components/HomeMainContent/MyZzimMoimList/MyZzimMoimList.tsx b/frontend/src/pages/Moim/MainPage/components/HomeMainContent/MyZzimMoimList/MyZzimMoimList.tsx
index e92775b55..a5823f389 100644
--- a/frontend/src/pages/Moim/MainPage/components/HomeMainContent/MyZzimMoimList/MyZzimMoimList.tsx
+++ b/frontend/src/pages/Moim/MainPage/components/HomeMainContent/MyZzimMoimList/MyZzimMoimList.tsx
@@ -1,4 +1,4 @@
-import MissingFallback from '@_components/MissingFallback/MissingFallback';
+import MissingFallback from '@_components/Fallback/MissingFallback/MissingFallback';
import useMyZzimMoims from '@_hooks/queries/useMyZzimMoim';
import MoimCardList from '../MoimCardList/MoimCardList';
diff --git a/frontend/src/pages/Moim/MainPage/components/MoimTabBar/MoimTabBar.style.ts b/frontend/src/pages/Moim/MainPage/components/MoimTabBar/MoimTabBar.style.ts
index e3dc468d9..9197182f4 100644
--- a/frontend/src/pages/Moim/MainPage/components/MoimTabBar/MoimTabBar.style.ts
+++ b/frontend/src/pages/Moim/MainPage/components/MoimTabBar/MoimTabBar.style.ts
@@ -13,7 +13,11 @@ export const tabItemStyle = (props: { theme: Theme; isTurnedOn: boolean }) => {
return css`
${theme.typography.s1}
margin: 0 0 0 1rem;
+
color: ${isTurnedOn ? theme.semantic.primary : theme.semantic.disabled};
+
+ background: none;
+ border: none;
border-bottom: ${isTurnedOn
? `2px solid ${theme.semantic.primary}`
: 'none'};
diff --git a/frontend/src/pages/Moim/MainPage/components/MoimTabBar/MoimTabBar.tsx b/frontend/src/pages/Moim/MainPage/components/MoimTabBar/MoimTabBar.tsx
index 3af6f65d5..e923c3ff8 100644
--- a/frontend/src/pages/Moim/MainPage/components/MoimTabBar/MoimTabBar.tsx
+++ b/frontend/src/pages/Moim/MainPage/components/MoimTabBar/MoimTabBar.tsx
@@ -1,5 +1,4 @@
import * as S from './MoimTabBar.style';
-
import { common } from '@_common/common.style';
import { useTheme } from '@emotion/react';
@@ -14,14 +13,16 @@ interface MoimTabBarProps {
export default function MoimTabBar(props: MoimTabBarProps) {
const { currentTab, onTabClick } = props;
-
const theme = useTheme();
return (
-