From f5a3ce86d9a68168dc1cfbe92880e14d827110a9 Mon Sep 17 00:00:00 2001 From: BZzzzi Date: Thu, 6 Nov 2025 19:39:22 +0900 Subject: [PATCH 01/91] =?UTF-8?q?Chore:=20Global=20=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=95=84=EC=9B=83=20=ED=8F=B4=EB=8D=94=20=EC=9C=84=EC=B9=98=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/common/global-layout.jsx | 16 ++++++++++++++++ src/components/global-layout.jsx | 9 --------- 2 files changed, 16 insertions(+), 9 deletions(-) create mode 100644 src/components/common/global-layout.jsx delete mode 100644 src/components/global-layout.jsx diff --git a/src/components/common/global-layout.jsx b/src/components/common/global-layout.jsx new file mode 100644 index 0000000..332c319 --- /dev/null +++ b/src/components/common/global-layout.jsx @@ -0,0 +1,16 @@ +import { Outlet, useLocation } from "react-router"; +import Header from "@/components/common/header"; + +export default function GlobalLayout() { + const location = useLocation(); + const showButton = + location.pathname.includes("main-page") || + location.pathname.includes("list-page"); + + return ( + <> +
+ + + ); +} diff --git a/src/components/global-layout.jsx b/src/components/global-layout.jsx deleted file mode 100644 index 7fd6d55..0000000 --- a/src/components/global-layout.jsx +++ /dev/null @@ -1,9 +0,0 @@ -import { Outlet } from "react-router"; - -export default function GlobalLayout() { - return ( -
- -
- ); -} From 5ecb6c015079259a16a2d6919abf9580c9d122e1 Mon Sep 17 00:00:00 2001 From: BZzzzi Date: Thu, 6 Nov 2025 19:43:56 +0900 Subject: [PATCH 02/91] =?UTF-8?q?Feat:=20=EA=B3=B5=ED=86=B5=20Header=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 반응형 레이아웃 적용 - 로고 클릭 시 루트 페이지로 이동 - useLocation()을 활용해 페이지별 버튼 노출 조건 분기 - useLocation() 사용을 위해 BrowserRouter를 main에서 App을 감싸도록 구조 변경 --- src/App.jsx | 16 ++++----- src/components/common/header.jsx | 59 ++++++++++++++++++++++++++++++++ src/main.jsx | 7 +++- src/pages/test-page.jsx | 51 ++++++++++++++++++++++++++- 4 files changed, 122 insertions(+), 11 deletions(-) create mode 100644 src/components/common/header.jsx diff --git a/src/App.jsx b/src/App.jsx index e7b73a5..f85f518 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,19 +1,17 @@ -import { BrowserRouter, Routes, Route } from "react-router"; +import { Routes, Route } from "react-router"; import { GlobalStyle } from "@/styles/global-style"; -import GlobalLayout from "@/components/global-layout"; +import GlobalLayout from "@/components/common/global-layout"; import TestPage from "@/pages/test-page"; function App() { return ( <> - - - }> - } /> - - - + + }> + } /> + + ); } diff --git a/src/components/common/header.jsx b/src/components/common/header.jsx new file mode 100644 index 0000000..455b475 --- /dev/null +++ b/src/components/common/header.jsx @@ -0,0 +1,59 @@ +import { Link } from "react-router"; +import styled from "styled-components"; +import logo from "@/assets/icons/logo.svg"; + +const ContainWrapper = styled.div` + position: sticky; + top: 0; + background-color: white; + border-bottom: 1px solid #ededed; +`; + +const Contain = styled.div` + max-width: 1248px; + margin: 0 auto; +`; + +const HeaderWrapper = styled.div` + display: flex; + justify-content: start; + align-items: center; + gap: 8px; + margin: 0 24px; +`; + +const LogoWrapper = styled.div` + display: flex; + align-items: center; + gap: 8px; +`; + +const ButtonWrapper = styled.div` + margin-left: auto; +`; + +export default function Header({ showButton }) { + return ( + <> + + + + + + 로고 +

Rolling

+
+ + {showButton && ( + + + + + + )} +
+
+
+ + ); +} diff --git a/src/main.jsx b/src/main.jsx index 12d36b1..e50388c 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -1,4 +1,9 @@ import { createRoot } from "react-dom/client"; +import { BrowserRouter } from "react-router"; import App from "./App.jsx"; -createRoot(document.getElementById("root")).render(); +createRoot(document.getElementById("root")).render( + + + +); diff --git a/src/pages/test-page.jsx b/src/pages/test-page.jsx index c03cb51..9a5e662 100644 --- a/src/pages/test-page.jsx +++ b/src/pages/test-page.jsx @@ -5,7 +5,6 @@ import media from "@/styles/media"; const Container = styled.div` padding: 40px 20px; - max-width: 800px; margin: 0 auto; `; @@ -51,6 +50,56 @@ export default function TestPage() {
데스크톱(1024px~): 보라색 배경, 큰 폰트 + + 창 크기를 조절해보세요! +
+
+ 모바일(~599px): 파란색 배경, 작은 폰트 +
+ 태블릿(600~1023px): 초록색 배경, 중간 폰트 +
+ 데스크톱(1024px~): 보라색 배경, 큰 폰트 +
+ + 창 크기를 조절해보세요! +
+
+ 모바일(~599px): 파란색 배경, 작은 폰트 +
+ 태블릿(600~1023px): 초록색 배경, 중간 폰트 +
+ 데스크톱(1024px~): 보라색 배경, 큰 폰트 +
+ + 창 크기를 조절해보세요! +
+
+ 모바일(~599px): 파란색 배경, 작은 폰트 +
+ 태블릿(600~1023px): 초록색 배경, 중간 폰트 +
+ 데스크톱(1024px~): 보라색 배경, 큰 폰트 +
+ + 창 크기를 조절해보세요! +
+
+ 모바일(~599px): 파란색 배경, 작은 폰트 +
+ 태블릿(600~1023px): 초록색 배경, 중간 폰트 +
+ 데스크톱(1024px~): 보라색 배경, 큰 폰트 +
+ + 창 크기를 조절해보세요! +
+
+ 모바일(~599px): 파란색 배경, 작은 폰트 +
+ 태블릿(600~1023px): 초록색 배경, 중간 폰트 +
+ 데스크톱(1024px~): 보라색 배경, 큰 폰트 +
); } From d043eb9cae4389c2d288a828e6435cbf032861a9 Mon Sep 17 00:00:00 2001 From: BZzzzi Date: Thu, 6 Nov 2025 19:49:04 +0900 Subject: [PATCH 03/91] =?UTF-8?q?Refactor:=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/test-page.jsx | 50 ----------------------------------------- 1 file changed, 50 deletions(-) diff --git a/src/pages/test-page.jsx b/src/pages/test-page.jsx index 9a5e662..8a39db3 100644 --- a/src/pages/test-page.jsx +++ b/src/pages/test-page.jsx @@ -50,56 +50,6 @@ export default function TestPage() {
데스크톱(1024px~): 보라색 배경, 큰 폰트 - - 창 크기를 조절해보세요! -
-
- 모바일(~599px): 파란색 배경, 작은 폰트 -
- 태블릿(600~1023px): 초록색 배경, 중간 폰트 -
- 데스크톱(1024px~): 보라색 배경, 큰 폰트 -
- - 창 크기를 조절해보세요! -
-
- 모바일(~599px): 파란색 배경, 작은 폰트 -
- 태블릿(600~1023px): 초록색 배경, 중간 폰트 -
- 데스크톱(1024px~): 보라색 배경, 큰 폰트 -
- - 창 크기를 조절해보세요! -
-
- 모바일(~599px): 파란색 배경, 작은 폰트 -
- 태블릿(600~1023px): 초록색 배경, 중간 폰트 -
- 데스크톱(1024px~): 보라색 배경, 큰 폰트 -
- - 창 크기를 조절해보세요! -
-
- 모바일(~599px): 파란색 배경, 작은 폰트 -
- 태블릿(600~1023px): 초록색 배경, 중간 폰트 -
- 데스크톱(1024px~): 보라색 배경, 큰 폰트 -
- - 창 크기를 조절해보세요! -
-
- 모바일(~599px): 파란색 배경, 작은 폰트 -
- 태블릿(600~1023px): 초록색 배경, 중간 폰트 -
- 데스크톱(1024px~): 보라색 배경, 큰 폰트 -
); } From 54526a0014cb08a27fa87b670f9d13796478db91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A0=95=EC=9B=90?= Date: Fri, 7 Nov 2025 05:47:37 +0900 Subject: [PATCH 04/91] =?UTF-8?q?Feat:=20=EB=A1=A4=EB=A7=81=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=ED=8D=BC=20=ED=97=A4=EB=8D=94=20UI=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .vscode/settings.json | 9 +- src/App.jsx | 2 + src/components/head-nav.jsx | 9 ++ src/pages/rolling-page.jsx | 87 ++++++++++++++++ src/styles/head-nav-style.js | 12 +++ src/styles/rolling-page-styles.js | 167 ++++++++++++++++++++++++++++++ 6 files changed, 282 insertions(+), 4 deletions(-) create mode 100644 src/components/head-nav.jsx create mode 100644 src/pages/rolling-page.jsx create mode 100644 src/styles/head-nav-style.js create mode 100644 src/styles/rolling-page-styles.js diff --git a/.vscode/settings.json b/.vscode/settings.json index c86e487..62d54bc 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,11 +5,12 @@ "editor.codeActionsOnSave": { "source.fixAll.eslint": "explicit" }, - // 린트 적용 - "eslint.validate": ["javascript", "javascriptreact"], - + "eslint.validate": [ + "javascript", + "javascriptreact" + ], // JavaScript 관련 "javascript.preferences.importModuleSpecifier": "non-relative", "javascript.updateImportsOnFileMove.enabled": "always" -} +} \ No newline at end of file diff --git a/src/App.jsx b/src/App.jsx index e7b73a5..c3e90c8 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,6 +1,7 @@ import { BrowserRouter, Routes, Route } from "react-router"; import { GlobalStyle } from "@/styles/global-style"; import GlobalLayout from "@/components/global-layout"; +import RollingPage from "@/pages/rolling-page"; import TestPage from "@/pages/test-page"; function App() { @@ -11,6 +12,7 @@ function App() { }> } /> + } /> diff --git a/src/components/head-nav.jsx b/src/components/head-nav.jsx new file mode 100644 index 0000000..7f22bb5 --- /dev/null +++ b/src/components/head-nav.jsx @@ -0,0 +1,9 @@ +import { HeadNavContainer } from "@/styles/head-nav-style"; + +export default function HeadNav() { + return ( + +

navigation

+
+ ); +} \ No newline at end of file diff --git a/src/pages/rolling-page.jsx b/src/pages/rolling-page.jsx new file mode 100644 index 0000000..fa357c8 --- /dev/null +++ b/src/pages/rolling-page.jsx @@ -0,0 +1,87 @@ +import { + RollingHeaderContainer, + RollingHeaderUserInfo, + RollingHeaderRightContainer, + RollingHeaderUserPeopleContainer, + RollingHeaderUserPeopleImages, + RollingHeaderUserPeopleImage, + RollingHeaderUserDefaultImage, + RollingHeaderUserPeopleState, + RollingHeaderImojiContainer, + RollingHeaderArrowDown, + PerpendicularLine, + RollingHeaderImojiIconContainer, + RollingHeaderImojiText, + RollingHeaderImojiIcon, + RollingHeaderImojiEditButton, + RollingHeaderImojiEditButtonIcon, + RollingHeaderImojiEditButtonText, + RollingHeaderLinkShareButton, + RollingPageContainer, +} from "@/styles/rolling-page-styles"; +import HeadNav from "@/components/head-nav"; +import ArrowDownIcon from "@/assets/icons/arrow-down.svg"; +import AddEmojiIcon from "@/assets/icons/add-emoji.svg"; +import ShareIcon from "@/assets/icons/share.svg"; + +export default function RollingPage() { + return ( + <> + + + + To. Ashley Kim + + + + + {/* //여기에서 함수를 불러와서 처리해야함 */} + + + + + + + + + 23명이 작성 했어요! + + + + + + + {/* //여기에서 함수를 불러와서 처리해야함 */} + + 😘 + 12 + + + 😘 + 12 + + + 😘 + 12 + + + + + 추가 + + + + + + + + + + + + + + ); +} + + diff --git a/src/styles/head-nav-style.js b/src/styles/head-nav-style.js new file mode 100644 index 0000000..e952219 --- /dev/null +++ b/src/styles/head-nav-style.js @@ -0,0 +1,12 @@ +import styled from "styled-components"; +import { colors } from "@/styles/colors"; + + +export const HeadNavContainer = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + height: 65px; + background-color: #fff; + border-bottom: 1px solid ${colors.gray[200]}; +`; \ No newline at end of file diff --git a/src/styles/rolling-page-styles.js b/src/styles/rolling-page-styles.js new file mode 100644 index 0000000..22ae3e7 --- /dev/null +++ b/src/styles/rolling-page-styles.js @@ -0,0 +1,167 @@ +import styled from "styled-components"; +import { colors } from "@/styles/colors"; +import { font } from "@/styles/font"; +import ShareIcon from "@/assets/icons/share.svg"; +//최상단헤더 컨테이너 +export const RollingHeaderContainer = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + width: 1200px; + margin: 0 auto; + padding: 13px 20px; + height: 65px; + background-color: background: rgba(255, 255, 255, 1); + +`; + +//유저 정보 컨테이너 TO. Ashley Kim +export const RollingHeaderUserInfo = styled.div` + display: flex; + align-items: center; + width: 227px; + height: 42px; + line-height: 42px; + letter-spacing: -1%; + ${font.bold28} + color: ${colors.gray[800]}; +`; + + +export const RollingHeaderRightContainer = styled.div` + display: flex; + align-items: center; + gap: 20px; +`; + + + +//유저 이미지 컨테이너 프로필 사진들과, 몇명이 작성중인지 표시 +export const RollingHeaderUserPeopleContainer = styled.div` + width: 228px; + display: flex; + align-items: center; + background-color: #fff; +`; + +//유저 이미지 프로필 사진들 +export const RollingHeaderUserPeopleImages = styled.div` + width: 76px; + height: 28px; + position: relative; + cursor: pointer; + +`; + +export const RollingHeaderUserPeopleImage = styled.img` + width: 28px; + height: 28px; + border-radius: 140px; + border: 1.4px solid #000; + position: relative; + margin-left: -10px; +`; + +export const RollingHeaderUserDefaultImage = styled(RollingHeaderUserPeopleImage)``; + +//몇명이 작성중인지 +export const RollingHeaderUserPeopleState = styled.div` + width: 160px; + height: 27px; + line-height: 27px; + ${font.bold18} + color: ${colors.gray[900]}; + text-align: center; +`; + +//이모지 컨테이너 드롭박스 포함, 추가 버튼 포함 +export const RollingHeaderImojiContainer = styled.div` + display: flex; + align-items: center; + gap: 8px; +`; + +export const RollingHeaderImojiIconContainer = styled.div` + display: inline-flex; + align-items: center; + justify-content: center; + width: auto; + height: 36px; + padding: 8px 12px; + text-align: center; + border-radius: 32px; + background: rgba(153, 153, 153, 1); + +`; + +export const RollingHeaderImojiIcon = styled.div` + display: flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + color: rgba(255, 255, 255, 1); +`; + +export const RollingHeaderImojiText = styled.span` + ${font.regular16} + color: rgba(255, 255, 255, 1) +`; + +export const RollingHeaderImojiEditButton = styled.button` + display: flex; + align-items: center; + justify-content: center; + gap: 7px; + width: 88px; + height: 36px; + border-radius: 6px; + background: #fff; + border: 1px solid ${colors.gray[300]}; + cursor: pointer; +`; + +export const RollingHeaderImojiEditButtonIcon = styled.img` + width: 20px; + height: 20px; +`; + +export const RollingHeaderImojiEditButtonText = styled.span` + ${font.regular16} + color: ${colors.gray[900]}; +`; + +export const RollingHeaderLinkShareButton = styled.div` + width: 56px; + height: 36px; + border-radius: 6px; + background-image: url("${ShareIcon}"); + background-size: 24px 24px; + background-repeat: no-repeat; + background-position: center; + padding: 12px 32px; + cursor: pointer; + border: 1px solid ${colors.gray[300]}; + +`; + +export const RollingHeaderArrowDown = styled.img` + width: 24px; + height: 24px; + cursor: pointer; +`; + +export const PerpendicularLine = styled.div` + border-left : 1px solid ${colors.gray[200]}; + height: 28px; +`; + + + +export const RollingPageContainer = styled.div` + background-color: ${colors.blue[100]}; + width: 100%; + margin: 0 auto; + padding: 20px; + height: 100vh; +`; \ No newline at end of file From 7f6f348a33257bfd6727c77a5b8d49bece427b6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A0=95=EC=9B=90?= Date: Fri, 7 Nov 2025 10:25:46 +0900 Subject: [PATCH 05/91] =?UTF-8?q?Feat:=20=EB=A1=A4=EB=A7=81=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=ED=8D=BC=20=ED=97=A4=EB=8D=94=20UI=20=ED=83=9C?= =?UTF-8?q?=EB=B8=94=EB=A6=BF,=EB=AA=A8=EB=B0=94=EC=9D=BC=20=EB=B0=98?= =?UTF-8?q?=EC=9D=91=ED=98=95=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/rolling-page.jsx | 24 ++-- src/styles/head-nav-style.js | 8 +- src/styles/rolling-page-styles.js | 175 +++++++++++++++++++++++++++--- 3 files changed, 181 insertions(+), 26 deletions(-) diff --git a/src/pages/rolling-page.jsx b/src/pages/rolling-page.jsx index fa357c8..9aacc03 100644 --- a/src/pages/rolling-page.jsx +++ b/src/pages/rolling-page.jsx @@ -9,10 +9,12 @@ import { RollingHeaderUserPeopleState, RollingHeaderImojiContainer, RollingHeaderArrowDown, - PerpendicularLine, + PerpendicularLineFirst, + PerpendicularLineSecond, RollingHeaderImojiIconContainer, RollingHeaderImojiText, RollingHeaderImojiIcon, + RollingHeaderImojiEditButtonContainer, RollingHeaderImojiEditButton, RollingHeaderImojiEditButtonIcon, RollingHeaderImojiEditButtonText, @@ -24,6 +26,7 @@ import ArrowDownIcon from "@/assets/icons/arrow-down.svg"; import AddEmojiIcon from "@/assets/icons/add-emoji.svg"; import ShareIcon from "@/assets/icons/share.svg"; + export default function RollingPage() { return ( <> @@ -49,7 +52,7 @@ export default function RollingPage() { - + {/* //여기에서 함수를 불러와서 처리해야함 */} @@ -65,13 +68,18 @@ export default function RollingPage() { 12 - - - 추가 - + + + + 추가 + + + + + + + - - diff --git a/src/styles/head-nav-style.js b/src/styles/head-nav-style.js index e952219..17d8de0 100644 --- a/src/styles/head-nav-style.js +++ b/src/styles/head-nav-style.js @@ -1,5 +1,6 @@ import styled from "styled-components"; import { colors } from "@/styles/colors"; +import media from "@/styles/media"; export const HeadNavContainer = styled.div` @@ -9,4 +10,9 @@ export const HeadNavContainer = styled.div` height: 65px; background-color: #fff; border-bottom: 1px solid ${colors.gray[200]}; -`; \ No newline at end of file + + ${media.small` + display: none; + `} + +`; diff --git a/src/styles/rolling-page-styles.js b/src/styles/rolling-page-styles.js index 22ae3e7..eae18ee 100644 --- a/src/styles/rolling-page-styles.js +++ b/src/styles/rolling-page-styles.js @@ -2,6 +2,9 @@ import styled from "styled-components"; import { colors } from "@/styles/colors"; import { font } from "@/styles/font"; import ShareIcon from "@/assets/icons/share.svg"; +import media from "@/styles/media"; + + //최상단헤더 컨테이너 export const RollingHeaderContainer = styled.div` display: flex; @@ -10,21 +13,72 @@ export const RollingHeaderContainer = styled.div` width: 1200px; margin: 0 auto; padding: 13px 20px; - height: 65px; - background-color: background: rgba(255, 255, 255, 1); + height: 68px; + background-color: rgba(255, 255, 255, 1); + gap: 20px; + + ${media.large` + width: 1200px; + height: 68px; + margin: 0 auto; + padding: 13px 20px; + overflow-x: auto; + overflow-y: hidden; + gap: 20px; + `} + + ${media.medium` + width: 100%; + height: 68px; + margin: 0; + padding: 13px 20px; + overflow-x: auto; + overflow-y: hidden; + gap: 10px; + `} + + ${media.small` + flex-direction: column; + align-items: center; + height: auto; + width: 100%; + padding: 0px; + gap: 0px; + + `} `; + + + //유저 정보 컨테이너 TO. Ashley Kim export const RollingHeaderUserInfo = styled.div` display: flex; align-items: center; - width: 227px; + min-width: 227px; height: 42px; line-height: 42px; letter-spacing: -1%; ${font.bold28} color: ${colors.gray[800]}; + flex-shrink: 0; + + ${media.medium` + min-width: 150px; + height: 42px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + `} + + ${media.small` + width: 100%; + min-width: auto; + height: auto; + padding: 12px 20px; + border-bottom: 1px solid ${colors.gray[200]}; + `} `; @@ -32,6 +86,24 @@ export const RollingHeaderRightContainer = styled.div` display: flex; align-items: center; gap: 20px; + flex-shrink: 1; + min-width: 0; + + ${media.medium` + gap: 8px; + flex-shrink: 1; + min-width: 0; + + `} + + ${media.small` + width: 100%; + padding: 8px 20px; + + + `} + + `; @@ -41,7 +113,14 @@ export const RollingHeaderUserPeopleContainer = styled.div` width: 228px; display: flex; align-items: center; - background-color: #fff; + + ${media.medium` + display: none; + `} + + ${media.small` + display: none; + `} `; //유저 이미지 프로필 사진들 @@ -50,6 +129,15 @@ export const RollingHeaderUserPeopleImages = styled.div` height: 28px; position: relative; cursor: pointer; + ${media.medium` + + display: none; + `} + + ${media.small` + + display: none; + `} `; @@ -72,6 +160,14 @@ export const RollingHeaderUserPeopleState = styled.div` ${font.bold18} color: ${colors.gray[900]}; text-align: center; + ${media.medium` + + display: none; + `} + + ${media.small` + display: none; + `} `; //이모지 컨테이너 드롭박스 포함, 추가 버튼 포함 @@ -79,28 +175,38 @@ export const RollingHeaderImojiContainer = styled.div` display: flex; align-items: center; gap: 8px; + `; export const RollingHeaderImojiIconContainer = styled.div` - display: inline-flex; + display: flex; align-items: center; justify-content: center; width: auto; - height: 36px; + height: auto; padding: 8px 12px; text-align: center; border-radius: 32px; background: rgba(153, 153, 153, 1); + gap: 2px; + + ${media.small` + padding: 4px 8px; + `} `; export const RollingHeaderImojiIcon = styled.div` - display: flex; - align-items: center; - justify-content: center; + width: 24px; height: 24px; color: rgba(255, 255, 255, 1); + + ${media.small` + width: 20px; + height: 24px; + `} + `; export const RollingHeaderImojiText = styled.span` @@ -119,6 +225,16 @@ export const RollingHeaderImojiEditButton = styled.button` background: #fff; border: 1px solid ${colors.gray[300]}; cursor: pointer; + ${media.small` + width: 36px; + height: 32px; + `} +`; + +export const RollingHeaderImojiEditButtonContainer = styled.div` + display: flex; + align-items: center; + gap: 15px; `; export const RollingHeaderImojiEditButtonIcon = styled.img` @@ -129,6 +245,9 @@ export const RollingHeaderImojiEditButtonIcon = styled.img` export const RollingHeaderImojiEditButtonText = styled.span` ${font.regular16} color: ${colors.gray[900]}; + ${media.small` + display: none; + `} `; export const RollingHeaderLinkShareButton = styled.div` @@ -142,6 +261,13 @@ export const RollingHeaderLinkShareButton = styled.div` padding: 12px 32px; cursor: pointer; border: 1px solid ${colors.gray[300]}; + ${media.small` + width: 36px; + height: 32px; + background-size: 20px 20px; + padding: 8px 8px; + + `} `; @@ -151,17 +277,32 @@ export const RollingHeaderArrowDown = styled.img` cursor: pointer; `; + + + +export const RollingPageContainer = styled.div` +background-color: ${colors.blue[100]}; +width: 100%; +margin: 0 auto; +padding: 20px; +height: 100vh; +`; + + + export const PerpendicularLine = styled.div` - border-left : 1px solid ${colors.gray[200]}; + border-left: 1px solid ${colors.gray[200]}; height: 28px; `; +export const PerpendicularLineFirst = styled(PerpendicularLine)` + ${media.medium` + display: none; + `} + ${media.small` + display: none; + `} +`; -export const RollingPageContainer = styled.div` - background-color: ${colors.blue[100]}; - width: 100%; - margin: 0 auto; - padding: 20px; - height: 100vh; -`; \ No newline at end of file +export const PerpendicularLineSecond = styled(PerpendicularLine)``; \ No newline at end of file From 5a9be670443888e48de252d4561dd4bc2c043261 Mon Sep 17 00:00:00 2001 From: summerlane Date: Fri, 7 Nov 2025 18:15:27 +0900 Subject: [PATCH 06/91] =?UTF-8?q?Feat:=20=EB=A9=94=EC=9D=B8=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20UI=20=EA=B5=AC=ED=98=84,=20=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EB=B0=B1=EA=B7=B8=EB=9D=BC=EC=9A=B4=EB=93=9C=20?= =?UTF-8?q?=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.jsx | 2 + src/assets/images/main-visual-01.webp | Bin 0 -> 139058 bytes src/assets/images/main-visual-02.webp | Bin 0 -> 60992 bytes src/pages/main-page.jsx | 210 ++++++++++++++++++++++++++ 4 files changed, 212 insertions(+) create mode 100644 src/assets/images/main-visual-01.webp create mode 100644 src/assets/images/main-visual-02.webp create mode 100644 src/pages/main-page.jsx diff --git a/src/App.jsx b/src/App.jsx index e7b73a5..b98877b 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -2,6 +2,7 @@ import { BrowserRouter, Routes, Route } from "react-router"; import { GlobalStyle } from "@/styles/global-style"; import GlobalLayout from "@/components/global-layout"; import TestPage from "@/pages/test-page"; +import MainPage from "@/pages/main-page"; function App() { return ( @@ -11,6 +12,7 @@ function App() { }> } /> + } /> diff --git a/src/assets/images/main-visual-01.webp b/src/assets/images/main-visual-01.webp new file mode 100644 index 0000000000000000000000000000000000000000..3bb5a598d8be5c5bd741093837cbcc3ab0b987b0 GIT binary patch literal 139058 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ባ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 E0KF%w`v3p{ literal 0 HcmV?d00001 diff --git a/src/assets/images/main-visual-02.webp b/src/assets/images/main-visual-02.webp new file mode 100644 index 0000000000000000000000000000000000000000..fea0756fa6a7879fc60855297356dabeca67e390 GIT binary patch literal 60992 zcmZs?V{~TS(k>j^PRF)w+vzyz*tTt39ox2T+qP}p-}7?sbG|diwdSZ_vuf2U&YDYE zN?e@t1_(%9OjuD(ky8T(2nY!2pC=a_s1+DUNLEp@ng|F87<<@;gi33t`?A;Y#7z`H z0d#@AV%Qj9I#aKAr&m{P9TF}nZJHs?BF8kOwAB)0B(d&y zarumcjGEVAM18}b>690?E{pZy#%0f@$e<#Qpu~W(B#uH_ZukQ@fjx5+RYP=R0fm6>3mzO(}$G+wvMVoWQNCdeu~pB^^&7Ek*?Jc1oIwzTiWAL_kSeZ%QV5 zRycCXSKtG8lrXWtUM`;z0Fl5xt3lF{WYcJT#@^uOMo`T3PJZ)25F0 z(sr-(66b}Co9$0$ESMLs@b`a?>JSqf3pJp(rWlf>P9ConJ8>bc+r1Z|OEuVZYWhG) z!qs5uKqOjYC_tI&v!H-vW^x2bX5`&MUUP%62K6K}D8y-BA zFHLrS*1duNnEX`@gW~*`7ddO!(+iQAdRVKQ0gAeVmk~V{#~ZgIraE|9msq#ttcGWj#(4g;~hl>>UoFM<<>L~ zTqfXj-z~^zzpMU0*}LPRpJ__}s1ussqp9WJjcdb&4$!>Mp5P1!6!ZiUU@q;}7QyB< zW9Hmy!+uoy4^}{WxlQ?-^$3{sR}6z*;}1FbTpREk^_w+O#uN3OM5J`~99yW-JVd1; zDJIBTo!fwiO*6my+-%4UKjF@q-(JxL6O6SqT)#^IN4(Af#YbuOSt6y*1YXv8^tL9A za=?DR)xEzczSXq)cZIAor#y@2z_oXzg#nEN186`eI|Y z*t!?mbj2-FkmrI_-|bR_cdyQ3yMctXutSl$@kZ6MgKBO<^{7Ud_d(|yH@O$*oyP=2 z=<68LBnlbgAV%GKh}eprwUeyiLB+BUK3VTj-^U=v)4vUCQf+8>e=>QaKb8C?4?gB} z?M*)6QeY~4!2rD~gCM7DeO2 zz69QM}m9o(TGwCqh`qYvJIn7`m>^s#HSaGb8wpk29_O zPEFsi=ggs?vE(ZmR9+XM3h06CGa{Wk-2w#35(s$_29ji37lLd5P~^P^;Cfpwc|Of0 zaGOSTr!gTqKkYnUg}|I7X+>|BZjMPi?FhM&4J;Wp07GVh0^dKL^I!kB6mKA zz&~HIzcFzy=)mrR2w}(LK3%!An)t$N@i4{HaCZueN#8 zWh*n$NpKg8d+DGn5HoE%Zn326n@uR)@X7%)y?zo<#}v)RgGlObpi)ByDz@yxaV&*=CTx4foknC<6A=F- zFN+Ji*$@m_ojRn=nbr5OhgdFd@C6Tah@C4{Cu%lf3e~yU4cfbHPrwT}iP_w9tS%jp z%v|Q>pf6G|qZ;(jdb}!_HWczS165Y?f&_ur;15o?7=VMAcQ*Ec@9 zfwq%tNPq_#Rd>&}9sjEC&H?g$AEOwAq(uhWvU@C1?+&{2wHG7P`kMTuXYG7kvw1@w z7_+Rs-Jo^9ob^VWdEnHmC=+p)(_hL0rSYfG;<@|Lw9mc?ekKc^Q^EY65<}g`Nx?g+ zShU_fN!~X@3V|ky?bfs5yY30 z&`+}E)|*Zztiq)Wc^;%ahskR4a}}Oww%Y+%WJ5NHLaO$v?$CnGfwsN(^>>V5d+4G@ z?T*d#M7KITzxEJa5jLMxt?rP2w!YD$I2w*kAs!X8rRnP}6mr6{_nq2?EV%kwgBMb+ z-8s|+I`lT%e2WVdeCJK%Pq0f!Fl6em`Y|ngttV7JJcvT;BW4@nni!`b5UgI5P+D$ynXHB;L`#wdp$HtCKvCPqEaj{;sEajkdwXNmYq&* zDVL9>a!r0{13hb(*LM7hKgS%uaSij-t~UFTCk$*s0I*O~egIT{baVDg`dMI*%Di+Y zKWO1c0Sf$A9sz!_w!cMH{AGvg!uCJfg0kd$VAEOb5wo(`wO``qpU1VJjmM$0h^r;B z?C8+F>z~U=IRHZ|A^5Qyge=7P^o)va)E*L$$6N`sr*c8}wXojR6Neo%8w{E`sz1=! zmERZUs_~D|^DnTW%{jEDuyezrb0zm)rnMv&6ZI!gTFqzX)ZW|b{lS(YxroGCYDT@> zBF+w23-ZC3M1i5aNdZp4*5wo%)ICCnV1dC4t0p52ykeI$SQ7WaxmCu}1wnnHfw57U7ah1LzV&?Y!XV7Umo)an&z>0q3|bfa;=u}Duq8{GeZFY0|#+Xwyqn!7N<{;Od* zzp`c$+R(Pv-6RE)m`~1E=elN`{5S8MtX5QRE}bYk%|30kvJ`dDF*;9c0Y(#@ke7`3Q1d1V0^H=c z`ZQ6pN*GqJ8JLqeYGQgW>!qU9W2?{`*2!NH*vZV^_(Sp<(nXPbZ{uVqzXhuYvKR~M zp!^LPmULz(6q%!xX~=;4I3`^dYfQ}R9?f&}IG;{t2&M>|^~st`>iDfZZi)F^_x;c! zm48BSAY>97X?Heuz(vslh|qy_{$ zEhyQ8Ndf9{JGXo79-hZxtZR)dRY3$&r{Isw_|^k!N$q|~%gA#an)Z5y>HC1l zQt6n5V+Ey=+R4sFrYb8eR9iiEi?!hAtjUfmDU4xFPt{aTMDz`qKonmo%qwn(dz1RK zg+Ea<7nG!HBwX$1gPnEx3wsB{uAW<5b*}VL75ZYy1KSyy%Xc%DR}U%Btt2-5$}Fr( zzR#_}NZLpLlXRI}IH*otsE<)Syw6O!$QTQvNn^?HwVj@&yr3qz!acs;1Xi#78{@|kK_pd)_47sc>o&fJoz zCH1VsNEyUs$b_u~noe;FA{N_FR@P|_t!qmKelxHz`Ky)^GAV0tD?w5VsU;}NX!>0+ zb4ZRTKO;>uP*xmAFpE6!d|hbfkT$8iSvMb&3*~TBv$jlCly97W|#Byx^5iV zpi1IN=xy1-m*^~oLy>CIGmj{c?tQ4lUDcv@1Y5uvwx&Bo zQkbGjZ>c&w8>Ax_^iI)Z{6@hm8k_5WM{WK9Di+;88x)^=@*LK2)qJu%sBWk!;shNoO`=w9d1*?Pm%et;GeBmkomX)%Za+B_ky_S|=wAry zJ(xrl`;Gg)sK*VC9>trL6e{1HJTZq2~~l`>P^F8Wz=~|EuEn5N95J^GB)% z`p{0=mgYCs$^3x2#Y1F(Fr~_|_+J>`4V?hf=>w`n1mOAu=e}?lqAE(M$~%`Fk`{ex zu!wu$&`LD?#aLi>h#lJ)Oc z$FRr>X2m6S2T)^2n1+0jaC^nX<*cYRns{O6-C3fe`!dZ4p*>Ro&(P1VqWwSBpHfRh`pb8PQHKo5Eoc{% zZ_Jom8A~t9ToT%h-N~ZtoMlQPx(F?2H03zns>z`@vJ`C>_8G3_RQ9=ZxVl3po9WCs zg(V>DJ22_(I-HqKV8sGc#+g*6@I^_utRSj80;mQ}ECq*uLR6+DYK|K)LeWweW%Wo- zPV=bfseq=aB<+8=j%p24G?>C^^fsM7qZe!##K^fI_Q3az>yl~oq^6oRW=G)$7zRy{ z`>pEOc_g7%W9$ll$?<-l4+`^wCr^mP>=RXSv6YD7gsxE%{H@yrfn9XpAQcJ~@AM3^ zHiTYbO>l6s-ioI^WK9I3w9CN5oPg1&-2sScNDQS2#?td?OW5Y+%p*Q zFs$(-AxC7=bQ957)N73$O)fDEhKrF?ek6_PMPSP389QCjlQOTbRoTS9~9d!9;Wjc!7pzYU!=$P#Efvudjlf2=C;_EYmrurY2WC&PvRXK^-D%zIBcw z1c6yszS%=>YX@qG0aliS)!(|^II?|#L77#Qrc()h_-&U$v1y#o|Jprj*`vTT%Uco!bsa@!3$*0t(V&Mz6>eOxzWNT+v^do?)igmjdh7}l|_ z$ANjey9e`PI~!(KQjrd@Tbqt-+etl*r7@K2`)@=E3zLOIi$7;F!b59Km{#7$62DJ< zhaBtFuDZo%4eN^0&=vzsq0Bjz3{EPb=NwbPf4>V2HpBW5$RrYhZvZ5cN#5#L}47<6d5gRj<24aVT`%8=DanU0^s z4J<@wN~%fur>%#lXcu~f#dhX8R%IE)1-D3vI&2CSjRN9klT5_LHh!7A)0)e^v;=X9 z3BIewSim0{x;U>UjqRspimfg#C?&UmknFHHZS3E>V#J(ECzq(9$YP9{ggaquNg^33 z2Bt3Y7nc6r4x=1}E+XN#Z3vrGUM}FPlPOA4G2mJoj7LR_i4$p}KR!g}H+=g9yQ2NnX(@31^AH0{Z$Zyt7C%lRFzD5~pX2-^ao(m|_( z+}P5To4pcAc5EfcK}zDpf}`g&=)1F5w~S2V(%U<*jq@G7kT$Lh zfngLy`OoyMTmx0L8g%@FOBI{(ZpIFIF`8@%TFsZTt45c~Ji4_UVrg~*S{3HL)_O{2 z=mOkbhvE`3T8?7UCv~Fj#=dMCKP#>QP_<7L35X0i7b|PnmcCK4+&Dzyu~NqR-XmZ~ zJ#m7%1TDquBkCm_dnu*i5KZ|cNt?mi4aXk8UeT~-s=z|2NTPY^O!~6v<4hP$Cu91? zxEfNpMv^oFp?f>%x+Jiv=nUjMSY95SIuw;P6|*EL>sYBHlb8)MCL5OyIHAr2Z%Z|# z5P?%^2&nBYrlh6{MdCUmF6__tPb@Y{Y?uKem75i;VCPb+#E1!Sw6a(w=^Gj%Q1f zJD7C7wls#8@A>$l;LY}gjpvzo&)Jj5(Ag*S%*q!WDhROD7`A(aht;|qxO^ZuU0kk;mC)yj&w49Nh#pMZAue1NDi*6y6t zj-+(c8vJc)?hUwpEd3^9c#cWVreGYNgot9m+DqY>$vOOf&1awL?r_vg#14ktp&YmW zy6?^6871PUwBmvz(%=!mh|(p(JrB^t$+YEH-L~;;dEb=XwIJ{7JfJNBIFg{$wR@lvZI`s@ za4cf_C2c@JhoTVCG^AOE{c7anZ3{u+zw!*L6z81ha&*l2s_Ee2rY(AcLd=K=#ObC4 zU!#$}NxMG<4fdeBV2k_rfgy+PiZbP8+=+{Q%6;TMAOq#V64n($@feTT|5#A=4?(c| zc;I?X5=OCtlbSNvBh+mSmr>4peZZ7bUskx*FQ`(V0fapDCLO|mFlv+@kEeyYT12Mo zMVW(+()#DOhDAV4#&U>Eu+yz*&mYN(@;8^5ttqvmBf}FRD4}8?=e0+8BeV$-<&Wfi zT)X*>4@oNvm}QJ&-A+C zcP@tD!Xr&rI7rzB%*HT{>En2>LJyiI%vfem@KI}`sGnq>cJWiIsm0*3_tN(bw)}`> z!Fl(?u(LvYpc7bQ)X2gC3jESZC_=IbAc@2Y(H%HJ;T8*67kk$B_vpNm!z?3ZUXv)E z2lD>dX3S<;A8%})aRfcLQDhrjNGY$ljL*T@RV`7$6#j4Q-Kv8U(Aw|;7>7WcEQK++ zcR}LfYrm2d@4ljSSSM7sPVsArnIt7`MM6*r77^y+hi+PWx0Y4vV z7b0GxLAFG>3DGR+6HZ#zK~`@=pTwFPyOh2oij;M0aK|gi&oNHM%GVU6tyg%1O~Qx> zzUq2C-h94!!2i3)5X?0mvqW*8Ecg+QH(Yb@P=s%sqmMEbM_JF*RCL<0cr2SFB{T*X z%4`}z*N(|Gc8UeFA!Z^1)ra=QTu2!rgqe?_78+xDz#SPJNJiYC8s#ZTAuo*7?ui1T ztXYL);<(o7eX+yF5PEk)v4gx*{)c9Eo-6ZeS-jqbc0;MhaTor(O{a2_x2^cTDu4mC zMcm*60n=NV<=L9;PRL1wBih=D4&JJ0dX*0JtM=^A$yF)QtAMhx6C2_}KGzKl zj7gc|2s<_skR{UiZ$#Ou-KIA%|2+efS`#oVXZNWK$$IX3Vi{wbJ7Q`s%uUsJN?6^r z$u-L+esCOOs1~ezMX1IS#SZACEqmM&cybJ^GYzP2?Oo5fqPYPhQ-Y4yE-hBUg6**g z{G1}h*Fs9H+X<(*Z6-m&@DDuS#uDI!Tzw7g8%8q5Xzaps@&Y0eru7A}M15HL^nCx7 zKn!^e%nM?{plAlZ#z3_t*<@a zKRc_^`U9MQp+*S`!eISqDrL6L%A_^gJ^AZHvkHo=qP-^S@Qi?Z+67Rt``O-jemCXp zY<$MpX=}2+*?78n?Jnv!Us9Yq;|Z$!J0SakK!E{IVU|JC0~s@b1pQ(p3@Mzj#7_cY zgpMH$@)zV46HdUQ>iV6lDlRK!lE5n#j{UC1w&yRo(}0E&m>Mh0nb+DGP9HnwaGM}= zwE2Xfc$`9Fj4}z>3_GuA@fTMN@1Z>ROuXDq*sY_WLd8)O-sneRAwke+Pjkd-Y3&7e zq)cLl`ON^TkeXZas=6~{>z;KpZ~MNQ-`8X9zFY&^lh~ZiS_v&MfXC z#1xZKPDwO;kC)rdY%nbYd9)lQNv5WTQyt-eL+#FPcH0~#1&_g=uXFQ?ALTO1vAqyl z^z8JSV>h)X#s`lfN%c^APFxN_1{@8`D3PTovLGUggEUOoh42EFV>-i;h*CM4RG&5M zX<09&O4i`46(QhaB4NThzx6qZ^RTqfnB z2G~T&)!ev55cAmnqM?6~sGcHkYT{A7>z*?N#c0&9eWl-Rcs25GgBsmA++um|N05qu zt*J_@5th;db}jGL@j^-FSGE76pafos|Br3JCuqk;QBnzDDN1DEmr=mZBy#}LKk8nO zFJI_;K)R-sgRi?KE;W-k$kpC1u#EN|2veA;7c_v#9blQ$h?21vh4hC^XGUZbf+Y>5 zCJtprmiI zw=hy3-Qw;hY@xuJcC7Z~>U3_2MT~iH8l#tqus)qn! zMNYY@S%xT+AZ~4*bns>6nP#{0DOYpLKBd7X*_4Z+*OZX8OdV0{mS{+!zoFSQ)B=1<`0lZ=GWh{TabP*MZ)c{)oVT(>KoFu&?De=v!r;s-yL_-#~8dxQ~MN> zv0SNyVj<)p_8MD5nmb!w8a=YNx`6wyM?XLuAeI*c=%0HWga}hWK=zHm*}&9!;BCPF zT<9Px?gS)0N?hJZyJ5JcWv&m#dE8Y4ssuijcQ1jK0f?WgPrZZF{xg{$GGDvTrJt|k zCdls!8;C22_nki@pbLl!Jppf(-;+NAtBCWvw0=82eF9N_C_i@JyU#(N1mC&Ah}M1+ zfEvHNZ`(KEFRyojm$>U)KRru-Nfb2KmGvEuWFVGc&BE1Cx z)LsjIzb~G9zZ1WeUKPJ)Knno)T40|5_RH_?^26+X_bwMScg;=uad*#e`~&@q=6rXm zw+`U{)BA1rJ^Ky__AB~f_gw@yy|~@)&h=LM^7!cjS^=0>n0L4Py;A~BeqKMr-+te2 z@3qyp@&JzS*O#~<8P1>KAAyh2E3-a^Qa{X}gzvO3vlqK7fjhv&N4Iap_u{kLtKIwR z93UQ``1AQa`@-;{{=M5raF9D9Q0!*|!253er1+eDBvAL${V6>de4f48J?NeKxdC{6 z0{u{YMSb+XAU^bN3iJZH0U~dHA7QU{Hw34C;{X>xz|ZV{?UCQIz>r@gfaWLcQ{boe z1(2sV2#EF*{TcjW`BZ<=d(A!V+32?W>H38HiT9KJviz{RU<*;s$|2vT4BqTVTyq}nEaddN;-51T%{{yx%E-16e-=P}szx&JV9b$NUeApb} zWo>npO6HmV_8K+q)|@`@cqCmH5}EvkxGdIo(E7Js&z&h7(aoJqhWCKdGEk=ES9BQ< z1?}Io9mrv5Y>54Daz8gz8V?EAgz3!FJqMeRioA0_yms)fyy)CpN~T09M>MI^SiSe! z{}e;{QK8j&SS_9?{IWs)2Lkt^D$C{H?K@+?kS{<4;MK9LS_@4t&oqE^ZydK9G|{_Q zfhwdn+@wqkN{$neuZ7nyh%7sfrhw148ntajaR)YQ+a5XxRJV72LTWVwRi+!X*1uS} z#xv{FWqfssq&UhQgqYNRdCzx89f5X5p}m?BkXZ zbP1WDF%PG?X+&|YIrn$GjM#*_Bg0HC={&vvOX&qh8qoEeOUfop3iFJg+;(DrKB+TE z`e{u$d!yicnCSHF_L*{f%#+y<28}9MN~)^ze54F}8nMCPmQd;!=}31v3(%#A>u7Ev z9^oBaXWn};AmnDy2VqHloP`B4$WoV4^+!p@6a;0kf+_Do*`J#x*pD?%7RsQBZ|dQ? zz^>g6^dn(xA|uB7{~2IX;Wd8K^9Uu4WQzVL#Au!~2m4|-69i>a zkiU;}EJfhc`Yz$x)`M>&bS&XJ&b8wuctmdG?e~cb{sVMnTZ&_Y^>+nE!b+;FY>u1|F@A4%u9x z^xNX3Sa1)Nr}?i}_|G+)S+6Q>5G{Lt+^0hhqjvsJ>IU=H7Q}pQ#{O=!npsCRcFnhz zz_$ymm|+#>kO$iUbj}jAaS2z#KC`RCX z`#(H}CC>!i8T^GXn!QV)YH?PS!oTKGLIm4E{%5{%TYc4|&^~MbxK`mWuSGZ=7lWSl zmqG=_-ip@K`~PZQbQsBX(@ZB2%ZnSHu!Dc^e~jfn{Pe$J*r_QFU`8%(rH(oJ3V@iJ zaZ=e!I&xsV;bF_DVxiLPgi4%Yh-VGdhm!0!@qMVZ`IpK4&lX0qR$#7h2-k=VHs*rn z0gsNs`1UZKbU-ghgKqu9xz>dZv6e4Hws(0qN_*Gz=NBiOpPH^FlJxi+#p|W~fqh1A z>3jM1@qK(ka(Mq8ME)h8kz95#Mb=LEOD?o#X4r&0xrLQ@lC-6g1N!?+D)V1|I>?Lq z`4+!s5Z>OXQ0@*h7I?-FZ`gh1#nC9f0HLVAXBhVAPs#;&mW7i7g9G)TV)R=9Oc?K# zUzt&DG+RI{k=^{p_x}W%{{wTgi7dP5^i7d&vgnABd@t@n7dJ&i+IQYtG50XlO|=Ev zUv}84hdWwB9H1*iS~Om!uBiU3tt*>O6Tti((`Ec9rbN!eBuMyh4kJf29yIs)q;lvG zFRYQT+lO<}5rzfgDVvXpQ%F=Gr-KCt=12I7(;+-Ws$EUEg22wg%EGiv|Et@7BeX3^ z3azFxpBl`-H~t8WMeiQx0=Qfg0wS$)G@px+L-oLYYf_2K8Ct1t*pV38+CB%76w7(p zbDRS;ckxO@FJ{&!{RtP-Th2uAgc#VCNNZ$?ho%G#9c4=lJAX6np>-18U$IC3Z$$YQ zXHg!T@upVQBR*x(JT($6ip5ZK_621q(zYzu<4||4b48{&oMMle>McO;oGpA@`ekp@n4}yWLBRjz}kvNP0#0_Bp|A zIAN`0X=QZZ9~N=jMomE3`D+#RuUG}tiBFR}o?sX zK{1*%c}xiB=XoEU$jQ@M<|6qo<3zbefOpfxY>8aF#w`0={GMO1-BG8`T zJv0)1^YT1I2i4SR(=`i@7k%{j8`OF5>5NN;K>Dck=*5U|4%g3YNZM`YP>JO_vE=Fl zjbuFi@Z~w!K?Sj4>B4bu^Lq|hH>VuG-26#Bb?vn7$91WX!^?;G#<{F4@4u7pf2H96 zUgPReN}{Y~l+reuomL-lbQ=|4GC9iKfEOx?I?0S-X#H+s6Wy}(;7>G0xg!VA;-;IOrOY$kSjfZ|-g_TIBz-`Et4 zZ`V6%Xtlk2rt2Sb-a8W?z{4+5ruR3bkKMZC{p?CreuL@1Q^LR0?k%hq^(#r&&0j()*aK?ndzw6LVEAm8{*Op(7%RA* zR*33J%)&DPUr6x(<2t|6{fvj6Q^HjB-n}SV<}qr9vkTob`%ywa8m!&Eqsky+$>@S$ zX7rVF`(QjT_#hx%-<1?Cz#{%>(^-gEH1maF^k?26<-mROBgk*ey?s`!tfp%bI&OHE zSX{*-+jZ&ZgfZM{Fb!3S{bdJ zby(g1Bavi_qu-Dzsc=$6r}rDJn%T(I^0|O0fp4L`hdQx>)5<1NU7mN&BtmB8)a{X7 zER-bM^BUz2g}*wHcGO;IR}7rrEk*y3$VAUUCv*UqTkx`!m-bT_t=(7K(B+_8D03!8 zD=H!d+3SfqLWqDmnQpD)qB#4l%Q!fOLpatT%e}Ot|93bhQG7;oHNajKsp=# z@$<>WL)Pv78`{xm@h4et`5U1e(7E z7AH((bs#DIO{kVxk%;L*lp6B8{u`+YL325K=Nv9u_d*)h){((4Ue%yig6DK)vtoBa zA(aglYhysK@XvG;FI2P!54^MRm^bk6@S;Z=B!yxSRx>D$6ppbMwaodk5?*inGENE4#I2{l4e1 z0)^P6>CHg|3|HPc)rAL4;)bfrkNBhM`MeZ2!H}Ju!%_+!7-K{6(elFW$|((q3MU`Cd1%S3~5V%)3g+-%MsWaLQPG1}JAD-mE&=crhB z$pEqHS=)au>||AZ&lMNSe|@W3B}k6AQ?e!w-Eji~!s6-eqQ3Ki1!7bKU9WLZYrb|E ze2jotJiO}4pv@MyDUv_eph0KO!PNP)S)T0ji|jl-bVoq4)5WrVnTYzvZ_ElJ|E2JY zIz}L#+5UDD*dQ%&wq~#LO!UZ7Fm0OwZjx!wvktt2;`xiuM=4- zsXVW7y&kw;`mjdJiDicePmAsvst}_z<^YEABO|({vJD^C)(#7~?VlL5t5A@`>pDyA z@|_;BM<^C=++WA=2an+3al6GBit`WCw$6uGDsy6Dc^iX%GDxyYC;W;%f_*aLa3IxAGSDAoXioWjh zs971j>%77OWYZY&ozp63jPA1mC~Jh>c_*27^{(KrV)^HLI@9f5e*Rn$_*J8M)-Vz8 zE9c@wl9ZOvj4YO;804Z`4l=Y_)lks5*8jV{+ zxwgdPTx*wUjBGfuyWjqamGIn=yYEzyM85(zu9|0bTu+VD_Szb_dlEJxx3i`q+lDc( z_mOtecnS4dcs>HQ0*E4>IUC-rwbs6f7HRf`(Up#_@fUTEo5ROVO1L5wE4?#=pnxEt zgUo9=Dhd}iA9kqt>%|0OvQWRY;gl>@C>r#bKq;>`ojWzZDeTeNxP( zHgpe6e)9O9Ssavq$G2uu2=#mcp*q%G?jSVJN@UBuwbv9`{xb>wqWG(Dx(6E~UDgH| zq@{SkQqP`KOrWtPX@XT*H1!z82TW~IVIWI}eVB7ga->*!-1W9tHFT=$HkE`*LHwFW znaKsh4~*swcPE;@9yZ&;KKBRFjf(humJ&8_Q9JpMWr+Nm= zRuX2i*xqsB17Qo~`Oo@1i;HPWYitacuFKfJ^>)nt?J^I%@D;9o3)I9g6FY!;jtJ2z z>~eRrh8oCM&GFVJ>u^xv#+C9ftlOO({aNdDk_Nku*-D&d+~T{utY)=vgt*96x_6j` zCpngNv8k^^?mZAf-WK^ z+1FlM^1vkwae&$Qs-ZNHl?iPyW2xJzwz(Ck`XFk) zd203;>-GfgNh(E_;4h7b`!jE2e+?pBw7WLf`|kBM%Ul^(!PYxi=>;6+t#(<^1C_O` zs&?HHmHrS6KPB1whG|1B(S=Db(K>?1mB9)#%eS6q6+X3nhJp(`v4^G(WerWje~l_U3Ao3DxOjQwd_|sgq;3AO=of7+S_5#tL5AWBO+)JSTFCB`Og!?+ONsS zOa4ylB`+xR=RB4g_FHcGKV{=WH4W1F-wJwBm|z{*B1;H*A@CUBFuojxiCEt+sT3c4 z7nih<5st1u?~}FECRO%$<9;uyAVdvouQ%frs z%A$oOKpkDkxxuQ3EFDKAR?5|{A0&=%)%S=aM;IQjuo?l>X*XEJaf5$=1*bhr-LOoA{#u|YIq8{xpQA*uwZuOyad|tCOR=d2 zHv=$8U5C+wPx7EuLPonQ%*tr358P6p%O0L65PwRdQ75?SPYnu_GkXpw#jKXAvo*p{ zVXkN-GA((9{G{U0GwxhLnl*2zGXoS3x{zFmxs;bqhM>Pe3qQ2iepZ&O8sn13&l8^4 zLCF(0@atP77wT4dy5^1G$|ji)Ng%-M)XkCcp6t5zYeMsr_*M&H{eD8{ey8xvSmB5p zU&Rsi)8reJzwU@JT~7!Op+;~{L`n=LQM&xR`>y+genv}Hr7oc1oy*h<{aa=tU$SOX za+K;$)~m)&AD(T45L|P;M_UPP?&TUr^a6&ny$j4Dhq4PqR9x;F*@g0Hby){0B_ZygSo7IXhdQ^&7iRr|}L*OIXT zb)qB+HrH!qN97}2s&FlWXY-Kto0|kF(E_RF`ee{sM!iZ4t1g4u)57ycixHgjxuO*B zO`3fciG>22D-FHdP56eh_+#-#evFSbQIv1Y z-qyvDj8=%?jx9syli_&6>tLNEh!( z$qvp+;l1uDwM{%sKTQCyBp6rVU)}yU=`BT7fv>R9@4o#F&d(cIoxdt? z#tu3ltkNhBm62naY5%;cA?|1}Hf<|ydHli6sQEq{(ZgAlqZ7g$$VotF)`m8H15|}U z_YCcpzOfVF0*_ZZ5fg&Ekr+hQDa_Py7hN<28ZRv6fDhMGFnd8^Mmk7NHE!z3+6P1? z=E8IBLD@@{cif3^wYvVm$6p%z6%;G;B+E5ub5YxUGtM}$2)K*?om4cYyP(GAFAF|?(S z(%K648{aRlTgz(oQ#0IQXj#wl2~JSL4X*JN%Hq)rY4(VWf{ZuMAjVJheLRO?Q<}b5X%fBl z5QPb;#d)0h4P-lM&K}6tloJMyihtmS>K1Lt578VhqRTkjF)ERSxtS{J5Q;HRE!hm= zv$fyIYcrAhgxbgn;Rz!Y6cn1uOM^STRzJ-i5Ui0Vyx|^YlFpk-V;R{ylMcTV{3t&* z;HrOl7VIjK%HOo5M0D1xP;TG4@rz8s#Wt}4vxA^-5#H1A(Y#FV{wephC({#`-MQ(aBPcO zLc3_!J%Vt%RYgkDFlo*Yz%&=xf>BY;DG9Z8l#%4)R@a(>Khw`4P0fy zgT-O`nh150SN6|eNlSQE)nT(Y0IqlYILUunyx>z9Ba2=`+Nv7Tb-<>+?+w<=F7?k} z4Uo&36zan?qptbCm-#7lZ@Uw2A!rtt#`*J25)twV2Fc^r2e~w0#4TZ9fkq_ap)Bbs zW{`5NXXBC!oF3j>O-Y+L5?(cQp)`%r#)v^}dtsnbSG6j4lmhZi zWU35pF0!NVEKm0FQRaM*5V5LP`V)?Dc3LE=!F+)`W$BKj1|dWooSTTfxmw7TVyNq~ zVKwg|SuVL8(-Uo*oW525z#08)Ba;K?1k~)A@h4Alu;jhj6PG`4;jfM(ziFpQmN<2M z4AC1->lrIzils!~--|Lg-4EdVLK&}?3zg4jYL{!z7pniXCtFL|-XOKUnFpk&Ly}mx zOJU#fk#I!Nu_4Zk&TSYrvXE)Rv%iE+5&`S_Z&^v58y4!)-L1f@A zLB6`^kZT=ozRX@%`{P~jO$hDX4%5wvt* z6o};)tbFI~OA=6@E`M)WZ3WX~7SGCFv5OxaI)%XKBu--@nJX?3ztK{wuOL0zEu2)G z{=Qn(-kz~=i?He{{%TsnXJ_=yVx(P|Fz`czYw%)qya<$vwEsFb2HBX@1F2zdIgmGw zwsQrX3ZhqQ8Om0=T1UV{t#K#0PFIx{?FfJ1R{l6-NPqBlPNPhmYpt14PFMw$$yE*b zVJppR;C1=DVmCj1Wse1FA0R|9l@nG5lF_vXvi6TGF|;S=jD=qy27uySp#VpJTmGcL zfK!dBp#@Ar%d_X6KYoaknaj8BwWEJHs)NmvNeB?4*d3%YJ6P?$mz@rZ67yR+BTiDv$nP-No7ptH7&Nwb zgYyG-Ru`ok`1?lP!8N5#Z2;~r8_f}IKk3_*PcAIoz|pB4XVA{Que_fzj35>^!W2ab zj(ae&);Zd>eLL>EJlv;0DAZ9J`_PWuiDaeEtmhE1hm=vdUSZa&f{gs5`d2}<(c zciX*2Oqjb$Oo+#4%tFBPkAs|KUU7>KzW!NShfphRtM4h^5S7v*=PLLr`(UCD;l`Sa zrI2jPJ>LKw@R^3{@&1uHe5qNXqBpSQ`EQ*mM$_gMy+B^V46MY!C&q`k!%O<173Ge~ z>kcxm$1s=|t^%H010?=be<{@pGx8eF+L))}?uKBT#3SK8V3WpdG**L6zltwaJ%_(Z zad98YZCT(ml*F1=ai^)1bIm&u1^!cvSGtY5q#)CPLiTTCuHrn+rp&k=1C3jL0Gw?T z3M^(CW2*2Xl(xsK=~k{@uJL@fxV+=DxG^jf?% zpW8ZuBs|g>xy_4Cip@UesN||1Z*D&mDeDErqp?1j0^(xwHftuB5h{v4(>yWQrroK= zzce3k^W#z-gaN@PrTiDJgSL@gBLvWl_?@J6<`uV^@TShdXo3qhIBL(Z7QSJ@XxI#W ztkGF5n}Ikj1lin{Rj82!O9KHG=?lkTyN-6=@S#a+J@6}}LWvEeMadQ}zVO%_u|q#H zi|=?}C9OYGi$X14p??Xn(VLC|3{pDisd#$|$i#L*@the)tW(+gfC+gMT$9byWq$3u zl8tAM_f@wRKMqrh13w12{gME^Nd-yi-^saQt*Ds|U|sVTR$}soB}Qvk6|XAY!!fwR zD`_db%}$(;#z)k_W(E&lo-g2t)uF;9WVwoPfJ2Ohx_P=_G-+bzv`1Z=1FoM5+Hu6; zY@o0Eiba(ZDRbA^JFj4MnL?bdiZ_gYUEI}cNt5i)x5H&+$<3B6(XgP12j#Z3tg-(q z;O=Y#C6kP`apFi*Dc7^P?9KVU{L4CmA)*E&8H?%};2sSj;hueD9g)QJKifA|MGJ|F zbu6I*SEOm`>9attg33E~kcDU9=&fnpVlHyk} zQ$AlsV-)38wyB63u0aN89Jhh=$R~;hfbAWNe*tAKejqpb0H7uXZ~0VJs84NDS8ynM z4Y+0e4sigU=hEsM6m!leSnDa{-&)%krjr|)l=f5W*D+#r;1INX)smP}*C^8VS?@N$ zc9@swk+8IH+9uf^-Mz8E^jo1#VVW8hWkPdrs*y;3F`&sSyZs*}UkMpDovG4~W`e~9 zl-IQk5eXHt#B91W)P)LEod@)X+DI`4hIVX~r3;ZG&RGXl|JoH8e z3^OGzAZQ?)V=?V8FM=!V3J8iGKfmXMC=9GGrt_Xpu2~-OQEKwf%H5mxww`cnmBkk> zr#In#x@rO?AT6)zM$C$@WP+W8kbXZc{Y`(h9hE*Pk=PmfgaXz& zlBNyqzZ-1<{;M0?mcL?cqKt&imdhl>c^C2=9R**VZ;-38BqFR>zy1wH3x3zk=&acM zGW|aXWm4MmCE4H^Xl;b#YT3fFS@8$E}LV*Cw4Y*LAA4<|6o=MiT8LUT=VjK)lVUgJGbhQQJr zLjlq$7-LnqAjUR1AiMNoXd)kg7+t!h(bLYj0N1>7WwYqrC5UwR%iRAwz(~-77?)D! zf6*)&fZ}NSF)E;C^*frWe{cpxqUZ@n<=&cD9nWSEl6|b*uk&?mdC+vqyl@ZAN^xIo4o<$g}D}-T~A~OE^e(mgAeJk z38Wukqlmk$uKrhZS+Qq1hGZf8bj{x&6J@!mF1*ph3V|fX=x&IMTULY2(FE7>D#Tuu zB5fK~lzI{?NEjB$a}V`1r|`XU@f044X?fh7vF38Nm1xD@BCjegewJ8``d=de_^0vs zq7m?NsKsgBvDw^jDX!|6khQB`I3Q6kMmc6Wf|}jtwGr`yYaNEclEv-uJ+&yp5wFZ> zH+)-7cn7o4$WFhk%fkQ4&w4(4t}_Q;*3+o3GeW^fj# zV?a-a%F%)7^;y#)V-dA`W~E_Z2D5w8N!dkaGwT$Ox_E-Ajg7qs*x>Y~TJ*j*uj118 z^~w)Tg6)5W{<2UjgNPCnPXl25zlm3;B~lM!V+ed!Rhb9sQB8 zQ_zt8)yNOFE?6JSGhw6UVpc*}BF8bL{vIs^2I>HmQtQvT?nwJWmYgMiTeK3j|D~#= z4cOT#6L3~2e`)P%hi>*CT`}UJvM_D~qubM%=djri83}=)^)`R~i`e!i)`1>SKPF&abxy3R zqQt%j*o>TQ*u~d4QV}7>Y}4Mn){L95BflD=^pA6mDEb)XY9Jc8I8}}Y^`*qL1ltiz zfQF$m`YE#03!2%yb>Y+a3~{9=m11qod6gxBO|L`sY`i8lUo&hF?2jUP0j&q(nHssm zuoa#pvoL+Y&8W)XX*aLcKs0E@E%mV#BWGM{DYFKZybJ)CR(k53ujSDWNL+)PI67Vo z$@;A8y^W)SIpV3K_(Tv)g1rmlObq^Axbd7iThgqh7DsFzIF-(E-s?re(SVpt3-^vN zvzLq$QhXE8O);-$0^6q8G|n-cA6fGqYFYCc15h!U!LOScqT4=qKyCwJz&8nNeDfV2 zzu$%^{AkDg<9t+E=m>>e0v(?>1wn;}3XJ+fxY6@q(%ae5c@ZxB?V6x?QMIoqu48$L zVhpP|=4ZGK%2Rfn4DgrMi7^caj*5_aI5CQtjva@#@ZS=Dc*gW{3{KJJ_;3|vP#$L@ z&A`gGeOE^-TWa{LbK!7Ptnc-N*{brYSVxCLGdfYYWBL%@XjJ=jJEX`>akB8(F6w1Uc#m{xsJ1x#b7+)f9Krl(Yiru`glX3VUGw+w+{9c5e5CVG6gAwkf zAZWa_Mv!UGatKPkiDYl*#fPtzke%vbTHYSl?HvnCgHoAk#2``AtBWx<95Cb$N zY}`wa6tE@@celSUc&L)0Tr`*wp`j-<0iAVgY;L10T~etVok$NT%&8K)2_Ho`X(N8B zI#*p+>G;GofD+ShOdwLNy{1no|HV7sE)IOueB8N*_vo2~>Pr3)DAsSc!s z*6U$Anr#sSQlW8SlQR$6m2YgMkyc z!}(Wk2P4D=+khxLy$4fSMp*RsszvM*bM7M>O(gc4KNBw#JP86oO{K7SgqU8QqKz{JnD!>>C{aAo+pw#m_GXmGPoPW(22k|-C*?zj zN>S?vf!uirjNR^kuceGG)3?eB4ib{hp@eIApPguG|FMxE_OcBqny?tep^I9p|g^O6=w|u#qFP z6b&ssyn*Dvgc_>-dg7KSLH(dn0(x!wsm7&Wa4XMdj7Tcgku;c6H$#f?Y*5&%%KU4o zi2SjbmlEPBlQvoQt#m7Z5S$N=FNi{!UTxs34NyZJl^&VzP{3v)_`Q$ETyk*;g&bTPqUTsYE=dzP&B z7gb7P?QyFFeq$>sdUuu-CnUIkOJdj#A*SM135&X%b#@)a2iturTI2HCBP zVfyPnTM5}V|0>@#U@jgubjP7L&yL1xOi2@E0~h=rpCgwM4WV42%BP6{d!C@P&GC0{ zh%MKy!_PvYEbLdzQGByvW1|;*g1{aGA3cwC0D9tz#U+tpd{Hb`W0b|GQCBze_&jZ> z{|Q^iN@?J)p^Pzd97unJX8ofjws1NnjgZZ`v`JbqvN||{e&qdkuz)dcIdddRs@!F| z3leq)#UPdqC$!AS!3JzAEL>bv^X-=!h{2Dg5C<}AArGRWu_ExLom$nFa1yOeeF z$hvlF)iBv zCc!PL{|bud)GIX8$Lmw9ng#X3f_R3z(*(FZjB`(#VqgfA02R9`n(&qRh|mBOU-m=S z4q+u3Pypx}m1@a?l0}wMo6kNL7N9#FiMizJnDrPMq7_5~&^Ms(3DP1S^O>RT+6j0( zJZhdp?&+GGR35M@mW$oXJFMtRg!zws&lzicY5QSx|7} z*iOO5Yi5Ab=Mhw&oFdus>7dtAxZAGexWKv^`>f`D*>aK5_KPN1KIH**Es_ZD5-8I7 z2UaNa|A7wQqOHUN6s*MHaWNfAKd{kN;-=-v273mZj{0fi=*&7hdlW4uFkcM;nB%)P zj1hoZ0Y1kQjULjlxbqiWr8laOU?3Zf-6UtliYrD_4z~BOp6490q#Nt;OdG`dkYw&q zo&W#<03>Q7Du7xganpP>*}ri`OWGc#AYA1m($g@dBXZtEgJZ1b5hGz)Bx1LdOS(Sw zHKT<23ei0}mFg#cgOBKYY4M8?oJxcH732-(mo1lMS6WA6ka9BEtKv2?S3vOdDM`87 z2UlSq3dJ3vzcq5kmp~_}qR2Wr{ng{7F$&Uz9J}5Sb>RsZ_b;`jqLJr*77q#3gE61W0|Q^!BjX?LMka_}ku_ z$tb2+Lk*`3B3>2F#A0LhaQrcNa%q6Pzo{Nbik^!0U2faX1ooNj+ogC$zSD^+7{HGz-n`4pXuPyjoqV5vQofI4fhPyS=8hcO_dVr#Pl?T8q!Aes;IE$S@5POv?N8soK=$h<@P@0O;4?3!1YdJT z&oj{y+@SRk;-uLzow_U`Pd@xMmF7!gS>35CodplCvYmc`(j;Ql%UWRHROftDH{Z^2 zl+4c;Z~Ot%%*c%rdILL?P~Quj6KJlx_#~ufaR3Obn7As$Lw1kU99s(Gu`c5&8m6l2 zRd$KY$V(L8^Et*&tzJ;=fep+@7XaNG2HC>I-lHa3CWduz=F%*3NOY%DT>?`{D=tv~ zm{e@RR}9FYeZ`Ds7k;?Az|%%sR%~gk@c}$;GB0OX!%8k}%G&BHg`fYu^jY{P7$-d< zRkria?X!sNU6jA|JU+e`r4G=*8tF!2CH3_glT~ZNK48BO!lJN*P)gl)p2eXqXCXP) z8)bIkFO~eTN0qX>ORXEdKqRk1e#%-4Z+rll7f8~s(fgD*Hu8hrLlqVCa`CA|;O^&e zBxWy>uM^MLP}r&3nR0&EEZeN-2pp8&mdTZKOdnU-yrDc6@+<<^z^EC|48i%J81ajn z=ueDMCuYA#PmvI?z$HN6C*g45@C@BfGU}Vj8F$#Na-GFunYbMYXS`{e&JWzGFf%I? zRv(KW9kC-{1SE!)20l(kF~3P%+A*6j$~Uc##GU%y9zfSf5SY=hMO7l4_((ragW$-P zLQfp>u(`J$lWb;~K_)l=_>jwF9XvvH!7L2rcNiNTf-H9@K zUNov&g`$qd2`YDjYC4lD{}iyS=v;3Vp)OIH6a^l~KyISszr_*ni1hq)+E$_$$S_BC z99>OrmfB1{S#3(O26wo6MN%SfO;ORYHG}0Sav|b26jB9`1n5Cy?l=74K8P8p{{tV% z{Tt%^oet(y4!&iXVQP=OQH;%qSz0)Y^%ux7PVvFt5(|-p@pb38Ev4|bFk_-y9vV&9 zwAs*ajR?1+oyvCAw>=ITJE=$i`Wi+qlA9qA>@vmq(Fu3)-vgonb4E|pr4#)QO;GGfgzwpG;*-) zoRBW_SAB*`cdr1!T2!k~RXnn`4aS=8X!z?N8?;)id_zm2@3mxF4#!CkEm;j5&~{Z; zRCtmKNxz;*`S%A+0MQ)}DMX|(JFI#>#n(7paG)tq9!7RVp;3d&+tbaUmYI2`@${xurs>$dYj*HM? z2U;L|8urxgKT|kv%7osm;v>@c+w4%_#U3Q$ckhj z%{H4q&x&MgJ9AKaRjSMlE-dcBJ5aXm@SktWI&{=T{;yb40sDjc9UdHG7My-t_EN?l zFOipH$kjA@ZShX=8d1E3&^Vh-JGfFsVqiplMJHDn>YmK>csW%>+zqhQNRS^Ipli=6 znvJvW*F-aB$v2-_K7)hb%vXb=zh7S(NY)rxQ6zj=mYIR$wgLmRu~n!>KTLpO==(@i zjp+ti=aju9J;oUKM#;+De5OyG?*!Q^#_w z{5$ikkC|7lM&P>E{jr4tD|$Q9m*N-FEgh&1&nK3Gxs(gYbV&6EpvKgQ6Z}Y12+Gq>4?W zI2E`Qq`QC)kdhEol>oE{%(0bTJGvPC#?J>JK2EcK70F*vvcmrrXnsAukU&G2sgGP4 z7UqnqptKp$b7$lPP>8wwq!m>(dDl%zX85?k<8LY8q zbp7kV4I^uE4;IaWUsM1eS#M3qo;ZKX+cBUP2`~l$v&m|&O?XR4isX1dDw!sWLaYFJ zk;#ROd>Zao8KodyTkl8ZckPrBJ2UTBpq)0Ex(SA(jkzE9Z zw;(pD4+!c>nMqoz*HWjC&qljOa?f1OYH0zEsvO=}me- zJ^m@{cO1!?Q}YZQpUaRAa@}vFd6GYwJw1_vY%nrQw@sepabfvmwcWq8DCDY(a{aS`H}0AJnN}NW-oZh*YH5djgyEeFWY%K5H7w)+ZpL22inIgrWVuAxli&3;T2-z`5YAe8aP=e@6wr zxU^?(063HOAS1&&B^gJwb8$p24QeLk%TXSTd#iiWZON2yj^YK_dG2@i6~h2I^gAe=#G01HWs zuZs?x3{Qmv+_zL(wl9jo#nDM!o6(*upmv33kMPXRKshX**jV&wJsg|s9Mr43;tqRK zmCwQ#f0ht+AJVVjYvNv7j#dkg4wiBgFstsUu_G?qdB1r_BYHzKh<5`n=^;JwD0CL4 zu5;i8ty^#y>^ z9+)53nEb&{Kt8JCPc}IAGp2_dixPL^c=TS?b~pjM@NK8(0b_EfS0@ZqOVb+DGG?ylojd9ZF(Wf0 z-&BcqIek2ug26N1Nkj!@X@5JXC1~DK`sUg;g%OlF7Aoq!55hm%xZsOJ!)ASek~~DMTqI@8sP+(`)oRk zxZx+VH+LjBvZab;IU|RPkYaB%nUg_|iKVCLa5U^KEROLxy}F z$v1`w<>~gUT)ol=9L!^D94kjmynU<}%;s2=KvUf6X@3f%@JyN5uce?F&5xt*$6|d0 zrOiU7zu+|;nkM2>KwxYLwd8Y@G#vzq`4XTgKEi6n22{l!>=6iVYf55<0v`b7_`Nji z`sduf_G~4Egj7n~3jgi)Tol_^PFymYVLJo^i$r@XG;@y~F)~x0fCUnD95p}cREIwR z#|E+9IjPsFrs{c27t5QX835hnM45g=iQ0L_OaL`}3|UukJn6w?LFC^snw;cZC@`F3 z|GXXJkid{ny!J1$9t?$$BKHwnP#z=QO|tr?qmzzW;!n3=oXeKg6P}Jr8jsEj0Kv14 z)K9azvh9##v0z+bZIbFatiRbq@7sTOKAJ>ortm?DnB#9OPskVbJ$#`;7$-lcXP>$S@}GH4f7vQLlJ$u52Zyi>plW*#&q!|zDR$fBtimd%=1T}jX#)VP_5LC`&Kk6gaG-mV< zEwhH2fdN6j&n@C_s8rQzf>@;sJXZggax*Aw0ZNxRGp;>=BAdsaBz^S8Xx>EtA$a)f zOvNbjM1f^RZs2Z1w4TU#4`-QOSMI^4WY$X{)M=Po^k3uN+)WCl!-L$){i(_C_6Vvf zFQ{E{j1uLuDkzIH(9eI7=PyA9IVG>$=`WJ;v+fd}<-lfWWa6B)J~0llmcGj4Pqri&h`F z>xFL+{DydXkmS%P()XkcC?4JEuE<^)GPV2v@QUUQV8!_W#$8Z@ZH?SPhab8zxEp#?2U~VFCCh zeN7-0=Qwz6NeAK_qkqxwCoibQQWAClpq2PvwPy^&yvpW=Kh=lF65r0 zQ=8W)Lk^791d*uA%F-c?Who7Bi4{5FxoUa%Qs!u7#>llDwOB;s4|5J+is9>LZl5Op zLfxeF;l1PcxU>3QMmmW${wOqe=0lRyz-+%yqrpZXM(0Jab#gi0q#Ou5hQA?UgL>x* z@bi@(TFLU{HvRre;UO8{qnem(+Z2(VGURx+oPXV0HQ-U8mLWIn^K_W2?;utN1o43q z;iVi{h9I*1*Rwtm7Lm*11Y2Xpo zOu@Gsd!|9q5Yf;!t+#7~*U^$?r3<}^kaI@rPxamhI(Vaw&Y*8rER#*AZ5A3aArCRi zMMigB+qE`!p|4ISf%PZi&dBEX1Oh8_?Y58s`l`+<~qT*>?mQo~=`(m7% zht`i~dLa_iMU65V`ZoJJp;eR6$#JxJ+yyI!s~`O;{TNWDx=Qb9_oWClQlS7o+@O-w z{jOm-LnwwdM7teLJ4-uF*WE(ooz#42uGrpZoZ{$OoWfvV25)MLhV2pq;=3$8nGcbx zlJE*lQnc0Vl27C~#g7L>M5dn|PLU(l`MY(`2?|KP#fR>W14#8g?3WL!rtEw|sPQ#;u8xqI7QHi+E&MRK1JkRxqYhdJ z!WGTC0znGOIC@M`qON@qu-k)NuWBzJ?L5yx)?+}^sQG6Pm)*mf3svb;!TL6|x{y5y z-ET_@;0BC8xxDB-)ow6GnlxC(1^PBHa-qhG{!#pdZnklnO~daWDTg>so_Qhj?+#Pa1=!I2DJH0}I$uUu%HMi25v&H`F5FgBQzV=o%{%oJ$4Rj~LE*L|sh{nE` zPyeeAN%n@#++%l2^Eydl?fl_~K^MQjTk%oZYQxkkl^D1xhM~BpUV)|F0N&zYn(Ib# zO>pr64E!q%QO7dKaesaijF9Z_PGmEg%etxuePq%f*1ePNFo0fKC+hL|N>u+CAg35G zR>JW-f%R8OxveioSw1K6a3U1*V)-AKnVFU743Pz%lK%E@+!Xg4Z8}LmeExL8Mre|P z7lz^sDXPTCzq58K_z-+=3|pZZB`?0gzq2(l&6h?ThwTNzA=H)(}}yOn`}1wA1nbG^|<%kK9AuJZgySN z;LWe4rAv6^`gvz{opoNC{xErBjy3~*(MPT;G zOl@}AgPmldJEj}Hm$i9fYVS6YnjjEKpJtB8-gWSg>06ce<|7YPv{fU}dmsHqt5qBP}nTPr#rHG^5)qlRQ> ze{qVy!)UBaGYx|-0DMC@-d$S{0sA*7+NO|3D$R1Oih|M=mESu@{3M$&MIf#SN<~;V zyPbDu#~xxyqHTvqv&DOI2Gsx1tK*k+-LLAMx?SparBWIEMNy(b$XDrcE0+>9Rh^p!WS`R=r(wx%8H*3PeENH zmp78vjc6FN&x8R!Ayusi>~|u<3-|%n>Om}S*HPX%z~yeChd7dyu%f6q)cI`iKiE7 ziR-*)5Q$#9?&@p)wD#&-dUoIANyu6Nf1yVsU5OF5VG&$*?yOQ>2(Sl?xUdLt5hxbt zr8>io@}V;>uc^LFkXa~68Y2zkH@~qLy6$}exA-|nir0f9nKc#_xBd*c`8!kgu!x4G zROtRZdjZ5eYq-Znd-Zs)WcW~C;@b(AF#inrW6$Lyi=82%xu;M7Z!T%rseOZx(c=Q` z8oPFr)hS&cO`EC(N$*UvH;ltedX1T5Gi@F!L}hiEakg7nf*QOvI2wz#aGnM8Lcc0t zKtzL&sv&F}^{ewoTL2o4FwD40#Ng;M&2qXrGupj+#NLbBS#BH2v=pq`^Z_&9#EK^- zaq=$8W6-3ZR+*e7X~=Y2z@JR4g5_8Gh(RI$*F$s_j90o@E6k2f7PkZ@ocYyU@g)Xw z&W(U3hVyo-W2PQRM6SfpNn(wzDGENbyhbUngtkZN#MnFoevqEN?f(cieO6Q9reb=( zyvS!vNjdU=0W3@$yv*nl!yWW#=Y~Gm8WCck48Q<#C<|}+=AYspl*}6MB-+C`YwX#b zv2nGIYh#?bwvHaM2$Ia|$=ewo=yRtAy`unH-?l#OA3!5y;= z14J62rEeS--aLuY5NOPd|3PCl*nqyBo1i_;%f?Y`GNf6t{P61MEQlf(^hP2k0=RUU z(q%zU6;SU<^+S7E1;Vxu!(>~eD+w*n=*1PqOQ3(z@w{P0pFNVvkGD0+Eu1IDS;)@L zhuCjAZnZ2Nsr~>E?Eeo~bq#dQ1OV*uV0IUF<)KC)$TAkad{%T8=C}x@Y0n63{4u>xfHUhG+ zo!dbhWjUNT|IPJ#uWnxFfvwdQa*tdXMa=|4gn? zm8P|afr+UnZYa~Mn!f`dCFvyC8O@~Hvo@wNC_6HgDU~=CQv<>)#FjfP%YLQ!#PI4P zTLELORJsq@xkgxzMfo+YU(J$~9Dr9GaJ3FqKIE96X*H9A<|pnQ|@oVG;EtDPj~Pm48)CkvHe|DVT{gf|qNO`1I{0fsZQ3xzhXZ zDbbyV!=E{6%THk%EWo=o8f%P@zW8kk_=zictYKYarB}L zOFf|3J9nHk3f9%K<4uwHfI4Q{<&DQR-!RAOVs=$nw`1BDb=z8yIceH1{LpU`KK`$J zd4?qZ>$(2RAJ7mtBUWn7Xj%Hq+6L>gfK|;(RJv7`15J{Sl3HvKzSp8vv~GonZrwxr zlq_^c)2C_8n4{cJ4D5z`w(~i=jAhTk zyz1oDxuANt0bDoIa5E^Im5S*<#53Kze-#Z5K5XL?0N3#g(}YsjoP^UOttRbE;|*f2 zDDa%0=-B&^OS0OA#st{Is(z461`Bm&x_uFweR^d|!DTB}5^V)-slgi6* zfDU~vv+Ta<`?9T1whixFX)l#8gJaj+O%V+)Z)8>x$P|L`yLM(}3oxAhvTm`HeLp&? zq|FQSG5MCph4lz?7FgubFWNtWJp&a;G$)YS#t6TuFV?#!aHOx~`S6JNo$x=5AO72> zy|oKN!faj(`@UM}g8s(%)6EpUaCRY;&7elXC^73@dSl~qWF_6mou}b9uT|ou9D)T3 zy~Zj=FEOp_fpyq4(!`Zw;yVxe-51<;O5VP4D*J3;gjOOm0mVXy#Mv1aqFG2!Eexge zPTOJ~)s&qxs{~~E>A06324k~zOa$9+ zv}pw^4LPXx)z2Qe4uV5l>E>?~$#in@asTQ-Bwu?E&d@|~BWgV)_a1tTTS)oq^?@b> z!qwyYJy88I4~Dm{>7T!8E%+bUq9;o1kz#v2HH}MdPar#7nf9J<*cb${Dq`j=HXtgr zQY9btwZQN8m!vbn$aC7?zsi9nG7}OjB3tP4=45@;+ai|y4KOK2ahP#GJaDHY5ILw; z>}Z(|Q5?fl##F(~J<*(8G?TJVAyaa(Gr{qva{Gin)BZy(GL(;7#LDVeVJURqeCYebRqEPmNMIrcX*M$_-J1DG@QH z*4og3iV!>%mW~wm2kE0PsQ<_>>rN8}cnV{uIIm)&VK+j?C)`Y$>#V*p9RGvl}b7 zQ`@5~;Z7Bh$}8D<8W{yt4qB)#P28z=<*JC|0y;BTCz1m z+sGYb^W``>+P`%fpWUOsdA&Vkv*G2-iGDG&O^rCFjy{LSfDok{fGwEN_7ch*>xPCb z-(VGc^zKRl@~yrPW{+X*u{7x6>`b37rW?0ink51aSBL~xg55skh8~LIvG=!{rXBh; zk=JPQXu;!CT|kLj$$%nzC|yXpOCtgi;~SWlGvqWX4UgUe{|4(f_l@I|c{jik`LKK8 zt~XS*&kbchi&p^~Z+0vKRB{%PXbi2hZh~>H{Eu3f4Bq9&B_n4p*QVb50p#N`)taM4 zOjoHZB(;v?-c52%vmB#P$Bugsh8A>5Dh6%v*!(2%@m`2g!eSlijJ|-5TCU71P#C-o z#}U%=*<^V~q}0nHzAxC|=Ts`Rmo-qWFg zS)F>Ir9qi);vixk7?#HnxR9(UEmicf4sb*iY37cnPQvg$!>u1b2)X^3k8EDgVi-DU zb>=yzA{l)dStA$sK~WH)H02}C0=bjwG*<#6+$!9P3S@{})^?xnMCoRq{WR9#kUJX6 z1SPHO4YaY;X{)rOLL3VFp!) zc276#gvTY5ol3^74(TN{RiBYh1Kj*DG)?V9(pe0;tVPXPRyS0xays$-lL)b|Ry?XR z8}17*&y}fLlATl)#1X;7q~lE9;E2ty+9l$ktWqPnh$^T=70zvsH&EqtrTdggmZW?- z;>{kuXwgiBLFNkQ@I6D)*)vmNmXGA^Ld>zFM*bZ9oGLBo{DQKn6QFg#TMS@LqFu+D z&d(+?`%svXd?XUA8)D8Xg4h=(`fvRXWYR3P{6^Zm zx+slwayA*iqGc4IT-qUgfiU_`o7>nbwgOo_fPLQB#$=;O@#s|MeH4o15wObQMpIvS z*^t)}KRS`nXBlC=fD>@%FwsHR#-q2RPe5Nu7fM=kfGna`N<^{t2lRN?4F>~r9<4HJ zD$(o#6BBcv)98d24Gf$tCBC! zFim0hl^}hFNUVZ`zXh&U&tG>+O93j|oW?ZFmAprNzWSB$uG(Z=zr7(d60A?2n=V2Q zE&GY?am2s~DVU=(9=+2ZwO^QY$veH;K-v-^`h@#`dlV_3QBfrtG+d753%EsI_I{uH zW)UWV?j7yOUjO9i)kq?`70cd_UgZSMd z46%ie?bLWPgERpcw->uy7YeoKz}toaA8{+6&Jab!2r@`ZKa4~heO8wdDv+?WPI;{_ zc6t1H7gD33;iknt)*d#Kqb*ka4hLZB8;W( zz27Q+lkCDI=|QeN=-FHN52 z|MDpQTfu0trkmh9;=K2T@w1NZ-5@5cE-mB_g0zr)zYZ!{YfLGY=4~n?!NJrZPJZ}5 z*|d{T_ODXhE-3P&1=(#{?f#oVnm2RZeF)PZ8yTn`$H{2@Q_ZsP46(_8>rv`gXZZ?K z&aDeAyAzru59|kFJ*JIPM-OeK^OfGB`xqxd#BSQ8d<5LmTk8^A^o1}>eU|9$p}1M(6IZ-Y8?7 z0Uf)QvgymHPVU>mhxcLv*cZ*uT2GBE^kIU?WvLH?13=S*vi?m|f(~v^WZZ-u?_1b; z|0;b>o$0#IEu3l|>NVoDL9zcRO%>OFNO{$m=g!s2`|&@JI>$P&3_vyWYA!ejf?YH zZ3-0D5_KXUZx52{p&A;j`A_ys+o$~{G>c7ft(bA;!Hr^On|R5OT~hE9f+e3ktKRfF z_mLmlhI#$v@8F*6LFj5=L+q_{!B1sng$FT;sx6{9Q3=D<#mg^wex#t+>n@Hgb$X(W zr~dq#2{F-%L-{tZLoyk$0)Sf`(Xa&pN@n%ExKF<_frn1z+m5j#4>=S_ft|W|RYNAc za1ViI3DNQf2C%?g1?^p1`y>U5=tR^DardOtWx&guQ%o+HBk z&W-@fsG3+@j6&Gpy!@Bw!5AXJuEZQW31!bez&VuDF1oauh@gB@Y)a4r4>XXRXuVzp zRBzzE&sT)l4a9bJ3H!1D_ai6M?4&?PoYQUcxCIEnyLeB*Iv<8J2{PMEr6g3NddsUl z7V;tuxA4d_>zcWFp-P!dI-@CJNGXYOtc6mzz83xmd%j8SI^F{yS`r8`RL^&-)EtP8 zR?_qe5d3N!tY4cw>nwP12#LYOkiaPdVGEFRX9h7ES|GI!Mk^jfkBH|mmbz{0dz`1+ ziTkw5y{mAKZYP~}>CU>5d?l$9149UkLr69<$0V^uu*SUdM zz#GjTyhS}8I@LiWLVl?_f(EGl*I#j~k(9!Q<~dBu+lipFrrdVPqe1A>xx-=2RM=eX zA~9b+vOGDG%`~;_^0!OWDm0GlOxmDaU$sV`IvthFbqoY7P{YpWz%mN1n5`vHAkBw< z%T4x8Mjcu<=nPWLH?st_65aXjiu=~v8XfgtERg=yodmZ=ENcX^SPA96n{3{*#R7+e zlNz_JElLxEC2TBTnbPqqvGXu>fv91KE8|WGAQNK%RbZ^3_U%;JPO|J_UL6WBRQ+ps z6QbeYNF0;G2t&WLkBcG1)KlCTLa~q$8da*G?onVDM*WCh<5?d@Lnc)KsfRbK(B=Hh zQN@6(EY)4-S_$zzE{9Fao%d%Y)^&GCp~<&{FHM21cVd0oDx8G9BdRRjD1OvbfY4)Z z=<}hI{@dR)a;IP>$WFLX{yXX*$qu(gaGHC4xzE>P0#pvV@u>bqj%NLd^%VM&nS2mq zShoxf(2}~^^{|@zn?(O_ucyxF7kj;y9RSu?8OwA(O84o92B$$ffqh%5PCfJ>Z_hn# z>LY6sK;$a_Zs55}d|HGR+c_J-pyxGv%kg2PWN^EYyEI&u*gtQws>9R706v*`HE*(+(&Y`O@PVm?unY_mf2P(U4f=sQ;HuyG z#bmBBsFpb(Kr)(MFQ)aos#FO;Tdj0}1}Jslr?2M}dB+x(!+Oru+p0l3`$BsOW7WW5 zBvHWZ=1O~X-xJkNv=v`_nVee5Yr^VR9M32BC27hSC)`=^_ZHh{e+rw8AnX+3#l8ht z!qG$PVz`juS|WXmJ0!H+M^sI5K<%@uAtJ4p)GeA5I-Il%K;i&HuVtUSYgH2(O|S>( z_k#(qD$!FXfB6II1*<@G^eTx$?uy)+1o@Ln+Z%P0?hn-LP|O^s^mCxs#dg#Yb+-8wd}(@y;aiNj~-$3 z`N8R2y{p7L!t;UK3IV^JNm3zg`@POT{5?~rQ(kdVs4~ zsA@-co)^)gOc5F*g2(Q-9__&V&0TbqMN|`VJwGYP!~w+eowZ8I6y|_tC}?`?l@~~n zoRZ4QOcnQ##N!fcEv2g@zZMDQ2AZMXPecp?SGe!vvOF%kG|C%Osht*1$3SBk!Rq`$ zncCLB;Bk+?f+>&Phh4ZM(_(`1SMsyW6gNOigGEHcfRD%kuSOuLBe!9l#ecObvv{Ol z*Z_&~b&cai_!T{6vcHG>hqpax1pHPMCuQb$82lgZA7U6_(6ZuDUUIAfAEsu~(Ttca zavNEjY&ofLXSmXDcS;r0u+YoF*xTUHiXG_LLK76no@TPg2i$_ty0mOrWgVAD1@M~& z`9o!G#>S>w{jcYBC(*)?Y*5H>Q9@CD&!L;e3NHpo_K8#NLOPaGY5K7YSr7dt?9zZx z?{Nb(?QE~)ao~i^RvV3k5*vk=fgtTDl889P=GIo5V^7Ll$Np1mf=SO}^|7Sc9kj3w zi#exz3AErq>Ybd*?wuP=$GWzgI0?3Tg(o=kYy7b9A{j7%g5>Y8C^J(zsy!aSj7;h! z$wj`N)ss;H#=c^QoN+!d4LJm`$+i$dw2JcQD>)UP%@tVHVmYEi#dLP?A(=!#Vo8P5 zl;#Kc6coYF5F$I}&u*W1<9C4_w{1tXZRY^7@QLxBkZw?-X{lyw{hzHBgCDOC8tb@Pxg<2ah7nqm9l7Qvmb=bBx zBSJ&5(B*|;JZZnJYrq*mRwi5^J&rDi^Bq@WqILaAGgeZ0+!XKy;;)`WFTHL?oY=EmVg8$eZyUBM z(+9vIbRlN#E#_UUp*lO(*7(P+k`U7B23EkZ_0eK!gno$KcQYgE8Ux-szotaw?zu%& zkRo0SxIbqyo<+6!gM7wKgxjh{@LHHa^0}n+m5s$@yHCIV6=fFG^#M+iGAzpkGX|6Q z+6a1?X&9%^22;2kt1fltCP!Z~vd1Aa%41D%D%bzuAxh-nx!@DE@fqCPNy$QYD8~5# zx9>k5N`797QUK^#7>zCP(Uk(?AM`bB$ocA^l&pTc7N?(Tisj&f-ME*x$^ji0x;Dot zi0YMi+PG)cI(G!(y;v?&Zif`fBS2Nv> z04@PHspGZ)-+n=xxu#OHMeOnC;1Rq?S+@{gtk=NlWVBjR0oPQGERI(_kl@yo?TcQ;fM zR!LcwC6Y%ngaqPNIKVeRO^WzHX+3-6`5+v%T1uc9PXo{VFzy|{Bd5LR9Vjc$#lZB{ zJ$tkETtO8N`I}O&;HDWKYNt&X?Ev)YUBwxgseny&c6xgS^prs&8Oto|+YjdoII9l% z&0M6zTbU+D5Y&6h4M7{e47oNxN$JixoMlPZ3m)9spOW&Gw;|D_UVt6)H~CYFHGo3M zwg%hAm$~H(A`@4oEQ0*=$&6U59n_8gFd@y-rp7(#i$<;xpeOynL{vjpz3!+9MyuuM ztG{~9CL-L7kr~5`(Wn@m#plpQ%vRuM!{!Kq+euND?fylFJ0I!@K|`0@3EKE@mLAKM zgGR2~2V;TBFjr?y*+3)`Iz| zNiW~H_VeXVno-LZbUSDw;^Iz?w_2zOPL&V9|+{1T4rp#T5OtAzOW2_2^fVkvMEU!^1#8e&WqFO0X!L& z%}(sI&+<5$2zEccYbAP~B32_?6$;++W967t?Tom6ojIr*ZPCtW)N|oyQu~b zH5>+tTzM_h*M$whoS!5nxg@)TczHg&IlWS+ryQ^A)x=4Fi^{E*%P`Xg>0u7Xwt;cp zL8gMqiKk`8xV^0L9@Z4ZUL&=ed6JxHjI<{_wjE{;+2z&*9_b|Cnxt~#$LP|apFNP! zz(q3|B}4Z&NBZF}$M4ActI_-FdSvx` ziiCKXL?n_vh-}8N*FQre>Py=_u>GVv_!@l0PERKV0#yXr-5Xj8RFt=@1LT5@xoOQj z`geXDvyNM2XrrQT!l`W4XRH?XzEZ9kpzES<-B#{XVelF=L$vx;HU}x~as`AkzP=cb z%OK`3DC+>DNiI;ocMg#|5mm)35ehYaloQ9jn^j!qAqfOYrmh-m#2a@~;Mak|fen6C z?__%hXBAZ10c!68*b6xTxpi#+O{Ry~Uc#KT8ebB>?6vK*zTP!(43q?^s*aSlBtA11N(GeWQ*H04O(z$XKjvWN{P3j zLFg!MAjv+jKR!1mJWtZ3xjYc4pbIc&0y?jP2QEex8HG+l*6HO+>u|c@t2YaRj-QOj zKhoho>DtVE<2ylMS|^yuBuMqcc{r|ypQVQ_!ZX2*#6(>Hg!vEeihY6fAEwGT4k+1st z6`p$Cv<;db(0FpP6L-L#W#DzmbzYERhXhmnEp+!$Ch=>P>~9AfD-ZGNu=&-sX;kw& zOn3*M0(3xEECR;$F(oj#SZmrdPJ0h#6LKmf66kz8Cz$f7q-ws)Cb7N>(__ z!!*%SqMq7z{Cjt2R@nroaK3e3J@(*Ha$aoCS&|xR4E7-aC+wAn)q4H;MSDNSHQedq4eruoml}It;T50Ox zkXiH!Ko{R4SbViBKVCVV0jO1C}-hq4H5qGt1fUtK>NKEc%8BsP6th&|a!puC+z2HUAodqIM&)$%* zG@@06a4yKA`7tEXFm4eCt?!NeL=4N)=R)dLh`7#y(K%8CxRMeaC=xSa72}e z+j3D+?D-x_*euJ}J0!;shaQ()K9^WCK^;x8p=7Eds|;JW$d{RL0q`-`(2~pPqJZa> zNx*4O@#CEWGa172AX~t}J)-35kirT!wL!CVpR8HA)L;2TXT$Pg^14`TP~Eec2pma9 zA|j+K!Fug(d-+aiO#57^%uxGVonmGc)j<`rVzvGXJfdR_oQC&g=aM+st%Rmk?que# zG6JpKo=ufC)=IR@NAlqO5L?%&mEKSD014seyYesRUxN(oO(;_}us$6AgJ75ZYLBuA zitI?PBgL9SMm|-@#nneb-H`^#C0@y>Gfif$^{H;<&-b+}IKU?~38>}z?@TI@i%y0H z4E~MEcVDQWsVp=F;jfO#j57tg0oH4+SSh{ew~0|>OgS-o_9j**l*@2nesDdpe+7nQ z7V`$Tz0mgJU0}WKF4SSEuq{)-PA2&WDE8`uh;*>=ybKK+h3LM37L!4U?Vta<-Q&F0dtiO`ln@4qV7)t8nX4X+@Se9C%{Z(sIl zm^*d&`)|%9R98TLr(Ut$s|$56p@#eAtgbHMlQR(ucMYkC?JAzs&HF%`Qtx(Qb?3_O zP-NGbBKrIX#xphG%Kv#UFTsUnCpoMKYktiHe_wT=?O&NiLR?-ZVczMkAP3_V96~xU zK!}O#2o4RV1ti%bFQEPTnecCXqp=KBK`P4NfK*F!zNhtWHZY)NMF`+GTcOVcUzZA* z#Sb(NUKi32sWhdm1^a<6p`5g&sz1RTd=$d7uy>>_Z3if^0Yk=tNXfij9tdB`t=5Jv zU_Pnyx`L^)-MMA+Q>eDv2X0m~!fa6_-j{qiIECE?YJk~urw`J)Xf6W|Czwf2MGw=+@N_>Y|qxxF6L z6$>dI&+Z57tajuKg4ML`&S?EK{%hFmFW=Ks$na> zhz5At-kL?x+};GAX$gi=u!i}PO}crl8o1N(zy;od37?17G)!oErxuGGznOF@#7{_L z-uG3mgB)#;*#kT&ZX7PlWd*iD#fypI{m1)@F=gshhq4EULq+E8pVNBEE~?Tr7W;lEh7RFqJSpg~ z`%lmYzmBYa+%nFdr@pJS6oedGBY0zU{YnmH-cw=din*$a5gY#nAlx_6*jaLFn?3%=R==S6*5(^NF>-^NZN25 z?d7hr=rKl)eoNwAFmOnM;xNCwC3qe53(@oW9~F)-etlP;kz)Ko=ihf{9K0)_Nge;( zyAhjM;^nP|sPS2}A>v)&15?!R3Sy8_!+g`1k>Kxb9}gbH2A9pvFw`>~tS<1-5!M(+ zh?ZB5fCY4w-b;OMN@v8HZLaCx?a!a!$cofs(9RGd)%T8xb)QF>t)UFdEQW(Ff@MIN z4G+9(Jc1xXKVvq?%Q9i+)W)T!5ozON=>`%keTT(IfHE5)#5X^M`_z~F?hyUj7`BpH zkG7Z>jWIdr*9;_~S485t-S7IzNjP~Tp2V+h_|mKC#Y!GJaLz!0-uUZX^Srp^96Q_; zalqr9qSK?`isN8UXYSD~bdfw?iF%bw@;Pr((P@qiL@vjqD8gNWDLS&EjMgP;V76qe z8oCfa&e^cSdb57s0~5lvVaTR4b6!BbVMl6-#U^zjsJ_;q^;FXIbaSif7ME47DH=Wu z>+tK0&mZ*MoDF#57WD;KP+dn8!+0!A`-nQ!aj)`$EEX%S8W4@e&;N2Uh4rOR z&xsDc|Kmp&57idcd`Z&sNX|1rTdW)zV;(}J&Qtc=>@<0Kf->_#c;hucFlxVU?vvg| zccaR5fhu~WDMa~Hxft3~o8~E05cTq;Z4j#pRhSpVft?5AcstpQM?*ASoks~08L4Uk zE%Q*&$H*$wC^F$Ux zeJR9MysWY9EJyhmI+p3}?Rx!JcVabye0seF?`OdGmtk(aOtQso$oEC*n|p*b+wyI0 zJPqq%cRyRSQ9~FoD!Z~M(iwU4x(9Co(q0r1KipUT>$~RpHkB(1v4mm&(P}hWJ#7;W zAIZDt&3rpXRFVO%W5}V;;#n@vFoW1~vsxU+hqhmI(Lp!4;mqAjQJoe+>n??l?wVOv zqzYvi!#*j-_#-`VXMZ}yE^ueGE zrGr&~&U%eXK2P1|z4b-ki(5RIITUv%@TKs9kc7yL_-Qyl9NnN=MW8CuOx`_+Sj8z| zr-G9ME8iADX@L)Ovf|681Kii=@>1zSm?l_Aba+W*dM92gwSP9yqxWP|=jZRc zW#7dLB&gmf<}s*fB`F)GPHuY!S0-)ZIZW93+S<X^^UA)`8!j!FF-c*7lBoGk?%<_^H#rL3vCfq ze?N{liQYtP5zUhel%i#lQx}#gQgu{O8MUX!C*y_V*G}Cv5eCHUBQ%r#@ znO{(iM9%GG2o%I9+4_q@Gw_RDtUA_jPQ1W;iU{U^8Cp;J~? z@fWAeBiFdiN3GL8!r>8Cz5F@GFwk1|p9<^nM~{wJH-f`c|405-qN>SfeUzFhNv^8i zrGfB=gO)_(TSX*sQ{cCtt%w)U@`LC4&2SqXT)oR>;Gw_7w7w2)TCUVJZo>Pr3hx~7 zCh1AqooSx^aP?AigFxb3SbDRm@-5s|g&ar+qYL-SxPs1SSKqnzj@W|bkq8_N=ae)1 zks@exIN~w=Z~m4~TGJNdwl1N&2U?U%wPdgiqwKCWFO~f`#9)c@zZlRBqCDbBw}{^u zU5hL?vP4s)?HGl4!<&GrXTw9C&z72W)RhD&7TVsMzPDc3)_2fVGh$y3;s4&WOC_#YbaqWM1s;m>c}9Gnfq!xjnV=zPuae5 z0MZThjV4ATSPvUD4LF`pR7lE%LCJ?beCwP$3NK5lTmYf@56TzI`N>1>wb5)bSkm99 zV?D-wlz{h}hy-K}S^ki(u2ya1ud+o>XwU(@aDnZKRyzXu^|N5kvgwxzmWzu^7*d#& z9@gFTJ(}$Oysdl;+^b6t;;~Hv2@{A3<2-lA`%GT!%_4EFU+`uxtuGbcd~hv>BR^tB z<9T4I>e%)e)K&|BypL$AVjqZ~ezMdD2rgpQ*cI4lQl-FSPX9)tPtTV7gtVq z*vR*12=6d@1A*i#m%$k`QD_OuK2-X)pC?QZ~b{wmf2sK&4H;+G8Mg> z%R;G;VR3igq-;^$`JK@186tBY1)qp@@}W}vF|j` zFLj3&sEi2c3vp1>Kv+;1XRUv~YvuIQ%jSOHBMQhw`!?pXHVhunryYsCW>!7<@Y>oZn8r36%pBAK{Q0NCkk zq!sjq*dQI5RXF=Kr*ZVKMFa4W8DPfXU-T@3>z$_cZRi>**LHsP?WHYooL*y ztE}dSYUxfW7fU{G#vBm$Ba(j6uYEzu`2T>N z`@SoW7_TY(N5-O{(sPXkAX}o6E~7P!`_l{%QYYsc=FwP+R>^msv@rBiU)6+XIXGBT z<7jtSx5K!X5946f9#`0?0JVei!gy%<0y-_WfsacNdhGraiJMQ-RBc`%z^r`$8V-M~ z>UXmDU)tdi_;j9J!cjJ6lw@vzQ<5cY{|p1@hfPzWLC29AJrfr zZD)|G?Wo|eb?SnZ4=;NrFlq&gB3gw+rfHhFCjsW=9;MfZZ)w}m)sXa0;{14X(hc$L zHoT;)C!O>7DI0}Ey*b#pK&+WZX!dAke{VlKYCmeGpe#;09GjT6<1Un>UJ)N#%K2|m z>Nn^{+OmaIn3BSreVCRP91zqz_AQzfk$3DfrOu8U$;#j!^o{?ia`N_i09M)wsq&e& zd}?PH-5HL2rxBuJsYS{i8nvO<#!*PC;lNGV`2f)0}>xrOV-dk_{nVy zY>fDpROMCID;wDLSIb)^txDDQi}E#^c04aU_Yw&-j}y zD*27qw6Qe|$UJT{EY{>4{e#T&waLj0x4;Z@tl{f&e!`ur)k@6f1GEAJbfHY4Sx`Lu zTj;Ce)?#g@t!e=8Nw<5AG7RGBElX?Hg+T4qdNB~rWI|9dee1A}Z7=sTLnbkp zD#nz3dq=?pA03J*KzE7m-h>2m9bHxHicbwAv|L#4Jf&cl9JlSJw|eNtDVB;qAw+w0 zFhxtml8a6~`BYU2&sM!@S?)W|3cYLj(b!aJrh5@EK9B)+b(TL{AVxxg6**c!W^+KT`QED5R(CK9Yk-ro=xXK-+~R z@NwG(79g#wY)!R)5D!i?>Ide1dkKs%EZ|WHkKuXah4Kd{RSrH`RB`llR zJmJn(C{@|aj}8o-#k9{JYVIMFKW|1jqp0E@y3PMi%Qo|p{O4|t1i)&Q^sc3<1Z8gR z#DOPeyycoQE@Iqu>jxQCdNNoByu!;FCmDsDu)&D0 z_2v^uhL~ngL*yHOR>r&DfmiY;%yx#C*Po&YZSWy2<2sbxGg$?o+8a4Q03d@d)C$w* zuv%x}_6zO&dE8#Z-#;=Xn}_?u(Xuui3&}1z78~!>SAZ@09ee)?Wa0bCJ0kUHiZnjL zYc<9TEKM`H?tSSSJ<+cEi{u{5AWN5hxFR7aOUJn;A;D5~FX-8@Mv>_`IdddG!Aqb| zlGgte4)kaSaA88D2|?759>M1S(u*cc6mq;y1gr_c9-xNbn79t(j-4~#{gllq_HmhoPO0AkU*(;{2JDt&{ObYMLvF0U=peq+& z9u^XS(jYc12~5jMmsrMDDYzb^l&X!B0YlsTzVT=kqh;a9NFGNV!n^OL{&*~~K)MAQ zZ2wgOEpiMAM3XI+R6kt@1(8Z23R)Y>1Z6-vc8mF(2OSx@0goxnFpW0Hagg%A^Pls(F#K-aoD~=cOcHE)9B zCO0GF+#y;HD8r=8e@|EXz0~afQUxaG^tY_c!7v?~ivIL7n6P?LTJTiRqnyiL<9-g< zN5%od_0Ho(zV2U?W&7)l-ASMoE(uiPBNm354cvDQ&b!-T8UIQpd8_VTov7+NQsB#C zEGT5O67%SZ$?Z?%83zUo$L-pd{)Il5unGvZeJu(W*C6?rY9Ns^g!!zSC!=CHu_aFu zI61lUXz9%9PZ&jVb9xe$yTi&Sukes`Y&?_izRBU4^k$I=EAWbD;Q!4vgxiVr6$u;4 zhK!z;$!Tc;iLs;z5R`j&kR%zBH zpB5&c|73FX?6v}EO&PmS78H=m-@*?+wCq{dn)K`roERe%q42?XO_%~eo%BLR9Z$uT zxgGf0zbG)G{hK%iiN9Jd2t_SSL?4sBYx!}&c1Id}BMo~+Z5e8ZqT}hXW3V91I8erl zH!LKaaC9P*v2z$1x^3`*0bN6k*rCqk_u094>;sBhO>PGr{qY}<{cKpKn$LZ{1_qIU zsw!$FlV@%xgR?EiP^xWS*{N7q0jv6|ErI9=hY5d7Ea;X@kzPGmE_lQw8<<|)_x4ty z1)&Y`)?GJ?Z`2cx7tb~?rPss7F^Ek?qcBsxhRUibBWXS5vP)oLSWZ4KydTvMmLh-j z5TNsFrXD_o5oIJbfT#kv9;2k?88YnND09sU&Ok2T{?*#y^@_794unF__DGF~+^O;o z1T?GNN`I_m{*mSGHtfm_v$UOBj133LJn^SKU976_GG@- z8la z-5~z7rXDdB?T#G6sbN9Mk1sd_pxo3GRT6IvR=edI0(m+X=-(negv za*^34f1jbm@nY?!yT6pME{Iw;=xZHS=VRairLxQf;}6`jT}3}zxD_jeZZ-#+QM9~0 z<747vLZm;$KeFEtQ9mQ)K9l)`8tj`()B(>~I1h=1(vZuAs%&7>RWD zVYBQ}M%C;Te1a0d0gK~r^N7?CWyaQM<~J`V-fcV9L)22->X$Vh;+aQ%+ug@2f5eLT zY?`|=Uin;KkdFWPBY*cT)3kBQaC8S!D zAg#lf|6?dlE|}e2ZyF}TLWWMdPiN*LYHoqTY=3C9(I03Y>T0>|xlZeq&HXcfr(7XZ z8E^p7Iwv$XwF}ko0IjLC$#Jjl?%c$Dv6o&)*1#O|Bykm;FI9!yqMV;;xRMN_G+_wW zGPh^n`}OS>4&${o2Y+h&!r}{ASDZ_2Z}g+etf4^L)QHv?S*Ii7`zOZ8yxkI7eIB}1 z2RXcIQOm)8_J^3^>O~#9RdPp z5!KGc3+P1WOfK4=$OMhy-!C?D{#~*@Ng{xXh!OS+7V6IDg(CzP7x!!&PX()mlU6Xy zIjb`O5v$-IygD7?sLjFKG!t-v%+LtvVXiEv;-6>~8qG6~iY{s(1~u*m`#jg5LkNKzgnbERVZnEQLVB^&Q==deD?dEy-%yX+dBlar-;GMX+%(4; zWrykq7D`0X7=3Yip&fM#nq-hN8e!y!^k4LlW04w^m8z%+%#}e-* zpRbVDy45jGe|#&dRn!Fw0fQ4D((Ux@Qr_oxGQ2N;PFS*FQ=qz50Bv>)?W*N6;f|7R ztN(M`MDtPGSsYr=YI-gBR!my#3!kOa!MeNRiR6AN@bGi@Tt<^5Q zEgjsNJUet}et%lkT>+<6g=xc#9Vf4knW2#9n#S9u`s# zW1tp{^HOc)DZXq&&QMWe>{`c z++x$OXJ0 zKR@5Xm0r^j7K-lL#?O?vjlA=`e-DuHx@iMc+rO1@vJG=tq%704z-s47eDsZ6m zuZwFt*P;%V+TEg`Zsf~!reO2={RT>w2J0kRj7v2^`KqpRYaUP@dCkN3V*|a7X%s)iF$*@@?WD70*w^cDLsBx3)Z+Fyn@4i z>qW>eck*12-ytAY%Zi%dyGyk28&G2UfY~;X0D+9K`fm@#hddlX9U@i!KCvA0y;ty2 zzJj+HTBX55nL{QNJ`UG zjQJiJsY*VG1aV}6n6k#Mn79-g+O-xQD~|okG*R#>tOcs3mw4qu@t?V;cb_A!{EP?N ztB$9{5fJT#p!M{S6%_P)C5m%`AU{ytG%{~5@}0!>5lXQMP;kbzz`xSgnSR?xte^Hf z*kI@L*h0Td=W`hc;f+OktNR#Y{LFS!!GylI>wL9-`PK__jhCgLxOvnAgvkDhYqiFrkM;kUQ`AyeJ+jU#AS*wu6wQ|wEijTY&S5X3>X_Ia3~$pC9aq;E_X?@T;`&^`Bhe8YMuUD z2vEUmjLto*$CQKQk0`N0cGs6)T`yS5PeVC#ksD4vbz85s$5E&@{ixuKTfy@qaonuV z@WQWlyoTOPhv+X5tSvl>w zV8J$|#)wgmKg9K6L#PR-!_enDlKvw zXsOT|!zriU{2S_U33S=f4(A^gKYkpXXq^OuzmVW>UPY&JahK?J{cq9UVw6bH)v{I9 zNS?BdJIsiS9J?4Cj!9TXr?6A2blhQL&+?cE65I`p$-BR75OhkxzseH!TV#QG-Y{x< z)w0e;DH0+DdJh#VWE7|V_O$*OzfvVk=pB7gpI6d8m3%Ml3x!6HBtJ;EX4-!%zZZN- zoNul}7Z5Z2R?>p}w=HM|gq4oWp;^UTz5{7(mh--#@oW09TDB$!8cXsgeV@5MAJvZ% z^SZQ`di7TH*82G96~=P8O15uNM&e{2E($)byIOl_lr35sG1z93Az+Spk#>z^BXDrpB&RX?Tswc$~Mr>EWR6 z7<`X$2)I0+gbcU?lV@4QNb8URKWqRcB6FMd?Eb6M%Q!Gq?nV&{ml~6jWgcIu<7JYh zoJEfrVM{A^s69|g?((9IB$ykuJ03+6ykb1eZRr?#DLsOk+n46D^;0nQ3?DW*~HM{3*lg`+!WNIG(=?o2#)trM?51-L|- zVC~OKmT63ooSn6sISRb!&t1-4W8(xm9Cy9zwjQqDZpnv2dlh-6}S zwB0c5m5@n47&8Zot)&`NdAbzy)iv)5SJ^4=nyXQhhs6$8M^-y@2o~^nBlEr>e7NdY zX?=8It*?@G_^uV2#r1}Zw!gw^Y_1Dh)09FsT_5ATd8}|;Vyg@Ifo`C-GF-r8e((bB z2wq>O@<-BHXsc!hzOW|_GPR<^P`Gih)V`yFw?j(`eX$5v2O5a+V8x=gY zy@z&oV(aXf8SC4u+~ZOGWBAokMq$5UyCC#<>-_}w?g>Qrzt zdx@%P%&wd*+_bbe2*p_VVs$is#2F@PEPt{=VGZCtQXf!Ay zSkFEz{w9`7z`MjptXvsjO7d6h@M|JXgXLzjt+1iQI}EuXA&&`b$9A`NX;4H5Jj;HA9Hp=JFy1StyCs~UYk$Q_*Oa(`!?3`2?{o6-` z@xUr?SdwJF`~0%x(LWtoMPdo-DWV=&T*>S?DU}w;PYG?G8DF0nQ>!N%{dK_*y8SiP zfJz=fDSLUux+W2t+3bk+{#42`0&6wU?lgjV&c93@w+qd%O+9ejk|?6i6OY6pmLRhdmfNj_lJ_rPYuQ9XGS*mbh9^eS_0 zA=`Rpbi5h-)Zr>Vf&iWF6f)@dG!dtT24TWq5tmyMPLg8s z%(I2ku{4&j-P}9N@2(~Jonb^Ms>(g0MuW2J)_xg?S6A4)mjuT_TSSCZCN=_eT0|F8 z;Xf2|`f-_}US;~;gon&Fw}S9rQaAaIk!i?4RzY*4B7}Dcp&i<7dk_{9d)i#Sqn%E( z2w{(uk>|U1m+h`phRwv=hS1fAsw(ckCE1Cz6U!WO>uUE%os*lQa+3!6Wd&pTUeRKx zZ`K4)p5W%a{e*${k(=}(SIOTBm8Fy}z@lR~Jmcl}aK2w+6j$DX8aJ4FkZ~h`l zIlbSF(&h&%MfwOTZfL{7kgi(;M4oz>#dtdk{T_hFqX0aPPPGZjgfylJwWNRy(i zBGSgkZwnwiF0!dCJ{ve^u}(Z142(sAVdWhQN%rUDV%e2V|Aaz zYi^NH7jT;#RVw|lrN1Zg;gqVIYMS?jE9{i_%~hz$!{Uc4qpKad1PgdOk@?>cK3sJx zw7$A9*4N28{8tLiV*0~H+h5@|Hdh6$Y04oRE|2lvJk~faF;#{9K(|m^87^Qkzjy(6 z1TQbs`6KBpv{kbMUsw}|8Cua_Nsf3@;*%2t{K6+Yv9vD;cM?eKG=jShrEjfG4T_#x z-ov{)F?IG#a-S2jmjQ(u3lUR%d*bkulEC~0Hsowm5PNBQW2v;mlh$1ad~TcLbt*U+ zJ;c>C=2uP@?pj(Kgkr3GF*=$*;tZ2D7C+e_v66AGK_W|5Ri)ZiePJ+bcg%H~8%o7v z`58oOFN^&|34|F|`h_C_cz{YMUS*3YT30xZ-EqL22aCR_OYgU&)M=u`nTXXuG#V5U zEN7n%W4ui01%l0fo?HH7YQ};{C?lR*;AYFr;8zmz#L4Fb#&G-Z zGyL&)k@zdKf)Cl*-7WbDCaosWTW8kOA&phE+Q{)anIEPhR)N?3?}mQbds?0#7$)3#7{4qv>hxVM+Z0$sLQDSw6Ex-FV<*%;FoJxYo^R~ z7r@vp9hKbl=jO(Wjx(Hx@D^8q$5Zrln`Lw8U0u+U6RgFHNWDd@CIX|<_D(7c{_Uf} zc;FQ`tVuFo{r*{UXrGR(qOk<^l+h0?E@bu`l*)@_Cxo`ojIYm(snwH>{<`3ZU4EMC zz$FhL6urFST@wh+Z1zNZe=21e0X3TFcN_C>Xj^GVOZ$wo3K?{Jnh4XvgELPg=4n_Bk$jTcmtHMtZ`Ew)h|8^sCrL4R zW?91NSei>%ZtflB_tz5q&ak2sRb?L0BSG19>pu*{E352YOM+vdt)fCI6B_|KEg}o4 zaG#1feK^d~FEagaLPO>oTfulQsT=&qNVMc2D$_3y@(45J?$=EQO>7X zgfPd-Nb}vhOZL|)LuTS_!)R*5)fIPNlI+CV3FVGCb+vn>PRY&DIZ1Vb@We%>6)|EoN~B(47;~U_@}mAOJN~IA1TZ3M=nGkxLTD&H~$eN zoZj!o>2m{>qWuIFH#A}3$X6|aqE9_cV!R!Nevd$7(SRODCt8H%LauJ%uYq_YG?%cd z)Q?#$N6#7qZ@!( z*>m7W?{I~I5H0y{?KScoiFYsp%T&D1&JPMq4zi!Drae{7h9=Q8ieB1LUNqTGBuUX$ z@^*M;x9nOD4^$CXO)M{YD0gsQG7_5nM>g_r3AjnCrc@-BOIq8~YWrA#%~|u*o~COZ zSVzrrOB&+QNorVP^x@!$NH-(TM7gFl+HQ1vAWOV zHMdBp3%E^=DwTfN(&0PPf^t@E^Kmp=RL?o1dDyYsb2e}_$NzU<55(C!|d?P_-i)y;{ z&7&1L74dkiG_$f!^~ zo^vU8WHGpzKNJaHi1VTu<4ecqRkX7fP3=x}_ z?>yY{gVxJ4!Zd7icWVE(3Hl=9?hFfZ%khP$knrs>VHPee{z~#b5w}$Q5MQgrRCtX1 zb`+M^c7`{!8HWq3fa9~4;pxSRf76xnDxKeMA8Imu#XVC<;rkqMx$elLY}Nr3`h`u= z|MdZI6d1fCO)~x@Hy7IS{kgNLZb?{bcs?<0pY3@{IL-zP*FMe?D+A-5Y~XOY)_ev=Hs$Y2t0A3 zy=xFVXcv@rTceVSg|0p}EE;C%UvF`kIL!Mr0_{@N?5M3NmVET!07`XjmRSfBZMnmF z7o2JT9gaicPGlmNHz`BPKK%;q5AsnoSq!LtdtF`clmH$Yb#F^N#rMi)PWW={uumA9 ziG!xGQ5bv zV&5m{Uoz%K+}MfgCu62s{(XbzJlsTsOoKd2{PG3=4T4A<`*j1Jh% zz9Or$pg+-10$W4OEtQ4PBIZT{d60iP#$SN!C;)h7FLqJlPh>0#pDw+)kp7Yp=WO4j zA$saC*=kqg!>wf&UG46^z143^pb#=a^@>PkNk&ZgfR*(rz8(`oUjJ_8%!gix(O(#C zO`EHF5AohP21M(OEOPYXz=%v>RF25J3d%gyy~4=MDhgN2zaA-2oBXjsUqthj5f$DLM>=&-@W1|Dj zWw&}|BrFx#o=^c>(rK}_DcSyY+x;(}WgOpIYa-Yb{5VpDDFAkj-8dn=ocNydl68vkUBGqzEm{+)8s*MbVk~i#}%9@e8l+?&a%e2Wz{#NqH6>UtBLo@i=WBxr=Zum$7X!ZnnOH92=Eifop4fH=2J z%lUy~{wO@Fdav%*+#_xKE$d4{WIXhrPbC_G5(DIqT&(ZZR2z&y*f#ocvd~Euocom1 z+lO^byo9rJ9WsLCLnEyebxROsb+>Ze%(3`g0Jb!f^tLC4o4zm!qiZJLuL65#Y*ZI) zOgmaix+1)|aAE=AUUzEZFg-OjuM@);WcZ0cs!|C0EJvt-u*?A2o@5Ve0d>F4dH_a@ z9dAf2Ga5B`=6GzQUq)q-D(7@_;+{T`x$0dCrZOG$rwdH>orKDF*Vuk|`wg0w!jH&6 zIRlTiJ_CuX9_OW#sNn1MUP)?&Q*+VDLD%w*O{g+$zIK zD=`aep5{?~u`)fCPD(n01CqlEMxSOw{)6xd8@ep~1`6)S&=$B8!5)E{&t z9~qs@8NK+UTB4toLL`pMWybOqGL# z7LS{jKj{8%sNWK^WrHtUNjSfk3aNbRmRTu#u9Gy#N<-VV%vDAq6ODn2xsPS(yAM zIJ4JaH-CXiOT*Ua{L*dod+Krow9Qck0+>~V*!C)_V}rI>O}mO;J_lRsou z-W|IPIicn&SDR@+m#K^#A&-z_KD795mYR|P0^yIGWj$6fZ$$5Qi(aAI3 zjaz4_O8!**U_FltYF4?uVStl3U0%${5w+RERITwT0QBz79!vLj@xFHr)l)-rg(N50QG-rl?$f_o z1Nlej7Ad5zg1lU*fgj?Q*w~@@Q}?7ZiKwEzr9fg~Wx0w~{AvEDyL#3~aP@FI$G)J) zztZsijD30h_MqA9<&}6IrwtqxtiU0V1p|L>-ihK)N(a1bHOQp4L4LL0lChw{pW$w*hndG0$46ljrBuFWtvf?ieL?}x`x061z>Q|59K7b?O4N zC}Ace`Tx4R*}vOiR%SlfU#o;}2T1Lg9DqkBYS|ncH>FSTU;HSl=e<`I+Yr<8RA!m{ z@xt0a$2PhnzZMqugm}Z!+1$SKK|8O-@;6#$Ir;IC8CKDcy2Uob1R(nE75ji{|NLQj z`#J3UU)o#pE8)GzG1?gJRDeF~*shRxT@*Zyt7$2N`C#su1#u6Hzg2}x-u;_2f`UT>8lGyLpz=P7O^T(4<;wK6Z!gu}L_VkgHtIzkARW(){aT&MmN5l?uBm4Lu z4I!qdW;j7UWGI|rv)vX0ccBkZvc(Y0ATBJ##SVKltU#z%Z?=dCQ#&FU6(S39*|-`& zR_GN$)o(o2@EUcHWbl-t+5o)QZL#v6o{{!Qhdu+8@;iN()M!M;wW>BjSl=5lI3}qG z<`&m92QLX{WYeo+@IvGMn!ab#j~YOoD6Gl z=)wUc{jCY&hhU)j`HT<0!!iftJxyT-v`zBpQ(BOXh0~%CigP}HsM5;?v!^$W9G9RK z>ah63OXhvamcmyLuDZ-{iGwzck@#|cIOXicbG$x->N?W{MqGF?JSe5G`SGy(4lJfO z{-XD)_}U6DdOp=K^ zk?!LvQ*Fm~gm7GVD*gMnrg*-p4dRNG5t+`0j;R4H*VxGhjXz*~0y0Qe4wF!}cq|3$d{GT1kkpBL zeDA)BQNxiFQ^~4wch$J#81~B=*EyE;Eop!u z%VcHSA?p_((K{Pg_U)!L4l&7yr2Vr8EisfZ56v^z#3A%w_Bx?zx*Wwz?{44=83VXC z5SU}yv6EqBfFG{0JX2Q^eN6`wE01!j9N0jn5W(ZNY zHN$Hjd15BI^1w4;nqC}rC=+&GjbLsDK|cX2{7H(kif_%D$_Zi8W8~(a*BYFiwr&b(()*TBNbF7|NH@~^EwC@;mAtajv)qQ_Twl3Dy-M&0#* zCx9QFu+(kUT!OKd6oI$#j?d!_w_VK|%1{xp?B2B4LEd>wN!VG#EnBc)2di$ zxxetdt}wB6wK9oD-_wqoo^i{U4f0t=eh(N#X55azGO(z0!`Wri&U~Gi@w(7x+G_t{ zuZ2Rc%mm1Y*kph*>9kUPg9pZ-+`2HSHE-@?+94<|euXz7FbUG)|Dd*f4{-x8NR7*q zU^`y-(nX#N$aM1{8yr};=_j1}T}C+k(xkYdPdzr7IgJ=;$$Lz%RqtYDg*$KgMX%HbeIq{3%0d!EIB8qX%2k3rxn1UOAo{ zDA&=MWJYR9?h zWa>CN{a2D&p?Nh=AF9F2_){gY|88XxO=xf|RCG7F1=_qg@4R~R`rmp0R>EehQu1O( zr1rSeea@?Ps_!Dmv}>OY+Lje=;;-=?;h9gt=_AAXf@ABs;dj9sw6P10L^J!&R+rYj zolQxecFlYL@`>+(D#;x`Nuz8wUBKB_f(``EI#hZI7ifm#(w&T`G>T#8$lUv71`}hd zK{KEm{$T4G_+;balXiW$lv_=VaUVTJFD9Z7k5wXpIk8ePp`Dmd*J}X#Z;I{EP=u#nUWE+7Rz`Sif!{Niqd8&}hEojqorlqT` z_JyCU5hn|?^A}t?QU)}jR{=c|k9)nco>%>h02O#9aqp^rLY)(w5(b=8A4Idgnq<47 zemDU^jpX3Puiwn#-YDTc2bKYPX~i}Z8ef+B4h2fz;<4gQw6zD_2?xe!a|UmIDAuT_ zzno%7={&q8sqz`}ZO>P3 z;1M+HP3157hbn%qVsZ_`@gZ)z{g#DrdUi~&*?3M~*(cx(w38)Z;RU1S<&XM5o9Z{j ztoY#g6#3%RpPuC0;@u`vbqUy(KHcLK54aaza)Se z3WStD>#GkAeqc~5>0Jn67Az!Wq%S}P+?Z!`Wk^9v8fGJ{xmIRB3C=9_*bU#{Qd02s zx<53VeIELpfh{vsK|rPzVKzOAs+i#ImN_$$<&J{pTI~c;gVO)$C(d_E?`qQl-4&@r zU55uYeD5h$QC-*MnDtEEKAMK3J`NYKDp|V5EJUtBfzAKg)JfMRjhvEmq(eSUTIZV;1v)(x|YcV<-#tjXtR2;A&=({W)p20JN;u zTwd5Y6IS_os|-qcrwE*U*$x0S<_4|U;BsZoNopSlsT#l3)ZdARa+>Y}1Y3 z{ZX>|LGem5$U$?(o;8vE9&y4uZ9}QnS~fSh{A%skAikc-#!wOEydyg2TlX7tcE&8w zg2cxSHNhkLyCE^vObCfB?8-=NV5`j2qgvH>va()ZTJ&;E_oG(X>QcXzKNt^V z!kU$?Z&+X?&KFm+G6ZdQaFr{3NT6SIza(P!UmtsWF%SC<657T+*)|6vJtZ(M`tk#%z;nSZ)!@!Y&9dr zz&b!N4Pcj*ce~;7)Ibgzl+^j0goYkYTIc4@Q!8;XE>y3CtCktdGEmZjs=H9{{qR`+ zLHt<^;0HnXJ}&9&XT%UFhc+rD=y31ml-F0`!Cg*d*hwmpP2GBctqK@Pi2i@>uJ&*C z*j1U2wioK*8-db0Wyc^9$=bF@2F>YH{1^WUD!K1f#dgFr{FND|e>`xukMYf}i0{RP zy`dg3^tN{|ywFbT@qCTenT~#ZWJXoAWA3p{u)zpEyM=z>8o&P-UVhGdKG*h^{EGN* zam;pxJCz_0y7nui9v4LqBdXd;VE$M;ra@do;_uaAQun`RSFG%%?PcMdKR4W) z*ThuA*Y^GDR+Mg(Oza}JrJUsE_)A)(PgYcdIcRjr%Wa{(%<&{lUhTKMN`%&=&oXGzE2m?rIso9PYPnilQ z7;N`NfZgar)GV4)&1(=U6`Sp%0u;{3h6PB1+;(mTkQKTGP<30+HGBr0 zWEngqD7JtvHQQ`_r>CTSk|EE4m28S=lt| z*t`(9|E8~*^y9{mCrT?af9CNSjf8q?%EOtn&wig~!Y{Z0t9Rz3Q2H={Nk3~sc%j%R zK7L~Z@9@ll`43ZAL9G*fx)j!=BVlytgd&{JpXxNS!EEWx<3}av1$wMLFw*&-a%HfU z!|SfI93o)Nqhx*@pN=_uFb^FDi{6j5 zOgQBPXoCS>Pr=B~@g}3cfB4^h2y7g>la2Uw3NpCc*o$YMmWs){KTN0%)##DH(Pr*+ z*It$gqg+ZJ5ZU>{5Fo4V=~;EIq|ZrBxNj5GL&f9Os{5BJ7~l_RH;Rm`O1Qh(LDA_% zqjXc^!?ohXryl$B!rNgf!H!UToR8_bRR_kdIqJBq#JoN=E;<`-dL(R**ce#xH6XjR{-cW|k0-OwZ zW*9H&e2c04X+v+eo3yqT2NMJgLvsaNRf^C>?kUe@#N<@9#_|H;tfpm@rn0k3Y?OJi zK?Jgi@oSI0p#UV|o*DOqR50z$ca=-E7Or>Q2v3n`4o^~U)6N0FDN$urW=g+)?dhH` zs)KlA1yb<0Q2iN2DHkDi+XL3Pe$y-QkP2y)pOcF20g$MjCd z)&09^jRTBwVktjt!OKi#3(h@4a6oG_N-=i(dkx? z$i~)hv#G-nIvIX`c8KofAQ59r_0gf+)sHk8a$#3khxW`9Cf9deJ1u4UAC=TsbU$_m z$~#~n`AXGpRLoOucSSH$7+l)Kxt^%g2iGS=?$QlE`?#*kiWiwG_GUD0(PP|itF24{ zoNoz+b{p-!mql54Fq#hgNoniNj2W=BAkp+fr9cD_?NQ}CjhTWJZOw4n$DUY;uDq}e z*k+f99ZCeGpUnhAMzJ+950TgRSi3xVm`W;q>a39ItA~c(Ri;17)s}i{_v}a(JBBCcg-< zGS_phc0jYFDr~JU2ROF zQMdHtrl*{8<->fIQJ;gx5gE55unep!9WeG;bn~AlW;|}R8aA50*lXcXtFr+zA~qQy z47zO;pJ2i9s5dT*DotDanD&TD3!kA)$V>usxWDKvp99=L%hDrqo)K9^CBKQyT>C{xc(rcPr<8ggFKE7h!CHA$)@C8>}o*5uG1MyC#9g+vF0 zA5>N9egt{1rv5@xw$Qc8%r3NqIapJ+|DH!RaM28^8W<3WFCL=I6NgEG1331JQ`|CSY%NXOmmndV&@zkxuyFxu?^hC zObi~dWXuWi>+A0nYqu+!bk68gpQu0(al-T==FnZRr(=e3z7G+dI%?_ry7vdNBNe^O zlOE0EwD6wQ#62A09=kga%4ogUM1qxN8=5TEm@G`zJ4#&V5d0$ut$bS38v?ue!~{>5R`TF^*)$@uYw^;BciNmbp!WgCYJpRLig^*b-J<4MnEhl{$< z(>+ zY$+Qx=)!kOz#BL-k9WkZ4Qijr(|)YKXBZ!;uHWZT$iqT;nrr=R^3^coS|ORx?DRY$PSG*_EGfZ)-kb$>n6J#}P!ZIusJ=Z1DNZIv9(iU?_YUz7cQP1N-%NIYU*<})cQ z2A&w2v|(L*lewfNT`0B{6=k^vle}A-vY%iQ`PI7(oe2;^^~~2|(LO- zB*3kx5)0nHu?|n$eZ>#YCG0)4PuCZfA5s&QRSi2ji2rN(hTF>idnG*e&g1%7=dVz0aJ3cw-9GG88uZ-H#JwcLkH0r15r6F0WKs-@)i&k@)^_OGtkHe5-_ zjve&{_;5~FpO|+3LFDBv(CEakfU7faxlNte_mj9ue!jjk@?YN+OXKEa2Wpbr5I8Es z?F(@au!qYJvM|8vf~jqaiJfBm^d21*mbw@I{o!^XYK1M-{3?%T?ZSO|DLZH8VDy+SQ6H%ah>brfj8 zw=CP(kbBg5qJ_(S(?&QAVa$#t!P_wYMNB5Iu)7y~x=2VUqx}E7c2OoDWAu3{h2w9z zd9f#e@KeKMnF+?C;Y)&&U0vf9^MyyNpJ&(QMddgOi#H`|IVhGeh>}F9_y?fjTRu0` zeb0OU4_LA@4mJ_R%``n5^-8@^bF@qNE!Ri_gbk)s3yiLMV4Ogh)_@HNW(*DccqDoe z&pvF0LHvS?Jv*k(<};)`)*iR$zFvEo6YW+$s4WAj^a^j0f34^HHf8vztzuD;fGpyq zG#AG2go0*PAjNNh1dh# z7cECcCMg%Qgf>x=vM%ylnhX^Rdg%nhr(GaqwQVwLyrtj>@|Qg`SqBwRy!Nx|2*#_T zfxiaM-Y171!A_d?WIIsT*r3RP4SPw!6$dxK>p=xC^MR78&)yGA%WH+TTSZsIkQMbb z=q~}EYm^LfQGTiRU31x({d~W;nX54cJh`1qzC88>trGHN+^Ut^y?oVMwCMej$)}L9 z7cDpRaz=qK;&qgj{pq2eoCd;zY7WtkV)u{3sNn4|Axh|4J@_Ia7u2n->YQeewH`5z~!llPghkk~mT6^MTBGg;lt9D2i9iT32v4=5nO) z5L?i1tdv* zcGoTM!ZrogUS&Wk>G&qbsNro-^VhPo7Hoh5vbuCWl~P?&??!O-${gCnqMPoC(}8{g zbZk@pTf5#vK2*R1W=SX^NPXbu91(mTL!(@5{GWWy`14FcN z@QWP*bxK|3Vv0s8R%3Tg^vzogjtpFWDE;I{lLt_gvxOb*3yfXQwTprAr@+wN_=~@3 z3+t>K9+pF9VX(bCd6wK|6$NMf>3>Da!sdKJX^_SKmIRpms|J+7;Py2+!%JYobET&$ zs|M!?nW*Q>=8Mm6&2n^wi_yTQ zz(tNIiD@9j<{_)Pck{IUkC~QC4>DhD!Je6>4`a0c{-Bd}2MD=t9~ej{I)cu)KVATP zbjMmk#uAFw`c}$-Axj7ku_q%JhvZdIHqHTG6X%eYU~OGPc*jh}{)urPaV*SK?%>13 za}ivA3AJOm-=C}^FzQupv$bc$4Ue58h`9<{(0xTUqtrr}RVO7cgj{ueYV~tNeS!PI z)rigqM5}GUxY%cnhoQx{P%j8FS19#hw=)Or-n*Ms4_X-pN}oPGTdcZR8z(nJ>|^r{ zfzGeOox`1l*t^}*LP13z=l$EVi7@*gqsde+8-2^oi982_o*NvS$4(RF21isLJX_HmuF91iBx#^n7II4x`wVzN%HC+@9_%?R&JUI3Wbl0;X+J?Tx21E#J z+D-_lIlc#42q}M@43$=X@OoxjTrH~FD!wFuuc@a&cnteopktDY^-r?vp3KMV<^9CX zS%@j+%<5h7=ddMcmy;&tRIc6Y=BnMNN9>MGJcWq4X}_bAGSO!z0dWHeYz#!_n)=^C zKYwK!%)gxv5nAKm4Rk9k$iBsiqq8ioG*OXxJ{eykKA~U zoNGLpT8o^K!nm@;00001 z00IDz0000100KBdNlgSO0000001N;C00KYo00000001~bNlgSp0000001i-MWmf?Z L00sbL00000BIZ95 literal 0 HcmV?d00001 diff --git a/src/pages/main-page.jsx b/src/pages/main-page.jsx new file mode 100644 index 0000000..7101e57 --- /dev/null +++ b/src/pages/main-page.jsx @@ -0,0 +1,210 @@ +import styled from "styled-components"; +import { colors } from "@/styles/colors"; +import { font } from "@/styles/font"; +import media from "@/styles/media"; + +const Container = styled.div` + max-width: 1200px; + margin: 0 auto; + display: flex; + flex-direction: column; + align-items: center; + gap: 30px; + + ${media.small` + gap: 24px; + `} +`; + +const MainFlexBox = styled.div` + width: 1200px; + height: 324px; + padding: 60px; + margin: 0 auto; + display: flex; + flex-direction: column; + gap: 20px; + background-color: ${colors.surface}; + background-image: url(./src/assets/images/main-visual-01.webp); + background-size: 60%; + background-repeat: no-repeat; + background-position: right 7px top 60px; + border-radius: 16px; + margin-top: 60px; + + ${media.small` + ${font.regular14} + width: calc(100% - 40px); + height: 352px; + padding: 24px; + margin-left: 20px; + margin-right: 20px; + flex-direction: column; + gap: 12px; + background-position: center bottom 36px; + background-size: 120%; + `} + + ${media.medium` + ${font.regular16}; + width: calc(100% - 48px); + height: 440px; + padding: 40px; + margin-left: 20px; + margin-right: 20px; + flex-direction: column; + background-position: center bottom 36px; + background-size: 100%; + `} + + ${media.large` + ${font.regular18} + `} +`; + +const MainFlexBoxRightPosition = styled.div` + width: 1200px; + height: 324px; + padding: 60px; + margin: 0 auto; + display: flex; + flex-direction: column; + gap: 20px; + background-color: ${colors.surface}; + background-image: url(./src/assets/images/main-visual-02.webp); + background-size: 60%; + background-repeat: no-repeat; + background-position: left -7px top 60px; + border-radius: 16px; + padding-left: 660px; + + ${media.small` + ${font.regular14} + width: calc(100% - 40px); + height: 352px; + padding: 24px; + margin-left: 20px; + margin-right: 20px; + flex-direction: column; + gap: 12px; + background-position: center bottom 25px; + background-size: 120%; + `} + + ${media.medium` + ${font.regular16}; + width: calc(100% - 48px); + height: 440px; + padding: 40px; + margin-left: 20px; + margin-right: 20px; + flex-direction: column; + background-position: center bottom 25px; + background-size: 100%; + `} + + ${media.large` + ${font.regular18} + `} +`; + +const PointLabel = styled.div` + width: 80px; + height: 32px; + background-color: ${colors.purple[600]}; + color: #ffffff; + border-radius: 50px; + text-align: center; + line-height: 32px; + ${font.bold14}; +`; + +const MainTitle = styled.p` + ${font.bold24}; + width: 265px; + color: ${colors.gray[900]}; + line-height: 36px; + margin: 0; + + ${media.small` + ${font.bold18}; + width: 100%; + `} + + ${media.medium` + width: 100%; + `} + + ${media.large` + + `} +`; + +const MainTitleSmall = styled.p` + ${font.regular18}; + color: ${colors.gray[500]}; + margin: 0; + + ${media.small` + ${font.regular15}; + `} + + ${media.medium` + ${font.regular18}; + width: 100%; + `} + + ${media.large` + + `} +`; + +const Button = styled.button` + width: 280px; + height: 56px; + display: flex; + justify-content: center; + align-items: center; + margin: 0 auto; + border-radius: 12px; + background-color: ${colors.purple[600]}; + color: #ffffff; + ${font.bold18}; + margin-top: 28px; + border: none; + cursor: pointer; + + ${media.small` + width: calc(100% - 40px); + `} + + ${media.medium` + width: calc(100% - 48px); + `} + + ${media.large` + + `} +`; + +export default function MainPage() { + return ( + + + Point. 01 + + 누구나 손쉽게, 온라인 롤링 페이퍼를 만들 수 있어요 + + 로그인 없이 자유롭게 만들어요. + + + Point. 02 + 서로에게 이모지로 감정을 표현해보세요 + + 롤링 페이퍼에 이모지를 추가할 수 있어요. + + + + + ); +} From 1c0fd82590230017d4caa19396a76a87febedf3e Mon Sep 17 00:00:00 2001 From: BZzzzi Date: Fri, 7 Nov 2025 18:43:37 +0900 Subject: [PATCH 07/91] =?UTF-8?q?Feat:=20=EA=B3=B5=ED=86=B5=20=EB=B2=84?= =?UTF-8?q?=ED=8A=BC=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Button 컴포넌트 생성 및 variant별 스타일 정의 - primary, secondary, outlined, plus, delete variant 지원 - large, medium, small, tiny, plus, delete 사이즈 옵션 제공 - plus, delete variant는 아이콘 전용 버튼으로 구현 - outlined variant에서 emoji prop으로 이모지 아이콘 선택적 추가 지원 - 아이콘과 텍스트를 함께 표시할 수 있는 레이아웃 구현 (gap: 10px) - 각 아이콘에 className 적용으로 variant별 스타일 분리 - hover, active, focus 상태별 인터랙션 스타일 적용 - CSS transition으로 부드러운 상태 전환 효과 추가 - styled-components 기반 동적 스타일 시스템 구성 - 테스트 페이지에 버튼 컴포넌트 사용 예시 추가 --- src/components/common/button.jsx | 191 +++++++++++++++++++++++++++++++ src/pages/test-page.jsx | 31 ++++- 2 files changed, 221 insertions(+), 1 deletion(-) create mode 100644 src/components/common/button.jsx diff --git a/src/components/common/button.jsx b/src/components/common/button.jsx new file mode 100644 index 0000000..a9fb0c8 --- /dev/null +++ b/src/components/common/button.jsx @@ -0,0 +1,191 @@ +import styled, { css } from "styled-components"; +import { colors } from "@/styles/colors"; +import { font } from "@/styles/font"; +import plusIcon from "@/assets/icons/plus.svg"; +import deleteIcon from "@/assets/icons/deleted.svg"; +import addEmojiIcon from "@/assets/icons/add-emoji.svg"; + +const SIZES = { + large: css` + padding: 14px 24px; + border-radius: 12px; + ${font.bold18} + `, + medium: css` + padding: 7px 16px; + border-radius: 6px; + ${font.regular16} + `, + small: css` + padding: 6px 16px; + border-radius: 6px; + ${font.regular16} + `, + tiny: css` + padding: 2px 16px; + border-radius: 6px; + ${font.regular14} + `, + plus: css` + padding: 16px; + border-radius: 9999px; + `, + delete: css` + padding: 6px; + border-radius: 6px; + `, +}; + +const VARIANT_STYLES = { + primary: css` + background-color: ${colors.purple[600]}; + color: white; + border: none; + + &:hover { + background-color: ${colors.purple[700]}; + } + + &:active { + background-color: ${colors.purple[800]}; + } + + &:focus { + background-color: ${colors.purple[900]}; + } + `, + + secondary: css` + background-color: white; + color: ${colors.purple[700]}; + border: 1px solid ${colors.purple[600]}; + + &:hover { + background-color: ${colors.purple[100]}; + color: ${colors.purple[600]}; + border-color: ${colors.purple[700]}; + } + + &:active { + background-color: ${colors.purple[100]}; + color: ${colors.purple[600]}; + border-color: ${colors.purple[800]}; + } + + &:focus { + color: ${colors.purple[600]}; + border-color: ${colors.purple[800]}; + } + `, + + outlined: css` + background-color: white; + color: ${colors.gray[900]}; + border: 1px solid ${colors.gray[300]}; + + &:hover { + background-color: ${colors.gray[100]}; + } + + &:active { + background-color: ${colors.gray[100]}; + } + + &:focus { + border-color: ${colors.gray[500]}; + } + + .emoji-icon { + width: 24px; + height: 24px; + } + `, + + plus: css` + background-color: ${colors.gray[500]}; + border: 1px solid transparent; + + &:hover { + background-color: ${colors.gray[600]}; + } + + &:active { + background-color: ${colors.gray[700]}; + } + + &:focus { + background-color: ${colors.gray[700]}; + border: 1px solid ${colors.gray[800]}; + } + + .plus-icon { + width: 24px; + height: 24px; + } + `, + + delete: css` + background-color: white; + border: 1px solid ${colors.gray[300]}; + + &:hover { + background-color: ${colors.gray[100]}; + } + + &:active { + background-color: ${colors.gray[100]}; + } + + &:focus { + border-color: 1px solid ${colors.gray[500]}; + } + + .delete-icon { + width: 24px; + height: 24px; + } + `, +}; + +const CustomButton = styled.button` + display: flex; + justify-content: center; + align-items: center; + gap: 10px; + cursor: pointer; + transition: all 0.2s ease-in-out; + ${({ variant }) => VARIANT_STYLES[variant] || VARIANT_STYLES.primary} + ${({ size }) => SIZES[size] || SIZES.medium} + + &:disabled { + background-color: ${colors.gray[300]}; + color: white; + border: transparent; + cursor: not-allowed; + } +`; + +export default function Button({ + children, + variant = "primary", + size = "medium", + emoji = "", + ...props +}) { + return ( + + {variant === "plus" ? ( + 추가 + ) : variant === "delete" ? ( + 삭제 + ) : variant === "outlined" && emoji ? ( + <> + 이모지 + {children} + + ) : ( + children + )} + + ); +} diff --git a/src/pages/test-page.jsx b/src/pages/test-page.jsx index 8a39db3..0f345bb 100644 --- a/src/pages/test-page.jsx +++ b/src/pages/test-page.jsx @@ -2,6 +2,7 @@ import styled from "styled-components"; import { colors } from "@/styles/colors"; import { font } from "@/styles/font"; import media from "@/styles/media"; +import Button from "@/components/common/button"; const Container = styled.div` padding: 40px 20px; @@ -36,10 +37,15 @@ const ResponsiveBox = styled.div` `} `; +const CustomButton = styled(Button)` + width: 100%; + padding: 30px; +`; + export default function TestPage() { return ( - 스타일 테스트 + 공용 스타일 & 컴포넌트 테스트 창 크기를 조절해보세요!
@@ -50,6 +56,29 @@ export default function TestPage() {
데스크톱(1024px~): 보라색 배경, 큰 폰트
+ + + + + + + + custom button + custom button
); } From 070f6de6fcaa4532746ce1712df381cdd9150d55 Mon Sep 17 00:00:00 2001 From: BZzzzi Date: Fri, 7 Nov 2025 18:44:46 +0900 Subject: [PATCH 08/91] =?UTF-8?q?Feat:=20Header=EC=97=90=20=EA=B3=B5?= =?UTF-8?q?=ED=86=B5=20Button=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 기존 HTML button 태그를 Button 컴포넌트로 교체 - 일관된 디자인 시스템 적용을 위한 컴포넌트 통합 --- src/components/common/header.jsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/common/header.jsx b/src/components/common/header.jsx index 455b475..7d0e16d 100644 --- a/src/components/common/header.jsx +++ b/src/components/common/header.jsx @@ -1,6 +1,7 @@ import { Link } from "react-router"; import styled from "styled-components"; import logo from "@/assets/icons/logo.svg"; +import Button from "@/components/common/button"; const ContainWrapper = styled.div` position: sticky; @@ -47,7 +48,9 @@ export default function Header({ showButton }) { {showButton && ( - + )} From abce54444b7292c7aa7289c9c7cf79d777cb1df0 Mon Sep 17 00:00:00 2001 From: BZzzzi Date: Fri, 7 Nov 2025 18:46:15 +0900 Subject: [PATCH 09/91] =?UTF-8?q?Refactor:=20GlobalLayout=20=EB=B2=84?= =?UTF-8?q?=ED=8A=BC=20=ED=91=9C=EC=8B=9C=20=EB=A1=9C=EC=A7=81=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 버튼 표시 페이지 목록을 PAGES_WITH_BUTTON 상수로 분리 - 새로운 페이지 추가 시 배열에 문자열만 추가하면 되도록 개선 --- src/components/common/global-layout.jsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/components/common/global-layout.jsx b/src/components/common/global-layout.jsx index 332c319..db2b1ed 100644 --- a/src/components/common/global-layout.jsx +++ b/src/components/common/global-layout.jsx @@ -1,11 +1,13 @@ import { Outlet, useLocation } from "react-router"; import Header from "@/components/common/header"; +const PAGES_WITH_BUTTON = ["main-page", "list-page"]; + export default function GlobalLayout() { const location = useLocation(); - const showButton = - location.pathname.includes("main-page") || - location.pathname.includes("list-page"); + const showButton = PAGES_WITH_BUTTON.some((page) => + location.pathname.includes(page) + ); return ( <> From 78ffe9b262d274aec6e95ab4a881127f76155777 Mon Sep 17 00:00:00 2001 From: BZzzzi Date: Fri, 7 Nov 2025 19:59:47 +0900 Subject: [PATCH 10/91] =?UTF-8?q?Feat:=20=EC=9E=84=EC=8B=9C=20=EB=84=A4?= =?UTF-8?q?=EB=B9=84=EA=B2=8C=EC=9D=B4=EC=85=98=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.jsx | 11 +++++++++- src/pages/temp-page.jsx | 48 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 src/pages/temp-page.jsx diff --git a/src/App.jsx b/src/App.jsx index f85f518..cf827a5 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -2,6 +2,8 @@ import { Routes, Route } from "react-router"; import { GlobalStyle } from "@/styles/global-style"; import GlobalLayout from "@/components/common/global-layout"; import TestPage from "@/pages/test-page"; +import TempPage from "@/pages/temp-page"; +import ToastTestPage from "@/pages/toast-test-page"; function App() { return ( @@ -9,7 +11,14 @@ function App() { }> - } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> diff --git a/src/pages/temp-page.jsx b/src/pages/temp-page.jsx new file mode 100644 index 0000000..fd8990b --- /dev/null +++ b/src/pages/temp-page.jsx @@ -0,0 +1,48 @@ +import Button from "@/components/common/button"; +import { Link } from "react-router"; +import styled from "styled-components"; + +const Contain = styled.div` + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 10px; + margin: 5% auto; +`; + +const CustomButton = styled(Button)` + width: 200px; + padding: 10%; +`; +export default function TempPage() { + return ( + + + 메인 페이지 + + + 리스트 페이지 + + + 롤페 페이지 + + + 롤페 생성 페이지 + + + 롤페 메시지 페이지 + + + + 테스트 페이지 + + + + + toast 테스트 페이지 + + + + ); +} From 77ef67a647e4e751352732c581a7bfb373484020 Mon Sep 17 00:00:00 2001 From: BZzzzi Date: Sat, 8 Nov 2025 02:28:53 +0900 Subject: [PATCH 11/91] =?UTF-8?q?Feat:=20=EA=B3=B5=ED=86=B5=20Toast=20?= =?UTF-8?q?=EC=95=8C=EB=A6=BC=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 컴포넌트 생성 및 success/delete 타입 지원 - fadeIn/fadeOut 애니메이션 적용 (0.3s) - media breakpoint 기반 반응형 디자인 적용 (small/medium/large) - ToastContext 및 ToastProvider를 통한 전역 토스트 상태 관리 - useToast 커스텀 훅 제공 (success, delete 메서드) - createPortal을 활용한 토스트 렌더링 구조 - 5초 자동 사라짐 및 수동 닫기 기능 - Fast Refresh 호환을 위한 Context 파일 분리 - 삭제 에러용 빨간색 아이콘 추가 - 토스트 테스트 페이지 추가 --- index.html | 1 + src/assets/icons/deleted-red.svg | 3 ++ src/components/common/toast.jsx | 83 +++++++++++++++++++++++++++++ src/contexts/toast-context-state.js | 3 ++ src/contexts/toast-context.jsx | 52 ++++++++++++++++++ src/hooks/use-toast.js | 6 +++ src/main.jsx | 5 +- src/pages/toast-test-page.jsx | 26 +++++++++ 8 files changed, 178 insertions(+), 1 deletion(-) create mode 100644 src/assets/icons/deleted-red.svg create mode 100644 src/components/common/toast.jsx create mode 100644 src/contexts/toast-context-state.js create mode 100644 src/contexts/toast-context.jsx create mode 100644 src/hooks/use-toast.js create mode 100644 src/pages/toast-test-page.jsx diff --git a/index.html b/index.html index d4c7e2a..cd6c3dd 100644 --- a/index.html +++ b/index.html @@ -14,6 +14,7 @@
+
diff --git a/src/assets/icons/deleted-red.svg b/src/assets/icons/deleted-red.svg new file mode 100644 index 0000000..e051b72 --- /dev/null +++ b/src/assets/icons/deleted-red.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/common/toast.jsx b/src/components/common/toast.jsx new file mode 100644 index 0000000..09ef5b2 --- /dev/null +++ b/src/components/common/toast.jsx @@ -0,0 +1,83 @@ +import styled, { keyframes } from "styled-components"; +import completedIcon from "@/assets/icons/completed.svg"; +import deletedRedIcon from "@/assets/icons/deleted-red.svg"; +import closeIcon from "@/assets/icons/close.svg"; +import { font } from "@/styles/font"; +import media from "@/styles/media"; + +const fadeIn = keyframes` + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +`; + +const fadeOut = keyframes` + from { + opacity: 1; + transform: translateY(0); + } + to { + opacity: 0; + transform: translateY(20px); + } +`; + +const ToastStyle = styled.div` + display: flex; + justify-content: start; + align-items: center; + gap: 12px; + position: fixed; + background-color: rgba(0, 0, 0, 0.8); + color: white; + padding: 19px 30px; + border-radius: 8px; + animation: ${({ isClosing }) => (isClosing ? fadeOut : fadeIn)} 0.3s + ease-in-out forwards; + ${font.regular16} + + button { + background-color: transparent; + border: none; + cursor: pointer; + margin-left: auto; + } + + ${media.large` + width: 524px; + left: calc(50% - 262px); + bottom: 70px; + `} + + ${media.medium` + width: 524px; + left: calc(50% - 262px); + bottom: 50px; + `} + + ${media.small` + width: 320px; + left: calc(50% - 160px); + bottom: 88px; + `} +`; + +export default function Toast({ children, type, isClosing, onClose }) { + return ( + + {type + {children} + + + ); +} diff --git a/src/contexts/toast-context-state.js b/src/contexts/toast-context-state.js new file mode 100644 index 0000000..2056dd1 --- /dev/null +++ b/src/contexts/toast-context-state.js @@ -0,0 +1,3 @@ +import { createContext } from "react"; + +export const ToastContext = createContext(null); diff --git a/src/contexts/toast-context.jsx b/src/contexts/toast-context.jsx new file mode 100644 index 0000000..4e764b3 --- /dev/null +++ b/src/contexts/toast-context.jsx @@ -0,0 +1,52 @@ +import { useState, useCallback } from "react"; +import { createPortal } from "react-dom"; +import Toast from "@/components/common/toast"; +import { ToastContext } from "./toast-context-state"; + +export default function ToastProvider({ children }) { + const [toasts, setToasts] = useState([]); + + const removeToast = useCallback((id) => { + setToasts((prev) => prev.filter((t) => t.id !== id)); + }, []); + + const addToast = useCallback( + (type, message) => { + const id = Date.now(); + setToasts((prev) => [...prev, { id, type, message, isClosing: false }]); + setTimeout(() => { + setToasts((prev) => + prev.map((t) => (t.id === id ? { ...t, isClosing: true } : t)) + ); + setTimeout(() => removeToast(id), 300); + }, 4700); + }, + [removeToast] + ); + + const toast = { + success: (msg) => addToast("success", msg), + delete: (msg) => addToast("delete", msg), + }; + + return ( + + {children} + {createPortal( + <> + {toasts.map((t) => ( + removeToast(t.id)} + > + {t.message} + + ))} + , + document.getElementById("toast") + )} + + ); +} diff --git a/src/hooks/use-toast.js b/src/hooks/use-toast.js new file mode 100644 index 0000000..47c8088 --- /dev/null +++ b/src/hooks/use-toast.js @@ -0,0 +1,6 @@ +import { useContext } from "react"; +import { ToastContext } from "@/contexts/toast-context-state"; + +export default function useToast() { + return useContext(ToastContext); +} diff --git a/src/main.jsx b/src/main.jsx index e50388c..84fb027 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -1,9 +1,12 @@ import { createRoot } from "react-dom/client"; import { BrowserRouter } from "react-router"; import App from "./App.jsx"; +import ToastProvider from "@/contexts/toast-context"; createRoot(document.getElementById("root")).render( - + + + ); diff --git a/src/pages/toast-test-page.jsx b/src/pages/toast-test-page.jsx new file mode 100644 index 0000000..b1ae12d --- /dev/null +++ b/src/pages/toast-test-page.jsx @@ -0,0 +1,26 @@ +import styled from "styled-components"; +import Button from "@/components/common/button"; +import useToast from "@/hooks/use-toast"; + +const Container = styled.div` + padding: 40px; + display: flex; + flex-direction: column; + gap: 20px; +`; + +export default function ToastTestPage() { + const toast = useToast(); + + return ( + +

Toast 테스트 페이지 (5초 후 자동 사라짐)

+ + +
+ ); +} From 58c6e747ec28631a0c09d8a39dba0e961eb5b4f3 Mon Sep 17 00:00:00 2001 From: BZzzzi Date: Sat, 8 Nov 2025 02:31:04 +0900 Subject: [PATCH 12/91] =?UTF-8?q?Refactor:=20Button=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EC=8A=A4=ED=83=80=EC=9D=BC=EB=AA=85=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20=EB=B0=8F=20=EB=9D=BC=EC=9A=B0=ED=8A=B8=20?= =?UTF-8?q?=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CustomButton을 ButtonStyle로 네이밍 변경 - App.jsx의 미구현 라우트 주석 처리 --- src/App.jsx | 10 +++++----- src/components/common/button.jsx | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index cf827a5..de1d38c 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -12,11 +12,11 @@ function App() { }> } /> - } /> - } /> - } /> - } /> - } /> + {/* } /> + } /> + } /> + } /> + } /> */} } /> } /> diff --git a/src/components/common/button.jsx b/src/components/common/button.jsx index a9fb0c8..f37e20a 100644 --- a/src/components/common/button.jsx +++ b/src/components/common/button.jsx @@ -147,7 +147,7 @@ const VARIANT_STYLES = { `, }; -const CustomButton = styled.button` +const ButtonStyle = styled.button` display: flex; justify-content: center; align-items: center; @@ -173,7 +173,7 @@ export default function Button({ ...props }) { return ( - + {variant === "plus" ? ( 추가 ) : variant === "delete" ? ( @@ -186,6 +186,6 @@ export default function Button({ ) : ( children )} - + ); } From 7b07dadaff24c80cea4fb0d09d528929d6be01d1 Mon Sep 17 00:00:00 2001 From: summerlane Date: Sat, 8 Nov 2025 14:03:20 +0900 Subject: [PATCH 13/91] =?UTF-8?q?Feat:=20=ED=86=A0=EA=B8=80=20=EC=84=A0?= =?UTF-8?q?=ED=83=9D=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/assets/images/select-circle.webp | Bin 0 -> 1354 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/assets/images/select-circle.webp diff --git a/src/assets/images/select-circle.webp b/src/assets/images/select-circle.webp new file mode 100644 index 0000000000000000000000000000000000000000..88fb41c710302f2a08e5253be1e08ebae8a6690b GIT binary patch literal 1354 zcmV-Q1-1H8Nk&FO1pok7MM6+kP&il$0000G000120037206|PpNSFZt00EFy+jiJS z7lI%Nf*=S&7*q@@1{MRGfy^LfAP9!=AUp`~!%kvJx1ITnhzY=mA+7ZgH8WFb&+-T$ z7?-AYqqZETimO^jsbLbAn{_|Z1a46qQqpMOYQstzDK;Bg{3vd<;g>PI*o5K-_)?ov zbGOB&T{=~pce`{&j&P5#`lCt zdn*~${El!@_bM6L@=DmKyGlkkUmbkfMKHqgR2gk2VWa?pQI0_7RDi~*0m?}cjC6Rg zN`=+-WK|ohLs*HjQv2_%tW;RZ!K&@7el}Jmv3dkn0m?~%#;E~?QwAcZ06-ZD0Bnrv zDKZKtG(HC|GoN(jl#SqJVUu1tQ@HdHzS( z++^R>KPkA``hb5b|99#u`q!E7n*XCX{T{UTYb zXP~*|cliaW{~1JH^{` zI;-PEb!2Bx{s6U!W+gV7JH!^&3x2Os6bTx=8vw&^)AY?~lhaS?C5I*+sr|VTy z`{&Z(z5xy61>tqh%BJTY2lhCNUp>CW&@c{R8I#^;e)g5iJRhA+{? zU}8V7Q~k?r-L)DW*^ckS$Gz}Gu-f*Pa+H0qhPed9w5@k{G39LP_FvrtW*G}ywtlEo z9wv~`h&=fXlrKTH;b;GmY2CQ9X&+CK)91K33toq~7BDzihQ?c54CbRwm@Dcwvej>w zN%j|1Al?8iwyz$yj>kT3ZXf@l6%f(aBBT_*?1A+t;TMJk-4V#man!Qc^ZNdqsNiks z18yv4z^f2L0Luy+tLZVC{`p6w(fck6*mbVm5QdQo?B16A-~0gQ$t**WnhI>$B4v0OTiOHgE&&782 zrFdV_;HrP0rCxE9-1W97AfS%|p)_@Vo%>u|ep&xO6JSmeoH&m{7_7<`eX}UrzHF*pI81}YS7NCLW_qG540000`Q$a~i0000uLP<>n?EnA(000mGkN^Mx z0RRF3kN^Mx0RRFxLP<>oC;$Ke000aC0006%@Bjb+0000uLP<>oLjV8(000h9Vr5qW M5C8@MWB>pF03!`~VE_OC literal 0 HcmV?d00001 From f5889f9760118cfa2422a8cf683652cc0d20f969 Mon Sep 17 00:00:00 2001 From: summerlane Date: Sat, 8 Nov 2025 14:04:12 +0900 Subject: [PATCH 14/91] =?UTF-8?q?Style:=20=ED=95=84=EC=9A=94=EC=97=86?= =?UTF-8?q?=EB=8A=94=20=EB=B0=98=EC=9D=91=ED=98=95=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/main-page.jsx | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/src/pages/main-page.jsx b/src/pages/main-page.jsx index 7101e57..de50804 100644 --- a/src/pages/main-page.jsx +++ b/src/pages/main-page.jsx @@ -102,10 +102,6 @@ const MainFlexBoxRightPosition = styled.div` background-position: center bottom 25px; background-size: 100%; `} - - ${media.large` - ${font.regular18} - `} `; const PointLabel = styled.div` @@ -134,10 +130,6 @@ const MainTitle = styled.p` ${media.medium` width: 100%; `} - - ${media.large` - - `} `; const MainTitleSmall = styled.p` @@ -153,10 +145,6 @@ const MainTitleSmall = styled.p` ${font.regular18}; width: 100%; `} - - ${media.large` - - `} `; const Button = styled.button` @@ -181,10 +169,6 @@ const Button = styled.button` ${media.medium` width: calc(100% - 48px); `} - - ${media.large` - - `} `; export default function MainPage() { From 1774516a7e4eb6ee0791bbd9a0b597677f5e7795 Mon Sep 17 00:00:00 2001 From: summerlane Date: Sat, 8 Nov 2025 14:04:57 +0900 Subject: [PATCH 15/91] =?UTF-8?q?Feat:=20post=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20=EB=9D=BC=EC=9A=B0=ED=84=B0=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.jsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index b98877b..749ab7e 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -3,6 +3,7 @@ import { GlobalStyle } from "@/styles/global-style"; import GlobalLayout from "@/components/global-layout"; import TestPage from "@/pages/test-page"; import MainPage from "@/pages/main-page"; +import PostPage from "@/pages/post-page"; function App() { return ( @@ -11,8 +12,9 @@ function App() { }> - } /> - } /> + {/* } /> */} + } /> + } /> From 6bf34a1a766eed4a4694f5c18a90d92308cd052f Mon Sep 17 00:00:00 2001 From: summerlane Date: Sat, 8 Nov 2025 14:05:52 +0900 Subject: [PATCH 16/91] =?UTF-8?q?Feat:=20post=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20=EC=83=9D=EC=84=B1=ED=9B=84=20UI=20=EB=94=94?= =?UTF-8?q?=EC=9E=90=EC=9D=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/post-page.jsx | 181 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 181 insertions(+) create mode 100644 src/pages/post-page.jsx diff --git a/src/pages/post-page.jsx b/src/pages/post-page.jsx new file mode 100644 index 0000000..ce67baa --- /dev/null +++ b/src/pages/post-page.jsx @@ -0,0 +1,181 @@ +import styled from "styled-components"; +import { colors } from "@/styles/colors"; +import { font } from "@/styles/font"; + +const Container = styled.div` + max-width: 720px; + margin: 0 auto; + display: flex; + flex-direction: column; + align-items: center; + margin-top: 70px; + gap: 58px; +`; + +const InputSection = styled.div` + width: 100%; + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: flex-start; + gap: 24px; +`; + +const InputSectionTitle = styled.p` + ${font.bold24}; + color: ${colors.gray[900]}; + margin: 0; +`; + +const Input = styled.input` + width: 100%; + height: 50px; + border: 1px solid ${colors.gray[300]}; + border-radius: 8px; + ${font.regular18}; + color: ${colors.gray[500]}; + padding: 12px 16px; +`; + +const ToggleSection = styled.div` + width: 100%; + display: flex; + flex-direction: column; + gap: 17px; +`; + +const ToggleTitle = styled.p` + ${font.bold24}; + color: ${colors.gray[900]}; + margin: 0; +`; + +const ToggleTitleSmall = styled.p` + ${font.regular16}; + color: ${colors.gray[600]}; + margin: 0; +`; + +const ToggleButtonContainer = styled.div` + width: 224px; + height: 40px; + background-color: ${colors.gray[100]}; + border-radius: 6px; + display: flex; + flex-direction: row; + margin-top: 20px; +`; + +const ToggleButton = styled.div` + width: 122px; + height: 40px; + background-color: #ffffff; + color: ${colors.purple[600]}; + ${font.bold16}; + border: 2px solid ${colors.purple[600]}; + border-radius: 6px; + display: flex; + justify-content: center; + align-items: center; + cursor: pointer; +`; + +const ToggleButtonDisable = styled.div` + width: 122px; + height: 40px; + background-color: transparent; + color: ${colors.gray[900]}; + ${font.regular16}; + display: flex; + justify-content: center; + align-items: center; + cursor: pointer; +`; + +const ToggleDivContainer = styled.div` + display: flex; + flex-direction: row; + gap: 16px; + margin-top: 40px; +`; + +const ToggleDiv = styled.div` + width: 168px; + height: 168px; + border: 1px solid rgba(0, 0, 0, 0.08); + border-radius: 16px; + background-color: ${(props) => props.backgroundColor}; + background-image: url(./src/assets/images/select-circle.webp); + background-repeat: no-repeat; + background-size: 44px 44px; + background-position: center; + cursor: pointer; +`; + +const ToggleImgContainer = styled.div` + display: flex; + flex-direction: row; + gap: 16px; + margin-top: 40px; +`; + +const ToggleImg = styled.div` + width: 168px; + height: 168px; + border: 1px solid rgba(0, 0, 0, 0.08); + border-radius: 16px; + background-color: ${(props) => props.backgroundImg}; + background-image: url(./src/assets/images/select-circle.webp); + background-repeat: no-repeat; + background-size: 44px 44px; + background-position: center; + cursor: pointer; +`; + +const Button = styled.button` + width: 100%; + height: 56px; + ${font.bold18}; + background-color: ${colors.purple[600]}; + color: #ffffff; + display: flex; + justify-content: center; + align-items: center; + border-radius: 12px; + border: 0; + cursor: pointer; +`; + +export default function PostPage() { + return ( + + + To. + + + + 배경화면을 선택해 주세요. + + 컬러를 선택하거나, 이미지를 선택할 수 있습니다. + + + 컬러 + 이미지 + + + + + + + + + + + + + + + + + ); +} From d90a0e3c28291c5381f1ea181f88d39ef54d5096 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A0=95=EC=9B=90?= Date: Sun, 9 Nov 2025 02:29:51 +0900 Subject: [PATCH 17/91] =?UTF-8?q?Feat:=20=EB=A1=A4=EB=A7=81=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=ED=8D=BC=20=ED=97=A4=EB=8D=94=20=EC=9D=B4=EB=AA=A8?= =?UTF-8?q?=EC=A7=80=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 22 +++ package.json | 1 + src/pages/rolling-page-head.jsx | 235 ++++++++++++++++++++++++++++++ src/pages/rolling-page.jsx | 45 +----- src/styles/rolling-page-styles.js | 2 - 5 files changed, 265 insertions(+), 40 deletions(-) create mode 100644 src/pages/rolling-page-head.jsx diff --git a/package-lock.json b/package-lock.json index 0982c14..1cb617d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.0", "dependencies": { "axios": "^1.13.2", + "emoji-picker-react": "^4.15.0", "react": "^19.1.1", "react-dom": "^19.1.1", "react-router": "^7.9.5", @@ -1814,6 +1815,21 @@ "dev": true, "license": "ISC" }, + "node_modules/emoji-picker-react": { + "version": "4.15.0", + "resolved": "https://registry.npmjs.org/emoji-picker-react/-/emoji-picker-react-4.15.0.tgz", + "integrity": "sha512-41y22VM7Gamcrxkgm3dKO2pAaa56AtFbK2bBImZbaME1jh0C39vlI2qPWCNFVf96HOYN4SLLSE2alRSyC3QYYQ==", + "license": "MIT", + "dependencies": { + "flairup": "1.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">=16" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -2170,6 +2186,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/flairup": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/flairup/-/flairup-1.0.0.tgz", + "integrity": "sha512-IKlE+pNvL2R+kVL1kEhUYqRxVqeFnjiIvHWDMLFXNaqyUdFXQM2wte44EfMYJNHkW16X991t2Zg8apKkhv7OBA==", + "license": "MIT" + }, "node_modules/flat-cache": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", diff --git a/package.json b/package.json index 8045356..4af7841 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "axios": "^1.13.2", + "emoji-picker-react": "^4.15.0", "react": "^19.1.1", "react-dom": "^19.1.1", "react-router": "^7.9.5", diff --git a/src/pages/rolling-page-head.jsx b/src/pages/rolling-page-head.jsx new file mode 100644 index 0000000..5245fd6 --- /dev/null +++ b/src/pages/rolling-page-head.jsx @@ -0,0 +1,235 @@ +import React, { useState } from 'react'; +import EmojiPicker from 'emoji-picker-react'; +import styled from 'styled-components'; +import { colors } from '@/styles/colors'; +import media from '@/styles/media'; +import { font } from '@/styles/font'; +import { + RollingHeaderImojiContainer, + RollingHeaderImojiIconContainer, + RollingHeaderImojiText, + RollingHeaderImojiIcon, + RollingHeaderImojiEditButtonContainer, + RollingHeaderImojiEditButton, + RollingHeaderImojiEditButtonIcon, + RollingHeaderImojiEditButtonText, + RollingHeaderArrowDown, + PerpendicularLineSecond, + RollingHeaderLinkShareButton, +} from '@/styles/rolling-page-styles'; + +const EmojiPickerContainer = styled.div` + position: relative; + display: inline-block; +`; + +const EmojiPickerWrapper = styled.div` + position: fixed; + transform: translate(-60%, 2%); + z-index: 1000; + background: white; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + border: 1px solid ${colors.gray[300]}; + +`; + +const Overlay = styled.div` + position: fixed; + top: 0; + left: 0; + + background: transparent; + z-index: 999; +`; + +// 이모지 드롭다운 관련 스타일 +const EmojiDropdownContainer = styled.div` + position: relative; + display: inline-block; + +`; + +const EmojiDropdownWrapper = styled.div` + position: fixed; + transform: translate(-80%, 10%); + z-index: 1000; + background: white; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + border: 1px solid ${colors.gray[300]}; + padding: 24px; + width: auto; + max-height: 300px; + overflow-y: auto; +`; + +const EmojiDropdownGrid = styled.div` + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 8px; + ${media.medium` + grid-template-columns: repeat(3, 1fr); + `} + ${media.small` + grid-template-columns: repeat(3, 1fr); + `} +`; + +const EmojiDropdownItem = styled.div` + display: flex; + align-items: center; + justify-content: center; + width: auto; + height: auto; + padding: 8px 12px; + text-align: center; + border-radius: 32px; + background: rgba(153, 153, 153, 1); + gap: 2px; + + ${media.small` + padding: 4px 8px; + `} +`; + +const EmojiDropdownIcon = styled.div` +`; + +const EmojiDropdownCount = styled.span` + ${font.regular16} + color: rgba(255, 255, 255, 1) +`; + +// 이모지 피커 컴포넌트 +function EmojiPickerComponent({ isOpen, onClose, onEmojiSelect, children }) { + const handleEmojiClick = (emojiData) => { + onEmojiSelect(emojiData.emoji); + onClose(); + }; + + return ( + + {children} + {isOpen && ( + <> + + + + + + )} + + ); +} + +// 롤링 페이지 헤더 컴포넌트 +export default function RollingPageHeader({ + ArrowDownIcon, + AddEmojiIcon, + ShareIcon +}) { + const [isEmojiPickerOpen, setIsEmojiPickerOpen] = useState(false); + const [isEmojiDropdownOpen, setIsEmojiDropdownOpen] = useState(false); + const [selectedEmojis, setSelectedEmojis] = useState([ + { emoji: '😘', count: 12 }, + { emoji: '😍', count: 8 }, + { emoji: '👍', count: 15 }, + { emoji: '🎉', count: 5 }, + { emoji: '❤️', count: 20 }, + { emoji: '😂', count: 3 }, + { emoji: '🔥', count: 7 } + ]); + + const handleEmojiSelect = (emoji) => { + const existingEmojiIndex = selectedEmojis.findIndex(item => item.emoji === emoji); + + if (existingEmojiIndex !== -1) { + // 이미 존재하는 이모지면 카운트 증가 + const updatedEmojis = [...selectedEmojis]; + updatedEmojis[existingEmojiIndex].count += 1; + setSelectedEmojis(updatedEmojis); + } else { + // 새로운 이모지면 추가 + setSelectedEmojis([...selectedEmojis, { emoji, count: 1 }]); + } + }; + + const toggleEmojiPicker = () => { + setIsEmojiPickerOpen(!isEmojiPickerOpen); + }; + + const closeEmojiPicker = () => { + setIsEmojiPickerOpen(false); + }; + + const toggleEmojiDropdown = () => { + setIsEmojiDropdownOpen(!isEmojiDropdownOpen); + }; + + const closeEmojiDropdown = () => { + setIsEmojiDropdownOpen(false); + }; + + // 카운트 순으로 정렬하여 상위 3개만 추출 + const sortedEmojis = [...selectedEmojis].sort((a, b) => b.count - a.count); + const topThreeEmojis = sortedEmojis.slice(0, 3); + const hasMoreEmojis = selectedEmojis.length > 3; + + return ( + + {topThreeEmojis.map((emojiData, index) => ( + + {emojiData.emoji} + {emojiData.count} + + ))} + + {hasMoreEmojis && ( + + + {isEmojiDropdownOpen && ( + <> + + + + {sortedEmojis.map((emojiData, index) => ( + + {emojiData.emoji} + {emojiData.count} + + ))} + + + + )} + + )} + + + + + + 추가 + + + + + + + ); +} \ No newline at end of file diff --git a/src/pages/rolling-page.jsx b/src/pages/rolling-page.jsx index 9aacc03..2c4aea1 100644 --- a/src/pages/rolling-page.jsx +++ b/src/pages/rolling-page.jsx @@ -1,3 +1,4 @@ +import React from 'react'; import { RollingHeaderContainer, RollingHeaderUserInfo, @@ -7,21 +8,11 @@ import { RollingHeaderUserPeopleImage, RollingHeaderUserDefaultImage, RollingHeaderUserPeopleState, - RollingHeaderImojiContainer, - RollingHeaderArrowDown, PerpendicularLineFirst, - PerpendicularLineSecond, - RollingHeaderImojiIconContainer, - RollingHeaderImojiText, - RollingHeaderImojiIcon, - RollingHeaderImojiEditButtonContainer, - RollingHeaderImojiEditButton, - RollingHeaderImojiEditButtonIcon, - RollingHeaderImojiEditButtonText, - RollingHeaderLinkShareButton, RollingPageContainer, } from "@/styles/rolling-page-styles"; import HeadNav from "@/components/head-nav"; +import RollingPageHeader from "@/pages/rolling-page-head"; import ArrowDownIcon from "@/assets/icons/arrow-down.svg"; import AddEmojiIcon from "@/assets/icons/add-emoji.svg"; import ShareIcon from "@/assets/icons/share.svg"; @@ -53,33 +44,11 @@ export default function RollingPage() { - - {/* //여기에서 함수를 불러와서 처리해야함 */} - - 😘 - 12 - - - 😘 - 12 - - - 😘 - 12 - - - - - - 추가 - - - - - - - - + diff --git a/src/styles/rolling-page-styles.js b/src/styles/rolling-page-styles.js index eae18ee..34ccb73 100644 --- a/src/styles/rolling-page-styles.js +++ b/src/styles/rolling-page-styles.js @@ -50,8 +50,6 @@ export const RollingHeaderContainer = styled.div` `; - - //유저 정보 컨테이너 TO. Ashley Kim export const RollingHeaderUserInfo = styled.div` display: flex; From 94bf9b92dc444b8db677f04b62c18898b952f997 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A0=95=EC=9B=90?= Date: Sun, 9 Nov 2025 05:55:07 +0900 Subject: [PATCH 18/91] =?UTF-8?q?Feat:=20upstream=20develop=20=EB=B8=8C?= =?UTF-8?q?=EB=9E=9C=EC=B9=98=20merge,=20=EC=B9=B4=EC=B9=B4=EC=98=A4?= =?UTF-8?q?=ED=86=A1=20=EA=B3=B5=EC=9C=A0=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 + src/App.jsx | 29 +++-- src/components/common/share-modal.jsx | 180 ++++++++++++++++++++++++++ src/components/head-nav.jsx | 9 -- src/contexts/toast-context-state.js | 2 + src/contexts/toast-context-state.jsx | 5 + src/pages/rolling-page-head.jsx | 25 +++- src/pages/rolling-page.jsx | 2 - 8 files changed, 229 insertions(+), 25 deletions(-) create mode 100644 src/components/common/share-modal.jsx delete mode 100644 src/components/head-nav.jsx create mode 100644 src/contexts/toast-context-state.jsx diff --git a/.gitignore b/.gitignore index cb0da3d..fd32680 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,5 @@ dist-ssr *.njsproj *.sln *.sw? + +.env \ No newline at end of file diff --git a/src/App.jsx b/src/App.jsx index 8bf0c17..1a6ba7d 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,5 +1,6 @@ import { Routes, Route } from "react-router"; import { GlobalStyle } from "@/styles/global-style"; +import ToastProvider from "@/contexts/toast-context"; import GlobalLayout from "@/components/common/global-layout"; import RollingPage from "@/pages/rolling-page"; import TestPage from "@/pages/test-page"; @@ -10,19 +11,21 @@ function App() { return ( <> - - }> - } /> - {/* } /> - } /> - } /> - } /> - } /> */} - } /> - } /> - } /> - - + + + }> + } /> + {/* } /> + } /> + } /> + } /> + } /> */} + } /> + } /> + } /> + + + ); } diff --git a/src/components/common/share-modal.jsx b/src/components/common/share-modal.jsx new file mode 100644 index 0000000..0e27840 --- /dev/null +++ b/src/components/common/share-modal.jsx @@ -0,0 +1,180 @@ +import React, { useEffect } from 'react'; +import styled from 'styled-components'; +import { colors } from '@/styles/colors'; +import { font } from '@/styles/font'; +import media from '@/styles/media'; +import useToast from '@/hooks/use-toast'; + +const KAKAO_KEY = import.meta.env.VITE_KAKAO_KEY; +console.log(KAKAO_KEY); +const Overlay = styled.div` + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background: rgba(0, 0, 0, 0.5); + z-index: 1000; + display: flex; + justify-content: center; + align-items: center; +`; + +const ModalContainer = styled.div` + background: white; + border-radius: 16px; + padding: 40px; + width: 480px; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15); + + ${media.medium` + width: 400px; + padding: 30px; + `} + + ${media.small` + width: 320px; + padding: 24px; + `} +`; + +const ModalTitle = styled.h2` + ${font.bold24} + color: ${colors.gray[900]}; + margin-bottom: 24px; + text-align: center; +`; + +const ShareButtonGroup = styled.div` + display: flex; + flex-direction: column; + gap: 12px; +`; + +const ShareButton = styled.button` + display: flex; + align-items: center; + justify-content: center; + gap: 12px; + width: 100%; + padding: 16px; + background: ${colors.gray[100]}; + border: 1px solid ${colors.gray[300]}; + border-radius: 8px; + cursor: pointer; + transition: all 0.2s; + ${font.regular16} + color: ${colors.gray[900]}; + + &:hover { + background: ${colors.gray[200]}; + border-color: ${colors.gray[400]}; + } + + &:active { + transform: scale(0.98); + } +`; + +const KakaoButton = styled(ShareButton)` + background: #fee500; + border-color: #fee500; + color: #000000; + + &:hover { + background: #fdd835; + border-color: #fdd835; + } +`; + +const CloseButton = styled.button` + width: 100%; + margin-top: 16px; + padding: 6px; + background: transparent; + border: 1px solid ${colors.gray[300]}; + border-radius: 8px; + cursor: pointer; + ${font.regular16} + color: ${colors.gray[700]}; + transition: all 0.2s; + + &:hover { + background: ${colors.gray[50]}; + } +`; + +export default function ShareModal({ isOpen, onClose, shareUrl }) { + const { showToast } = useToast(); + + // 카카오 SDK 초기화 + useEffect(() => { + if (!window.Kakao) { + const script = document.createElement('script'); + script.src = 'https://t1.kakaocdn.net/kakao_js_sdk/2.7.7/kakao.min.js'; + script.integrity = 'sha384-tJkjbtDbvoxO+diRuDtwRO9JXR7pjWnfjfRn5ePUpl7e7RJCxKCwwnfqUAdXh53p'; + script.crossOrigin = 'anonymous'; + script.async = true; + + script.onload = () => { + if (window.Kakao && !window.Kakao.isInitialized()) { + window.Kakao.init(KAKAO_KEY); + } + }; + + document.head.appendChild(script); + } else if (!window.Kakao.isInitialized()) { + window.Kakao.init(KAKAO_KEY); + } + }, []); + + // URL 복사 기능 + const handleCopyUrl = async () => { + try { + await navigator.clipboard.writeText(shareUrl); + showToast('URL이 복사되었습니다.', 'success'); + onClose(); + } catch (err) { + console.error('URL 복사 실패:', err); + showToast('URL 복사에 실패했습니다.', 'delete'); + } + }; + + // 카카오톡 공유 기능 + const handleKakaoShare = () => { + if (window.Kakao) { + try { + window.Kakao.Share.sendScrap({ + requestUrl: shareUrl, + }); + } catch (err) { + console.error('카카오톡 공유 실패:', err); + showToast(true, '카카오톡 공유에 실패했습니다.'); + } + } else { + showToast(true, '카카오톡 SDK가 로드되지 않았습니다.'); + } + }; + + if (!isOpen) return null; + + return ( + + e.stopPropagation()}> + 공유하기 + + + 🗨️ + 카카오톡으로 공유하기 + + + 🔗 + URL 복사하기 + + + 닫기 + + + ); +} + diff --git a/src/components/head-nav.jsx b/src/components/head-nav.jsx deleted file mode 100644 index 7f22bb5..0000000 --- a/src/components/head-nav.jsx +++ /dev/null @@ -1,9 +0,0 @@ -import { HeadNavContainer } from "@/styles/head-nav-style"; - -export default function HeadNav() { - return ( - -

navigation

-
- ); -} \ No newline at end of file diff --git a/src/contexts/toast-context-state.js b/src/contexts/toast-context-state.js index 2056dd1..46de2f2 100644 --- a/src/contexts/toast-context-state.js +++ b/src/contexts/toast-context-state.js @@ -1,3 +1,5 @@ import { createContext } from "react"; export const ToastContext = createContext(null); + + diff --git a/src/contexts/toast-context-state.jsx b/src/contexts/toast-context-state.jsx new file mode 100644 index 0000000..a5eb1e9 --- /dev/null +++ b/src/contexts/toast-context-state.jsx @@ -0,0 +1,5 @@ +import React, { createContext } from 'react'; + +export const ToastContext = createContext(); + + diff --git a/src/pages/rolling-page-head.jsx b/src/pages/rolling-page-head.jsx index 5245fd6..707c2f8 100644 --- a/src/pages/rolling-page-head.jsx +++ b/src/pages/rolling-page-head.jsx @@ -4,6 +4,7 @@ import styled from 'styled-components'; import { colors } from '@/styles/colors'; import media from '@/styles/media'; import { font } from '@/styles/font'; +import ShareModal from '@/components/common/share-modal'; import { RollingHeaderImojiContainer, RollingHeaderImojiIconContainer, @@ -138,6 +139,7 @@ export default function RollingPageHeader({ }) { const [isEmojiPickerOpen, setIsEmojiPickerOpen] = useState(false); const [isEmojiDropdownOpen, setIsEmojiDropdownOpen] = useState(false); + const [isShareModalOpen, setIsShareModalOpen] = useState(false); const [selectedEmojis, setSelectedEmojis] = useState([ { emoji: '😘', count: 12 }, { emoji: '😍', count: 8 }, @@ -178,11 +180,22 @@ export default function RollingPageHeader({ setIsEmojiDropdownOpen(false); }; + const openShareModal = () => { + setIsShareModalOpen(true); + }; + + const closeShareModal = () => { + setIsShareModalOpen(false); + }; + // 카운트 순으로 정렬하여 상위 3개만 추출 const sortedEmojis = [...selectedEmojis].sort((a, b) => b.count - a.count); const topThreeEmojis = sortedEmojis.slice(0, 3); const hasMoreEmojis = selectedEmojis.length > 3; + // 현재 페이지 URL 가져오기 + const currentUrl = window.location.href; + return ( {topThreeEmojis.map((emojiData, index) => ( @@ -228,8 +241,18 @@ export default function RollingPageHeader({ - + + + ); } \ No newline at end of file diff --git a/src/pages/rolling-page.jsx b/src/pages/rolling-page.jsx index 2c4aea1..749a8c8 100644 --- a/src/pages/rolling-page.jsx +++ b/src/pages/rolling-page.jsx @@ -11,7 +11,6 @@ import { PerpendicularLineFirst, RollingPageContainer, } from "@/styles/rolling-page-styles"; -import HeadNav from "@/components/head-nav"; import RollingPageHeader from "@/pages/rolling-page-head"; import ArrowDownIcon from "@/assets/icons/arrow-down.svg"; import AddEmojiIcon from "@/assets/icons/add-emoji.svg"; @@ -21,7 +20,6 @@ import ShareIcon from "@/assets/icons/share.svg"; export default function RollingPage() { return ( <> - To. Ashley Kim From 37c784135c891252b0bb45e4a117e84877d7ca45 Mon Sep 17 00:00:00 2001 From: summerlane Date: Mon, 10 Nov 2025 11:26:36 +0900 Subject: [PATCH 19/91] =?UTF-8?q?Feat:=20post=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20ui=20=EB=B0=98=EC=9D=91=ED=98=95=20=EC=9E=91?= =?UTF-8?q?=EC=97=85=20=EB=B0=8F=20=ED=86=A0=EA=B8=80=20=EC=98=81=EC=97=AD?= =?UTF-8?q?=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/assets/images/img-car.webp | Bin 0 -> 45762 bytes src/assets/images/img-park.webp | Bin 0 -> 80814 bytes src/pages/post-page.jsx | 71 ++++++++++++++++++++++++++------ 3 files changed, 59 insertions(+), 12 deletions(-) create mode 100644 src/assets/images/img-car.webp create mode 100644 src/assets/images/img-park.webp diff --git a/src/assets/images/img-car.webp b/src/assets/images/img-car.webp new file mode 100644 index 0000000000000000000000000000000000000000..092e6337b72a0df3180da33b174519f32bc67c4f GIT binary patch literal 45762 zcmV(yKAMM6+kP&il$000000000_0RT?{09H^qAV#tP0I+xgodGIP z0Z;(|001SGpS*sI3P9K4{h)Pr%zn82ujJ3}AHr8J_Z94~!hWFs`TwW=cin&Xd8PRe z`hHZL?f%F6=k9;_|NY-t|Kb1Q_GA59{U`lDa9`99@ju&w78k z-}V1$z6yUx{@?#2{0HvO&1e13`JeDT=zoa+egAX)pZmX%Z|a}if7^R$|LpcZ{o?io z{?`&V-9)KPr@NM&t-hbPE zJbu}Fqxlca|BC+k`$_%l{;&GKjxX)I!1X`<|3SZ(ea`%s_)qiy>p#VR*#A-Tzx;3U z|CD~2{&V~9{2%k5=D%!yLjNHCTmA?8Z|wj0fBV1d{+YgS{{Q;__#RNZXZm0J|K`1h zy`=h|`Ty{r=YQFMmjAo{tLby=f9ijg{l53#^`G)z_`k@0;{TcZ1O93K8~oq*@7q7| zfAl`I|9j$R`0w`~z+cnl7E2MuenJiYEST1{5iXEW^p}rZm*#40&XnS^ZCsnq5OfK@nR1(T*tD1cM8Dz;+ zNk)+8v(kiDrG|XSfbKh><(QSMYJ{P}e0LxvZx0=U$&>au#@@U(E= z+pWh-QhudSi`ym{%;e{w1qnMcU46u972AG3|86b~tUvVxT=D&Mg)wgo;m~TkrU_eX zsr~f_?t!NR-qKs{33R^9^TVhW2m=%Vm;GWwf@%+&L44QnnVE;PuWv6J6{wM*vN|%7 zhQAtC8i3qHp)Sy3!FRmax+Ey8WQd|KL;0VdUxJ&L2ES}CT_53k1d1A_~UVGOk;*MEUcVqAg=fp ze~u$@?RWGFZnYT5=C%yzKYXlt|LgGN+!p35+$;0D+olZ0+{*IMb4oT&-jdLcC#g0! zRo^Ip#ed2?O%*!c1n&#cvLh^@(k^fMF|w>^Tx!%7azq_~Uijvtno>^hrc{(ca&r49 zoAI0f=Lp4Or`u#wYJD@WG3~5R0|uJFIBHSUh_9INQP=YGbGGD8Gjpe&J0{)h7hD-% z>3N4cc+VQHT@jlGli};qfG!XJQAPlp1CNS@9**bcCBt-FC14r zeg$bpP)Idf;5#k&(vK$B-CLWSgB8PxO9y23oTjOIZr9uSrKcNhcgIj{GtT_MU=XwP z+s_z!6gQ%tB*v${r6g9d_8!WT)^#jCxNv?lCd34Z8C*z&6MBA&CPIoAAL?)i7pM7$ zR)(wv{QK?GV-z&_0B+}txKf*=K4=Ti3)9pAiJ&Y_%QOfb@v6qmK)^O#3u9Lw+2-O5@=?+MP-liD9;uSP`_)ybAFDbQZqHi!I*Q)YT%1pjV= zOuZZUpAm*6g6jthIq5+vb!?E&@J=h6m0Tzy)|=P4K1pg<{SQ^jIZq}^(LN~T(DZvj z_SS7+^wVYAt(U>D9|zSfa98JR>hcayc5w>Tkn6S-RhJ!^&|pl?H+tIj|O#N zpgAwBCx5jaN^h65+hBu|6A)-fF zcHKQ0$6F^Hy}p8O{3y;rf`CHvcs8wEaig8Ha-q!m?a*PSZqi?3XFhXqNY(w}U})1#oO{Sb z;E12k$^QX_i5vY#moZc5_zpjbrLuTMDpJhtP<=C`YN#^l8Uw9{6Bt`@oXQ~Vla{t?2$I3!w>eM74s8`#@y zjU4>GheliNk;HREky?po#sGt3^(7(_cXeqLJ=uUd0_FrhI(+tv)V3f9$*&SXVmn*^3 zn=k@QgPU7$^6!&s%XXeLrBkROe-`}wU##A6-_$k$+M;7*y&xH#p#c4j_8}?9h{9xR z&yMFU=yAHhOzoj!y%+QS4j6^Fm+k3=c#^pdEZ^2lU->fva#h;P#=K=k@JF(iwVM=X z+W-XojbZx(atui&ia_(!&-JgpTl-0P?~iGSPKP4wxRWicj#~f5zCB`W?-$doeACAN zSwD*}bo5hJkyew!D2qM58%L~fXJkxivlatdu99G(ce!E?f(Qt**_4k}({F|myK^!e z_juVO1V8OPxVF!oIwsn91wg=g)qG}1CvUl$I|uaaLn%LA|K*o9ee2V2vy-AKsT0Nz zUutO1J7fR8th(-gW0lcO{05pK7|**Al5?^KAjpjwPYI$a-SDxxfKP-`dnBUy*Iqf5 z-!rME*TK0Z<#3b>91({IRki$(nGj=1y^{!2u(QX0>E~#c8D?t@wE&Y2HVI1kxuWVH zClQ^CdXt{%Xj*ymjby{vI4wfhJR@LmVJDfb_cyHhmaV;|rS3RQx%vA)`qVmlJA8wM zkn1@f{N(}+2R)aa5^k=R{y44Pt`rLxX5n@yJ|)?DC$;VWR-8L_;&rhCc|-EN+gtS; zAj!lgM?ZD)zY>&~O?EFhYG)6>Xa)no>%H7X)5DT=+bJ~=@Xp3GwABQNq7f@Des@Sq zo8TpY59TW0CrS?#j zc(4Gw4qDSAn>c;?EN@3&sbCjITQ%qVnR+Z)Clfd6>e+W({|QDmQ2qQ!Z+kLT)4aK+ z3{-LV4W2?L6_0+s?59Xl?QL4+QFrD8w|LQ+ZvHrTVIeQRiHn&9JYb0xJ&F>!Y&(eC z_|oFh{l3k*Lf}eToDreSb1h3$bWlXU%v*83#DTk@t4zT$RqY;V?FM#DQWPdf5U?Me z+|4AS8QXuhYjIPdGwwW@An>Ga$H0)%d91+6<>e(0M^3+)Y#}_!66JF4xCcq-*TJ_A zY`Aq_M(+`x`9y`)L>se#WEc9^6^LViS5Uo6 z@?!@dccK!!b3g~iIt$wuGt~q}*emaCt`x1t9?>Qq%f>er72v7-tad}kp40~^K~zUg zQ48|Det!6*@eZ$}3x7#@jJM?TuW(%-AO(uQExOoZ)ykH=S=Xr4o^=e2AKtYGiYNa4 zNbBDh^fJ_YuWPKlUGdC2JqNYZRg%wkKedtTMg6*hl%{MVS0_}0>jbZRNJptJIZ{%Q zwE#=C*ms+P>#GD^$MC1!>j(q6VK*m||d^SW@8J!t9o^5YEmy0QSg}!D4YKj0Q-gqpy z1jsll=HQEZy%{%7iaUh(f3mWY=s<3)1=FN);Xd+o&sZd{`lz>p-u&ywCcpn^SL~91 z^CpPB?(XIDRZO7^7bGzh-aNS!W-i5{KwvD;N}6OJ36WHEp)@s+9^aViC0K+ycuiKq zJOh9cN!X}u8wHi~)s#Xg>KMkIlZ6{0BBhuB7=j5-!AhF63X@}f<}YJ$0VaX1iU=SB zA`lJm3LtY%fdK}TBa2#kF!t$;Khw&vjcu`oD;LsK5>7*55H{G-yZSc%|! zL|MRD-Ck0)l4nDOI4X!l9|Wr0ka0&;6lD+v(te0WgQ{FdgH`+s8UdE^WK5>ma5V;TipgCv?LnsloD}=skXl$Ru0OfB^+`({FE=RV);yoy{cRGbb(9(bODF4xg zRzo;`>U!7_;JzzO>BnDypxs97B82181>=adXu^dmnuP*sFFR;}oWw!)`6D^IWLRd3 zxxHQsP-c1fX4(<^nJpSpLq}`S(XXaO%L?x7eo)%Bj?-EB*GtC>O__~d)q+CuIM zy2BZX%fz1a{3Dg{K1hP$CCtv+d06Vfa7VuQaGg2e3!5f9`WjT2xjw9`i?Dlk44n-_lKG8*s)<+Z#Wk9Z1r@|)PE37)sJUmt;aCX%nu{{PtyjM1V{(%+*5AfM$b9) zW(_xc;$B&>r5zUw_Ie|OdMga)3m9{cGTA34%%7isrSK~mDy<@u^;;cu??S^18=F{m z!P|Ufo&F-rIP(saHYuws7qcDXLoB+JP(`z~4_DilnU0dfUSX-$LN)YNAw-3dU>kVp zFjER=@xKUl4}{iYQdt>9h|NYx(+Sgy!jp9ig6v&<%PQ_KPA(c4cS$f&UOy8bN8S0} za1}3{hwY?UpL9g>tYKKZgHP1fJ^hZ0mEVM`sF-b3>xfE$w&K z6ow}?lhEkBezV&h$sJ^2kS?c7MZi6z)_$wYo*m#%mo>wT`NP;9<=$(yfLL~Zs_v7g{rXMuM>%=@KqCi6y5UkP`*(|Vg-%w6i5Pg_C0VXEPfKq?U7x~MPK}e z(rtZ5Vq5K)TVN!kVntT_d@Zt9hJU{PNc?d*_yjd^ID_L4bYstIv60Vtm)2d=i6BK3jJUHterd9c|Bnj^C z9UWl*k**MpGp?$+4h_J+eIjTQp+M93_tWXO{qPuw`b1dKeY&xE1hwd!Ln+ZBQRj4y zMIie?oJDU^G@}r>+e^ z1AsmH?kFsPXRyscOBj?mIx6DAcEa2?n_KTAo5s0w?~ z7Nr}MHJ`TvY0&kvsuuC(>bQLuq<|^Psy*bXQ8QvXhJfmT6l{gaBQIz&6wH`59*u{& zujL(Pksve&-)ZXM++hbhcV8>fbSm9L_vll=Rq0H4y~<>g#oEO}+&w+&-C)vt+iw-6pYd&Qke&rH@#fpv8fGPC(}gCa#aJ&v7#@ zBE+yc0ySHP`awZQ5zVE76B!4Q+0zFVKhX&neJoh0{|4C|t+-!$T8(JLO$Rndh`%gz z1j>m`yp7!JnR8Hye!JfEekl(#k{=2@ z2u_`e!h`pq}|F9uH) zLT+wzL3YY@?onsHbGgCzbBr_z>CKcDGS>y1P4u7udVMrG)7-tznRk=a_^S6w@MdW# z#XP!kaCkN+*^na2yc;Jp4~B-s6OwD_zDH+05S)HP38?94SWsaXI(|QAteT3y*X2)8 zBsgp_BQ~v1tHjTWeMxP~35n{*NzwD@^ubcihtj`-0qCv1e!<|~tSH0&B$U7F_MyrQ z5WNbfgSRdPO`0XG5nUIKT}9lW2|&sz@7a#LapMa7068kXFZD)V2nCj@a5z~OMs@{$ zp5#8ESGL0Yh$UGkKM;+cEezekN*Zk z{oco%ueW|vo{-RF0tcxUl=b=8vjXgm-ODhz=#fCOAo5BdO!aI+DgbIXSq_O4JP*to z{J>#a-;Zrx`SgN9q=CI|w=B(iY@=Toh9@Jq{i$(O%LGa6(UrTlY&?EeA`vt?S%P}{Y0;8T(*k4w_X0n{+;Z5j<~C^2G?!wT5sITjdgde6?*2_ln8wBzgZuy0Krvw zON`5sE`}h7W~y)bUqsvh%|y^b`t!}rswGklBzrYlE!s!rUx@WDHrMBWv%=%gK8hD& zf#5n1o*mkV2Tf-Yz(rnhAaie^c6Vx8non^iOOiB9K&>ySa&wEMwL=;1N1f43gUeTi z5Tt3=@UVzp!a~_lc>Q&F{ZS}2nBzz75ivVY~$`S3zo&J9A6>IpH(6cO!y{sMAII%!_z#nV+)5k0DcrXwX9}LsAcDr=Ib4h)a<(_?N!)0ZgwF87HQm zU#5zQ+-xltAgE*btH%8l^(Sz5gDN-JLkNX&zsc_mOe4{u{!%990S+aL;HIC|N5laJ zujsR~x;%R@Pqa|N9i=Pza8*l|QtmoD5UVB*h*=eH9!L5H`3-ePpz8M>G!p_3H_9z0 ze<3OZxDS5jQdA|qePib>u~NOp-2hzG=Q;KbPWjEVKuY6PX}|?ZQi#Ki$-*M0El!5N z;;zIQxPnRETpEin6dx;rkcE;LvvsSu8p(kB|FpF45CQOfI# zwv|ag7SLS24q%PL3ClNhm0ug(eQ(H4T)$&_sZl>YG#5F=w|3f*tTj540MH3y0$e#6 z;JILCLpvzqk1A-9QO~Q^k=4fIX}754R@SAZ+SRf#l$b+Ybe?9G!_!hjyo?uNt_^Ue zk?=T42$fxdhGf6l90Q2Wx~;ff#Q4P=s-el2R-i~slE$F0$?lq-Zmc%i53ln6hV8L$ zp;@_dovxuVufuu4K8aw>n)D-#H^U868}}c3Jqua@mgpNVv!_fU*a@aeGQnAAGVtvG>c2-v`wU|8LbJ_ zm%E1BlOljd68+H1yoYvFrE189xKHDZ1bi81w0ke$M+iA8iT47w_B=xTlU^vkOCc(;>l!92)_(M6gfm&?|Z+}|;*>d%Dz$j9& zrM)pk@PlL_mug9Z0Hn3&l0t0VxHxL+x>3zfHlU&1?In0leJZXm^jE zzec=}-YTtj^h&QMxQ+6WnW;4DSQ-4TJt zr)7nH{JTT$d>P&MNgl4G7iUIl&2}}AD-N|rP%emyJm$WEE54r zNWDeen<4Z%fWPn#Pj+O8Pg+bS8zp5(q>2y>VdmM_goQ4prTBJ1wZ}3Hn>)kg5kD^6J7T; z8AU8cR@xXB8z@0`6ybezz!WV3l}$K-9iOU#m(4Ekd`Y9<9^BgMN4U3*S%PlFWY$QW zB)^jm_%$S!_t4{H%FqPRF3QG%M;d|TCnoDZCYB1rApK5l%_M=a!S}#P8W0P z{d^!u7@r}yXQ40*jr_v8j%aKkPwQ@cGuiGHJz*nvU-*b+keH=+(kQcpO|s-PjRLIx zB#gF`3(qIVd$h6~>Z z&6U$i@ZgIj#PEI-+}_$c@FbCcNtA%M zbDB`IZ2YhyLsiZoQrYDK+S*!e4dHb zC#&9-sth<;lS)y5j@E{2Fs=l^2>ZSOV{$?rxS&c*_u%a-0zDLiy?4 zLH==*SklD00@AhUBZAhWQY=z0l%bgJ^MA=oG`!4;I&&Mvu>GQ1 z${|iGbw-uC!N&B%CT6D&?K$nfbwHSR{45mSCq*M9*gw+-C)hja5ayaFYK1x)e@}J} zUq;WIT7I zLJwz*>KRuz*ZeMG{7s$8x-$C}wm#hf!JxDH*ro7^S<;mN+4v;AHyeknm!an>+)bE0 zVM$BYlmZd|tIDH2>xK1%tN~~WB3mV0W}2W|Ie>@@zS7V{O%AT5g9Xh8K+YuC@kr9B zdDg&q${rKOqjpmYb&*bU9FiX6HI%`-zTW`XPu4=!{_gWlD4$ zUt;zYz)L6*8RK4Lm?}A7#iTBm%4qnJH#%Z=c>}sqEn#}NJ?df~#g1(PMx)$c8bBxI zaMy!JOP2aUkz{jp(&%b#q?0pT$p+G0H>Mt@*WKRirmRYhxig$RJy90bGR9 zVST-Vo*)*t%Re46wT2O8f|~8r#IF+i7=Rrjtw|BZ4SDL7!8xa7$K#|7Yla=~1B&u- zly<7%{^g?-DxlGsLJ{GD^MZxiQdwC!yfFFS%vxl?tP)e*zQh*s3yE4Va!w5xmbg-M z$_3CiY8oQS1NV)zi;V6r-+EB&0r8O^sGAwbEIzwj<@Cbr9d=X}Dt_AO{Bh~(j8@iq z=$6#!>`%aD>@Yy|R`d=ll`jC!dt#F2kjXYJ5%zE@)yxyl z^Z)F2eF+;i{`dna_->4vicl=Iq3Zk592YO1aOLY#&=-uL5JrPYtrz0%K%kGKvDu)d z94VhZp3DT!tjtx=NOJ~wMFmt?*DD#R_9LC&2N&c+RMtEA8FLAULDb*F>&HEjap>rf zw(NccFNOkUAyEB0y1rrCr9g$9hlYaM|0`&d2wQoQN866Jbp$GZTyMYr zLX6oK?0ah2Ivt`@I!>|uyFHOus0$BIVMF19V>)2xmvEBw92NU3kv#YT6fT=oHUuIq z6^6(PuqEeNU)!vwVtYvUuH@!D3q3w!;WAU#|L-qzTXp5<=1_9wd_e!y&uuwrD9bf! z8~Ek<2v0KzMDh4VGYH61ar!161WqxaGU3B<_ed8^j94t=p5Q+;H5j|NwFSY$INa&l zPC)a{)=OPMH5676W;YBm2S!PkgQ3(ob23-H-QiZ03nG*|kT2K_DS#tJOi^tUa7xc# z!M2Ll{TBN&qNQ_?9R{^uGsh|7B?+-p#8bFp1To^FeE-<6IO3YEFV?ac!JY>`2T@9* z;KF(`ki}XulV~Rhxe}_RC*PV)-@Hl@R&@GA&p(Y=+G>iEui_sXfCWj_b)xTVU3&Cb zzkC-ASRd)j452CIbQ|er>uB+kvxGq96si#Y5e-Pq=G=T|Q_f}PSpIPSv7WYr>6M-W zGAW_ z)bT0$A23&k^Ns=?Tagq1N^;qGM!<=WTolAj1F#zxL?b}q+FLRfOUJ09mVE9qb)zJ} zP(lLz+Fjb`nc@r~ypz_tZM%_-b`@4B@{})fON+4Ru9nWU>D=CxgXXBpnR&LtK7Fab z^GGT%;d^cZtkVKElLq1(Xqi#{28Beah8iulfz07sxLu0Zw?0r!k0+HiU!;6txKJS@ zGZ#Y)G|PiCH*(8c@65Pk9{ANu--9l5j2JL0WfKp!iP*s8l9UEeD9*D^Y3|c^c;Pf> zwccrT3YyH7l0=oK_sC7wxWEHjvKdGLv`KQ zN;6EXuqSUZfjbM3=s*8Pwrwz98RZX|_9m)u;-ZJ}gfSxL`C3OY7&CP_>Mb<%sR3hX zI0hOc*|i^d@E71)7;wU+>I5q^r9{92=n$v!Ax*Y!dcQa6smOpBX{iTR453O%`*R6KQP^@(Xs+GSa5N^*2BXXya(e zOf1gCGhstMG+ybEI&MiBg~iAyBH_z~aS=uy{tmxjC)NpoC*wST#idBZ>J^iKgI&G# zLxE!zm;p~vIzBq|n$KmvV6qqDU)y~a}TI4>I7oPSym`;N8n2IQbgx_ul!=vcMPiL%+_G8RSXFD-nFj0TgN7>sDi7{ z_J29-0Q|eU!e;KeB;(mdP$Ms?)-d7hfRDBhbp1`5{@)FGB%zp`#@bSwWH9-x%|nhB z$RK)fcbc|SQ%eweWSUk`W&1MCgpuF?*& zO{(^>H&-#K&yF*t3XRyhg<+u0Cjg9pes$xl5p_waW$K)Cyn;eRs{m}ktZ<%mD=|h% zHv_)B=bLn&c>#!13NKpW&bV!_wkI+ zB+_4opTj}a_+y5H?bk%N_XtpgjL;jtqwE|Xe=w_+k?%hIYLG>| zI_E5rbXCx_Vs20NO3Y&?Gm`-Zo~(;)H<_nTTP~?lA7}fbP$gw#s*{pzK&k%b9FX_m z&;c*nOn>vuT0iPVRfgdCbZQXwbgKT&cHx6QJz-}>bHWdH&Kt}!NRpC zQo+|k93@beo}1`Iv|JHkPgavzt}=ceiAF+d6l6@1Zi#dR3#%g^$a#vTe(C8-ns@52 ztQn>s-g-5z9x=0bMo6zg)gqSg8l;biM9l==KEKFQwb_C~KN$^S_GM@t7F>qu0#a#f zx=GUOIjL{3Jv2AWAkEb zwRyTm$!9&#y%Q>qQ8ojhz_ZT7eoTMODB`aI<3gZV{1h?_o{58%Z1T2VVvv!|QHTbw z7Nq@t_!$bO-EB$^X@79Ps``|i(%j+|skcVg=lpEGSMpE|72oGN_=5EL6=N_Cy&5sF zJZWm<86@^V;v(i3uHn-2-d^bBNCbKUy@4Cf;F1mcKiu26ieYWjBJjF?NM0utPzjs> zS&uhY%oeeOj{P1cY3~P8^y9&rhBT475;a|u(d#fvfak?&)ibt@v`|jI-ed6N8PY{WfZ=7nv}qA_ z58gZKoUkz5<{~WaFBcirQ_6rmf_GPt~F z`9V@v1qG==95=+`TcQaQa2@2|wmy1iQc04OQXS#v^vP7LL%%J0?6&vP2VDjCVe!c} z0FzP{V7avTV|$LtZx>$8-<$u6ohYQR6Q1#eLs(^G0bdpSq3HHC zNl-FKkc7y1$@fpNp~)eCGa-5rb-Z4Hk|9ZG0rt-*in<9=HiTL81@DQne`*I(R?2 z?W^71RtU*vSARksJ`sXfL+OO zDKx4yhSaiB5s(855-?B0)x2}?XdKQ?n3r!Bfo8|1(UsB4Ql zfQVi#Q$@Tz)y7m0KX}>tusf@`pwP;kf9lQ?KeqmqDwt<3ul`!4#@lbHMtMuQdtoDD z5V&8V)Gh}DT0;LRwhg^`KEk!y5c6Bzvy*`v8eMi7_Oyrolt6F-Zk&5>Kd_{Q?S6;3?R1fT4C)^nzyg(FU|ERFROhzz~ffdCox4ues)&&3lR*q5oW7acmO2-(3k}{BRGfXNo(IN)q)_e z-SBWxnu^04AR>cqO!n5~W-Ez^dN8pMpqs@;*pGRMRwOGefS3w|$Cl2aLM6G#bYKcQ zqQnr)rzJiO<~YxZlfy;>=P@b06{6;s=&@)m=MDvV>r`GrF09c){3@BT*+`}goU`?8 zS5_FxnPQ9)%9L5+@H|agAU1-=pL{VF)CAui$m-&)aP1wF>`cJ8EG*9yLP*@3<_8@} zgN78@$=AU`AWddBLTALp6o>GQT=)19?C)<;1 zp!-|56kXjAV4$0v6AQwI9}UW7)?}Ni0T51umm^fveJl#1kK^H+Jco`y z!*Heo3&Fdg6{1#4l@;78_uFf8*_%v=bD_xJO3@JU|C$Pox`l zn4`%WtAD~tH1!Hpznx3P+UAi(uByX#ptOZyDny@=iLerk!FY;j21z~nok!x ztQc2(1ZyeX*Vv?%Y(n^En2MjQLw2_Xb3G^Y+S%p@Z>os&je)lf4P(4h#5RuF?aPX+ zI8!CZEKdkd zTxd4xf2n8EwNslDE#Ty*Ho0VfBn z2iUss2p3e;hE5rsEu1gYj}Gi>4?UuOL=*o&cs&LVHeQmt&skTjP}<7cMZG=Uso^2wPn2^1xty1AuoVb3)m?vQe0(* z3<`eR1{R0a62}~0Iuq4PS!=-`55kRIaafnM96j4*t~0JQ(~L$|pg}r~aFC$t46G3c zG4Eav4WiU0$JJ>j$_YCK=rY?2248(BZ3QxinXu}&Heij$;BkC>zL>JnXmE;YC^I#f zr&lXi`GQG@Z_;InyIvP#|Dw4I>3ohHyFXc`P^p)H@|RJBhpCIP`BVBDD8^ErFBNBR zah}hjKJzM3zU^_H{G$K?Syfx*&kBBT3#L*{?~fZYX7B$gU6xGgN+Dho_(lVE32MOA z#bB4AYoeI!K@${0sKTzLYrE-y>#ryp0XsE>KrDb#^EKe>_|pIZh{TNJBJ5F1Sa!Na zS4H+nST5b4Dd?*aCo_ZfM^0~^A}G*rIgI(Q&u%Ch)xZP&w<;}2%}dRjA7+njMBi%*z4vdGZ?5mIRwP6wfvarKRmuq(8K?oQr4Vo64v5dI{D-2Mfm zRCNaxXnx;zz5*glRj53Q)^Zhjbu_Tn($+hU)(*c{j5rf^O#-MjU2l6l*)U$!UQfs- zlIb?J##77pMt31D-eHc+lJ@k(qvR21#xl9h4)mq*bnF+aG3{-7GU9v|M|QBcu`xC# zh{JXS1i7!<_@_~*6ZzM^RW(Ku!j(=nt;vSS8UA8$z2rb)1)$UHXp}}?a{9RqKW?CH z{d&?99U+!doB_D-ww8(j{TjU}1C3o^feJK<6&xh4ce)2!$Zq%kO} zhi(uCAN5fQ0I8FpP-VTkHHyr&-Q1AwdmeCXDyh)(0hjlTS97ORtO&=Zw`kI>$N*$m z_suVfl)LhvVnb}`qITo2uG6`vtKNT{^k&pJ96^vFecFt@>Y1hdoD)?k0=)!BJ`4#p z&Xc~qFbMx@1wYt6oxf3b^tj(8=3gtZSpy8S*y(JzSkGB8Yi_A@SpB_(|!>vo9xG(J(g%6En zM)={9y(Z4VA}0kCKkSb`LoL(UIh0IGfMJxwt%rlSsQC&= z8Wb`joQR3-VBj*2H7;7qja-AfvpwUHOi6M1pn#Pe$ zJOjlH zz_EuBkb~_la2>3U*!8<0_rf|=y{Cz30pH8=*TKylJY*Ln9;bNzEh>E`u+4N`l_WAe zq89%WA1AN0e)#<$0G|TLFA_uzG%<7estOc1L40X6pu#+M9~9_2KQfcKOr&4rV2v~~ z)#-NOoKVkkJM8COHe|GqB59i-6H#kc*o^a_2+ z|7%-HY{}usS3`X{Luu?kCBrnAd9|3@w}RaIP?Y@p)`>Nfw}Nx}Onm}HAX^+bBla7- z$ZyFW%^B*O@){oys+Q%SH)`P=9j;P?QCo@=Ig>$?q>afWCY8^+ah7en+JV2@1^74r zGratL?B+p?GyK~z)~UXoV;j$#`}KFEj8~)t5Pg9nQt`{+_H{Kp9CzeFNPl6eD3Bo6O=Za8EZ$fr49kb$7>+>#dJOc1~&*(h=utt-GW{q-adFH zcDSR!jT3(#bK#J6iPkrEfR}v*vlWze(HAYa@Sbxw$xGbEsmBu!x6`W~tvylH&h}ly z{sGy>gsJ883$2nOelVYEgUJqEEBRG zI~xmN--ki}hevhBsxeFyr!`>3H(|)MMkorG!$QBaA)aon<;0;EOdZR$0Cs!ynxrbU=m83D^a3}F6J77U#o-6KK zG+%&ZI|?cWAe6{^3(nICbEclzMVsd#FQlI753LRv5j8+1#Eem>TbF$Lxl|SJxc_FY z*^+&u_w~ZyF*eT;O;D5k>0n8d@J!`o+}24{x~v5DZUWI~qlG|2l~LEjrc+MKh-=jO zWOjKincrZ2{b^T_BIxCcG$zGBPfopbf%N9#fkmYe#&Ey?K`OfAgtBg024 z2}|yv=}bTpvk2c*)Te7uNsHahvajCZFHFm9&PSBf3gyfKi$SW`7+l3As&|^mO#s^! z@}V@GgP_~`ot)BO(oEN9+!e+&B%&e#ap6^PP72gIj zXx_LtPd=CX30hGL`gWBkRGPNc<4c>CVjN+DA7p#}Fm?HUJD=Px#H@aGfLK%^iaR!l zh*P&5eh7CrKVQ65KD%VBmqXw5N5ccQ`zl(~YoWKaf&+VqnC;284y}yy6cQj*&sM2&Y{wHPNNJ|OZ=|#IP zYr>v)`DEj5^9KG+iEk+SG~>vT9g!WY(^~5^TO?7}CxxA@TJ4GFhmwb0K;_dclhf*^D7)0EypZjWJg;pjnmJrD! zVti)_+^`O~AFwjw`t{c+SOX-W^6`WzFpQ4xKNfR|j#DU68)3)KnR_uz!o7G+r;3c| zejV`TkFf-5yeJEGZ4reXRZqn+Q44jv>j{o3bD?4;(~_nu+T!PV-U~>5AM1Af*7LwM zX<3}0zm}zA{4_yKf0s0B;^#SBsy|ybXN{%s8GD8vsv;mG9sbXzs`A1mK@N03>mZg4 z0s!#=e5OMK!lrA4mbVwj>+6?<5mM!bt<(DXO8M-oo{(a>$AuQ=_iI=lPehv&P2dTl zb7lv35P-ploe7dGL5Jt&;i~(5tzcoU8gj0TQ8uQa2dW~R_gSGzMiK2!vradT+Fd3Q zKr4sH++4hk9z(iM5}5&OJrKUJwOf=;gc916?3tV(0W6rtOWvzjVIdrU?jU-TY&vvQ zk+%P!KsxaCT?`Wl{?(2JWR37zck7#x9B94HEWiaWQ0;Z|549#V^yq_8Oe2QOh?N>X zs}Fmic`3DgmF;dj2rU<*Sk}NCe6xp_uP^1W%{tSWWTaJLhk?AStE4T4TlLlmVkWPs z;?T|00vRBD8-bGHPrSiN2>~RH8l0!Bh2x~qwqFRQ4QA1HPm_tb)SMerD+moX%oCkJ zd8i$N75XcGTw+z_S9L86R@ZMXECzTALYUJSLc3gMMP-H|bM^pT_QoE?V-?!>Okp*> zEpDYs-X!h}s zPGzs8qW>o1$gZ0QV{FOr7fAM(#?atpIv1-h_7?OP#Kq}bAGW6>!Vxp;-G79U@1Hxnk_j%RC4!m z?b^lxr1l|xHCgu6Hz&EfZih(Ku*tT>ufq^Gtb*nRh2g`nFBQO9|2#=*`A!dl=NBLZ z-~Fu%Z_S0g_o=5Zac6)9hCJKhnOUFnxe|oD;IYoxl@w8e5{}X$7zyq$SPO1X`VZj=E<3cjaDi6Z6{r@)~sSD?M!2EHxy_ zA%}F2R!+#8N`+3_zA9r@nScmeZUkfJs#&-E$4vW1A;3=p1v#Zj&mXj6wILqi|i8{8WU8+lxBX2 zTMgWo@ya>KpIK`L!%F4dT@BFM=vEk>OYcU{iLMOAQWTd*LCawlQxN%j*P~Uy5`$>L zwE=DsiHrQR>M~yXy$OX^^j3TYrp;bAJPR6UcxRw|kBReDdGafB6WHot;ny|6gGe&f zb6H47HrSC@lGPaYC8lmk4#=}Sw)gVRNP-DX-3X6F)A1kcs6H0!-5pjABa+OF^7%0X zK>sB70>VFP&KX%{nzX0EemOyXIa0GzR#rAm&kKKW;q@K>F+k40(5C$lB7fT(s1#^u zF_%>?*lR5{crKLM&$!7uLU>7ex|LvcbsuYjEexG7OT^!w=?aakk}KV^;ROMf{J*Hl z<0QT0UF9BZyx7X#d}CgVKPVPycX8(uzE;idk$11m2KftK9srYfN5JE*G#eqY_`8b8 zrB=YW$-h@VUlmjLcReyErK@dbImK`%Qwc5RXfSl@c>4?LgKC)3!h^ruvuN^7)|dV* zk<2B${J;{SaLN%ckGrQF3+c9<%c&E;`(FfJAvUy`SkNj!7G;gKJ_=vIM5=8wFt5l@ z5=&bXJ#$5WSYYRPc=WnGSE>ihpPuh>nh^-l|O zlprxOcFkLSarSGB*J3(mD;rD!vWIPR9%MHI~32;O`dux71JGHpW z+*9Y@-8j)G(FI3nf;xOY39JlAUj!_#p9d$av(>_UyBtpXvd-MP=yJyrxN=`)EjHH@ zaltG+ejZuX$N~R~+D( zA~4r~lNuj6R&XP5#y#tICc_D$9Siffik^H*m6f7-%wv?~>c7IsGBPQ;7>c-gf16Rg zp_>$AiyEh^UNxx|;Je90w5*}1708gJ7d|5(b0Pj)nS3`x(7v0B^lrV}njyjll=piP zqMXzgCX1ic{My#XeasTz6abB}i4+85FvYMQu{)?zueCg8QYkeUV#7SbvC9ts{L|39 zkdX`|=j>Pfq#FwNMlX{@qxic~P*$|dax2zK>G4&F`J@@7*+&co62!dC`m@g+|8W$$ zauo}Z*oVa!iY>o@jRtV*5?qZg8?*t9mtlUaDOY9ocXc1E@&7H;Yt=eZKB)hDP7tZ= z$|p}D=L3Mxlhk<%%YGuZA`VbIr{_g0a3qX*r(v3Tf3Nc8U6mV}d(M~Ai9_bsG3~m{ zl9jWG2n-spc4gkgrY}t8UwH%ibC0?I)mMxekABu%ynF#7WOMHO)P3apSUlv@kn$h_ZJdUo0lo_l0CW z0d;dDU~Wv*OY15*_W>z*SuDojYNj>&(hpnr;O#xHo%y2a`rqNMzF_aZ7vcr5$*2heGO4`vIAWY zmMpv;;I}n_^_zeBu3^-2#9000nmwB-s;#l`A`2B=1uqCY_)~BToZr^yFrHt>;)(Hg-_LTd33aiYCm3MAf=8(;qu}Tif*U-1aG{dD)cNkI)22&HHL0Y_x%tvIK zZY^!qgh#p1E71_Sa_;?8dZ`5f?$G}fjPCvsN5Nu0OIkvAtbds1t1;J73_)}84Ly}d z&Z)mYu|wO(IQw?ECn#I8&N5-Yu@|7nXeo;mtt<-&T`GJOhCDpvQKc0{1s)hlL>d zaF0M4kqk+*saf&uu^YcaYRVE@zz0cM8qI_%3x)V1^?7MAYBGk^K;y;e4I)PeE)BYV zKPK~Oaq#o_CE57-o-LuE0>jCACZG*y{H)698Zja2ty@AhYUR;RUdkaFaP|3+Y8(vt zFnin|*0**NDq9n>b`odrbKWa0k3zWBm5j169JaWyEfotVjHAY^nmSCFZWk3#Lhqfc z4_bL_;d_t)-a;rfKw=W1X}gysl9AHZ9IOIK?Xnn^Z@3rQRW!QZ)2T&Ot7J5anVmLS zrWecN+gXj32xy1es-3&~@6!>At^l*`=E^8J+fhJ;I1%3h)qZ?`J!iV`hP~9;<3$64 zjy)H^q5j1c>g+4!8)arX8kpl;A%`}klL|ndG4)|5AoWp@*((w{a~<8$;Hm*}`D`#4 zt`bQbB~$HR4_DS_rP|_~)q6jEKQRisV^u#d(nPC+%6G9@h6_=&qWJ~@Wxdej(D zSR;`o+rcLUT|lKFUzOk|XV90U$e7n~9*E8GEflLX3!-v{pbx!(^A#GC3^+Q^O@5YQ zbd8ZCaiX*b45c>uYWyXqBIBlX$H>Pxs z6lFhY`Z1GVe=*t=FE*$DLO7YaRU|{+0F|=M6z7TIqK-d}d(-T9iQYZ|LbhC3Dj%D0 zS?p(N;=-x7KoYrl`XcuzM6eS`Wn__0Yw{YrHjnAQi%j)uOoAqOSh@eS@|ESDHpl2! zSbY7-8g86C+_RP!eX000MD?+Lg=Q#_ew1V$+lzB7c%(9^3y9{Kq0zV(y9AP;xE%kiZBXpZSGa+fnhhzc?tIJ?0=ywx$;K z*5Et`hbKa3B`X_Mhz%)>+SJY%{3^_X^iHJQPd9Rrb_iEl$Zpo>{2&1u(qVV$cLJlb zK$5*gJA^yL->x@-#Uxte5878e&8&Giut+APej8-WX#`A+TE=SbuyrE4Yl}=+@#v}R zL4L@3TQ(tC1;#zbaqJL3S;##`QhW?~Fk`&hQmT-<1&}C<+BLKB(~LjYke*ig3A#j{ z7@gq#3ouf&DcOR%##fSALx8)9r8A=H$Q?J29O_a4V?b#dJ?98X$5hK#-f8quf&LW9 ze%wnde^ke94g?~2MbTd+2Rgd6fNF}=8=OjyFIK30uNHjxj_vL|kl@mA$%HQUg8pL6 ztckI=y-k>59*sS1KdC^pybw^1#emKi&AknKo7=eGJ%|c zaO@KpsM~7m4R{V)7M=)_Jf`||ifVrLGD)u?^dgAmZKY6wUIK$eSJjVqkUn>k!hp!~ zdWnevKLDRVtiOOWq{LuH)b_a-3YHuP|K1I^`soopOa4qF!81gvIP<6zkt&M36w8Qk zR&90xeX5_TWI9+>S7927M7P*tip zE25k#pa>n6K>naiM7L>pD;UjSs6k)9t*Mqz+;2r>Gn1dW+1>f_ii!gpS6CCyriI#n zipDXgkDbMAs)l6bGwIb*QWSllsTQ8oy}rVzcIo--_}SruSCwGSWC3>7gYAajKsdCt z=TQ7LK!Q&Vq6hlIen?hfswnYnV7+8ABip)OQSj?A*HG+OX#I(}&VOUs>zlFus$WBf z@jg%I&}cP-9?|-~`sErKT3Q@qK)QQ`JQl6qQ-1F?p7b76=kn5LuAk`JZid1`8QMzC zFaBKQ@fI9?kh1_tj^91GOG)qg03%#>oNc5mY9D}9nluclue6_ogKl5W;SB#hUAEmO zxp_p?!U*SsUBToHQD*z#^r{*h$4i+!F%s~nhTiE#iqtm3R*&`Un4WUfy*d|F z-dIJop~xl!+8<@wKRtwK;_r*Br4EP?W$c^EBm|%i&4p9b{_<|q#2lm8AhB0f(qQtt zlU^u_DG<${H065MRtY_Mg2jZyKWU=_y`37C4q}v#xCmuAd9?PVcQSpgdi7_bKal;wRT3xEw$t8?!85sz=^XYz4T#BxDn-_S zEjv;mf&BkiXBLw;+3PqSW4TSV3XAz|1L(B>P8cEnEC;(nOIbYH2QS!$`63E74->)& zq5;B6T7U!(Csc+|pLa3K<~upBof`WfCAqnR^}z{*{pMUouZb|!mDWBN<<>n&q;Rv1 zO?>f*;nak5A8pq@+5c6F@sg#}8j<56?e%513u=JY7bt5TQPi%*mdRzN+8@{5`uOJS zLkcKB4xhrY*`0n&nW;(_5)r!)3GTnnoQU>uHm>dVU&i`i#@|NL%+F22x^ztFEY3(} zRLT2Az`@ifb=rk$l?IJYPeBP!UP=;`J!Pvw6^^e>IuumXIg%9nqBar#nb;ku;`>aa z(dQ{7Xw*cSYg&wTnnAUr`?w2$s|c3cG84~NVVXut;Yn6>i`W9DXn9#}8G@)-8C z2_4Q>1{@6kfwwe#wooY%yO4qAR7dY96=e#$z!iq>R|Uq&*ga_~IgCDpXGtoQLhqlG zVvraNh!>5iQ~i<(k@&`ZjH0>ut}m&*$5XYHYt&bb5={*v8|}Zs5evtg)vVHs`1JwX z2G=meaBR>*;2$_nA$IPp%`sU9T#dvX#sq$&HDi1167uo`j)V->Eu~U=M*E_D87hoV5 z4zycG4suS6OWeeSu-e7OpG}*Gl|^_1p0LRlZ6op%GM3vRjg_jdTMqN*Z8cowXHSd< z`#Ki0>tpc$^npmQt>Ftu!VpM`#i&?`Y1<>m$h{dJ8LUs3t8o(B!N?kWKxOkA|HCxr zeodybvQb^$aoXyk{NxY*lWN88`#dOVw61rw{|AV*u#mkP_-dx0v>9}j%c~K5+!h&- zH$f!y$n|->H{{p-#bED7kNE_|kf>G_EKGdZ#|fJ|h|1s>2MU=dH>X|R#N<3vQ7_9) zR3KL76-J?}vA4g2t+Dt~oP{%`cz~;1;RkzbU?7CyOB2&eQZiKEAn43N*N<`WT)vtw zP^Z0d>oTsY1h2l|qI(b>5bt6(^4nhLvUUbZRMzZlRg7M zF`YFrXY`YFwrM#{svl9BYO}HXu-+=gwuX>e8^RPlkz!RR(zmHl7LY40cO!CG9AQ zxO3!y*Id}u#xz=m2tUGwUjGzZMR%C%n4~qes4dX*VbnJ`<&!d8;q~5YetSC43 z$TSz;WdSbc-P{K3#?cNL%3I(tW8(!aqoGwrT>!t`Rb}b>(-%@vo_t!rIfd9VzvL8Z za){$Gr>^%54>98K6PlJWE7~J_3RJ~G7dxqG`k1jJHTc*K2$6JXQfb|mn#}6Yc)S=2 zcD(!s+PlKQnSzVvdQKSOm1eZf;fuj(*AIyr(Oi*%>+#t8fjAaV zW2cRQ6c{)PwY#73qx^GH6H~CxUn0iio;MKgRcILEHhLws<1f?N85nWcsuY26&yA-0 z6bkl?LiXkO-+WkASh9&k+=B!B6(_as{BkKr47)KGK6HT0*$S@YZHwGRm{h29K_to_ z*+Qv2mx!o0Xx-an0Ge1W;fTUR017^D974aYs>o_&P{BlIw%VAUh?PYTr|bIpISVb2 zgAOgk45S@Nx%up2R-ja^_CD50AKto)7uiL=Kbru7NhIMPK@HkO!iKm}xv<_yvI)@@2#0_qkNl-Kd$|RutyBPt|E7jUHT+I#J z#&AniwV-RG1YnuFZo|8m`|2ACMIBIfPhyNa>HN8jcT9MfEU@Zzb*=|<^WPkmNw}zK zfJvwRsvTTVH?P~`m9I`%r-Z8Yex?0KinAg6p~JgK8T-;pRtcuks}C(>FOiWj{b=Yr zs~R=u06y${85uvbA^}l`xOkHmgL*=+_k3?PRq-R1U34e zL$7mVtv_m2yaEpsvCk7^`h4am4!Dqro%%-0HD6^7Mbscb#2y982*6G%{lJwx~M>1Inc0UJzTQr0nervsj@I3@%v z0LGk3M4|^!T0OHuf}!4t4{j(e@DiB9DkWH&5~#T^JB;ce*AfMzccRW5&)hcfSMIa7 zqLU|S<>umuPuHNzMG{&AL*+S9K!rlIQ+3P=41O&s#KSOIqSR>O_+e;Z)AT&si3s@8 z4_`xA=$$~|OR^!zF$p6QTaCqwAZ*%&^4$wR?hf8xq6@_2EWiLyGsnx~M(ZL@31f*V zhLBnVE1SWQUXSP8=fybJ**7Aqf7+{!3+5N2yRg*ne^$KsJ04DGuP3zny8mVD$ISKO zl<=@R>CIyX2Dak6dm(VY8+MfKmB_)1s_DqrhfGaN!+=&%QCF$(X9|60opW}sZb~G& zQ_XaK3QTWT-xv^;TF+UJgaOJ-awF7%bP_?8lgKTXjUY8vO&(!_od`hRzsmTG!@?{7hBQ^pV?)3ahPk`J)_18qR+*}>$dJK3%)i>)y?7tg%JD9EF&3%78lk;i&ut5^0}<^9?NKhxSp`x&MMO9X z28v?5hmTCfT&WvB46sUesVs?V1=;H$|5?pWU9Wr(Eg%A-$l;)qNaQ9ZipI^FUpA&+ z!>#XT2%-q+UA)}1nUr&y9AHY7fX=b2pX%?5nV~i z&SC#9eFM~{ejcPozUnta#9D8H&UI2$m3mjRA7%WbQrAB-6*0_y#tn+Zk%Bz z#0E(dpY{z$YhCM|_%%FEG#4sWKJrssd(BzDhTpnU8===!X&1xEdBSNA0l)o~uxfzn zh1CdGS@qXkDEP2pm2=MHu5LLP<;?GZz}SKZ-@V@>%9+7Ex@_UsbFZ7!{;8?Qh2diZri`{bt7Z+Ue@v#U*Ey9^(L|=;QFCx>W_X3WPM{9oJPwU{6@D39TS8C!<0_z*)_4E$R@xQ6{9 zlDi9*o8LaLQdlkx{a#}W@1d`Y1P@YBT~)nJR?(~^2f^c4kuJ6VoP{bHm;pGb`T^}D6w7Kk_&51KHzWC)kIj(Fh+Ag)Z(l#) zCu-}Z(e)WFVFK#o*wCQW# z0fi|T;=4Ejz zYVs)%h6A2hTYVq)zE^!ZuFvmnMYgig-J2kUbv!)Px@L9hW|Pdi!xxWgX8T6Sq)U(w zhSBs*+IeIo7f?#7b-MCJE}Hpnn(Y9LWBx{A%oG`=>-A2%p_wGBw+1bEI?QD>3*v%O z4*2eoHVG$6N zhORK@(aXX?eA}o#^+^ff*E!^>@i3zl5jCw0kmTgK%N9brr1tQu|L+`DPC+;2sua$5 ziXlV%1LUP;N+PY$VObW=Mqw{!5|XBy{$t zIdW(B-U?c8Rm4P|%=qB6U<#ZEB&nm^uUdS_8Xe4kQ5{qK9brUYUH+K|9kup;5bVP=cYjNaMfBbiPMj{zT_!>66uL2R)W>(%z@NUqj~9BIE=C zMs)zBko@d%7VMQ)&=wCxqYoQ0o-c5%f$%DrG;lDaVZ@d87^~WVlu5h1sj)%=K8{;(l~!` zV%7J+qP!^d`rt%HM9;m?w^4Pn|5)7odxo^x0o$jf0HrSzG2mE_tHmi;{(x^9{z?aG z8bnW6rh36dag*$^&BLzpyeNFG(eO0?KEXrQK>EPqp=qhO&5C#T2A5X0G^wbR3~f;`h7KcTAVh zhig0S*vgQV2tl9_YXm^P^U_K9h1wEB*8zJ^O9PKm)R;XtI*nRI9`mJ*${VsFQSlO; zQe41rajoft7*u4d38YJuCDjs&GlIiNnlK%#u6hn@&*y9Y9&1WzxL^b|5Bi8g{| z7;f}av7ukbFgU^qKu{g&O^c~jsC@TF3pElH=@fc3?|wrRf!((HLo*@!ptV9P+=E~D z%1VCRFb&?cILF8^tK5)!d3tr1+nYBO2(TpUiiyRWoz#Rq*xvMKCiv+JWI5MCb1T}x zSvV-FfSaoBRqvM6pNWUZ05<6vTzmNKXzt-iNfQ@Xb^IK}ztVzmzzPeXzg~86N3ec673E9Z5G$~XB?!c)fr}7)>LQla8ZZ8mB?Hekc4P_QRQqp+i zCy0ZE_8*c89ZhbJg}$i@#HUjlOFlNWsT?(xy(H+yH5OVjEPX)`AQu!8+^|=KEf7`xB=XT zM~2LNR@;^DFdsftRE^KomBb4I@Y_ZY>>JH7H6DkJLFw)0vg$V*n0YnBX@$O8ArCUy zOO^~1L%Ik|nQdS-ryj<-!#+LBPhjz>78x1|{R;dsdm~_keMPz=&T9VsiPEQh9c|}a zl@V&@sC?`jXH-3m-~8`D|%PaR8wk+*{mV#lA zy$)&;5*E!QHnsls}T0f67JY>@WjhadudG7A%I|{UCyBY_O2c8RLvcnF{4wWHy~8> z9ai>7N`}CG!t*FR5CoRtt8>{?yl-EzCr*P=;G`c*_0+7L7E<|b=7WcJ>e&bmr_{-X zhY#8lbO>c=w6-zJV~`Y+zr!b4`s@dto8R5FBFGDGFsHUyCBz+~uoNT9Wbt}I89#!)Vjo*?oy|2E>vSp)y&g#kp6*p>q*B5cCoSkO^| z<46-$hW^zXIs?E)2j~tLu8g6)A?;cb7q#AAwTb1bY|$nY*+^c3(h=wDlnrT6$RqvO z(k$)NoZ9S-yrZ-h)rD#!=d7SX1B65tIHV)W$S_na_sxWrv!i2KTlBain3h?+pEFo6 z!ejJrj%Dd+M7hyE=t{x2wt@Y4*dD{;c1%At96?2RY(BjnHRmpym6-CXb&SSS@@ zv=THqv?G?AdE)s&Cqb}oj0$3^8f}Eda-20s`7cHGXwp39T(ue6e!v_&()~NW4yLl_ zS~hzk$-yXjBX9^YWF7bN11qabh(^r%|5E*Tv*Wq;H5Ou(3Dn{kan)&UDwe$2&zS~f zs7#Tx#TeYUWy@Cgc%u>~h9fHo{Lwa9y|?2ds(vHPc4_aqI=P)XZYTc4*npp-)|XvR{Z9Su?{f5C8<9sYa6Zf5^H4r85>Z7o+ZC7FCbmtzIiPA|U} z6l(sfC^gMGunzoQJ*s1{2538{zMy#JV4fU?koj>)zyId&R#k5`-7)FQK^?v!a-cK9 z3S`fLxu#H9@p5(bEG|Ar-7;kG8MnsuBGdYZPt&niXWcOwJHqldsVdDn9DxnvjJN zwi)8M7bU2C8oDWwu5tct=qHeyvuZ7MK`VKeVDP-^g$6|huAv*>RtT@J8m5MeNG9nx zcT}J(H`GIyYgQC`Y;mZW%gzDk6hj64ssVL*rw<#=N5|tJ>Dk)c&04qbe;o!NUhKmm z5&-x&nDJt1qG?>vXpL=cMmKXk_I$Qp4-i>_)Py{TUpa zRv6?7AClRgHE8#Ah?Vm)lZt_tWRm{kQS9nGFGyS5ynpY z-jXC`Fwr}GAju}q$u|u%w&P_XW$N<7_G>N4uR{?d&S?pDbG$i#n)=B3iXh2LwZm5X z#~2#KVYS&$QhD}hu;?27hLRbsiFa8vtBy{}9?_kpD4VjuH$<+imOOTJ;&OsqJi!IO z?Y#ws0ewvknhua8Vj-o$0ksH{9&Q+m@l+`e>EG*B1vtp-fMC6~rzP~>d+TLs!PL>q zZ41gk0!Wjp)9vO>yI1#}z8e1PL~w!OC-_&V%Hwp|ng)K(_X2zE91GSlU{#9loz?Y4 zFOHzk{B-IRtOb#&POswFdG3_&`Q$oVBPC{&=G+m?+g3s3&G?^x=qOw;Y9xZ!AWHpN zW%(ID$l|@xS>Aatx310r|3Uw8P7|ezcN2)Gr3|}rx<25T4H3~;Ovvb z&>vLdQ|{pmB0_8ViuNB75BJxE^a+D*3Lv)n)I{hn=R(x?JF&IDN^V&Z|D_<+BR~ zA(apZL{N^Ky)KSX7iCUSS|`z+<-szMFBL{~yP()g=-fN34XBj;t}}BKS~|W7=MoJ- zOEQAX-$uZamSACh{;(k3W2`YuG;;7+>m!KF_Y|{u9kq~sjY6~mOMmm7V1Q|FS&zsX z!&~^lJ|IVjc*7PeYEHD_8yX&-abo@urT`0Fl0ZoU{Bs6BwEv6&Z=Msye>&OC@Unsb z?1ZZI_oF$zzrb?ae)iLZ6_|<--q+ZIuT7-<(K_y3rnCW`#rFFTVw1H#!zdJOvsum< zlQFi+h`4Y=GgZsDj_fbr`i|Ck$8qRg2G17^o!4R&?b2a%7seW!)NPITa_96X*qR;{ zAY1K1;K4yM+mxO?G{m1CSSkNA#y&y4!+TmxE4{E!X78_j+z%f_P}#bGXpWa>F|cgvlxHid=36a;&3n{!OnTo7 z6ZJjsbh2%HOM+2XrN100vEBUDI0aZtd zv3HdnWAiR3SXHdgAuG{2XkWY?>!i99NH9@?Uf{!sR1tEO2zoH4MoDDIwke#|JKhG7 zIjZkk%FHDBG=u|HnZ=~Gfx(RTy0}NlN9si8BdpI_GuXoNudz=6tRhzm%pWLldvv6e zz3yEBv`C4uG5A~(DY%Vy%cF-nhBM6yrt(!-_KP`9_*oAE=Ie@zwfrx!;KLZ0?owT z@hti`%vmGsMs#4=e5*-q)9gP)3hX3Z@d`oOpX{Db*rWq3E%LHLh^`P}@L?pWs2h7$ zFmY!b>Zs=Fjt1s=78*!~6Fbd%&Nqs|p!v$q%a}$r7FN2Nv)}{fp2gABw@?D_ZhcJL z*CGBVl7o=4&GZt?rV$oX?rr;*DwLjbvy2Pek^|`gq z<2{k!rL<&%04TOBASbH0>x&G|RyECbSM~OWy?0Sx8lid6v~nUteWc!JDk2k0i%bvk z=u}9cKqVEc_r7*|gmml0DR2+kU+DbvCsg^!B>2O-oh*O_ad)x@FD^;Lf^}3=QQvtq zMyV6M1J0C%5*_Zu;sP%MYb4sJv+Wz_}O8;d?x$)*;be4E9+|b>|obD=oiodXbyn{&bU@<*FA~spv$1d|(j zG9caT1eVrAiHR+u9x~!-S!n>Z2JDdFe2SR>Z-9>pkuQBdROsli88pyn!1*A9{)1|5 z=(^q@+~S8tDdZ$RDs&aNYi|<eC4e!@`Wr(vqRktf-IsoEpX%GlycQA_NYcg@RN+V{WeiuoW77?kM2JE>~W*P?1 z`8Y*6Dr*oN65td<7)Ycx=&u(??4WgUBEr%AS1srBIPm%xa+am(ooDJ<>^TT11=$m- z&bxBZF1v&N{g~o{*9ssGr^e<$?Y@w!L$4VF+U!dArqJEt(zJGnGn#qr#lNT!2D$Xn z+5|4J#vKPxkqteh_ti_~d^OC8<bb-kuVGRja640M-#ByGyh+SL~pkeb8wqp2rV zu%|2)VB-V^8#z9}!YzFXP8bzN?|W9aUtA8`$OA`2@zGQ>T}Q|T)>B2CFdBU=MS@g6 z^T)0$3w{OHn!@*&>UUYMbT(Ry@gaEr^773WaMyrDEBq&nmxS`K6GA=sHmm&uqzIx7 z?`j8rgU+e^x(`^{KSS#IGN+}>uM6K}6eo`^WC;#VaLaQ_(gS ze+op%D9aBsf~h770|IabYc5!Fn4#8WM>X^tuff^8*fa75e$D?3fp7XJehewhBF6je z5`FV$C3~bx@1Fk=SHPWefJQ+@$9*|!GpM~xR)4`_Ss`s#0-9EiTQoZ-&Vi!eY>O@} z5lFMBeDXZ=9uM0p{P*4x#taKq{txbGt1#MDxZ_OnaQf8yqLM^}c3YZ&eC3~~S3kzB zP3HDw7{f=x{5z_14exXua-zdPOCjh;n{M~lUVRXahdsL`$|{xn0_$AAf>(poum6U)!)A&RL12^->c7(g_#SE z@qT|CXU|sn1{ET2)B~ma;7Bv)Z)DQ!2tAp%Gp*ik%ZDM0o@m{;u5cvUzeXUohNPf9 z=ouAlPnXLsfJ7E6s=%k-M9EetQ>@cks}%#rU^?cIDq)5S)8iK}J;A<8aS-pEi+RRQfRpy3paOg$i zgV;Z}E+^%@cIIWpK)J|&6cd+b*O!C=DgiKCr%ZOusVo-(hkpj&sXhYXP5cAlV*{Up z!b9sAPDPpLzl`qyovP`2jZn;1 z8M?0Kd>t8U&t}4Ov4Xc2?RYm01ipwFGil^lU>z#(B4SaezLo|(mulAbemCOT{uuZ# zSK6Q~EKK5jNd1%VnjU=T`QmBGa&$|8M%F>B;^h#$*dD}Fv`#vsm_og9nM>PyDsZ2Q zSS+caDjvnTU?a&K? zb-}F{KG*my;in!mKE>=lnG3R36xJsPr(d`rdE-3@uZe@#S3A^giE75+jpR44ac2 zxo#@McaYivsF5?;#NQh8^eqxr$8*=)d$u5mL~j^UWCel_%F?+1CQJ0=e>>n{kTMEY zy7c-yY{$5o;rPC7i0u==`;q1T_Ro7reLhs9pD}#zQ-b@)tbJS@a@J&m!^$$AW!#<= z9jp`?6OdY~J1W)hBSW@`Te zr2be*hC<XQS_g02`N^Rbzu1boA*T-C`%-%otqn7G9F1ArLi1UaDj0Ui|Uk_X?wyTFGUcNH`!!o$~{F_FSJ+ z2JyH2Ku@ECSQE*ka$hqf(qtzFf0`=7y92~bVgc3n*f)06QlRjIFkOi)57Nw+By@v4 zBrdP7eD!G6m1st%)UgH1$&fA58m7mE>_xrQKe^kLSwy7?j`kRYUX_2g7Uc2DX_H!S z>^nf+Jc39L%n$+wB|*JyIFU)muAkKTv>Y-Og|0oDM3I(sohraQ&fwQGPwaj2P3<_3 z=M&Bl+pyAueg8-Y2E{WV>-R9n%amT^HVZoKxt}?cZs;Rj=Cqr@69FchBJJUBCco{8 z1EE%iYN{?F-qfrkUwjF2+3)k2l4oV=@aECGbM`!UcJ3L&Icr>2Epaqt02JpD3h=%Z zI{jS@?%-9eqi-mFS?)?47b(GP0RF_N&)yux0j@_o`&Y1RQ8bwbv!~LFVarqKr*fo7 zpXO^;Hh+a&17-9FLAxaW0T-llRlPyHR2dC-;}y0RU1FgTvwM~VTlx=}UO8lE@MLps zUQ;<;EuIa~Ic^~>8{Lqs4bJ%B(f!tEc5E#0tgtN<6XEw3&$9?msFulYkM!N+O)~QJ z`aaYY%e1h*ySlAqOY5Bp#GOoWI^~6}_PN5Vh!JZKH{dGxGIWhc#4}7_508H+J@9%t zsqSX3kqiYLM6)x}t}0=}L*Ur+0G@?50Wa@o8U(Ll1KrZWs?yx!L3~z+j31NnEMRov z{6s;YcX33@B*JD;s~0DsWnt*Md=31%BipR+%3+_cc;-#eMzy1U3Pm(KAY(5>Ax)eZ zzcN5X^uTcpQu51~(@EzZZ`T0j-n3t?6iVQCwh^03%Fy++IAqausXD}tAs;s`D~&_Xo!UqfZ9$fMr0`8LB9YB)0I@h;BKIV6V6gCZdtA94 z|7vy=2RZInE!G$6shfLd$BV^)Xr@QkuDK8x%D0131h za`hpAOp;}0^yI|nv^~FxLRVU%fO!3NEuK-=ulwt3ad47Do{3T%fle3;)}BEL7Jhj8 z8Je}Q;rRBaaIoMkM$~`NLs0EF{xjDCE&$-1bM_z9o)}7rI_i4Wk5JCXf&X9SI7fUJ zLz}QgN=x`JnMGcuuYdiY*m4A9$a4)1H2K%W1Jp(7j}++{p^}d5R3N80q)C(Y{k^C9 z;bbUd_N&xncveuSQC{zl0RBidL+cX~m6*}nOi;zz?Gq;M8&nk{bYC+vTSGF4*aC{j z9F8Jc!Vf{$=kH69r?1Xn#b>ru!o=(m*=&~bNxlUfD6k9_4aFU(yWfr2=M1B?6@&j_Ozp# zCc}oR^>{{8NK^8_0FxVnNu$S>MD|0Xo-SY{o+`^@SON;;LpR}akylU74}9Z;cBx9i z?i6dOOR}j$QjDgM%g{{_aNRo!&8*%DVg-C?18(@^ohoPW(o~0+@7N`620k;_v-5v^ zaEM`jDe4TnO4GMDfJnGM!eT+rTLoGZZl;(X0{{Pvw{fRFr~*mtZtvf4WDjTeN&B zT=PfGBB&$tF!P+z+Ky z^jvv;#~6m3$T~uxt%Vp$iXHcQ1N(!8_UYQ9`dS~zaet0LEI-b2xB+=GhRx{GmI_aP z$**TjQd` z<@(7(;!<_RH6Kf29f3^X=PRxe3c3DYu(sUaR-9>9K?__kTMM7WvplF zuo43t-w|=XFuc$v3=P`3`&>x|V+RNRoiJ#;_X_-IKwcIqQMfN5{kLC+Rd>PCJphqr0N$q0u{gDKM7zR}HC08nprG2J^~abq;ruKQ z)O65>VdgXrvY)Ac^t7Yl$WN+x+2ZgIC`;haBTp`Fl5jvFVe2*C_~8l|{s;`uWb7b2 zzBirkVN#>XXCX!m4ib9;Joz6^Cg65!g{UP#VUAn!A-cN#O~j<~i9xs+WR(LJg9KrF%fJmiyz_&W0+IMp2=W2SO^z&=U_Pi2TkQVQV zS`b4iXj2U{@3v=^sRxx z6*zlj2KQe*Duf`GmSvL$D;BS_n}YAp`D6Pcr`QACfbqA({eu$|nE8)z%pT{r@56om z^ag(iI*1tW#x#r1ycvm? zv#vaDE?!vb!67CY3oc4fr%ADACt?ks$3- zYQ;5kr8<&93)9jrf(M(ZSg1#vs`pvr&AnuD7@=p*e@A}*R!!-Id7PE95PW zU>7*%Lmb;56b6~gAEeVUkKg0|^k#zw#Ac6Hsurtc7u>ii990-9UhWH7;D6$Lg+cG&r% zolFnu+DF|6#84v`udg$ZS1XoV;ZMrOq@RcVV-+>1C{xW9O24U~ll%@Fgqd|IiWdV& z30dILd#5FPG`1 zF8($}3ki;e^Pwfiv3_W%C?8{H35O_a_{=qakr>A6BzS-hP}^Da7VZ}#o zJ<>WNSElNfc*|0N`+x2wrPAZ7UOqb$HnE_0+4da&?3nws;@%qFuos*Z|J|)ZlCTg@ z{6x+#7PHZyV=P=Lj0EGYD{y8G@zkXo<1m^1YHy4Un9Ki=7bO^Py*)*-vf_K_Ao_Z| zEs&OxHD1(Sc+Fpo%MKe-g>Q3f&nBQABhjPR?en+D> z%Bb2|l0kG~Wrr!VsB`{dP7kaG^fV;>-6t_2>tf0M1@7teh1Z=LEZ{0wHoJgDU5Byq4_mh>e~rk3 zt@X!rUHY|qcjM935Y+ci6nn0~ezdqs-%rJDuO2~rVx{&$5qvM$EhQwP#vJ_F>`=a)zv_-%JdEfOex zHKcavc)4boFY6CaXBh^BVgkYYTFHv6aZ;l_H15jN1evt!zU z2TPk+urodPD(J;!WIdpWmD8Ia2_#_=&@h@mvFCj6_c=cO(6pkF=2Dg>=57faNe?k& z6Ieg@Ox&K_7Xf)f@fC|gfwbpQ&!n;a613bIo=h#1@t#L^ zvE@xyA-^@_o>CxO?{QQZe-MqToMxT-f3~F@LAqKp0%>5ab2r=dY2CS;nSG*Ji7~dmw~-BuGEu(thTXUs`dHF*P!GEF=*W={v+)0-0)4lO~TMJ@6 zeVgL`mBuFAAO{5ft|Uppt_afENKzr-Jh{yhq7D$Q?AgM@%8&oVjxnfuY6b59{QNbG z1_AL;6cpwCto9}h${K)eXW(y7D8-&fxE@&`+Y;44u(BtUKW0&ovR;HO2d(h8-+!Al z?n?Iu=`oubrk_SrRJ4yRY}Jvwpmj=!`EFFhX%<_X0|)>Hf_WMqkf{GWn#gd*9}w{6 ztef}ZD-g&`-=ysvq^paQH68VgJ!2)q!wYs_dvoyoT-uU+hdo8Uff;cak&Z%M{~Szw zp_o%UDETVVDHH`&*DrgWJ3DuA-IlmIz9uY~c=3fQji?z6Xzs#_l_g!SBpV51R!c`^grmL)rs{7{&Q z?Cxv2x>Q}FH6LD{(8myT(6 ztyNJ?u7)V(2agCBf@#eUk8u*$av?mh$1-y>iZG9gRy}-(TN)2N{r3<&4 zikK;~Oyq-VOPhu;8R*u0G7l`Lkmlnk>GQ#W%x#~5EfJ+86+N00vplFviZlt{tc|wn`F3tnR*2cLp%4A17CnWR)Oksv8la+)ZBMB(F|)+6uGpSXSJwOjp4UDq9daqeP}f{nYt1%P;|V zXcra_|Ik9BhGV&=bXczTK1WC{MbpZe$>PD$rp4Jkk6Y(`JbOw!#B$1!_@zSeVq9&P zd;2StCU8E|vnl|n1ThZ)y31C5ocrk`m;FxPPL57)U!OiDNOMK>q)q>j@f*c@?pO*W z!hrF8ZlS@@rI=zLD{MH}U*-&K2PgOf4k7%EOPag7)5fsL=yJj_@zeD7Ze7q93jLkz z`Lr~~p(<@S;t-$6lqj(w)fgiVLD=l!&5V9;mw8j>-^Nyzt}u7{{#8mANKQrN@WqyU z+Hi75KUAZXEe2tC>2T`hX6oLuEE*5VN40df=Yo_oSo@SX>R)(uEVyanMdoH9dpJ?p zU#PQ|T%YAN(>ZU?ICOCe3wJv)f1gC=*rAj(MQBhRbwXNNl*48g9_UG@k7= z?nTc+sFO+jHU&eYw!N?aa(Q}?W~;{m(AK_G@mTD9ncSHu^bHPeyrDg*Y_2xR&YPiU zSM4Kw{+#wxLihI@jZAeD|0lEFaF)9IUA`zIHW$B|k(IQTv4YXqAdn>A!}Mkc*>=%& zG+5Ru7QTmp4bW;<^tfuqwH(ojhPs;N|6Y%DtC+)xN1pLb|PA$rI-pV(+ zq?S6M$E{;Z`YOU~*zI`=roxG4#Os>0_1!6?H5Ag9G%%=<6C7$KYMNj8vPAh4kYz09 zSOyNn^Y-#gNiZa15iLXl-npW-Wa9W(66ROul<#=ZE7j#>k#dwtj``3Eb(97Up%7Mi zLZyuNp2O8H{qiPa@1-qadpQcb3qWIV;$FVqr4lYwlgR0G_+R>Lr+HCb9DR)VC+l-zJ>z z>aLT?k^ALigxOTIx!p~&OcW=R%c8jN7|nNVE5=#u*D=i2xl#8Fa)YE_Bxgd2TclHH zu@X{U_lGfbs9PRhrOq;o6o7Xp1-~8?$`FiN6CL$Z<0KNksLJolJt5a9V{s-~R#q4@ zokW07_w#LqS}b6zLdp*?tW%Q2KV<6uIUY;XLebCl$jC^#%yfwBO-A&n34-%K8?f-^ z?(z{e4C4`1Vignm0jxiPigYr$HCT4bu3R_I0_7)#-p6Jy(cU4~opa7&ziph6xM}^S zvK7`0-3`bKuiUqs)zKOTBRM~lt3|>Bc^7;Fs_61mLS@KjY@={t7_4Kv)r(Uf{3iZ^$xQ2` z4U1&DNiv(>$H*xq50~gz*AU`{TEH0Ig67tgu}Ms^5&}IBukuHAQ&UDsplSTOE+edJ z>u(wA6Fp)+DH$m3&ClYgbe}$!RyiHeW`Y9LTkvE=N5DaqK#b}d@1W4pangzl0cpsZ zw?edPMOCBp6^D{tmFt09rt*1!!#oWKO0>uR%_tLV>2YMHzM=8uquFem6cdIr9}_%m z%1afue>|bbgI406O{n{@rxcg=2fzS8 zMY#fLN9B+%aF#$ny@Yp7$$g})*T8g8&504RX4HY+H>}O^ww15ch;?b$&|+}5 z_#t5|anPLjl>G4a*5l%w%HctCYfMUp6rMltwL-UZmhNB+X9ecQl%pCnz;0J}nx6K; zuo?`P)h3}d&4;fOXZHdlC^$y+Xd|{sL}OdY6fD5Y2t%PIvLBBIAleF>&;nS&{#p(< zMTSZQc@m1mYSCGXVA<>#+Zvq3Z6f7~3`X*x?dAMvFb7wJ2jBWkPTsx_n7bhME#8%Y z20DDOHFFeu=B9y0Bv|`^XxT1d!41hLWW)=H64Mx_V^7k(RWtX4>22Xl>>+=<}O zS9KjJS*nkI|ELGLv@G7(bnB(~NJj z@S!lP206u{t0p6gAwfDfJ6j)^0&A6qBAEzykbqQRmDkIjMwi%YJna=E{jVA{@(|?$Ay9kmI zcuj|1$(1~S?wqHf6kRI>$*je>vMl!+Dc&`djv!~@Aey7o-^u7BCUxT!p=x@Kd>Abb zdgDZ&q<4K){1lCax2n%lyi{hs`wxk0ppEFE_El3q(DI`D69%gyz~or>q-l=`RL0Ma z^t?juHST(UX+!_|c|Kl?I#6}1(n%%xIe^gksZ_bGuFdeRDIM?Ernn82OG*8&fA=1M zv`Jrr%v1R0LA`OcOEUD)*>bWQhCI5i$T8b`)zFl(q_b89GrDeIGQ47Ck5rSydPMY% zu+-7be%oTS;gpHJ#0;C{Rh3HDCz>ZnGG~j6Lh|vDXzBBV*S^=f(?0x{r1KcIqw=$) zBV+T}=Xy>L|4i9K&}K8&JN$IDng=pJ^-5iNt)y4;9(V&Wl^yp*aKmdFv%q{+8nW0GwP=)b8eL*Yn)kh2Vido08@c;+s0hzQgGli)b!hLc0m&$nxu+d$n*Ycj?fU%3 zt$Io!@?{nMdhSJsT`Sc*-43DW`fSIkuc3>Vxc#I9&K3^V4W9A)Ie>BVLn!CwcqS*4 zR?Q6dW2dkt4UJ>DlFq#v$Z;KWk3gt#Q93X4Vw>l;*9}|r?w)@3Y}0ebe5=T%>&ZQ= zuowZJ=;J6WPa);xa)ck^^d4Zh6$9+OV-AY5sjJ%|L2ChkjD#&n*eu8m4r+?hn1CGC z7UyiJFg~PTU8lJd{lS(XjSnt+dq|K3^Y+M20r3a+vH5+EYimRI_#>HRE%u_o=WI2C zT5j6|@s52fa=WGKCe*owL=-k!{S=1%Ba=3)-w>4dv;S#kW+=AOK$l-XJ z2PL#CdJ{X{=S-5a`p}2aR{48>&q;mSL9TMcO9AiNg^;Gnfr~ zhOp2f^M@+TivQFp;$zd7bq2-FMJs~H*){r&&y12rlQrtO4z3o#5eY9}=y5RB zIKg01Bq(+e;*$>eN#(oyEtcqWg5E zCFze932X(S8S_gR2Ri9v?f?DWZO0h)NLvHDAR7gA<l=IeLP3P(eZ9U5P;N=LpY3d zec{tRG1>gl4GP&YSFWTCMF@ffQuw}fuIsSoZ zaaxCk1E_QkS*h5cAFVrsxqXnHb*FNj z=s$S+ADn?6#kfb?An+EvT)@(wtL9B5z-(++r+SD^!inDKt($R72U5+fIjRB(q(+V_ zqOykL`~!*ykK0Trkh-UZ80zMp+qgz&^4F(>z_V)`xqA4?xq6X*I63P$!PA~$2E%`=HScF+A@85EsfdG-yqLLEyBqOu1cvi z8l-1va6=6Sn5JRCDB0drW6%<;?G3Sg5_`VqO^>kSl5u zEh^|UT)++}t+zhE^e++$^b3e8I26Vg!IR0gp|7mL(?e)|_)QIhXCZd>53`wyJqQ;U zj<}2hZ<|XPHbPT`f@}ufe0k6|{aug;GS*D4)iMXQB<~A^mAvK&{!GOt`xiro)2_jzK7u zm!CVAg9ZRqI5%#+1^t$Dx52y3d-Ty5w*vsh;~kRYHFDSm;>O-Ia#d>x(E3(5F2}6l z%;7+z(gpxof@_f^m)ujDN}ih~Dn%DCe)#~?=Q?+1tLO!|%%MIDJnYq45W!TO#6B|E zMaA|B?r=|H*~lp(+Yukq5e=FraR~XMNCC?guJ>K@2r!M%+xW4V8@4@zv@B zlVb$OV~@Pjx0nocD4r^?^+S1YAO!n+V^opTGRDbwS!uWYCC2i)k5Fpk$V6OR*xIH* zvBPaRZ8=F51RP<^(q~INJ9PY9kl#257;+fEAbEOYBb+DgO_=`pyx!}NN9Ps}yCE>8* z?%5fWAb%_0Yrp#~WJtUe-G;1YuD7#?(}Ji6F%l~|ZVd@R>5^jR)VEM+IU;dce1RU3a zHQgC#n7o$Ty-1f#iK9iG;A!|Sh*Mx{0sO8o;Xfz&$N}@5Gy_KHr-1Le55h9LU6IlT zjhF&^s$(qDF6e|}4jj-{pH=Ae^uh*DK&m=B+=9jI&oVG!rRBcV^gCncR+F-~5Lvs3 zbVkL(A$QTWLg%zhsk%izC;L-XiNH+M?pzeEiO8wJs$ky4?wsPYETlT2YL+NDpLiKAan6Z~WPX@F;MMDFgPXfzr+PQ z3Oi(eM}Zd}HjAhsM+ukwr|P%e#k!esnUWb#-m$*{fI=S_{P6`xsp zMDj^>gNA7&jN;gPVzs#hSTVKDUwz|4&+P$P>tz=>HldoP|=mdI~t1dn|I#{S+4_KS&ph!|Xs z65&&l^?)uP`6jAxf}$;360ZE53OC+ri3PTu>44GvzqefGXEzWIXaTh%9Hn(mqx6>F z`W78U@BmOvB^)4c{_d+rS;11*`v$jhLMX4VgJl8A7r|a!j-6_NHNm)&W%c&UaGS#o z%igVqyAX|mD$ED2W`4oOD3$=yxkvH*jg8`W&SUhhka942ORRPDh-y^N<%RdUj7nyk z_~vXuY>*4%sus91#fYcuumg&sooKjod^2hzv3`*kEK;^A6$__lfvLS2nKLR%!$mu! zW9VOd2@)>L1KhA#4|4*Na{9O`JLB0e!7xw3FFv=zn`uqf1atKvUqsR%jrKY|BJi<( z$J*(BKDh+M_o*BK3L-M%`=0(A47k(9D&ldQ?eFw&fO2rolqwpL@$OT;pq+P zs;ncRuZJ>U;r2R7hWMo`nhZySRv7`cl`i22h?t}W>HRK6uS1~}B%Z;bWF^MBCcdy^ zLFxSSJi3XEoBjd8TVI$V@@yGu3t#gdrV;{-WvUw9%lcXrU`$NWmM3pMyk{o25;=?+9-QLpmUeX*-#@X%0JFcbl)Q5eYNcuu74%AZy<|E<%Ia z!F)f&$DODxL>|$~S%q%Dh*%TQvQ@-QfdBm5C5tQY+JsN9Z|uNG^+!;1y=iWnZ*-BK z>18ih2iuT*mf^Oke7LoAkNccy+vi;%Z`W!nmL03r5Z&TVL%rNZmU7OmO-6-WO59pI zm)O4gx56vdloko41>dI~&D-Qs_ke+6=wv<>b8$ZE;7!efh>GOwMrF+Cg=>#h`uUQU zytqisaiQ$xCzV%W8WFuYdn!gj28*p_v4my=5jHJiQwaLQnNl z{pke*ca^?`OC7BQ>C+f9Gh5J88SS;qY%vC(3n|CHq$akJ@BrR=d>#gJ1XUHXv=?fg z!rgHr8qP6oT~rRIn~(8-t_*@wbBqH^$cV~gsPMW5q9pL{LPsZ7hW&6=XXu)fLxf@= z_Pj~iEd;|72Jt$t{OT+j%U3%;0dhF}XBbP)jOg1JzKJAhM_mie?_T3t9ebo0_x?Vd zJB)$*#)8n#Uv@H?hBAKtYaw&LI8y`if`!+s9iB$63#?~nk~FE;bCLqa}H4;sIfRjCq1fI-mL+^((csTauZs)IBL-I+QSnSl{k!u z$cB$PW7oo)42{h zx(opBZH9)%BpNa`obLpe&D)sb-10eMaZ-nWw*^>Meb2|4M&Q4V{zHd?{2*OVpS}}^ zcq-WLTt*BAH_}q{4icyP-Xoe+F6mF$XK(#9Z}G+{{b@9E7`v9A??zE>!3tA^Uve>KR34USs?hxQ1cyEIaKQ`aUj~ z$gfYfD^K)4`^}tWFyY!M^OA6wvRM){piE{-&JZnLXGGbH*w0UFd#S{_#`X+PGtEHO zejE6#TK!qZ$CqivUL+Cfo_wy=ib8T>>Phl@=R*i+pZSgQ10%ME^d+SlZ4_5*hIGc zXGi5P{8dw)QaG`5r(CBT94o^51qFwLay|_|TGEE#p671_NptSk| zXQH#F!i$BzuvB657!Sm%V6X6sx$7xRVAK=Nz2Q69l>y0MQq_oIx0BuG$3wLUr39}C zsYvTu0-dpH)S+@VwGeFDtFqbIM}n;2ElF~1f!)!@3}36$C+^IIwlI$eEmXuTg^aK5 zQ{UyAj>Ejh5%J2gy5liYMLp@q*34PMnE`JoAR|+frQKPgG~Il(N3(x=4YCZqqmtf4 zIcL0x7ffP37CpMA32NuA{t_(PFmq?K=-B0y72B`Sr<^?TF!rdYx#xp3c43AO_3z(8 zemQe_-1bqd3a`lowL;gcEv*EPV_Seeo^ITqquAKIh>Xe&8&<`NzqFiid3+mK?p>{c zSHIg(25gcSC6Te*@YgM%ZrzSZUC66vVh$0_K?WiJT!-R`v}DuTwllETQ5J0>$r^IJ zRl5WXj^m^UlAZ8v+-aL`WGh#F#Yb%^hXAdXcdLf0h?#~PQ_Kf zW_zP6 zwf?_E_g}`RMz|T^BO0NPn&GMCNcMrMH+=Ipcxzn_2=GejOKyQosKl9tgtX@Iz*29m z2>L(Amr366Z>CA)Up8DF3n4;E&>)!9@ot`*a zY&s!IQGc@yJBl2%sLt~d5^$qhFxS2U;6Cq)*oFCDom-Zjz{U7QFf_IK#ra`bCsi;f zh=Oz~Ihuf^SE({fsTB66g}|V{Q$z6_vCqrm!7%a=eh+ZoE}y->evA%{I}x#c+4dq$ zeF9mE7$0Jxr04RqE4A06-Q>POt4l>D*No0VBl_jN1Cy}*uNMQ!PqJ-NWh;e*Zw?3Kvu{|g6okZS(cM#grPBmO|;!8M>eV}6AZ4INn2t50T_Q}OcqFcS+dL+a6 zwOi$$U{;&ZRtLKRN0?xRr zbR%5YX{@6^zCdE&JNG1&8QdQ?6>A1z98H}1u4ZC5mIZS+USRB=w0%U?R@Oj~URq=x z`b4Ei&-nFN!yx;0Um_O+a>tqaphm%#h~)Fw9ag8tVJ`s!sQRZY^3}N1->m3gq|D* zOj8EbdY=?qrB;@gqW?SmBR`}!yxWtELg*8Y7j&S4q`m6Ns14)2D~EF}&;;@Nvi(Hf zlYt!+{;|4ur#>%{?G{Hpie!s;&;TI|MUXWjSC&F}Dl)IL3)HvAst1cEX>0Zp-LsDcSB-OIg<$15fJwVZnUKJ144Bo;+-YK&&&NB_tbAkN{dH66g&d`k}&H-=<~ zFr*iA@k^5!ZW$xx>92q+I>;)OjX5)RjXwEQ&oiiHeNq(uYcq(JhnzAVW4GbT8-uv= zP4YseD=l^c*firTRq$n`c-0Qd`U0Cj&3lur(=mQmE5yyY5b*-7tGpqH_o)g}4Rpvi z(F24%fsVf~nX*_EKPiaV40kgC6LB;D;xf~=W$mlnLEB@)3HWqA&9E9bnUuQ;uvdj; z<|{%6I&i22@%>L3QLeGGJ$NrYk~5F^9jw$>dm59EFRv=lAdLtq7K zCG*_CL#Pj_n8x6$j{RXi(f;}GGnh{no=C6#655M5nT{+r5SxoZ>X@1jZ9)e-xJ>5R zx_SK|?+#7G=QYn|Z?KOy9XtDXfuL~p$QU_WHwz^84>f}2CG^00>V210%++cIBgde^EWwLRHE*ybI* zk4YVaV`jbI3gU=rp6U}It7;`ivTY(bTZ}c=xr?4(U$n%OzKIC8_=?YGGTfd(sau=t z5VFwok4a}FkX6=o4I&-QrPWcqEZ+c1t#K6$to9ukjh_Gj08mpwNlpL&060QPO#|%! z000005CD(>000310sxQz000310ysiRO#~T1@{}1_p@n0Zc(Eql7!Ti?wuK$_;)BcaZpZMSTfBQYa|Ehm`|B?B{ z^6~#0|6lvB_TT;g;C+~XWB*hCqy10&AO1h^e!~Bve|G<|`%nMl{a?@r{crxB^WVTf z=YRJ9ukHc++2r2%o%wxp@dx$m)PKf4LHa-T|MQ>MKeB(e{~7Tw``6_EV}+CS@m zqxy;WfARn1|7-ue|Ka&h^)Kt6<^RHd%Ku{jwfF=4Q~2-oAM4-azrg=%|G)l+?Jw|u z<^RV1-+udhCVgZ3ANkMkpSmC6KmYy!e-{2n{e$}__#g4#-GB4{sQhF8AN>#b-}Ha$ z|Gt0i|6lP3^bhQR<-frHng3(`|Np1^AJ8A-zsWzWe`fz7{v-Qm{@?V!aDRFKzyEXp zTz}DiV}AL6-u)c?DE`a&Kl|_g)b>B>-mBPEN7@Ix{l0?mClq}(Nu9=*U*hV zsdB#h;lzR&2558sdx@+$p8qgD_FTP-O0)*9&WgtUi=u1Td9z6XK%#qZeb7LAZIZr@ zh(b|R+2Se&K66anMejK2x5j2RQdZHcz##F5S3-+-x1ral-`_+oRv#Kg!GwGP^zq?i zOP4e$c_LT5BrH`&b$!qClU?hxtg+>W-DeIf2%e-Le!vz@OVfsuoJ1T?6NQ1f-L*%I zvChS6`H*jZx>d7*=^Kr;kBz@92!^Cx3_ZIp6wZ6{2O9%_S7h% zO%6IiZrurpvJ)rNv4g~rqxyTOBInhpGP_ph+^bFdN9y?E0r<~4Dwr#U!CJu}g0|!- ze5jwtF#$k=l*M<1UbcqWDt*Evb!a|+3zX~En~#LPNf=S^HT z&Jvh#W_gR4!Upe(jX76={IPQIWjM9E&Cnf^XQF$cSPZLVA5SS}Rw877>TB_k1{Py^ z;n$=xG3oc-uSJ0>4%N)9`-$9%{ln%F_UPhhIoCAFnlXBm%#i@!h7QZ9Yjpr9jw+Y8 z%A`xuuWikx0WH6AH^lSyoY7FuX^diHeI$qm7rH(sw)29YI%R%d>75$^C#j0mB?=>D zTXHA#V(hg6U8_#TLo~cwmRr|f&$A}iVVy=J%Lk@^T6xm9scePtYz#Ht{IutPAzT1j zn*ELAQ2H(ex3SE@CgG`vPC$*?hH7wNG!8HDJg*+79Z7vF()STPlPWKPJkJ zRi+cFwdR%nC0$X_dBVgs58bm$E{R2YSx}-ie4jcl zq@kQw(GARnjxH)m&-=-=G)k~~ztOuuXQMPNQ+uDPG>hjga{HU&;!Yg^yZ&?Bg(JqY zh*j`|*=vDchX$5}qER=M2^p$@jq1Z|P{Lkk8vpROHp|UO(oN2=Z_N6DI1)Mz8ht+t zFtPsArbS6bxbf1`f3jwT%Kdf4f3ej2tXRt-qdU91pNw(JD01Gjk?SvA&@D|4o=LLc z@_Y{4ssha+{1f~L?;vH}(b~w3B5c{V?LAQ|h%~dSrI;;*yT%I{%gWN; z;p}~mZQZQ+g4k5Wip4tUtB6J+6#fS@2SQhDPPCZnsP3xhdV&+0|TxTw= zH1i+j+=asI8gkmWm>RB{;iKr5TQE$>d?Ub4jj0oZrt>H8fs&0?{jl1={3E!?xsZ}HE+{wN=l85fqnh*_0c1-WiQ_fx)joAL766W#hu(vRD)R%j!m9S$Id zMT(PPKMpE4(ckqI*#PMP9dsSr40q#0S*?o*ae$qoH!M=s<$+n3+3JSpke?|bg&YwA zq5!=F-naD&c^^u2hct0~n0Qh}nk10W|57l4@oY;*ZAWFixwVY%)sq0{^T`I6)vzG~ z9kpJY2=3@ZDiCff52_NiI};JoLPtvobO2u0kp-GArjlx^9BHPVy1VFvCK7h&kg#XY z*c()j!<7_F$BI~JV~t97sT#?W)%()Acj8xxa*&`)j+#8?mgl}#EH`J9! zrZ~=n_k8bLmsjLD2uy$9l|yF>O$8%Tux!YajC)SpDJF&xb(Qct>uug?uO_d(4&$vY zBYhLQkI-I@Q6r+t>@3CX4!C|Zp9{YSy%~RqbcfCFF2?Gla9%{ui3=fKc_-=h^lRm< zdF@^>X`i?0yOAi?99l0B0g3*AV09-+HqEev52I@MltO0^!Oz#0UolN}MG%(V*Ll{i$((?g z8!g714#~3wuK8&GIy66|-AiqpVj)=}sCQoO4w_?14kwndSEJItK)qe7$Ldp0acrx+ zpQk_95?*FFkm^?7`f*aQM}S5^Bc?>g=TkJ?F|9L~gPknHIGUSrWSn!-#VV`^MQN~* zUiqd{+ z#lNXc88Sbx`Ds$88r(o(tP)AO0Lr9sHKL$36K)?$VSzPL$A-;PiAslHy(UoRs zn4gYAZb!mxyoEe5)$PvTse%d!QmT+$bf*dqkDbyK*3H9NR#485W=g1?MQEwZJ@+{#N;0DGuD=`qNFd} zL3`lteXq~5oL|)f#uH|9=v&qiwukTccCy>M0!8i<-5|lt=kdpJV_L19VW<%QhBypx zvgj`V_NP6a3dlShTsf!;Y9kk?AOBn*)mLjhOK(lM*MtJ!_8jPi%tF9hdk{wi@B4ZT z-hnqyURtgX@uFy$^tpOx{wD0`5OR7pvQ)e{osC1`Tieq+gkmHE3g9}xZ8m!>8>RyV zN7y=jRYageb&V#6+otv`2}3Za78NMUK9vj52jwD$o9>pvXwRY6MGhE#!mTnBNqe>* z2t!ZN`h)mBVoM66Kn8B%w=aX;$K@cF%N z3X6Hsf#Uv!)!<|vi#@ay_9ou~|N8t*aY-P8Ipj%+&Jwe%hZtB?59o9<6OEL5%-oCW ziU0XPIf6vRBLyyiM2W(#Rixm5|H4n?Agr$z8@v&4>ChvTXIzER3K@vj4PQNhviv-; zW7`f3NZAGMm}JSD8s4cYW`!%$^XSZT=TsAHOO;OY`GUODM**r+jhCXZR4<4soRU}^ zI{xyNM+x*9uaK?7T+$G32iIyfD2wL*C`+olT0+WIBN-MhN8Wl7j=uxG?(8d67c)5q z(|{2ipJ0;flCkqjQdm-&!0`VQR9DFxC33-*>Q}k}9p%80ZVBLZW%6Dj^F*+UN!&Md z_vsjeblavpIU&XJ9TG51UML9Gk&NtEQU-1k%N7PW&g&j=OGF~n>>C=E^Xuq;%1U*t@#(?V&@Ry^gdpvgrNU4;Sn)zA?tc zq8~nn4n_o?u>SAZAqPVfvb8cKJ3}QJg0^%9xS}a_^OOL9hvOc{RknMFE+}Z zk?ob()m;t=%EYnh*V{H?Gj5c>=s$dGDzE-iSNi^1cRQY-wR1kHA$>pYCaQGsSjx)1 zxRfKz5x;bsevEL+C&#(twf9d)AB}Gq zpE{INXd@ciOY+_>P7<#E!g{G4 zRX9YvI%dZ2JTbZq z1+|UIf0d?+$=e|thRE-k1xslYlqn3O>c4uvCF5#iN-wo$1Xn-Fvq+MTS%}++28Sz^ zE9MjB8sZ(5B28|pmMnbdb*SeEQajCFsoMZBIE+9Z8*Cn5J00I2e=s|S_gb3xtn3fx zul<#9vYso*O`}a9k}`r~G}#EKWZr~~Q(wi!KI*Zbo#icGW%>?O;aiCDjhLr4q;f0y zy*$RO3oeR5XYjT`Ic3p7@4NH}CmARpQ?zAJH0Jxv@U92k$)h)|%exj|Ex)BNf4cHLf*j*`zE0KGqa zdlqBX&f0mEy1T0EL|d%B1DdIz_Gt5Brd~ zDz8ujF(LLq^6oYYHj;6AOW>jzu;XF7&zC}$VCWx#6P3{I#ibw1CwrSd`4IbQFG~nU z-i+jc(f_;FP9YJl7~cz~g*e~W9)V#mEYmPOdjS@5=IoLzKo}qCyCe<1KY&`A_2V=> z#@Q@Ek+M6zRObFx1&hyIj|@jB(=Au@ew5#fN^R++>WzIC7LLtQiD3<;z@hE2xJ!#S z@SzKr2?Q{}vK2K}Lbtg!d6oN8;liVEk^9eMLJbNZx^O!$s0AREkwayA+Kf3FI8`2a zfL}y345}U{wiA4>IYSmo=$_PO#HtaIzbLo2+=QQFkX>F4U+_L{RKayN%Sz6&=<>%w zN#9&<7U~B_TSK>1O)=M4@dYX z7~EaMc01;>iLnhij68{=!Z#Iu7sswPq?k6AXeV}s02csWxQhFNg11Y?J=0Rc;Th2`i zNNk1z$O!m|oz*E{EL-z4_x4RfSS*yERRg8}mgr^F5(sMgS&SGQmvdFl$v?KBYLTjT zpj)HI)99>Npqfs>Qki$TXQuZn(5Jq9sItcB31)W+*xk;;DA3LQms?91iN#{;w6a*J z#^-{a(3bQG)hWMfvUQ&Hz{A@u_i?Z1bl-d}EHzDo+TSCLvD$_XK*0fG_}D(IZtlRL zM7qcMpt|Fcygxs}WpU$S`k<@G$^0tK>SdMNQ}h&c!MNrIvUwjuG`F~Y&`Ww(A&(sq zqjf$Crg5cZD~9@>m^2rPLxd;xuQksIr&CKZVlX70jSo!qv?3D1$WY`}V?i@zaiCU3 zdF>>m56!I;+}D$xlW*bWd1!G`m`Z-nk`F5(zX_F2jr8)s@53!RM!Z|SQy$gfud@Hz zrv&y?6((GPszGfTid_Q8UVn7eGn(M3<(BwLx z!b>((t&4KeM1mM9ZU#K@nvvtzGXSev~aH*wT<0chq(f=FMVv2oP4aLryg z#K^T5NhRQ&t4Nr+Fog!~1@C%|=jK0t@`^NYbHSIn-2H<8ic^8dBp!I zqFkq;VkKO2_&~97nOVq{#nuL`E%45CQS}RiS~R;bZaGNIQ6ce&zoPcjr9(X%O~@EY z3!|=25Rk$ZoLdk5l5*1+!t!Rf$Vgutwa_xWcBjkQhO_F7+Q&=oS|LI~BIpy=?;rZb zGfOsbi_uYJxM#%ZWe#z(T|e-_hUEx^WdSt6E-gW{hnglFx8G&JYk5t-?T=Leap>HK z+T*BMHJ>#&A!`ONGgnX&ts+!rFTWw;!rRxmH20_kd`b>$EXcr4icN4VB1l2~J$^a<|%A_k&kk@9B1N6UNS z{!I!44*@6;NBYzP1L@)h6S+ze^d+qbtCr|2z1sb`Ht#$+>GN#lvlK3UPjjPERNWib_V^VAcNkTDVvT<< zE+8WC{?P?gus3nqAk<_>K1kv}1S!?+=Fe*_G78emVo(h(eu%BmS%5z$Tlld?$FAmc z%J+CeIQxsCX2LtuH|9bQmQaQPPzcx_R-V7?V%NpEGF;%Q4`{&`4|2AhEb);Tumpq_?h$SI6wWSl_I*RLgCyCWUbZza&I0MTnjy&WhvyeQP8YY*K4-} z*&g~X&yudt1@;tHiY3x=c*o zLu0@swT#0-0F(xPBj(env1~&j>oLG<94b*WWvb;iR7k!R9RXV%45=>Kq2d+;K5lq; z#c@6$LZ&!RFxKUW_?A@mP|{*Hdxs;^C@I>|zb*U^3hOUFTY3Ton@0w~Dj0D4MAM{K zdE(TJ7E8)Rd&Rj|a*xBJ>X|2;x~^Xj@;Q?=GkMLS`6|CG*6I^e6<6MA_!4V`4t4c$ zxSyj^v{aZY_lZY`5?5#4ntcqg#?vMO`cpm8cmp%w^b%L_w8ApiM$VRu_z<|OB|i5@4Md}!EieQ7d4th3#@eEbP6mWuMMk0`qQb&Eo;$Jl1a62nO9Cp=z zJOW%{NDku#ARtnkuTIA~vMY4a?~Sr_?6gCd-Wfk0(g17+0y+r8UnQhLNITdKDhiRU z`@R+MiGi$JEF*f9agQMWP1J-)Ag;aE*C)}x$Mf$Ess%K&e+5dLIU zT}Gxzn687vfE2%ggJO%SFzlgd_4!2)U7sogD?`1NRoypA$Y?eE$k8zO8Vt*V`^wqa z^f_l9K# z$#LM$GCdnBr#a-!k!_}p+gE@Ndwii!IMW#qIq*c6o|a-%ju$i;!x{R^1q?I4iDD1D zp1v2CNgvjxI*`nP5q9Vr7hg$Pe3RM0Kp)w~Y(Ib*%IL9*;KPS7Oz&#H4`Wq65**$M zQgT9VOr=Q@+NAE4|6KMwZpta@?wHhj=q*mAD{ttw>9JqScWU0+OW-wqRS_>4Z&Vvq z`v53*gUVps#|}_C-M9AypcpvWp3&V&QtY6^nx!D{>6IJlP&r|kRiYV5@zh!m-fiG6 zz)`tx=3UNVPs>&X%juO<*;3ncu(|}HzzTZOnX6fT#vnqYY-?}Wr z3Ig*|-Lg9v@;#7%zSFk#blKKu`P4DR>HnGDQ7qCIVZ&%DjxNJGNDvIp?r*o4XNF6& zk*)yC)k{I|9epQj-@v7RWzW24?nXJ~%3s8vUx?+$o@Zd={79bnq)-YnX#z`C%r2!O zIH-go=b<8byc(T~t*fQYDizX7mIc;V+oPfx10qhb6(45;zN4zq&$DG(43R9Qf9M%Sxte*l4us(`ZmMpIG^=4RKb1U)$V zWRu10zAeh4d|8{i7L87ttZ#|Yr%A!ZZyY%&J0xn?57j0xUf1G(mN(b+Ge0G7cT(p!GP1C|z+W`UM{$!bbHUOLcyki*yblw4gOGRYG zDmH-9=;=PdH?B?=hUziL4v;`_5oaH_LZKsv&m#~%0mTsy?%|4*m|QhZ_OFd7yHu-A zpp3skA;ZDx(#(mw0+jUPElA2^E6SA*b~pN4fnmk=+T(7IaQz$PO7ybN-fHssWK#(8U{5Aidd7sf3dHYI6mMf**q4hf3l}d%?NX zVSWrcBmySG%Pq6uz4q}A%QrvVak;?T0kUtOqBnCQzkM`zVdr;eB_;aMPoKk5wg_f9 z4IuO3^mD-UKdoxGjXXMNVh7WYnQD)XDbM@~0zHVXEk2OI-d^E2SErIY*boL?@-%17 zLWX9u=kxn>siIQQC{KOxBG*h>$DaLYEd@0Ly>R3i#$H1kLV}?ZPSps(`KpU0ivob5ep!D%&2g!k2r@OrU>fyK=oX%pfRtpuj%e9HQS1 z!RLu2RF=RiO@qv!*mo29Uj5C0doyc6$yNa0@6+svtXgkrx0TJrra$r4M)c+3rhkFk z%mu0MGou1K+4~|5ODrtgTy5W;3;iw4>6i$p=-@UbV z4^rCB&%!TRxx_s{ma}&zuxX!?3zl-3ltl;bigZj-;r|QhK;%nM?I?nJEbd`~%XK34mY$7OG8J1TkkRVn%9yaznu{hu*#0)nCq$}IpEw)Db>z! z)FPPdM2ffK9WV`5KZ@|Kui2b?ueCvVnOp`$$bcSzx&Wt(B>D9I=%u9y+_&rO;5M(r z4sX%&YI{R7V_L8M(eYsiLjdE5?dU2#WWJ!J=fw==G1VKKqjz0vPubJa#}K`HT)BnI zz;c+*QX?EQfI-Uycw-lz2e+(CVpmC)0D!t$?{#Fy@*=*P;FGt4CenL(xi~n<)NsU& z^CHS;^o?3>6M8hJVM=sx-(!7i6*AqDy4kguaw8T{@M-FyCNCLt98{Gti3Xef+2tYi z>m~CfcU6pgh{;w>!2rt91Si0|z>L0OV1Xnw63dL+NNn7vCJXrh{#AkkhInZ9TOjhI zcUiIpeHc?h0CMU`9DrOmhk%Rt9jx_5DIc#MhbmyN^LuS>8|A`LT76%aI|`o87s4@R zpY7TvSsYxIg2Mw*E+j(ZZ!3)w4Q`=^+m9VHB)1nRVIoohH{_l;>Y(A5du{R5yfZM8 zVSiZK?g&YfZmeSb#COZ3uH)VeVL;vm)vx-YxwhzE?KmbCAT)|XE~%dW;I8fg~d z`1Oz6#J+x}v2kqx09@k6tOb>@k}?d~wQ;o*!e0vQX$z=)jbIJw;OopW$@35a`+oRM zM}u;4+2q7;zd9BXhdB`>i&-~zJnUU%F__VXzFcX_Uhh$6U=^jVa`moW z=`)zDhEK#qtpmPIdx%gKT&-$kcnb>NCMOl;&1C}EFPJrb^*ow8x9i?y zQ2&~5pE%m5l_rHuV;v^;ZgX-y8`cMCvv)4^6m!4!uowwd%afs9B{*h`p`(5klN34r z?jiOn_MQ8?}WA>-~3s5$q;tjX#(Bg}X2gb1f zvaIos<^LyL=rx`^)(Qh95RIBLcA`{7kZ+~OC*+>S%wfw3l>xQ&h60j#C04py4Y%Fv z??s76gQw2$KX^11QQ{pJA;K~A-1NhSAjPg9#XA)88i;d2>Xp5yPmvEg;p8phh_*zX z?Gji>eAy2qNSXb_uD*%a$BQ^NElp=F&8noHb70w5Akj0OrKc&_KUJJ7K2uIsDcN^cVbS%u^ zI~H25fw}600`pWxQa*su8_Buqekac$GquTtVc=ok zI=XYIN?6J*lj&p*$~+HlJvp=~=Y>6D@vjhi7NoaLv;Je^8^^#+{N96DLbY&TfP)$& zi@bZA?rc@@1(o4H0Z>2jN+FQyyq!{8;Zc9@`(Y5yL_@O4g~HL*9n#CnM1#y)o7{eiGFfproNf4N zZB2I3cxHE+>Z~n~Er4AMc+Q1^1!y(#dE~iRkBaj^bCG$k-)DYeo(shr-WYqg=RsbUJ*4{~X3-|B^xfKLjEp?8v|e`*k_a$B7y^E& zl`GIeg){buK&k;ZHPz*5dInzsOQ2QcX_RHIbO#?x3D7SE$V?^eR3Z%erq(RAR@5%% z2p5F}#H8-(hU9PZdodFD&TFtqi{RWjQ6Q5|`Hw{-Kn2+v_aniI|HkWjGfxkGa9(?bHn zKAPqn!=O0|+Bccg5w%qyrq=1`rg7(C9k$$99DCFKrvDpIF)0i{J_n!ed2Tk{wO!pr zRfg)7an2t^8e}e;9o-q3*C~V?d%l~D_wH7Aw6ieM+6PkJ=wM?Wp-j3`$@Upgm$FqPStnRxhEJ&;MsJ8v?r)XIx`xN$Va8dGzf_(juzXTskB!^p%&}fxeg~1 zrMNQ>5i>mx8m+l^#Qh8qY{|!3k#XO1ERNAAz>}QmRw1X3RU&Ibv&^M()6a_HqPUJw zTF8n0E0X_nFlx_hhU3&qQ{}mk>Pd?*0sL2^6Z8Z#*_htB|6xg$wcxf>+XLpIMw;Wp zK@<(w1a=5MZT+_)#|LtEAmwzcv-ESu$}LH^Q2j2os^QIcFx#}`dr)42fx=)3CxxFD zW`SUp3sMU$%*ogti$oaHL-)(W`SSA^{3rwQ2Xd~+MzvjE>PPwwuZp=@t;xYb)yJE| zCw$Y&n4y!g&Rv<14qnp&?!-;sG`rzI3rC+eW1*JxU3KdVH;StC!5IQncs7x}qfi5Ddv3w8{Rq0Q6B46uCD1o%eb zY20CIQG|+u{3oQhs9$$anY%mjOIKH=+Y;mUp0!_) z)UVC;9o+y^x*F1$NzzIW%CFZlo5{kBcv2*NnW(loSn=1*=dQQJw@O@fDU)tj-4-iN zm|CHVKl1IC{_q|kiW3`~1H9CW0q9I3q`?5}k1}}R`m@;ADQm>3qm}>>I{dmtZ=Mid zDO`YjBfTxeRipC+!=V^O9KDshAIDYQjgvzY{UYmm1)^z06~+32eUT_n;uz{#CW?8o ziGH-L08;v-lj|YPwGMdGL>Ds%B^-R*LQnI=N5^6Fld247Lik3cc80imH9o?%-NxY9jLaH8-ydgw}h1b>{ zv1XRAV93X@k2ybCyUg5L3*=N3h6hy0xLLR2yRAgDlyX3si7yGOg49zau0JefIgyAb zEi-2LI@pr*SHYFs)_k`%zD&K{oYXp$_y`m?7|gA50Nm8EAfpLujvYv9LvmTAsNI2z!cSE&~W&a9EDm6|iN zkfDly;!f2wt#AQ4IWxZ-PMZ^(R;=qa1SjhMK4>B%CNWUp@xoa}MkoqL2d$TMTIg0I zmsWZ8J&Ytz?UJnLZ1#q5UtzSLHmUE?1`8aj%IyCp1OX8Np%Jac5K!yQs+?Dm3_miG zj!QF#?cXBYeG~3HBEIPWFNuORDqe?D`2Rrxq%ZOiKG97PKbxE=g2h=fWA{OfgODE> zbHNTX1^%!!j@+5?2-owtsHx49b41`e4;eRVZmF7`Kr#<^)@sBA5`JMyQ?Bs?h;I23 zMHu403v=cBU5y|>03LkpAP0beASHY`hbx_67QV|0usU#JDv$xh01}3oG7Ka;1~Gxs z#dK_jGh?b8!#Kq_1-MFC0np?%Hpn9=U;^I{!h&=6wZN!qzon%nzl%-cccqC^tR|CM zsNe!XDq-GNK!y(BPoC@{I58(`ts?3ZBzmljJcgJ^)_gvYb zudKcq^!gj0@c@}EpJFV@MbCVwO6$=_F=`Q8AK=3nDSsUs zUg4Wp?-%|JPw@4Z1I5D7(Rd&Td+Nd?Fp-~_ry(_^?%4*~itj<|M0WMk2T5HZoV&Ez zr=k3pQZ3I2r}2XzwXT}bze@dU`#L*z(uokSzGj?kAXFo1j9CX4!@YBG@d6=gP++^u zq45~;Y^}6If0rxz{87GYKv3`l`caI52f)?WL*y~@9tK5WEHt`?>~Kz$``ffL^hnIs93%0RiW(7 zEjrWCux^O-{T0n}lxwV&4B!=H2{II!TP(9lXkx6L4~QtN=EWq}IU&A5tte?A#g9?k zpbqkE(o;O*P31Bms8f zn0n@181Pp-W6&}YK>H~T{#D9yDB3ZSx@YyYZmR6}G%R*qt$IXVPh6N?6pSixpWG;b;cl%aGvzYNfsWU&Ch-?g^0MIP^#_JoEtN9lSe1xmT)2L_#LrA{|j zL^Z<8N_Ftow33|Gp9miZRoFMh8!j$qtX^*mGjO(1MH(qj@IFAeBB0n@93{4%JcB6j zEN{K}d1l=SBBf}+^O$O3R<4Y`gF43t>AbAEJI6w+qX$pn{Ff&pru@)l^Csqp1^6pm zfut00$7BnB)}?W6 z@~pMd1bhOHP`}yliF=#>*B~#!$h=gvY#QUfR{^u9v+*L#zefk)K=#$jpQwZ1O`G^n znp>=h>El1}rtA&&kPSaY^Ws0D?@@5cfreEgeEN|P5AJF!31_j5v|$HSb@83I3an5| zri*^9^pgO3rlZ3BMS<|OCd5P$(f`svV+0WU1R_#Lt?V^^L1^P>*a}Xk*wsJ=1?l{` zwEfT0AqXrg#wZ0B;b$v~Rz9F)Z83oSt(dz~iiRo4nuJFCn=)y2Ae+Q&-7yIdGNDs21!s9u}?|u zue!bR>-E}H9ENjE%9G=VFz)g27Dit$(qTwM0qX0~Q=N0KCEWxup=h)Pc@{ux25ms1 zHG6XtT^@;&@tg5&)Gdf!)`z0}6C2nYBj`x5Kl}Nd2*&S1!{8&C&`fCRhnv5lHOcP) zyy8(HwL=$x4gMcuCk~0bL@@9}(I&5GrBt{i-W_2XCqW3nZokXyd%uj;e!Mf61m*>x z39!*<`zCLCO30p-y?`pnx!SvMikzlgHgv_&ZgoUZyxLq^H045P#A|JrbJ#tD@w&I>U6+Mgt!ULeRY^ZyvkeNLi!7>3NZKw5STCwgHA5!$3Lg-~H#E@-; zz7}aHq+`E`lPf~(mB7SQ2d(~!LY6U`4WYcz`&c@)yKXGcsA3Z>m}4-Q{2hjsj|HQ@ z7WI++AiggumMlHy`dVS2Ks7!RE>4ZEej3Txyh!1NIcxfV${wDV&kCquDs!swhq~(@ zeCi@aE7>wTto9B+EXf?=p8>yQ;VK#yQ+)?4Q15#^Q)KdDH7jGyVZ^^9N zX7+;lEo>RJN+R6@gLStiX6XDsU2S4&naei09DtTm zFnbhF*9+P~Lm#G_9N&G~8#?5*Pxw(TevHj)>J|s)J8toYj487{%EPijlPiEvJ_`3# zjKU7FPvt@beTuoxj;?-;q%&%3$2iZX`x!P8?Y} zgt|PCnG9-(&Zd7c;a#}5k|86BCfK3|n!&qbQX`o0qUw=)NJl(iK@ZvAyrKa{X*=iF zY9=Yg^DyQJg4o;PmA`hhT(#~j5Kuvth;O1W+8kJN)wT7_Af*a;QbP20D1%gVF)UH- z+bJ@pKAl0@@A>C<{)!SK$)C|qnME%OFvr`Yq;=d=b~Dnz?j8_FFQUCxANRW=YKi4cPxmEyqu8_#swfZZPVP1`ZJll@h z;Hzx)5TqhNJOz$nzzQvfpsVHa)Cy*TGJDffgkiCBev#+QSzJ7COwd3BIp(0Qd#hue=DT5Sgtmi4`mFj>BPr<^bEf zsGu8d142&_-cvFml%>0zP=!Qr`J$-e3(c-g06dW3qOzR;xLXm~Z~-?syg>1p%UXf> zVBPW^{z1sV#7<%zY`LAH+5JFxpxYTPhZF}L86o1ufsFY2QDvY{10(p=3>10zw}3VY z;rt=&63cd5ysvuWAtMeX2W;^zo^}1R%h~f*2NQ2>ioxtupCb1%iEqVB9$Bcs*%^-d z-i=3V>;a*T$hB!-d5E-24!NGu}4N`Oh$cd+O3E zM3Iam{B4&I>`OhVx^k*LT!N{KDmQy6uKF1xPd1$6>38Vf*Jk2{#EmYUiHD84 z*m^bFQ4Ny&x(kTRh>dY6f1D_-(U<>}=V$NQWvDJ4;(XOdHa`P(FgLfo=0<)J*Nykv ztK6AnN^bol{^~EDaGBtZty*RzEivlV64!RGD$i@$Fq zSpAHVdBnFwp?4WZ-KErz)h{?I{w^X`qUygXqq5;W4|gFs&m*Wn!)(X=Yw<@ z7Pmkn-9eviFKBcsLZ_7=YZp#;s>91jlm6C=HUuAeSD4 zyyV%1a#U<%TuV?uI==6P8f2;9L-#SeI>pc+M!Xem_CGa36$L+a!&)t<8_17f^wshb z-sqH0A3AiwtDpFctq-gP(W+*D0iw3>0e*S?C3XJDazR@X>Fwb@YVf2mE)Ggca84ef z-N8Qn3C^>N)jKnr>sKJ^k2N6)p-&l%b1zdPP8fJj;OIXll%PNAHDm*oh z^@;QROCw9DAE8w@_w0Ylh=4uHW?s&3j6^{DrU|gU!B04^8p^TT2k7-IybZ7+6n{;H zD49O#k&Fzmz(;KzBZh?*WuD98$PceM5djbA6r0Dl;1!A=-zQ2KT_En)*J$QUdZ*UZ zf;miM-A}4ZhjqNaXBdvyDJ#(l)S{9m(rh&UGxD+Tv-T+Ha|9L43&d+SaOWmjH|k0N zth^&BVTK<8CKTansDAYC&~V+cvxJAPlCG^WK|5~*tv7T%HeIQB4t%vF;ro1TCwzFjz z%EjVvNl9HNEb<@4&V>KUM3=G)XP;Q&YgMY9Jg&Wy_KQd=-<#Y+f2NC6-f9Ic1($cM%TE}#g+s^=-50J4ImZ?8`@Te2j^o7lTLeo+)S`7W9I@*5=P%BFye1ir}ePU~+ z7Z}@AKX2NaYZGRSeAVOs{eP=nwP2afLT&%K2(u5*s3sPce)S#aI5eFZHNWK@5dG%X zn44Lp5zEDhje_7~;z5JVENpxV<@OX=7IwlT4a>POqbjR9-O8U*5Qd5MS;4}C`54a16z_r7IJ8d7*pwbe=;o%MzNp;t&h z#nC|#Rd<==>6h2a|44$IDq&zU?C9O?>jPg-o6;uq@U4i_rz~O+r^;&}7{|SXk{k%{ zjwZEX&$zOWh8e2ZZjX-+Z}Nz*L_wgiCB$vF-1vk_nb&@}?})GQ%R>t`iYC1`6Vo8< zHCj5T&0pR#+_7*!TSEHI))u8NxJ3ZGskAg{txkjhI(yw<)tC)S?m15mbcOa7SYu2G zT9(1*pfYkpU3MIhg$X5v(?$pJtxhx5m-&am?AQ!Thu1(Nf>m8b0;28h-kTc3;_brz^36W=tv#9Wqd0|x4ceiuwo|1 z%iFle!`Vbi*1?;oys98iE?TCNcFa+M8p50x7M}ZcIG{PsK&sxI*~#gSmCKR}(9ds< zN%o2zm%Yl55!iW-r^>6v+e#dMfgr1iom@j$H_i>lBrCs zUloe>Xf-nh@mFHXrTzMX*_TdKWIb|$DqxdXrxyeW#M47_nLm{>t*cLson|KOkqo~+ z*q$#iz*Wo#qUe~fd>+mDJ1{uq>{Lu6ANV~~5bIsrR+%=bsu4fmfdjDWOwN1r>F&ZE zsdWT72w`zzdz^ZLcOJOp9n2c3wvyD9ADijgg+rNjA=s;K`M^9F60TWmzu~$Hx82Oa z+Z0bTxo%OCIFqMjkCor7P)2pif3V_E;4yAWPNL@0waBA8`IX4`#H`g$sNG657M$d^ zQBuBB+(;EbBCGw`z=Zf|Ko{9-0kXv}hM4uJN|TFadm^UaNxQ^A?6M|&Rm|{z(G|%W z{tJ5FuZFN!J}SNupTER0XOMq7Fs-K51EOf$8|&D$fP~e?oHb)Rnya;!n2Cc>LeY`>N$y_+=g?E()dBK^oQ0&`)m4}n z2?@yP!BR#x0}vUk2R>i^zJI65>>se=N;a}-9uewlD8M2$Dd;-VQ~biSPjOrIruo(` z#qX?z`W8KOrb_PKiFAs0+4I+7D-)J@Z}FUH4Xt|Q27=kHN+BjUMSL=MbUec2P?i3| zQ_mYT7~9NmpM{URfUqs|4iTB+CLs)bmK@Q=6NPY<8Olzi@bo@>27vlNxF>u?!F;T) z?}B#Zp4u7DUC7g!;`yDL-D9{F{hJiiGH(<{&tG{37UnQP{nv;t^wD^+mizviJR z14!F?jbIahGaHJwnz=xN5Q>E7ZfAUNmmyFH^g&-CP&tz(D~feJeifV(^7`HYJ#x&1 zAO>6&Rk%v$Tqf|o1;ZLC0X#IgDMyKgY5U0ZB$*un{ytmxC_jUb5E};YX%r|63u?l{ z0JaZsGC+KF20>$7{PlPz(J!$CdX+M+#IFH*h}7&rAizw)8S0SYC3TK?2nzUvUs-H8by%;P{1@#b9 zEV6Y?%gVp8nr6pjc77(*rIdCUFN=s`{I0$2W5jchFLQ6N%SOH3hM`Rm&TpnhO-IKNtxcmoP zY+`rW-*a{}G9z|F`f2d|g@bg3rlqtf6_`!5-?lQ_E&DLncNLfi8r2}=r+c7;#BZsI z%YwT^);EUYU4!>t>?b>2!7O&8>?Gf3(*xzZg=sVQ!e$R+R|2T*40$g{;YwAd-F}}j z&x*vSy#-fj{{qujK9$Mq<{m-JEXV`gMHT3Ss7&E}Co4j*RvTGIsuxFfn`Swg@Ebdw zDloHh$j$KAw`)N!+wbIj#t&3ov3GBuXB>gOl@?H_g`38>! zNeM9Hw&qK4PL`wTW*4HZ#M)roC3L(a*HX5O^P(JQ2hy2Uq|?^+-};orbiL5<-baJo zQklEPrJUQA@rrfKON_9&*9)f53_h|+j#A%>dC#W%RSc zB&!S5Y4vKl%$9kVk>@0J91eyayOV8|nynw=Qk(X!m^dwNsRVLE|B|VfL0Gytxus6M zX`?jG;#RdNHA6^IO6%(PhN{VAM+tTHehU+)JtYN(D~85Z{UDDb|8*%Kl~b4^*7(?B zU{FvTVGU1UPPlS@ zik#hm&~!E_9jP0sSM7&ev_maZ!*~qMUnoA?!smpskJnx#fiL%4#Yp4G3wiJa*~tF& zN)<+k%;5XU>xtaub*U+WwF$WEBuP{ZoT;CmY4##ljXo)gQvi!zz3jQPybYti0f zr%haA@JlY2inR^UIYajAf@MDnL+snDV)_%|zw-<0BU>H#iM4btiX6z#KH0VQq)t56 zz+(pMzP6Tl30`P)Y<7yN(Cws$pt_W#D*-*R1~iiMJU(VQ9T39kmcmM*XVgu*m@%|@ ziORvcMw_NZ>3}%5!yZU`TP=QVQ4Zr*)N8O55L4FJlSa5k?SPkuhsx8CW^tQ7xwj8a z)hYW?PDUc1V#xm9U2*H&x%0762RtDQ6vZGX9z8WMo^?YV&~ZwaZs?%3i;pBYdEETO z7mB(A2jjoXBme>=F=iQt>+Qr8@k)9mwFf((cY9>ja!&k45JV^6w7)MM&UL)**r|iWl z2nywxF=foA@J+@*W=<+YrSoHvzZ;n_hmuchn_O62gxVr8K;L9u%2#W?SHBpz9r_*A z)TzV=H^M$CzGq(+Sptx+^gmAH10t<_b!Eu1*ZpIpb>Z4sVWmLhNR5GFTosvIR^Zcb zyL(|uys&i3=P`Y#-{u&zoi#oqhe1p=O2!NUQ3{xF-@ALMjK|sm9L&@A*5Eo~Hr8eNrEmb2aA+fSAa)tpG%-i=u5u)3~6AOkfs};?* z99Y@r>96|L^DfMGz3}O6DbLB{Ay|e!$&818NL-<#<@CWc-={x7ev?z)o(C6BB=%9A zBnyvd9sUBm1#ViN^Dnk~^GvJhDu!_MJ8nfZh0pm%-78c{9z+#NXqAdr+R=EIS3WSl z6yT&0C2h2bze09ZGz6m2Vlc^P=NJ@M4zhz8hvQEdueVd;w_<{WXjmcfW`!Xp+2Dh= zbVPEz(tXu!-i8_D%m-RV?#zghbbP;v!Xx+Na7#TZMs9#D)m~U2kVc0Mc4{2~+tc=+ zOSV~Oz%rGnpd$Am!>FHq?Az{Ttq;{|+~HC7omYOUsbFwYeceQHI0v_c-W<}{Vg5Rq zf3bOae!drTzAbCiMAQDX2QW(-#raHQUqP76BtSOGk-K|OMfjD9AgW^R#^h@v;V z$3c0T6^ot`zd}Yd%kmubR_PI_K_|(uW2wZawihWz`thgocaB?QZnAx1hpU&v6`Kd# z3)(+1!0l{%+2LtJ#m#N!)XRRL{=cy6IU^P3DP>grSSS`ft6rhEsj7D9m040QT$Au= z2Ww*q8A_hMmKZ-HH20sgJ4tMJFG`9+ORyQ{p!U<5Y3qC%^eE9XHO ztA9A6M9aMzq+2M-UTTcGWr>d*f^Z64gl$`Dn!?IDIx~UMj-j`1;-4 z4uo@P0Vumzr%9RNM%W};L(-Y9GOM6@@TiIouZL`PG>^t3Vw-YB5b)_W-hqelv75mevV;I!_dgNbL_`=N!<+)HBdb6-*g1X5? zdzcw*(1^86uj|3MHk2h~3}A?Rp7Q^U<~58VfHglcCVwQVZm@<7+08F}tY?CaeT2z; zvmY!soKc~p$D9HGMkVx*!IqY0`<3&Z&u9S1JZ$lVjZ-7S0O=jE$yY{ZK@dQqi`G4v z#6?})0Cg=N;BR4Bs%u7f&;T>R#7%BswIDX^3m;2Rm90|D-G*HwY#Ezy0tQOjQrx)b zdk4k%q>LJo6nU@UG9tvGE2XJLBpred!ZzCGY9F z)T$+dq(=cOl^Z7tF%Ndj9C~L0&e$m%mruI)(mXk);VUW7lGd&O_IWSzOTG=}UIhyE z%+HU82xh=OM>Mrf&{wS~0Jkosa&@cP?FxjX!?fGv_e#S6O_G}TlBVHj?ZGT!eXz-2 z?$joZSeRsmvdGP!_YbGjavV!D(~}uwu&A=F-X@96Q9(Tq-5sBD3}wFBdCBZK8crx-Vp{g4%n&r zwEW??Gd`{*=aE$>NeweF=sB=Mk!YymD4h8!8ygn|8taw@ zt8eEK%dG9rcCl!}1r^Y%HC7jGOJd?7$#*KwrYMR#@}-jeg*ZMGq8Wb4+iwo}zZ#EI z18x~e8#w#YxvmCk96SoeOJwYfF2j*K8K1$dzj=9dN zzUkXY_t?*T&8HT3v!cX1Og2AI8r%;F7ba>`0?eQV-xMHFk-Ty1V6!)ttiUm&)g$m^ z$mOy*^DNN*xrJ6o>)W$H5NQbB+|F-Nb*{uZarWWO67f}5l<8rgWc55>G8r_bjjPM& zX~E=$jyF!xMfoy8@8mXu4&xcALnq^ea!n_u6)3DIIQS{yg4QBlY+@PXaPqr3LPLSP z9<9@Q;&K6upcPgvG>BM4v!|Qan53TwQ*z>u>rJG8RW<+X*uL={!a|ry8hR*ipkQ^Y z5Q`tITQr21KDHRFtYLx>;iSuTjy%d2HLb*bT*8Bv9>t!MNS;Nak+|OzbGnbc~Jw38n*%NSg!{t8R_t>p%gzV_*0`T zC_jbVP*goWU4mVef1*bC^4+|a#pRN8RzPLSvGj*Er|{p_`4|W=$jt3O*95}b{C_ry zPRjzWvdT>6%mh7|rvGl0`4bOL8~?QDn^SG5+Vd4hsY_{cD<{)D|EkJ{<0(2)ya-QX zzGy`6CWKmNYufc8Owx=f#&EUH;i_HQF{!p_7ML^jR9ySyeQk2Up|Q^5cANe}Rj1+iFgxb|2xXvw(fJU`>S(fzL4WhZuyqQP zVwGuc>ZAAJb6xF7znMbsp{Rbt?B$q%rbH*YibJ%@%UL?!pX8aze-4Q@oz6TTzlBxR z3Pjh6Q+H@y$sLN+;MX-NN|kA1Z^SeynUXBHJJ_|77GlSR`q&m10G~{d!}tV?Mgm=% z#p>1oas8v0yu$n#baLi9XnLxfhTPbk8h)5~hRWb_d+H+btg(>7Ghe2jy*3;mlwTM2 ziv|UmV(5Q45=s8QVoaM9!Wbzt`X(6v89bLp;^Dg`Z-VH3@-(t)!IhK*S>b|$X}IIN zzHM9oHe&)rMcig6sVstdzE_qoBn>72d`Ncyb37PvDd_pkXzloc5VO6m86d4v{{6=V zZ*+2y_2$yg=!~=Xu{wSohA@WcLz-0PAp_i|6&lH359n8sZlG_Q>7LB#cU8$1{mxlj zaK>!fjQ@xC$PlPE<|XXouwxXMD=Bkyv zZ);q0X?yuzS`T^YwHo4pNH=lWs7CMd#e~niF9*!AERTQCEou5af^Qzh;?sX`Ne0bQ zNKqU0WHikx(B1re)EiTYXH&+HLw<10kN>(Xe?9sH2(h@aCDJ=?vWHb2Nu};^XH4R6 z8&`J{hXgiNa%$LsdkGM^T7vAV?sFeuRrMcEA9|!@Np5}M35eC^;rI4lJ4XR*ApF1u zc^h%b?_5k6;WmUC*Yi%n#{pm#E;L`@0js#i!;bG9X7^;y0&JlL$gOwdJC(-?i6MB2 zz1e4^$T`_2?}Az|TDUilEsyFp(BcOq7`QFkU>DixhA~ublTx#9lg-UT^&=k!1m@E3}y$pwR=TT;%34!p; z5cMK!T*7IFH8!3$?}qpa1zyj-t;=&ELoe1n=S$dkJ_dfM)7%+X=_66-K4mcO577y0Q@sb4KbI+*xC`dWJ%;2HXbgA_ zuVWb^tl@&i<#aAhAN}R;Tn~w9)6Wl94;sEnAu9tV9q27iJPIxi@EmoNlbTE9L3iyE zAQyUsVhCHGJcnAouu-TTwft>vnT3xeqD-90%9ne$D#yN93F?y|p8Wy%R;k7eiIcAz z^)e_{SKX|B&=Tq}qL;vZmBDE70~y{P43snnS9Qolp3~2Jd8NK3Iy%Y%t5b>ZfDWpE z;EyOrQ<2jacc7k1m7YdmhVEZbPR_{`TCq*7{moABN}M(l`s2 zj|dRBhjxGxBJXB5>QDO6VrETW!9z+pliEc34c)HP8J|6s_3-BID!bZXTn2hH zVb6XMRj?L=VG~1RTKT3k9m7aByU{X(}vgrFo|!Mm6(`J&JYAg%ib$Zw_DYl{Fb$ z*ePM4S}z#;rMbP(b=IOkg*W=*ommFRWDS*(^VNVp>|gA82foE{ZKS%Q;5&nrh>I&P zj&v}sl>8+z?V5HPA$i$c;gPp6gH!{zkMs=B!viXcU-vt@K7nsIN&mSi}jC zu4Raoro0Nv$>*YJ`gzy}&VojSAJIF=t{=b9P@UTrh#$C~TpZV!V?RKq9}=QRzQwe; z_@vs5{3`mERNMNs>QEyNaar6l2o_S9^Qz%0hlMJ@M!%m2-nTV?dMC#Kr~W__D;$74 zTFP?}_ht8G@6@Krdr@l#ntq3w(K%`H;)&suB0J=SUsfGl|W*py4;LbyN$(V9=_?@6nqq=XUk2v%XhnmIWS zfKP=bh}4;nYL`(&G>JtYm>tI}c3>eiJ+hN&(1<1!V$|q@@JU!^|8-0svTby||7x%BGkA9qVs0;gi0fmgO;M+Mh#OeU=?r!=n;V0-#Z#UUH0LtfIL!o zs`GY9x^!e}7y!uT1Y$gX_uJ`*IFB`PEyOwTh}BVY;N~lG7 zq6WT>))@zFW6Wx!$|PUhs7zW_Z`IZnWI9QKRVQg&}Yw zDa;WIcg81sMY2((z*Ah%^7t-v-Lf&sgJgmEQtX)*DKyR^k9$lGT6R(L)cEDt5Klx?IOciSPW0!4$D|1NAZl{hk34#cAa4=8R3+0dV8 zYWE{7_dwDt5V|;c##B5@3PfI_=o`k+&`=F68)|Omr?>S4dX)DGq&?Q8(0d(tXP zyCJ;tRVu|5(%_Cz>H&Gyf)ebS9mA;$h^CZk>YnP`yX zJD0_ZZ6Tql;)P#h5l=6{cs~km=yVbjN;FEw{-rv-poqOp2PMldff=V`;Yo+nB(6jw6||sq7A!|zcO;u zQFc2~c!7TSXcixbuJpm=@Z)r5M`jMXEEHw^T~+(#lT+8eUwDvM@+4PbNp^-J;fBPF zmQ4#)#D=SvS9f3hzO{Oj=gCWxcXf3s5r{22XYR0=tQIhx_5|hBqqohXQ`~W#DEKQM zHQfVgn$nW5hAQ9ZE08)ndx~v9O7%w=NQJEuH8lA+OaBtdMn3Xda#u{{7)?yf`FYoD zQ#D|WJc<=Cq(NUOM8{t^Jp_yN&%tb0^i1c(pQ62`v-lEHYd%@JZLYzpLkau@S8m=H zfZT0DQMnKyti6|iuZyaHTuH(7cOA$&(LOzRRk5HQ-vNVcgs7nMG%8wJC09l&y_=PO zT(|&14@8;`EN^Z=9`sd; zf<$dhraxp$H<{FZRgM(N^QF)m^N_lvUQ)vq19&)E9)@V|1{#U}yf|@qUA!*rHLV8( z`la15@%qokO+vCh-|>0_Q+aa z5NJ64O^+g0-=G$cDH)G$)$GL0YAnsK?2dX-K2VKN)dDG!!N4bNN5C>6<6=!mMVMtow!` zxOZGT9(M#N2nS7;Pp)~{yx~a}n^w8OxK{2fY(<=mm<@`TL`L^3olRg7r;Xp5G@2B% z=JWo%t1NBYm%~2O?cIjRP>7WOs7nyur|Ab$S49E&^5uRRE#2!KnQg>T=!@}ECbki3 zKCU3K#63SnU(F65;q<|l94M+4ykS-2xpZO{ygPL=4J4O;#rMlP_y7h1kJ48ra}p)O zbUYYEoL3kN^?|3Pm6^rom?84P)Kfug=R<4{-%cHOjm^l_m0Xt*+E%n#bnc=w46A!G z;4Wu5M4&RBM(=Tmi(}twdG)1=`q-I7*AxD<{7(j^DWKXvF^2DFu$_xUcAuCQ0dWM` zurw!t#`}2cCRb3nVV1jq^Eh0r`b&DZP&U7aQ6jS~+N=$dT?I7e?UX#GRoZvq>jj6o zAJvE29QWrNVhk5?rayy$w`pCuKlJ|+T<-MVa3iBCzPEH_Q1e$J50Dl>US^dAN%9nh z7k3=Fs%5s|rX4D%4ugB2(L4is5{cL1Z>$yjF+Cby2$`7rI;7@gXi9sF=*JH(@&Qfq z*j&27Cns-wNsOgjBJ*|VeQC*GI!OpF-^bAZXcmQR)By5=A~Fptx@xHS+BH8~KBD1~ zcC*YXjXTq1j_(5?=@xT^mR6JUb4Y|2F=Y*JffC1~gKM^P;m5&ay|(VonEL9+CGsRR zWaoJ-dYL_ThsP}sf<0qBK`>qn1b`au{uF3j1(YM^?wFSc4EBuO@BXOY<|EGV&IZ?d z>CL|y*rM}mK85`mpWdN7L_q$GUId=cf-d=)@z+wt3n#A>FO?1@y#j8y z0E~aTp(1j`y`Csc0t9ilr>+C+iP~3!2&#nq%UAbBN*_!x0scw$R>sfs7S}P&XaD{u z`1rPXVZ?*SlhMnlEti3u?&#J5hsbj;nKy3FuO#!WId!=eJ=MkhA1YT(#F`%c8cn>vly$cn_@}B300wh34WY}yjS0?Wk;mklGRMs#=s2^?)@ zVzDRc%OE}T01SRB-0ac9|Hd99)|6*Rj`puf4uaZfaLJKk!p@Z$@|L6HUkjoe?M?GN z-x+v8&aa;>n1A1l7z|A2&WUhEWlm93QTFjvu3B2kFhm7^ND0~2!c3S#-l~NbL2IP! zlX#;di*x$_F_pQ9A_H3i)Ls2$e6YxhyBushkiINBv(wv8tPvRTf@2)MK?La3% z#mWHH*MzbMWCmcm06>Q2L%wO~)lP`cawgI8BS8>au(?acV+P^0CBl_$>V^O=(y9Cr z3u#E8Rlp6=NX8q^Or%0F?%}@A2LGM{PAeg>e8QlImK&K!2-O6j>Ebi%-;8!MDM>Kc zAbCdIEA|I?fJ6$w!YFeDK^4H*wm@kB-qJ?5OQok$#q?>q?fbwY07Ddx`rL{hkg3$7 zxB}RJDpMln&vTNzfy>V`Hw6z{n^&=XB5CTjUwDkA73#B#39o^x&U|@v(;{t&XFOzg?EkD)YnurqQ6D9X5Qd=j>d9yRUKzG)~WCi zLUGVR0C-?Ht<3=hmF=i#(dkm>0qP8Q7M=vh#I!&8*{J`L4J5|JKF(~A6+j(D5!3(% zcf}mw#HUHe^-3(#ul^~BIN>iRpRMw=IE(-a7BJ8NLPLNC?L=(2j6@5h==E0VZKnu; z0*leF@12NMYU&0uMITpi`YD90H<}>$#gdaDX6Cci9?aR*2rZIO2@8zlZ0_^YznVK^Tyi zYfBCP!Z%8lR@|QYw}S=);@N3jS2puAAU{pbjv6I&iQK|Gumjo+HP6H=xIC7g)ZmGL z&XLEV~vhiR>e=&hNtl7cM<)pK&cNIsgvR=s-H~{+z|V~ zw(L3Tv0E)iXSk}cWZqkQ%s5mS%h#YD3ZN$34A>+}R~sz}<=FW8&22}J24UEW3gd>s1${wC;Wo^9o;)?}4=xYiNBz zn!~Di6kqV}BtE-3t6qt3*lbx(+KVroJGQZguwXtSmDEa29 zRL!-mhu%BZg59hHoLri?p3xFv*_M9C)9FR&vBc7&uP*OuX~UuZ~DQ z^?l`F@9=tVcedhIV_``{_R)a)5 ztb5uw!(@9Am<5o_Vb-iC>tjKsj7sXv>2{i;6r4lxwFrcEls@A)^34HQAP}nOjJ+*8 z?jk(7JTuaFta-S`AXLD4;-s)=9pe!c3=QQ>4p;o?+o8*n33)$pTi}|Z`dc&j zU3z<^S9r>jWI@l?M|(S{(aWlF9p04OgUyV7g|glmn-B10NvxTdN|z z^wdjf?JE{S7}YIysK;3}**KT!A(q!N$i!P{B7Mgb7EA7xnj^w*)gSoE`Qx2e&%_u2 zwmuRN?3fRnvva)&9C@t;c2QBX%MuxOTk5RpP19o2dII`({zR@6?BXSTwGJPS|IbwV zLuXB3P#de2<-TYUOEH=uJlQBLcH#(~Ct@JJX!5YbuvrCpfQ@lMz?gAKP?5h>RjVO# zmYpRem*kW5DD=?#%sN>tiJig^{JzOL5ou4=dgAN-b>7k0V6c|_#`5R{3!^2(jnJjJ(Ced8TH^( z1t;qzyBmG7GfLH-5hOa;?%f|IunbBePy|Jt)$%Gg=5VdSC>g9bOs^lQSpkODQ@oEh z+=rBO2!^$@5=h%GfERp!JT2(%bVdCw&lVa2fCyfdo#Qv+n`jp6gw>K>F&imBbYcs$ z5HoK)rU1JuPGkG>8P>jqIsfQIV&)S zq6|L0UZiwdG2@yw$u#lgyv}F4nvaFCtou)Ev3kep{Mmy{X?hT%(Y^u-^ z^z~`t*a`t=;!ZUN`7Z;|Cy(T_J}r1kELztyR&~xi%nXzzG$iUCg4H$^EGHVT^3Ic$ z=GaqGQ(%XO>P-PmYF7giI-hvF?t&?M{vImEMI>T%G?j#!Oigx|DT=U9=TX+&+CHyA+9}Wy#0F z;j3v}?}{pz5Dn&fxO*&9L~~89LsWo~=gMMUj=BzJv`^fVp2lsM$y&b?ypC=AiKB`Z z=pb%_n>n;vXV%C&9~q!mFadvB)EOc}iI*-(G+2;JhDPD4JvGd>O7=$uz;8jyDMG3< zwzN14@4nAgI3t$7-Q{SEi%miY2{kvjQay3{m&ylpnQjf+ypU4;okS1>9=l7h3(SC3 zE{n64x;zI0?JvUDw39NTnI`O4JQ*dN0j)fHtavTZ!1cAmr+Z+eJD&(jC=Fn1VliDv zesz86>}fMpA|`yfi7+$JoceSq4rGFFIBFRxqOK{MYa-AUJVHVn=KlHe{(w}at<*D_ z6tmcKLdg!2=j6<28F8Xmzo-@Nt9XK`6?Ac3Cfvj?fHq+D#ytXx)9^=erp;(#y=YOl zRMU^+iR*R&p;E!GE>n}jC^bd=(nip!VoAX>F*=WN1tA7x!MZ0kX}|Zw_{)N-UJH_&Phf2ZjAb;WhNiiR#;|0mP|jCKE!5eii> zRF?;KX8AlF%Pn|yj@Kd4!|!}@n7y}VCSz# z*!4zeQIO{1M`0pPusb_=HJVc5x(O}Z;1)^G0BwwD4^M4q z?nxm|gG5$fJ;GLO6LVuGo;@J(cjvYa9mkj0G>#b;vj=vYX~RyI ze%|@w1;uwkS0B|z5AF!+Gg zMjHA?i)CF)=6OSvUs^B-C=*NY_K>lEkNH?B$)Ht>N$Ivs-aT|zsHpTZKCiI1-4sRG z{ZgDhJ+tUdL~K)Pfn3%~=~l7QkEC7Q%PV4$gnpLHHV@yzPInxnHgtN$$D^%z8|U=vGtGly$OROdn0PqVQ$Wk%hLXz0)%+o(X4~@HPAI5hUpG76EM^ z&k+Q-wl(x5c+u2DF*2^jk2mKH(IYryod*YuuA4t~F!jhCJ(3m$?dZ5TbrcT@NWNms z*v(0f*9K+_hMFhNwJ7V(e8~?RkI3RcJ13ZjK?BBblODHl1y3oYCX~d~K|t#C-p%_y zzmfwk(0KWfGw8PAQFoTGV7YJC`X5_XVvc?zEbI^5Uig(sM@T-j12@T!<^F6-IkQ8` zX3$xCA!+Hs|JhaX(5vi6EsAFw1az@`Db|-A-%~>KtmUdJ`h$S;>qa@-7A_A$NRdz; zlTRhEHwrEmd`;}hlchj94{`N1Rp|SrmMck#2}hu6(Yxr*=>9EHA@}GirUuT3@FJ!Q zpKqn-xom$aE}ci2Knu`D>no$e)Bp~ap|tXT<$;fbgt{SYl}|m4k+d`9*j`7L1>o^9 zS0Sh6nQI5rOXHc99@IY^d|u-C?|B`6|Fl``C@s2IvQPXx9;g$k16rAG<-ygFX7CUj zSuoCyF@;bbGrl-Pw8l9Z0$Rk%6kFAdLL4@AyFywa8amiyu!4-GWxArYQi3)?mX+){3~J zE(94`Y9fBz9#k`irn5_)yf};5R`Ygxnf=eJ+JG|21Zp(U2Rh0nJUlHgi+DCfQKSiX}&7OYOC(8E@^n!kq5Hp!dZ05 z!~xRJ^k7`#7ZRb}@9#zf^5$CUtsI+*^A6IQ?m@`39#y+0CJj372%m=&L9M~S={|1P z;m_+-Hj9s9`Lsho#?>rwWxP#jXGM+_^t&ESqmEbdCv7kGw3vk^88J1wuRRxx6w7Y4 zDsh|XC9rW!%*U30(=XM667JMN1}u8#sZ~c@ZjwyC$4j ztMt#daj~|8>Akbq$%~AJ-37e+<0wCW`Dq~KM3qeAlT^uq5G9l1+uj1A>7=2fpLV#U z(zyM(;+^52nJ>rB=UO>u5q~ZNBFTTubS3`ik}}#;x-cN~{W-o@Kenb0@oHCx;?bD{ z_f-Zu&8bHIz#-#gNdpPLQTCv1#1Pmu1#8=h2r5rsN6E|yR&Y%-NELG9&uML#?$J@1Qw9baHXr&J)mrIrDQ0JX?ak%qWB`1;R4EBF z=c>G{d!x(t4;;`^H$-I&rh!!7cMd5flNj?WnS7o2Eoox7r~)(rspY7mG|e-NpvHTd zvL?1Uu4tkhtaF~x^+6Lsg(50*gieIRN-#e1p^XFqtIhrl-xWdOoNVNZng4oh4OD(nL+-9^v3_oXsGfB$$1;@&rjORX6^t#5d=%9OAO=p`thHJV_h>D{rX z5Nrn`&>Oh)QSgxBg9qBi9P*-^QAsE<0~QRyoNrKoYb1L#q2e5T)YLC`go4TBY{c7< z&}N8sxx{TF($v&)6IY~c+!=UsmFq#Db`XxItlqV!LM73xH(!n<+=IT5b^|Oo>IIG! zV${+kV+>^*&+I%4b#helmee@4X=Ynl5Bb82%@Z#OncBW=9tgxo}YZRU92(rk68DRk@EKcn+ ztS^vbWupXgw(gcdhvaR43gzGQk+Gh-1wxkV5c?jbOSY5G??aj~m^MSavyxhST>Sww zl}gWa&tpV`dTr^t+*3}c%k{f_^C)@^j0p!9OHYJ$G|KY%Nb9xTQqC0Olnv4|T;D_U z8rHMaD3YCpD&FybWmJ(GS8n4b4y>BuFz}<%jy~yf_+NfI?9BVfsQ)tg{Im{sxPVTQ zbEj45_NNE#fzeJE2#z&?L~kn-Yvra%!%|T=j^~9dn@J&aR zd5ybfYJMN%{4H~(FA&1iPH=m6W2J8OTgazxdbLvX@AKA{1uu zK}_B5?sS8=KZqs3sQ`@?^=|9i&i&3*`zasOc^9B2)>2^_a_4SSUap}f`RfYRnBuxv zY_@y9|7p*!ef9z1e;4q`abeNi7Xj%!9WbqJj`Mboc~-sUFp-70Lw>zR5tTc>$KUgGg53tMfs@MgMZs%6Y2qEnvc>%o+fc z^VmvxqVq1l2|aLkLVIR?8WVfWXBpmj)l#fFrR{;)cqz`Ar8j*4a>@*6q89dOByt7V zA1Kb;V|6&!kel!93j#kb_A6D@2k2NM$rBvCslAR>2s=af7uXHPjXZI#|Z)THR5CE^x|0<`cn*jQPLu+E;m&~G%h2GCpcb-WOYo$tZuXx75T=V5T2Jy_xe;FOav6 zg9%SwNqd@EueC~QMgV|R2}y2$*K*X_42%_@A+6R4IKML^Nzw>{ z=PvTb5OIL>*#My4-hgu)vE|lO12_Q5}yjzUDq@6wc*OhG6@x8tBRh`IwzJ9o3<*i7IWd)zye?PPbGMT zJysL{6dgbW#uZadsXGg&L=AwHVj!X7OSE()$CsQ>bW4_pMjLe=)U2)dsDMk0A}_Mr zCS4g*Y{etu?FQg98U{w?`g8D*u1!r=^>i5`>u2K593laJ$o@;!?OPmCX=+`6?&mY( zvh2Dg8k?o$ndanJwzLsUH`(cvUB|;GxVig&1(*z1OrnMQU~z5PZqvqipxOSHX%jNuZI(PviB^CKa4+{_?sjFSFLn|QsSHg-szuz>T0V)C-E!cgLz z!n)4+bsRJF^4ZB6*1Rn*e%4i?7qJa=x@D%c0O=Fr@o^lt3gEw3WH5-+4qP$xdJl>Q za2v@?Q;;m=U(KeRhBmgQXoLKQIRK{X?J@Dvj{Y#9d!XG>VtABR6=*PpS90q-h54C+ z3OQ3xak#Ff05~#%@xVgrK&RE$0J2#QxdjkkXBjg^b?1pzGnw6=^stn0X1B_4wI6|p z?u1UBHk~S#qZkc+b+h0`Y}rjF2sK zFz_7A%%J)a;q!`ph`$m&cm6yRHA^nzYPcadg%2hBWRonL{drMSfP&YP)Ws^?jds;= z?KxcHn2aAn&$xgQ6{CW$$d=#l9ILtgtIrqd@|0$bFt?q_n8Ek|MdT@^JFSvHXx+e zjBw;D-M-9dK{N!Ggo4XCyT}Z7M*9FE+2LZGn28ko3DsI*I1XM4zIFwrr1uPP5)Ww! zG<-0Ltn&uyMUoltua|Lht0bPr#UEiK>KB~WBamoL*jAGYpk>~zrUaU3KvW9o`}kzP z)7G@^cYhYU@_E8_2&(sSGgLQyo#IU$0q2z4;lo+Hl#hFK7k!w{neJs(liKA}P8-QQ zrxON3DyLh|CD1$Y!(4iy4!z?E(Ee6a4e~J=B*H zzLGIY(A(TrldWxas{lYifzMQ~=bF1fUPnN)DnnZByMtWI_M3r>5}YqC#cdHix+&++8u*ag``@fpm~ZMD zi~D(`{bs#5=(#^MFb)TCq+JF#9vq6?T6}4=l29BM)^>TqQpf#s7o2MnJ-{Ud+&^TWTv4-ivznmzwDmsaQ20; z7z@>Svifs$@t=eqtt=4cZi{`*z~hFk;m9S=`Q&=@?(W&a56FW<=JpU)kp>a z4;YlaA#w1Ed>(Bk2mwc+HomB!%NC#TY}9tDhPsgp6O67-gKI~zNIm!~=Hh@9UMY3X zW2$OQP0QBj57liK0Y(%&Z?1WQvDNA$yb1gn6Ztjwd|8vK( zgzKCmA`B~>jTr6=jgwZenE>V}$Y>O60%B}1+|ZG81@mlp9~ABR+h;0_vT>8 z8SZqkO^D?5(#r=MEa;YW$BWi3T5Az3f_u`{0o$e7P8lr>sFFu2@ zxl()jFlat+T_FZrZz4QL_oPkz7d26qaaQZo$H#QU{p8jwS*SwjN3;Qr)f7&q+V71+ zjhXNkgTV~uUe_E4Y|<#D|Mu|%-Q1$Hj-^v*;{9k6-GX|I&Dz!nfBZxO;Ow2M!46@> zSyXOI6qo?kQ2nVB8j(u^H_mt9zlU4M_#6%Oe);a@(egB9X8yx=5)<|&Lz8a}qRQru z@prA41vY|mZ!)}tXEPI&y0wVghvnUNk1>bUFB;&o;$u!4uj3}O6{#$jQC1|M2JGl4 zlkVT0@vOQyxZawln_x%XamsMIw^=Nn3u0)z;P^>H@QAs?G18wZ!rEhsNy$=~g1CEH zAy|$mE*W@FFzatQ;2DxfjUCxVLNvNgnO&{eT)#0WmvP9UuIAp{*GTN*0jhf zkml4k7A}@0g8tHo!PUo=@cVQ{MKLpY;P|ipvX30Y)d4}Bf_`Noa|9Wl(3?UQIC0LP z%Ul+tS}Sh0HdqR?%i@iVow(3H%w}RoF?NoGn_}@Yd>-UBTzKT%rh6ElqR3ei5VkP2 zMer&bAU!TTbR^m#PA2%M&&=u)c}FAMg9wI9OFh0G^!OtPovEbS8>OMN7%=a5VlI$f zLFu&dyR@2KKoJ{f^GhG@C>M1k*TJCPNOTu5U-!1|uLK@s%kyYb#)?sslQ_+c0*Wga z^N<@`KPa)So11xJ9T6v;*Jc(?9H-K96Jr>+U;c(&!-E%#;qF{(XlGK3T7V8_VGnUG zl~K%YOAP;~10+TxSq>}Q$*D)<=&17Ot>}J_giTruT9AsPMMA(GVJIP#%))hox@!Cc zTHdR8ezWfpp==Kwq-`^H-H>F)dq^?#6)>7^(Cs&x;pCWn`6ihP1Ujg;sp7grv{X{_ zjP**i_8cvd`64YJ&kACLXK^mDtDZl*!iTV7@3dyA_?iZAULQ;W8Q-ts0#B*t;3nn` zn)MBs*|oc2*a-2L%nh9CmPtQSJ?a_35YDi;y80O`pv)qS-!MS_r@yq)9^u>I~ojQZqe0_#VEd5Q<> z*X-8|)yFo1JmNL${eeg?(y&xWIQ2>Pi8X~{2HR9%=jNDP=EbkVov*%VVt`r3b|%H& zLBl4k3=z?chF;@WZPfD;W6X7nomt_m8lhp@BpXx(#t{@Ye?kIO(%*T8<>`~><7tFC z!r(G@T8v>HIE5AuYI4!_&~eVic&Ke{(gkL%9(3Mb`Lc>udy>vc&?A4;s2?y0WUs;8 z5GR=LCwixbkDrd%=td-nG<_IO*RiKJw&OC9o*QvonmDTElqsth{R%}+UX$P?RntIi zGH^#irxy_7oON^x_}HkvZl3)5fW%P|+_IA{`5~S37m{sX{rAGbTO&A;33M7otG1N8 zJ#&FQ|8R#TE9DBphgqv!ueOshCNS8As`yeGmgAV}z`+*%+DZ?WvW3fPw=v&+BvKw2 z$7cj2N($fTrfKg<=xMwScfXt%z2OweJ~y)|xq*y~#wf`9ZiJQB*U;OJmNWj!?8M0X zk84s@+@(8k;-NTplFoV!DjILCq^(Z(Y8REO4r?X|@w%oI_#xZC!945zW3Q#H$ygOH zQYyOu$IP0=Ej!HMX`+~D%4bJfBl+e+C$7U*NoqF&?U#(YgFs3sWPsnxBGs1u5U2e} zPS(k#__`!lN?Y1+ZF5Ydq`(fFCzWyf0q@5Or4xVHQ*rJ$T@>={MOcq=<-(kn&!Q=L zwv9qyl1OuJ^^Bc)<&b&CKnl^=Xir#syj}+MbG=9-gGI7XAPK7^0JU_)9|m?|sSJJm z_8bqV*~2bv?f@C^y(C**PwOagk@0A9HDLz-alfpXnmNB6V*9uu&^60LOy-i^sCppqE{=i=T|DB>!(!D8Lit{-u4oDP=T+<{!m+GdcUW zh~}D&(vbQcpVFkL$UA6Jg2d#q91S*qRxi7iIZlB@4J8zBLs`i{4*GNd8bZEFZb#G8 z6av(ZOp+wzEjfoeW8HNSCcWh*f%RoD0QzGwCk9CnE-Rsy^q*G38g*XU+4Uiykf#f2 z(7fgd(aWhc7JuULALb6!^*G2&*+M|L(-AgX-x8w~(=5SK5}0GP=pvJ4pK1y8Q-#(7^2J2T(Yx6CRPJAPLg?nO`=r+!eO@{m;xsc#TpBkWk!Spm%%Z z$RU1seys^dr~zwPKH>B+u*nPqkm^e6pFY zOXV%cMF4El+i6gwFmJCiZXQrx!*!^zhA!iY2bDp0pjeA8Nan8FswY3xp7G0hPF_(5 z`w91O7tehMo`Wh%!ydHxqPj`dEaZCPzII0a?sObKT5l}aIWc8u>_p3@lzLi;eB1E= zZ4;Wm?WlpoBgX9Gf_w*SmAZHh4j1u93T77ur#RpcE=7&&JwB^L&7vk4@IzONdQLWc zJy4Tga3NhJ{85p+WzxlKcWZ_Q42uP~^bn~0THN4bznUk73%--9qH*4~bjsLnO8NM2 zL5DvM9i~>h~2LsH95BCn3;(5exT!?g&#g{+~)!xBljK3Cmlr1|#(=5pb!g*v;Dtu=3)nMDMfJm;?2 zsO)LBEb=FCJpn(m2m`HCWdU0ytM2JDbj|>#j$~-ryqww{Y40hYcNk9_Avg?b5RV2D zACa(R*e6B#dCuRBl~vKb=o_s|0skUPTalut66gq8&{B28Y5QOIgxNn69o9tA)ZBDO z8o*xvX*NxkLBr0jJcMtY{eBF_f4aIaFRI2ixnqdA%Q>qjQB-Kx>~IRX$k_}nDj#5n#0Yi zrMXZ+JV#ZOYoUqA-O7`SF+Yv9q$K^>ns*^h^|GV+;ViFRMY`6chFdI*W}@Vq43%6MePfxGI^{`i6sRys zrSQnITR*MRUWk}exE?$}fFzX(58uzD*UW1!pZPsX&#tI)L*K=AbIxcyR)Q$M?Xm)q zK)`k1tS-~l7sPRTFeF3i@NNm<;DBtZ>P7&{^Uf`JPlV)=k82rL0xG<=2#0x_OaI~1 z^Svp<(+zwN#+Je^-y*6z`!FRLpG|}Dm6}F9m5cN)`hs)Uu`ZykJ|w7icMo`(RwBI@ z4^VO|Vg>4k99yID1mxU72jSlLHJawH;4C?y{r2<2EgpX!@72a!5yCPHuJeSy`%R06 zS77f|FH-pcHu1tsR`gEBD0!TeqlR(>A2rq{yn~2Hyf2Bg^5e1uEzH!xArv2QuNMCu z`Wq+&c6wRrFW~PKUqy^M1XU2QjcpkW@dS2bk*rh`|4=$uCMPM)K26L$F#&Z;hT{4- z98+}|-a|W>waRMzSK<&ik4Nt|1N*c!*T^R7mi-{$lT{zEKuGvQ@2Ey*J!a$RU);Bt zyT)3#DmZP1f|tO#Kn&p0Xvs83g&NH|0d}fAq|I_ZcWwfnhwNMAIDpi#-SrYg?4fs# zjVyOZstz@XB(`0!kSgtM5eayKLKtVllnTIx40HRYDa=Nc8ggaN5;%e)a& zU>NSz&Xb5revuFCt-n8WzcM=kfbQCawc8~1;3Pi4o~4S*az+i(n0n7VKG{E~Zrs`J z1$uexb`y4p%RH7YN_e;p;E#i&63r>D@pKvC zV96B#F@QWUeiWf^n#rg;&A`J-wf1NhYk=@pbZWRi-Wb`*8@I?eiB4zAVh2jGu#%GS zn>!=_gPk|Ofa9EFU~GXf*e8zhx5yK_Qt}Zox1Hc)n+Nu4<-k@A21$_7E5gS`9Rt2D zGbV6HSru?g18uHe2U~~A)k+(yi}Eb7ILC+r78X;U?i;Fas>{)|A}*$2G*PYzR>~{6 z9})Fl>&dFVX5`&Oi1bO`3&WmWWeK>T2{XA_x7P1K9IfBQ_DJ(I)2Utc46D4x%;&-9 zEtYGl5F3bmkTZu$A9UNO39iadH=ZD)}~YiHxEkj;?pW2i7)y+Vt6v9udtmjM{X zV?p14GD|Jot&heuXZ!B)RZt5keIDuq8dvo9@%T~*2b=&_X z@Y1nlQ6WAEO#)HCUp{8Z9iZ)dMQ8IP%Rd@}ciaXQ0m7F@`XZ9V7Uzo22rC9}vUAV` z6TW`mhWgG1g!_e@zA$yG`KL?vrb_J1!kHJtW_f@ZLxd1LL=JcUO%fZc;-4|*?;E94 zmfYv3#U(FX^kZCm7u2>qOXm>S?Mb}+2d68VOz5{rx9r2na4+y~!b{gUh#z`SD37+Q zmOff}SADIj_JWT8TvG7eki!j%emN6>sku#0Q!XZ@@9Mv>wv9sPCg0b*!-1Z1uKgCJ zv|SBc>Tk$UUF&V_k|3t|i_?fD1cmVoIS~599vT%QE|^zM=A=L0-W>V>wkt~b+eew) zvwC2l$6`NyS<7f6s_*-as%VN-U0ba?Py6*9sgCrhEO-Gfx%Ji2FS+x-*YBnHC3T>_kWldoeAC%etHa4vPY(lRWx6@U*v(4`nxViCb zJ;p;MowQE9X3Y3aMNhszNKW=&9oZefj4g zf;Foz4p*`b6f-ec21%(o8fBJi{c}_dl3JhzLk>ZLPH>SbbKE!z6o_GtM&YE{Hs9NQ zwNdQ!jFjWX*JCgSCgD5Z)%p)fJ!Lb;qax|9GuE}SZtI&-ciKHu9E(ZQ?D|i_t`pv- z?WdIc1S8fg#g3e~3fKBQ6nhqDsn3PHD7E_$>pCMq9)`irOKUfZfJ8nl=UyyY!mVH|m9=Vw1F-Zy%sWtFlyKp5ou-ft4iB`6HdNA) zt^!UiW*blZjIikX@S<<=Gf4T1-Z-B2$6Ut75hAUCQib_#fvLR?YilxTYqEvO(Jg}$fY+W5@EgYq4^AobX!q3yFC-j6C|y0?_fooaXO86f}njG9;8wB__jV;nGv z{?@zc)qI_HQWsxKM7TW#RZtVXF8HKVuL~igGuES7;>5W4=A~*|T|8Q| zt<{(m{4k}1sa4gECQ&T$JpH>N3D#V2^5i*l*#2}=Z0y8*j->b~eVh~E>{%%7jFHhk z%T2Lx^3=g67S~xCSHtXe$Rd&}y6VaFY~8t`;n`jgGW`O|G@ypZODsK?_U^5+bF{(_Wj16;vz?n{c~o%9&blZPkUc5!-g zP6iy&uW;-_QXN6(3#&XnaSB)94ixVBcd`a^;C&a66n49lDFKK_-toA1l4kJCLa{E+ z7RTo!w@C1I7V!h0g0-^6uBTuWEW#m~-IPLOcn0v2M51duFWDP3t>p#48RB~2b2(CY zk^)rlnq%lppJ=lGF3R0xvYnNIC9p`(tQ?kysa)~K+}BjamTfv&_MI_brxT@c1B=0v3d}y`F0%`>II1Ym0e_s!erZ9&jUzIHdi*QUE+{ z&&0Z+%z@@2A3J_21MbJecUwnjI3kw^h--jAJR$E!N$JwIYj7TipXF=j#evLKPUw1u zpzU5_!Mnrx0rbrO!4L^;!Z=44VATzM9c!lX;OSrGDI*y9!-d1W!tWOhTw4FCtxbA6 z7P`ZuDMkkGRa6I^=g#W0v!Ac4%HocYT+J-3kpTj>F>Kb$nD>qj|nB2-w zaNTxbr_INH+iQB1br9ZV%c@!}o}%v5Z?naD{iH7xq^z>DErHNj(91b*hX=QsQ4$6PtpTJiWI5+f8gQUHWD=0Nx56iTd}Pxlf1bfGW-xHX69HFz_OjDWYA!#2Q0 z_=#o(-$?Yu&DFXzWV2i|g+0nU+=|u@H)Qa#z@>oZ=FaJS@GT*HUxH*i^tq;ZjOpg( zA^&!c4DaRB$3k=9IcKUUbh=kve?eGA+hCc2fh3jb1IFz;+68+6&49EubYDH+yPN0c zrMeN75a03Xx%CZR=DZ1#8LEn(?mRp`S#CZkg4J*0KSw4v7&ha;?QR|AaMQYFcg!Nb zvI8q=Z!lidkQx@N+}zujfnQ~`J2_X{?=4$4^s?SjteB^&8p!$`aj`#| z2A)l#m%(^|8HmF@XA3QzYG&JO9j6MMkmQ*3OIb?5$T%Hf&J?f+pEL1KYkfM zmX0*+?M8|1!nEJ4-furXWyaya1aKKQ7NqSFEWK||g0HA(Sa-N zV^%QYfW8DhW`m6oQS*WfQ2Cj`!&@_v8D)n27pLapvm)rh@8Hz=Z@l8-`(1)?YwY}{ zCOXBPAc@G9^L_*Fd`<-XY88 zl#WlIlz8ll7Q+x{x$!v^M#klwV2~mjAwk2KM}xUv$z`By47-Lu3$6~9<36t&S86pj z6H{eC5!C5Qh|{MX)6NEi3B>)+V4KIhHZP7~fstmM>dGFWQz%}o;VW1ncKupYF1)yfXmGxe|OMTP?Hsq+yE+jfyYjDYq`|p7+xKs zD&>eqQw|*uMQ%6v%Lv(=L3{&i;B^VtgqR7bh?l$3`=)t7b(A7Z9og|H#?)XDR=y2u z^ObTDUmeIYK<=-qFS2AVO6v8{wKY=N;55WmM@CP1hYs>Q&aRb-o(fShMKlME)|*ZV z0_cK>c^$Q$2nJsAjPE2FA1fA5y~exE0-yx4v951jIJ?B-Z4m))NCUsO^xGD0YWJ*r z4j8}Uii_xodNYVc14!}(E(z*Up2f~%%$jY0RP+jNO&fLfj9soPG%e#Och@jIX)8|xo~{P@TYaNZcHEU*`#_?!a^hrT11RNW zr@G`(mne)}B|t^-0*0t4E}9QSfKz1-9o)6E-Ahh39yfWf{@ze}k> z9HBga4&n6GRemLJbi9w7k=LjRAX3nYivQY`RbIjS!GozA){_br@PUb&f-sSMq@C>o z^KP^@GJ+l7tpS9~-8|&MBw3 z4Vyg=U_<(~S_eykPgh4#LgXSg^xG|<%YA*p$*U?Y%$sCV+@58|v3AD3kKQ5HG*J#h zrMwqUp5sS=l&{WN>y&BIJLNEg3+Iq*ff75dC_pS-#M8jPPUZJICtvhpR+SLG#cjS; z2Rvs-eu>{CPX%7GQ0{S5KC%w4;aapYN~T$vKXs3@usAdh%(r=gC0=Gr!FMD{0TC}j zDlDDF--H-HO3b_!p%TR6E%Ei!W<=XKm{DQNoy2sL4nxG{kGPvHt`*mXVlI|gfMoRq zht3V0GkXJa~Q!$^Cxl@ahEzHZfSL{x915X^9cnhH-#A6#T zXdZ5ug-)v?e3NhvuaMSD?3g*z62nJ z4QeRr8VyhHDf+5^3>TiMOr}ux!Tk?8_IW_YI*Sf=izjX9Q`Whoftj|wNQYXu;c_Sf zJ=0(u$-TzGDTM&KTotQjjh^{fZZzLib~k_aWNigF^{>Jx86IhM&vmS2_v~%{kd(L3 z@ibLoN~a#~g+FPBxXcRwNnt$iBOJ54IIQ-)3U(Y0dRU54i(#r5#&-~v{)k)n#%_}e zL%K!+N94Mm70CC*YYY8?PTcwM&$3T_CSLsjp=|lCEVUr(ac{wK<;*r?WS8p!mEt~b z@r~BK@&G*?hUl@!NyW|>lq*H6xjK0cy5dG*_50<(KF#Ek2uJP!ND!U|KAKm!I9AAF z)H)0tPO|)sX;yn@ULqvqa9gt-oaC`|T$uaT}t*w*lfpwAmi zq5N{sb$x`o{I(eMC2WI)Ov6d}ZMz0A)BTkmEhC7m z?N-Nz2$@^I387{7(d)ZU{u85s63pi+U{lkG5`E;rHVU$RQ~>!syA(RpImqW*RY zwV$Whd8ri!jTpi9lH*jYll=k+ga4#bEk&<3M)d%_yC#&9wVip>uQ!s{8kPOfCYK76 z5@7w`3o)h#ApYP~NJmFu@+~^^qkH-}erT>YZvJ|t^~Fj=i8V^sps#M9o4{_H!+m`Y z?e{;e2l6rU`#OZG8YJv>|NlI6=xgH-D%hcn*jGmH8*D_apRS^G=?lWDyw#@W5smc^ zOVt*gml2#y21bbZ$yFC)GgIE+Gq`TKRb8ZotI*eE30I;3OLlkoUZTA9v`dGC%bUWY zMQpnGmI{QvjIm7&w&;9^q$l;aFHWwLv0#Zld;4?E4Ql)(Vw#TMP*wvUgZsL`LSBf1 z+3@ah@UW4~g({S1)Qjg3@{c#hMR=sOiDBAj3k)LpYD<{7B25O%XA|8zHjDp>(?~K` zZ^--kuL=&$WQ_4e!@qsqoenIP%upF1BB~AJ^GKdIHmb;2$gJ$5x%wm@*I$O}cro2x z(Zk!TukH(_4+3aEUDUjn6pG!6c^X%a_HGW;Y`FACm9l{Q%HVTG}qFcV7Ra_~C3yTWQH0mL_f!kj^!*2f8-lrmDiR#&dlYP(mLC`)yfRyDl7sZ<0p1B3e5iCO@W?bWi)0Skqv?E3)?rb#8=qeW$ByP zairdKpE7GCq8UEhsCaqVD0L$s%0c~temP|^7yU(+0?(tlLI^V1uk%$B&qZi;Rilai zJTKa-@LT22!3D=Vx?S2x==YO>Wr@X;oK2Sj@7MEJeC=^nwoHrSQ5uKKU8?OxRCFe( zx*+tV$S>&VS7H}O2R>|&T?5E?_I$NRytqWws z434?ik4fWtAV#fg%A6FQ#Y}|t%HVaoi)t_E^1#c;PVE=0x`5`ug3szaT0kfDftj8|S+uukLL?`Wbe8_Z)Gv+IHo?#2`xvB(p4UV# z5g4@T38Yka5p`>s8A!T(4*RL{tug|9$Gtzsa=OfTT+X zg=_c0TjVMJa0GOKyn)@dPs%_|If!xg?W_!b=D|c?Q(w5u{E-6gf|gT7hXAsff)-rO z=<;jMNAvg4=BB8YzG!xjv%jspZ3<4C3ppgIQLz@xqG_v9(M_-YKjghaF0Ow(>-PJ7 zK{A>)u>w9z_%-!(_AYbB`NKC|R3)3hs#9S@h zPk8=GBuyG27w1AeGp()I@AtqY5&&S`A}9~)c{_~MlcPpZ|GCYe2@EX@QD>qP$00>^ zxjO8(hG=|>d-t18!V5nk7v4k}CMqlWn(Eco&`1RezDtKGKDO=4GzFPmM>_wXIbMP6 z(UoW-a(r-$H|%|4uDX9$j{V@n_Gl=`{Myz))CAH`6up|sB<*JdfG=%STpia!9knfH z%1jkdF0yx=dLKak={NGi(WmJgp0LE*E$<}yfG=|%Cu(Iu>vhI03A@ENq~;3{+wWun zysr+`g*mMn1|7M_6at?nMFDzc)4J#J7I=o)A8a^_8}DQxKkWV^YeaTN4;%m0dxI4# zk~<68D7mfnQl^o=Yk@z{BRd*YVML0xVy35CmAN9(mm!Q0yEoPhf8F z7`U-Fn3YT!lw~``_(Dnnn zUpj=j{tK^ip4E|R=wOCb6qT9VEkac*Y_Hu1aeM6x8dotX$)#?rhs;rwX1q$uuL|0T zol%k|0L-JP_&~36qumg+AU(pha3~OBPQN*s(V)m0c2G zVE}Db%@M=mF;)(*`9QTy$#PXZ#NZ@daUbb`PNNHwt0ZVJqZJ1(l2LVNS@2 zS!x?H_}&2iCd|S`2GNUy+!YS0Jg6~!QUl?dmB6sbIzM>8(_EqeB^fFZCb$gdi{uAw zyHR9*Xq=xzAASB(JeOm@((a>2#ZKve*ih^KTon#x6epS-Td0=C`_tZ&h}O8&^}|U| zb-ar*`N&p9BnZu%X}dZ*4i?a!tRRnLFr0JeYcP=IsiD&_+-{L7%(6qtEjux~DO8ul z^^8QZ`#Gx$#B6Jwr#-H3gPmOQ@H(R}e1xwK1BO>Z#u5XJU)=V4>)59R4={m$^=BWT zz!UMQ@XZ@+G>X}!c51{+PjK(K2(N)E|IPUPcw6`!6|P9AbMBFxy+-?fJiB}9&2`;7 zJCxv4;co4t#u&+_{BpcQLA-t^PoIt{G&#Ux{Bw33^ZRIUS*!M#^O>V+)g|0SoHTnT zQ`ZLdb@a<1TFNmsy{Ki0lm{O;`bWwOl?EY*(r|9ovHnB z%e6}P5A@KlRIpWqhvgz1)TY+|CP*!Rb>}Le4Nb;c8a~k)<-6X)+;XXnSDYNr1te#r$R}a!zMF-NTc51gD8>D7Csv; zj;*3J{mo)@oPxHE&XJr8OlYPyj8|%#M+1f7QqYtd1%E=H!FF+g$}fY*SM>O68pCNb zTT}rir9luN=r`8Xoi((AY@=}w<}!3?<~~(8cWB_ps{rIfPOp{tV`!bVfkJWTqBk$& z8PXh;6x>YuB84f(Dwd-uKuq>u1kSwi-{p$1QShh~ZMh!eQ)Js#>5>=~%9xSkT~X6c zQ+->+dU=(1n#!#!3ZE~mzT3gT3_k0vm@Ecv^GD_VrZ~~3#rq;!L^tpc;#(_R=u4&qcrh2RY0By}wb9~A>8tW*P{WB@&})Sr68wt3fs zJwOd@SK?nt{T*>Hc0$L5cF8xA-W9{VxOZV&-%QJ>&JpVe z?o#>ZO6-%#s8fs`(mZjgGr{%VjS{PlF0I?D|GMn{$8fnuTz&$V=xg-nu45H%S#a1d z^W1;ePTusyRi#SqOO6>$!*p1?=ZbKSOO;2w;PWMh*6?H|H9LXQZwVtOL7Wc#MChxD zlJT=~NmA()2# z0sfANLm%kj6qlfUg8;BWE(=un8-GecTO+aEtNN>4&`uK%>OjUV!n157 zM>@Hsb$s}}DFFy^aWZO5C4=9pk{@ds7J-EY(3y?64WTq%)pad^D)bekk0!?9$oL9w zSs!bez782m3s)m*Lj~6mQ!7RiN+HUB)2I--NjbL;NE!D`vQQUlHs1KG)S~lYb2>_(~h>0`uswr!s%z?dj)!~w>PU^ zJm2c)-&sFhN#N{;4w?mjscL9a*x3?{wohOEHWO4I|8$+_)gVLYnX?*2R63n_a!-k3 z(;rzM_qZ1bTXqCRyws~tU|#fc-yZD>EV3@6lRJS+Jd-As1{Qs zDAFS>`#bnjU8xP?%68NH`L`nP`fU5IUaNMDX;O`U8gd)(J@&GgMq{s}fKfcoEOc@+ zjk%AkvB_SwxtYYkEYt1I)70a>Y7etQVMj#f5{s@0s7XAX_p!DLi1D7;Di)5CZ>%!qc8DvW(+tog<9 zg8(x?%)fqfsmzB&j@RHwp78|Z>sQA(#M4fkzA|kUM{vUiktQAzjsC=Y^StF_$DIF` z8^AxZSQp8FxCpo6<8HUN_;IvCP={UOPlOkWMgDK-nGbi$<^1qVyTCkZfsM5_bb|gr z=3if5{H@Vl|KV0j2r)g5XqPZ@iD@3LsPJST&7m)z&gKGyk6QAxLu05G=SPZcFZp2R zu=-7D6lu3ssqkKYV3nm08RY*O+_Y>048*$(tvys{;OV%(TsWCHDDvm$YMNbR10b4D z!c3N)b;`&9VU6CwR|6C1A&Q9rlmpKUJ~5pCEv$vQ=Dvd5M$wJ}mdc{$g$2b6Lmp;# zyf1eRr64ZnO|UTQ?5Z$vyF?zt$A#LD7iZDCn?umSlB@lV8LSflM-`3S_;<<))`7IK zCJNz6PmHRRY}~d-4)(TX)!=`_N3Zq{f;{S^nCD{u2;y?rxf_9G_eSL zJz7j)GfiLKu9|w%hoigu#h5b^Aijy59s~2hizhYN{1@8|al6=Mhy#BxGVk)>31);0kNRc4667mQ0$N zr}e--QNF9aeizOd7wmw!ZTYd4X4x)~K~WZwNr$PEY3TJ+l>RutsJy?d%$H4Khol?R z$5FE%z?~cpE$`x-a4_MbQ}GyvHTTe^0KNQ}KW2Nk$HJBVH{4*fcZ~~1jXz=~J68h; zIrS%d=w)o{=wBLFZG4}I$cj75?=V$C-K%8h;X-LDdI;od^QfG+$T!tCkV0wTM3|Ef z{%2xz$x?U9qD@!$thE=oKs)=IJ88x)>)r>7f+=&Ebtn|imG%8MjXEl|Ij;9^M}7|u zmedoEH?&xUQG`R;H351tb!i)Oms~VmA0}C@UK!t~5WTJhNu_=$Y&U1;S!K zFgs)$IfLuSkpU>;SiU+?6%!1f9jE8aVv_gNX>>23eHI?q}qvkg#dC=Q~#lKkdg zIt^A_g?DJT71ju_r-j=5fU~i71%_E;=ffk zv@e6);mD}1OCGOwDsbmb8oX4ARpLY@x~xLQyqB%^#sej^y1{kQ+vR3o*!VB?3;osQrA9fsjr7optzY$n0qP$)Sqd62ipUw{yN_Bii=kbo z`QFsUkiG($V47ok(*$#3!g@3cv1Z#OO59D-i6PiG7FUTQum~@TiVkn)v*hK}8esG9 z2kpMF62p~xdSpQ)usg|kv{{wNci%Mr;sbWKNyhA>ZP3YSlktqI=%^)Zt;^ znf=!T2_&+idjsuO2H`<=J@UsYRYPZ^2;p#W|S2_4DPpRE26*^ z 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 literal 0 HcmV?d00001 diff --git a/src/pages/post-page.jsx b/src/pages/post-page.jsx index ce67baa..b505096 100644 --- a/src/pages/post-page.jsx +++ b/src/pages/post-page.jsx @@ -1,6 +1,7 @@ import styled from "styled-components"; import { colors } from "@/styles/colors"; import { font } from "@/styles/font"; +import media from "@/styles/media"; const Container = styled.div` max-width: 720px; @@ -10,10 +11,19 @@ const Container = styled.div` align-items: center; margin-top: 70px; gap: 58px; + + ${media.small` + width: calc(100% - 40px); + `} + + ${media.medium` + width: calc(100% - 48px); + `} `; const InputSection = styled.div` width: 100%; + min-width: 360px; display: flex; flex-direction: column; justify-content: flex-start; @@ -29,6 +39,7 @@ const InputSectionTitle = styled.p` const Input = styled.input` width: 100%; + min-width: 360px; height: 50px; border: 1px solid ${colors.gray[300]}; border-radius: 8px; @@ -39,6 +50,7 @@ const Input = styled.input` const ToggleSection = styled.div` width: 100%; + min-width: 360px; display: flex; flex-direction: column; gap: 17px; @@ -95,6 +107,7 @@ const ToggleButtonDisable = styled.div` const ToggleDivContainer = styled.div` display: flex; flex-direction: row; + flex-wrap: wrap; gap: 16px; margin-top: 40px; `; @@ -104,17 +117,22 @@ const ToggleDiv = styled.div` height: 168px; border: 1px solid rgba(0, 0, 0, 0.08); border-radius: 16px; - background-color: ${(props) => props.backgroundColor}; - background-image: url(./src/assets/images/select-circle.webp); - background-repeat: no-repeat; - background-size: 44px 44px; - background-position: center; + background-color: ${(props) => props.$bgColor}; cursor: pointer; + + ${media.small` + flex: 1 1 40%; + `} + + ${media.medium` + flex: 1 1 40%; + `} `; const ToggleImgContainer = styled.div` display: flex; flex-direction: row; + flex-wrap: wrap; gap: 16px; margin-top: 40px; `; @@ -124,12 +142,28 @@ const ToggleImg = styled.div` height: 168px; border: 1px solid rgba(0, 0, 0, 0.08); border-radius: 16px; - background-color: ${(props) => props.backgroundImg}; + background-image: url(./src/assets/images/img-car.webp); + background-repeat: no-repeat; + background-size: 100%; + background-position: center; + cursor: pointer; + + ${media.small` + flex: 1 1 40%; + `} + + ${media.medium` + flex: 1 1 40%; + `} +`; + +const ImgSelect = styled.div` + width: 100%; + height: 100%; background-image: url(./src/assets/images/select-circle.webp); background-repeat: no-repeat; background-size: 44px 44px; background-position: center; - cursor: pointer; `; const Button = styled.button` @@ -141,9 +175,18 @@ const Button = styled.button` display: flex; justify-content: center; align-items: center; + margin: 0 auto; border-radius: 12px; border: 0; cursor: pointer; + + ${media.small` + width: 100%; + `} + + ${media.medium` + width: 100%; + `} `; export default function PostPage() { @@ -163,13 +206,17 @@ export default function PostPage() { 이미지 - - - - + + + + + + - + + + From 3a5503c1d76e19578a57aadc4149dead0275dd27 Mon Sep 17 00:00:00 2001 From: BZzzzi Date: Mon, 10 Nov 2025 12:17:56 +0900 Subject: [PATCH 20/91] =?UTF-8?q?Refactor:=20styled-components=20transient?= =?UTF-8?q?=20props=20=EC=A0=81=EC=9A=A9=20=EB=B0=8F=20=EB=B6=88=ED=95=84?= =?UTF-8?q?=EC=9A=94=ED=95=9C=20prop=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Button 컴포넌트의 variant, size를 $variant, $size로 변경 - Toast 컴포넌트의 isClosing을 $isClosing으로 변경 - Header의 HeaderWrapper에서 미사용 showButton prop 제거 - DOM으로 전달되는 커스텀 prop 경고 해결 - styled-components transient props($) 패턴 적용 --- src/components/common/button.jsx | 6 +++--- src/components/common/header.jsx | 2 +- src/components/common/toast.jsx | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/common/button.jsx b/src/components/common/button.jsx index f37e20a..8ba1af4 100644 --- a/src/components/common/button.jsx +++ b/src/components/common/button.jsx @@ -154,8 +154,8 @@ const ButtonStyle = styled.button` gap: 10px; cursor: pointer; transition: all 0.2s ease-in-out; - ${({ variant }) => VARIANT_STYLES[variant] || VARIANT_STYLES.primary} - ${({ size }) => SIZES[size] || SIZES.medium} + ${({ $variant }) => VARIANT_STYLES[$variant] || VARIANT_STYLES.primary} + ${({ $size }) => SIZES[$size] || SIZES.medium} &:disabled { background-color: ${colors.gray[300]}; @@ -173,7 +173,7 @@ export default function Button({ ...props }) { return ( - + {variant === "plus" ? ( 추가 ) : variant === "delete" ? ( diff --git a/src/components/common/header.jsx b/src/components/common/header.jsx index 7d0e16d..6c4ee3f 100644 --- a/src/components/common/header.jsx +++ b/src/components/common/header.jsx @@ -38,7 +38,7 @@ export default function Header({ showButton }) { <> - + 로고 diff --git a/src/components/common/toast.jsx b/src/components/common/toast.jsx index 09ef5b2..023441f 100644 --- a/src/components/common/toast.jsx +++ b/src/components/common/toast.jsx @@ -37,7 +37,7 @@ const ToastStyle = styled.div` color: white; padding: 19px 30px; border-radius: 8px; - animation: ${({ isClosing }) => (isClosing ? fadeOut : fadeIn)} 0.3s + animation: ${({ $isClosing }) => ($isClosing ? fadeOut : fadeIn)} 0.3s ease-in-out forwards; ${font.regular16} @@ -69,7 +69,7 @@ const ToastStyle = styled.div` export default function Toast({ children, type, isClosing, onClose }) { return ( - + {type Date: Tue, 11 Nov 2025 03:17:28 +0900 Subject: [PATCH 21/91] =?UTF-8?q?Feat:=20=EA=B3=B5=EC=9C=A0=ED=95=98?= =?UTF-8?q?=EA=B8=B0=20UI=20=EC=88=98=EC=A0=95,=20=ED=94=84=EB=A1=9C?= =?UTF-8?q?=ED=95=84=20=EC=9D=B4=EB=AF=B8=EC=A7=80=EC=99=80=20=EB=AA=87?= =?UTF-8?q?=EB=AA=85=20=EC=9E=91=EC=84=B1=ED=96=88=EB=8A=94=EC=A7=80=20?= =?UTF-8?q?=ED=91=9C=EC=8B=9C=ED=95=98=EB=8A=94=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/common/header.jsx | 1 + src/components/common/modal-layout.jsx | 82 ++++++ src/components/common/share-modal.jsx | 180 ------------- src/components/rolling/emoji-display-list.jsx | 24 ++ src/components/rolling/emoji-dropdown.jsx | 108 ++++++++ .../rolling/emoji-picker-component.jsx | 62 +++++ .../rolling/header-action-buttons.jsx | 55 ++++ .../rolling/participant-section.jsx | 34 +++ src/components/rolling/participant-stats.jsx | 23 ++ src/components/rolling/profile-image-list.jsx | 25 ++ .../rolling/profile-overflow-badge.jsx | 32 +++ src/components/rolling/share-button-group.jsx | 70 +++++ src/components/rolling/share-modal.jsx | 55 ++++ src/hooks/use-emoji-manager.js | 41 +++ src/hooks/use-kakao-sdk.js | 41 +++ src/hooks/use-profile-images.js | 36 +++ src/hooks/use-share-actions.js | 52 ++++ src/pages/rolling-page-head.jsx | 254 ++++-------------- src/pages/rolling-page.jsx | 42 ++- src/styles/rolling-page-styles.js | 10 +- 20 files changed, 814 insertions(+), 413 deletions(-) create mode 100644 src/components/common/modal-layout.jsx delete mode 100644 src/components/common/share-modal.jsx create mode 100644 src/components/rolling/emoji-display-list.jsx create mode 100644 src/components/rolling/emoji-dropdown.jsx create mode 100644 src/components/rolling/emoji-picker-component.jsx create mode 100644 src/components/rolling/header-action-buttons.jsx create mode 100644 src/components/rolling/participant-section.jsx create mode 100644 src/components/rolling/participant-stats.jsx create mode 100644 src/components/rolling/profile-image-list.jsx create mode 100644 src/components/rolling/profile-overflow-badge.jsx create mode 100644 src/components/rolling/share-button-group.jsx create mode 100644 src/components/rolling/share-modal.jsx create mode 100644 src/hooks/use-emoji-manager.js create mode 100644 src/hooks/use-kakao-sdk.js create mode 100644 src/hooks/use-profile-images.js create mode 100644 src/hooks/use-share-actions.js diff --git a/src/components/common/header.jsx b/src/components/common/header.jsx index 7d0e16d..97e6df7 100644 --- a/src/components/common/header.jsx +++ b/src/components/common/header.jsx @@ -8,6 +8,7 @@ const ContainWrapper = styled.div` top: 0; background-color: white; border-bottom: 1px solid #ededed; + z-index: 1003; `; const Contain = styled.div` diff --git a/src/components/common/modal-layout.jsx b/src/components/common/modal-layout.jsx new file mode 100644 index 0000000..af54b7a --- /dev/null +++ b/src/components/common/modal-layout.jsx @@ -0,0 +1,82 @@ +import React from 'react'; +import styled from 'styled-components'; +import { colors } from '@/styles/colors'; +import { font } from '@/styles/font'; +import media from '@/styles/media'; + +const Overlay = styled.div` + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background: transparent; + z-index: 999; +`; + +const ModalContainer = styled.div` + background: white; + border-radius: 16px; + padding: 40px; + width: 480px; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15); + + ${media.medium` + width: 400px; + padding: 30px; + `} + + ${media.small` + width: 320px; + padding: 24px; + `} +`; + +const ModalTitle = styled.h2` + ${font.bold24} + color: ${colors.gray[900]}; + margin-bottom: 24px; + text-align: center; +`; + +const ModalContent = styled.div` + width: 100%; +`; + +const CloseButton = styled.button` + width: 100%; + margin-top: 16px; + padding: 6px; + background: transparent; + border: 1px solid ${colors.gray[300]}; + border-radius: 8px; + cursor: pointer; + ${font.regular16} + color: ${colors.gray[700]}; + transition: all 0.2s; + + &:hover { + background: ${colors.gray[50]}; + } +`; + +/** + * 공통 모달 레이아웃 컴포넌트 + * 책임: 모달의 기본 구조와 레이아웃 제공 + */ +export default function ModalLayout({ isOpen, onClose, title, children, showCloseButton = true }) { + if (!isOpen) return null; + + return ( + + e.stopPropagation()}> + {title && {title}} + {children} + {showCloseButton && ( + 닫기 + )} + + + ); +} + diff --git a/src/components/common/share-modal.jsx b/src/components/common/share-modal.jsx deleted file mode 100644 index 0e27840..0000000 --- a/src/components/common/share-modal.jsx +++ /dev/null @@ -1,180 +0,0 @@ -import React, { useEffect } from 'react'; -import styled from 'styled-components'; -import { colors } from '@/styles/colors'; -import { font } from '@/styles/font'; -import media from '@/styles/media'; -import useToast from '@/hooks/use-toast'; - -const KAKAO_KEY = import.meta.env.VITE_KAKAO_KEY; -console.log(KAKAO_KEY); -const Overlay = styled.div` - position: fixed; - top: 0; - left: 0; - width: 100vw; - height: 100vh; - background: rgba(0, 0, 0, 0.5); - z-index: 1000; - display: flex; - justify-content: center; - align-items: center; -`; - -const ModalContainer = styled.div` - background: white; - border-radius: 16px; - padding: 40px; - width: 480px; - box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15); - - ${media.medium` - width: 400px; - padding: 30px; - `} - - ${media.small` - width: 320px; - padding: 24px; - `} -`; - -const ModalTitle = styled.h2` - ${font.bold24} - color: ${colors.gray[900]}; - margin-bottom: 24px; - text-align: center; -`; - -const ShareButtonGroup = styled.div` - display: flex; - flex-direction: column; - gap: 12px; -`; - -const ShareButton = styled.button` - display: flex; - align-items: center; - justify-content: center; - gap: 12px; - width: 100%; - padding: 16px; - background: ${colors.gray[100]}; - border: 1px solid ${colors.gray[300]}; - border-radius: 8px; - cursor: pointer; - transition: all 0.2s; - ${font.regular16} - color: ${colors.gray[900]}; - - &:hover { - background: ${colors.gray[200]}; - border-color: ${colors.gray[400]}; - } - - &:active { - transform: scale(0.98); - } -`; - -const KakaoButton = styled(ShareButton)` - background: #fee500; - border-color: #fee500; - color: #000000; - - &:hover { - background: #fdd835; - border-color: #fdd835; - } -`; - -const CloseButton = styled.button` - width: 100%; - margin-top: 16px; - padding: 6px; - background: transparent; - border: 1px solid ${colors.gray[300]}; - border-radius: 8px; - cursor: pointer; - ${font.regular16} - color: ${colors.gray[700]}; - transition: all 0.2s; - - &:hover { - background: ${colors.gray[50]}; - } -`; - -export default function ShareModal({ isOpen, onClose, shareUrl }) { - const { showToast } = useToast(); - - // 카카오 SDK 초기화 - useEffect(() => { - if (!window.Kakao) { - const script = document.createElement('script'); - script.src = 'https://t1.kakaocdn.net/kakao_js_sdk/2.7.7/kakao.min.js'; - script.integrity = 'sha384-tJkjbtDbvoxO+diRuDtwRO9JXR7pjWnfjfRn5ePUpl7e7RJCxKCwwnfqUAdXh53p'; - script.crossOrigin = 'anonymous'; - script.async = true; - - script.onload = () => { - if (window.Kakao && !window.Kakao.isInitialized()) { - window.Kakao.init(KAKAO_KEY); - } - }; - - document.head.appendChild(script); - } else if (!window.Kakao.isInitialized()) { - window.Kakao.init(KAKAO_KEY); - } - }, []); - - // URL 복사 기능 - const handleCopyUrl = async () => { - try { - await navigator.clipboard.writeText(shareUrl); - showToast('URL이 복사되었습니다.', 'success'); - onClose(); - } catch (err) { - console.error('URL 복사 실패:', err); - showToast('URL 복사에 실패했습니다.', 'delete'); - } - }; - - // 카카오톡 공유 기능 - const handleKakaoShare = () => { - if (window.Kakao) { - try { - window.Kakao.Share.sendScrap({ - requestUrl: shareUrl, - }); - } catch (err) { - console.error('카카오톡 공유 실패:', err); - showToast(true, '카카오톡 공유에 실패했습니다.'); - } - } else { - showToast(true, '카카오톡 SDK가 로드되지 않았습니다.'); - } - }; - - if (!isOpen) return null; - - return ( - - e.stopPropagation()}> - 공유하기 - - - 🗨️ - 카카오톡으로 공유하기 - - - 🔗 - URL 복사하기 - - - 닫기 - - - ); -} - diff --git a/src/components/rolling/emoji-display-list.jsx b/src/components/rolling/emoji-display-list.jsx new file mode 100644 index 0000000..3e1492f --- /dev/null +++ b/src/components/rolling/emoji-display-list.jsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { + RollingHeaderImojiIconContainer, + RollingHeaderImojiText, + RollingHeaderImojiIcon, +} from '@/styles/rolling-page-styles'; + +/** + * 이모지 표시 리스트 컴포넌트 + * 책임: 상위 N개의 이모지를 화면에 표시 + */ +export default function EmojiDisplayList({ emojis }) { + return ( + <> + {emojis.map((emojiData, index) => ( + + {emojiData.emoji} + {emojiData.count} + + ))} + + ); +} + diff --git a/src/components/rolling/emoji-dropdown.jsx b/src/components/rolling/emoji-dropdown.jsx new file mode 100644 index 0000000..b5bcc45 --- /dev/null +++ b/src/components/rolling/emoji-dropdown.jsx @@ -0,0 +1,108 @@ +import React from 'react'; +import styled from 'styled-components'; +import { colors } from '@/styles/colors'; +import media from '@/styles/media'; +import { font } from '@/styles/font'; +import { RollingHeaderArrowDown } from '@/styles/rolling-page-styles'; + +const EmojiDropdownContainer = styled.div` + position: relative; + display: inline-block; +`; + +const EmojiDropdownWrapper = styled.div` + position: fixed; + transform: translate(-80%, 10%); + z-index: 1000; + background: white; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + border: 1px solid ${colors.gray[300]}; + padding: 24px; + width: auto; + max-height: 300px; + overflow-y: auto; +`; + +const EmojiDropdownGrid = styled.div` + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 8px; + ${media.medium` + grid-template-columns: repeat(3, 1fr); + `} + ${media.small` + grid-template-columns: repeat(3, 1fr); + `} +`; + +const EmojiDropdownItem = styled.div` + display: flex; + align-items: center; + justify-content: center; + width: auto; + height: auto; + padding: 8px 12px; + text-align: center; + border-radius: 32px; + background: rgba(153, 153, 153, 1); + gap: 2px; + + ${media.small` + padding: 4px 8px; + `} +`; + +const EmojiDropdownIcon = styled.div``; + +const EmojiDropdownCount = styled.span` + ${font.regular16} + color: rgba(255, 255, 255, 1); +`; + +const Overlay = styled.div` + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background: transparent; + z-index: 999; +`; + +/** + * 이모지 드롭다운 컴포넌트 + * 책임: 모든 이모지 목록을 드롭다운 형태로 표시 + */ +export default function EmojiDropdown({ + emojis, + isOpen, + onToggle, + onClose, + arrowDownIcon +}) { + return ( + + + {isOpen && ( + <> + + + + {emojis.map((emojiData, index) => ( + + {emojiData.emoji} + {emojiData.count} + + ))} + + + + )} + + ); +} + diff --git a/src/components/rolling/emoji-picker-component.jsx b/src/components/rolling/emoji-picker-component.jsx new file mode 100644 index 0000000..fc13037 --- /dev/null +++ b/src/components/rolling/emoji-picker-component.jsx @@ -0,0 +1,62 @@ +import React from 'react'; +import EmojiPicker from 'emoji-picker-react'; +import styled from 'styled-components'; +import { colors } from '@/styles/colors'; + +const EmojiPickerContainer = styled.div` + position: relative; + display: inline-block; +`; + +const EmojiPickerWrapper = styled.div` + position: fixed; + transform: translate(-60%, 2%); + z-index: 1000; + background: white; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + border: 1px solid ${colors.gray[300]}; +`; + +const Overlay = styled.div` + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background: transparent; + z-index: 999; +`; + +/** + * 이모지 선택기 컴포넌트 + * 책임: 이모지 피커 UI 렌더링 및 이모지 선택 이벤트 처리 + */ +export default function EmojiPickerComponent({ isOpen, onClose, onEmojiSelect, children }) { + const handleEmojiClick = (emojiData) => { + onEmojiSelect(emojiData.emoji); + onClose(); + }; + + return ( + + {children} + {isOpen && ( + <> + + + + + + )} + + ); +} + diff --git a/src/components/rolling/header-action-buttons.jsx b/src/components/rolling/header-action-buttons.jsx new file mode 100644 index 0000000..b00531d --- /dev/null +++ b/src/components/rolling/header-action-buttons.jsx @@ -0,0 +1,55 @@ +import React from 'react'; +import styled from 'styled-components'; +import EmojiPickerComponent from './emoji-picker-component'; +import { + RollingHeaderImojiEditButtonContainer, + RollingHeaderImojiEditButton, + RollingHeaderImojiEditButtonIcon, + RollingHeaderImojiEditButtonText, + PerpendicularLineSecond, + RollingHeaderLinkShareButton, +} from '@/styles/rolling-page-styles'; + +const ShareButtonWrapper = styled.div` + position: relative; +`; + +/** + * 헤더 액션 버튼 컴포넌트 + * 책임: 이모지 추가 버튼과 공유 버튼 렌더링 + */ +export default function HeaderActionButtons({ + isEmojiPickerOpen, + onToggleEmojiPicker, + onCloseEmojiPicker, + onEmojiSelect, + onShareClick, + addEmojiIcon, + shareIcon, + shareModalComponent, // ShareModal 컴포넌트를 props로 받음 +}) { + return ( + + + + + 추가 + + + + + + {shareModalComponent} + + + ); +} + diff --git a/src/components/rolling/participant-section.jsx b/src/components/rolling/participant-section.jsx new file mode 100644 index 0000000..7d27ee6 --- /dev/null +++ b/src/components/rolling/participant-section.jsx @@ -0,0 +1,34 @@ +import React from 'react'; +import { + RollingHeaderUserPeopleContainer, + RollingHeaderUserPeopleImages, +} from '@/styles/rolling-page-styles'; +import ProfileImageList from './profile-image-list'; +import ProfileOverflowBadge from './profile-overflow-badge'; +import ParticipantStats from './participant-stats'; +import useProfileImages from '@/hooks/use-profile-images'; + +/** + * 참여자 섹션 컴포넌트 + * 책임: 프로필 이미지와 참여자 통계를 조합하여 표시 + */ +export default function ParticipantSection({ profiles, maxVisible = 3 }) { + const { visibleProfiles, overflowCount, totalCount, hasOverflow } = + useProfileImages(profiles, maxVisible); + + return ( + + + {/* 보이는 프로필 이미지들 */} + + + {/* 오버플로우 뱃지 (+N) */} + {hasOverflow && } + + + {/* 참여자 통계 텍스트 */} + + + ); +} + diff --git a/src/components/rolling/participant-stats.jsx b/src/components/rolling/participant-stats.jsx new file mode 100644 index 0000000..e52c89c --- /dev/null +++ b/src/components/rolling/participant-stats.jsx @@ -0,0 +1,23 @@ +import React from 'react'; +import { RollingHeaderUserPeopleState } from '@/styles/rolling-page-styles'; + +/** + * 참여자 통계 컴포넌트 + * 책임: 참여자 수 텍스트 표시 + */ +export default function ParticipantStats({ count }) { + if (count === 0) { + return ( + + 아직 작성한 사람이 없어요 + + ); + } + + return ( + + {count}명이 작성했어요! + + ); +} + diff --git a/src/components/rolling/profile-image-list.jsx b/src/components/rolling/profile-image-list.jsx new file mode 100644 index 0000000..22e5551 --- /dev/null +++ b/src/components/rolling/profile-image-list.jsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { RollingHeaderUserPeopleImage } from '@/styles/rolling-page-styles'; + +/** + * 프로필 이미지 리스트 컴포넌트 + * 책임: 프로필 이미지들을 렌더링 (래퍼 없이 순수 이미지만) + */ +export default function ProfileImageList({ profiles }) { + if (!profiles || profiles.length === 0) { + return null; + } + + return ( + <> + {profiles.map((profile, index) => ( + + ))} + + ); +} + diff --git a/src/components/rolling/profile-overflow-badge.jsx b/src/components/rolling/profile-overflow-badge.jsx new file mode 100644 index 0000000..2fe5249 --- /dev/null +++ b/src/components/rolling/profile-overflow-badge.jsx @@ -0,0 +1,32 @@ +import React from 'react'; +import styled from 'styled-components'; +import { colors } from '@/styles/colors'; +import { font } from '@/styles/font'; + +const OverflowBadge = styled.div` + width: 28px; + height: 28px; + border-radius: 140px; + border: 1.4px solid ${colors.gray[900]}; + background: ${colors.gray[200]}; + display: flex; + align-items: center; + justify-content: center; + position: relative; + margin-left: -10px; + ${font.regular12} + color: ${colors.gray[700]}; +`; + +/** + * 프로필 오버플로우 뱃지 컴포넌트 + * 책임: 추가 인원 수를 표시 (+N) + */ +export default function ProfileOverflowBadge({ count }) { + if (count <= 0) { + return null; + } + + return +{count}; +} + diff --git a/src/components/rolling/share-button-group.jsx b/src/components/rolling/share-button-group.jsx new file mode 100644 index 0000000..dddc833 --- /dev/null +++ b/src/components/rolling/share-button-group.jsx @@ -0,0 +1,70 @@ +import React from 'react'; +import styled from 'styled-components'; +import { colors } from '@/styles/colors'; +import { font } from '@/styles/font'; + +const ButtonGroup = styled.div` + position: absolute; + top: calc(100% + 8px); + right: 0; + width: 140px; + height: auto; + display: flex; + flex-direction: column; + background: white; + border-radius: 8px; + border: 1px solid ${colors.gray[300]}; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + z-index: 1001; + padding: 10px 0px; +`; + +const ShareButton = styled.button` + display: flex; + align-items: center; + justify-content: center; + gap: 12px; + background: transparent; + width: 100%; + height: 50px; + border: none; + cursor: pointer; + transition: all 0.2s; + ${font.regular16} + color: ${colors.gray[900]}; + + &:hover { + background: ${colors.gray[200]}; + border-color: ${colors.gray[400]}; + + } + + &:active { + transform: scale(0.98); + } +`; + +const KakaoButton = styled(ShareButton)` + background: #fee500; + border-color: #fee500; + color: #000000; + + &:hover { + background: #fdd835; + border-color: #fdd835; + } +`; + +/** + * 공유 버튼 그룹 컴포넌트 + * 책임: 공유 방법별 버튼 UI 렌더링 + */ +export default function ShareButtonGroup({ onKakaoShare, onCopyUrl }) { + return ( + + 카카오톡 공유 + URL 복사 + + ); +} + diff --git a/src/components/rolling/share-modal.jsx b/src/components/rolling/share-modal.jsx new file mode 100644 index 0000000..a1a18dc --- /dev/null +++ b/src/components/rolling/share-modal.jsx @@ -0,0 +1,55 @@ +import React from 'react'; +import styled from 'styled-components'; + +import ShareButtonGroup from './share-button-group'; +import useKakaoSdk from '@/hooks/use-kakao-sdk'; +import useShareActions from '@/hooks/use-share-actions'; + +const Overlay = styled.div` + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background: transparent; + z-index: 999; +`; + +/** + * 공유 모달 컴포넌트 + * 책임: 공유 모달 UI 및 공유 액션 연결 + */ +export default function ShareModal({ isOpen, onClose, shareUrl }) { + // 카카오 SDK 초기화 + useKakaoSdk(); + + // 공유 기능 훅 + const { copyToClipboard, shareToKakao } = useShareActions(); + + // URL 복사 핸들러 + const handleCopyUrl = async () => { + const success = await copyToClipboard(shareUrl); + if (success) { + onClose(); + } + }; + + // 카카오톡 공유 핸들러 + const handleKakaoShare = () => { + shareToKakao(shareUrl); + }; + + if (!isOpen) return null; + + return ( + <> + + e.stopPropagation()} + onKakaoShare={handleKakaoShare} + onCopyUrl={handleCopyUrl} + /> + + ); +} + diff --git a/src/hooks/use-emoji-manager.js b/src/hooks/use-emoji-manager.js new file mode 100644 index 0000000..c47018f --- /dev/null +++ b/src/hooks/use-emoji-manager.js @@ -0,0 +1,41 @@ +import { useState } from 'react'; + +/** + * 이모지 관리 커스텀 훅 + * 책임: 이모지 상태 관리 및 비즈니스 로직 처리 + */ +export default function useEmojiManager(initialEmojis = []) { + const [selectedEmojis, setSelectedEmojis] = useState(initialEmojis); + + const handleEmojiSelect = (emoji) => { + const existingEmojiIndex = selectedEmojis.findIndex(item => item.emoji === emoji); + + if (existingEmojiIndex !== -1) { + // 이미 존재하는 이모지면 카운트 증가 + const updatedEmojis = [...selectedEmojis]; + updatedEmojis[existingEmojiIndex].count += 1; + setSelectedEmojis(updatedEmojis); + } else { + // 새로운 이모지면 추가 + setSelectedEmojis([...selectedEmojis, { emoji, count: 1 }]); + } + }; + + // 카운트 순으로 정렬 + const getSortedEmojis = () => { + return [...selectedEmojis].sort((a, b) => b.count - a.count); + }; + + // 상위 N개 추출 + const getTopEmojis = (count) => { + return getSortedEmojis().slice(0, count); + }; + + return { + selectedEmojis, + handleEmojiSelect, + getSortedEmojis, + getTopEmojis, + }; +} + diff --git a/src/hooks/use-kakao-sdk.js b/src/hooks/use-kakao-sdk.js new file mode 100644 index 0000000..54f240c --- /dev/null +++ b/src/hooks/use-kakao-sdk.js @@ -0,0 +1,41 @@ +import { useEffect } from 'react'; + +const KAKAO_KEY = import.meta.env.VITE_KAKAO_KEY; + +/** + * 카카오 SDK 초기화 커스텀 훅 + * 책임: 카카오 SDK 스크립트 로드 및 초기화 + */ +export default function useKakaoSdk() { + useEffect(() => { + // 이미 SDK가 로드되어 있으면 초기화만 수행 + if (window.Kakao) { + if (!window.Kakao.isInitialized()) { + window.Kakao.init(KAKAO_KEY); + } + return; + } + + // SDK 스크립트 동적 로드 + const script = document.createElement('script'); + script.src = 'https://t1.kakaocdn.net/kakao_js_sdk/2.7.7/kakao.min.js'; + script.integrity = 'sha384-tJkjbtDbvoxO+diRuDtwRO9JXR7pjWnfjfRn5ePUpl7e7RJCxKCwwnfqUAdXh53p'; + script.crossOrigin = 'anonymous'; + script.async = true; + + script.onload = () => { + if (window.Kakao && !window.Kakao.isInitialized()) { + window.Kakao.init(KAKAO_KEY); + } + }; + + document.head.appendChild(script); + + // 클린업 함수는 필요시 추가 (일반적으로는 SDK를 제거하지 않음) + }, []); + + return { + isKakaoReady: window.Kakao?.isInitialized() || false, + }; +} + diff --git a/src/hooks/use-profile-images.js b/src/hooks/use-profile-images.js new file mode 100644 index 0000000..8e83833 --- /dev/null +++ b/src/hooks/use-profile-images.js @@ -0,0 +1,36 @@ +import { useMemo } from 'react'; + +/** + * 프로필 이미지 데이터 처리 커스텀 훅 + * 책임: 프로필 이미지 데이터 가공 및 오버플로우 계산 + */ +export default function useProfileImages(profiles, maxVisible = 3) { + const processedData = useMemo(() => { + if (!profiles || profiles.length === 0) { + return { + visibleProfiles: [], + overflowCount: 0, + totalCount: 0, + hasOverflow: false, + }; + } + + const totalCount = profiles.length; + const hasOverflow = totalCount > maxVisible; + + // 오버플로우가 있으면 마지막 자리는 +N 표시용으로 비움 + const visibleCount = hasOverflow ? maxVisible - 1 : maxVisible; + const visibleProfiles = profiles.slice(0, visibleCount); + const overflowCount = hasOverflow ? totalCount - visibleCount : 0; + + return { + visibleProfiles, + overflowCount, + totalCount, + hasOverflow, + }; + }, [profiles, maxVisible]); + + return processedData; +} + diff --git a/src/hooks/use-share-actions.js b/src/hooks/use-share-actions.js new file mode 100644 index 0000000..fb57522 --- /dev/null +++ b/src/hooks/use-share-actions.js @@ -0,0 +1,52 @@ +import { useCallback } from 'react'; +import useToast from '@/hooks/use-toast'; + +/** + * 공유 기능 커스텀 훅 + * 책임: URL 복사 및 카카오톡 공유 비즈니스 로직 처리 + */ +export default function useShareActions() { + const { showToast } = useToast(); + + /** + * URL을 클립보드에 복사 + */ + const copyToClipboard = useCallback(async (url) => { + try { + await navigator.clipboard.writeText(url); + showToast('URL이 복사되었습니다.', 'success'); + return true; + } catch (err) { + console.error('URL 복사 실패:', err); + showToast('URL 복사에 실패했습니다.', 'delete'); + return false; + } + }, [showToast]); + + /** + * 카카오톡으로 공유 + */ + const shareToKakao = useCallback((url) => { + if (!window.Kakao) { + showToast('카카오톡 SDK가 로드되지 않았습니다.', 'delete'); + return false; + } + + try { + window.Kakao.Share.sendScrap({ + requestUrl: url, + }); + return true; + } catch (err) { + console.error('카카오톡 공유 실패:', err); + showToast('카카오톡 공유에 실패했습니다.', 'delete'); + return false; + } + }, [showToast]); + + return { + copyToClipboard, + shareToKakao, + }; +} + diff --git a/src/pages/rolling-page-head.jsx b/src/pages/rolling-page-head.jsx index 707c2f8..f165398 100644 --- a/src/pages/rolling-page-head.jsx +++ b/src/pages/rolling-page-head.jsx @@ -1,146 +1,27 @@ import React, { useState } from 'react'; -import EmojiPicker from 'emoji-picker-react'; -import styled from 'styled-components'; -import { colors } from '@/styles/colors'; -import media from '@/styles/media'; -import { font } from '@/styles/font'; -import ShareModal from '@/components/common/share-modal'; -import { - RollingHeaderImojiContainer, - RollingHeaderImojiIconContainer, - RollingHeaderImojiText, - RollingHeaderImojiIcon, - RollingHeaderImojiEditButtonContainer, - RollingHeaderImojiEditButton, - RollingHeaderImojiEditButtonIcon, - RollingHeaderImojiEditButtonText, - RollingHeaderArrowDown, - PerpendicularLineSecond, - RollingHeaderLinkShareButton, -} from '@/styles/rolling-page-styles'; - -const EmojiPickerContainer = styled.div` - position: relative; - display: inline-block; -`; - -const EmojiPickerWrapper = styled.div` - position: fixed; - transform: translate(-60%, 2%); - z-index: 1000; - background: white; - border-radius: 8px; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); - border: 1px solid ${colors.gray[300]}; - -`; - -const Overlay = styled.div` - position: fixed; - top: 0; - left: 0; - - background: transparent; - z-index: 999; -`; - -// 이모지 드롭다운 관련 스타일 -const EmojiDropdownContainer = styled.div` - position: relative; - display: inline-block; - -`; - -const EmojiDropdownWrapper = styled.div` - position: fixed; - transform: translate(-80%, 10%); - z-index: 1000; - background: white; - border-radius: 8px; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); - border: 1px solid ${colors.gray[300]}; - padding: 24px; - width: auto; - max-height: 300px; - overflow-y: auto; -`; - -const EmojiDropdownGrid = styled.div` - display: grid; - grid-template-columns: repeat(4, 1fr); - gap: 8px; - ${media.medium` - grid-template-columns: repeat(3, 1fr); - `} - ${media.small` - grid-template-columns: repeat(3, 1fr); - `} -`; - -const EmojiDropdownItem = styled.div` - display: flex; - align-items: center; - justify-content: center; - width: auto; - height: auto; - padding: 8px 12px; - text-align: center; - border-radius: 32px; - background: rgba(153, 153, 153, 1); - gap: 2px; - - ${media.small` - padding: 4px 8px; - `} -`; - -const EmojiDropdownIcon = styled.div` -`; - -const EmojiDropdownCount = styled.span` - ${font.regular16} - color: rgba(255, 255, 255, 1) -`; - -// 이모지 피커 컴포넌트 -function EmojiPickerComponent({ isOpen, onClose, onEmojiSelect, children }) { - const handleEmojiClick = (emojiData) => { - onEmojiSelect(emojiData.emoji); - onClose(); - }; - - return ( - - {children} - {isOpen && ( - <> - - - - - - )} - - ); -} - -// 롤링 페이지 헤더 컴포넌트 +import ShareModal from '@/components/rolling/share-modal'; +import EmojiDisplayList from '@/components/rolling/emoji-display-list'; +import EmojiDropdown from '@/components/rolling/emoji-dropdown'; +import HeaderActionButtons from '@/components/rolling/header-action-buttons'; +import useEmojiManager from '@/hooks/use-emoji-manager'; +import { RollingHeaderImojiContainer } from '@/styles/rolling-page-styles'; + +/** + * 롤링 페이지 헤더 컴포넌트 + * 책임: 전체 헤더 구성 요소 조합 및 상태 관리 + */ export default function RollingPageHeader({ ArrowDownIcon, AddEmojiIcon, ShareIcon }) { + // UI 상태 관리 const [isEmojiPickerOpen, setIsEmojiPickerOpen] = useState(false); const [isEmojiDropdownOpen, setIsEmojiDropdownOpen] = useState(false); const [isShareModalOpen, setIsShareModalOpen] = useState(false); - const [selectedEmojis, setSelectedEmojis] = useState([ + + // 이모지 상태 및 로직 관리 + const initialEmojis = [ { emoji: '😘', count: 12 }, { emoji: '😍', count: 8 }, { emoji: '👍', count: 15 }, @@ -148,22 +29,11 @@ export default function RollingPageHeader({ { emoji: '❤️', count: 20 }, { emoji: '😂', count: 3 }, { emoji: '🔥', count: 7 } - ]); + ]; - const handleEmojiSelect = (emoji) => { - const existingEmojiIndex = selectedEmojis.findIndex(item => item.emoji === emoji); - - if (existingEmojiIndex !== -1) { - // 이미 존재하는 이모지면 카운트 증가 - const updatedEmojis = [...selectedEmojis]; - updatedEmojis[existingEmojiIndex].count += 1; - setSelectedEmojis(updatedEmojis); - } else { - // 새로운 이모지면 추가 - setSelectedEmojis([...selectedEmojis, { emoji, count: 1 }]); - } - }; + const { handleEmojiSelect, getSortedEmojis, getTopEmojis } = useEmojiManager(initialEmojis); + // 이모지 피커 핸들러 const toggleEmojiPicker = () => { setIsEmojiPickerOpen(!isEmojiPickerOpen); }; @@ -172,6 +42,7 @@ export default function RollingPageHeader({ setIsEmojiPickerOpen(false); }; + // 이모지 드롭다운 핸들러 const toggleEmojiDropdown = () => { setIsEmojiDropdownOpen(!isEmojiDropdownOpen); }; @@ -180,6 +51,7 @@ export default function RollingPageHeader({ setIsEmojiDropdownOpen(false); }; + // 공유 모달 핸들러 const openShareModal = () => { setIsShareModalOpen(true); }; @@ -188,70 +60,46 @@ export default function RollingPageHeader({ setIsShareModalOpen(false); }; - // 카운트 순으로 정렬하여 상위 3개만 추출 - const sortedEmojis = [...selectedEmojis].sort((a, b) => b.count - a.count); - const topThreeEmojis = sortedEmojis.slice(0, 3); - const hasMoreEmojis = selectedEmojis.length > 3; + // 정렬된 이모지 및 상위 3개 추출 + const sortedEmojis = getSortedEmojis(); + const topThreeEmojis = getTopEmojis(3); + const hasMoreEmojis = sortedEmojis.length > 3; - // 현재 페이지 URL 가져오기 + // 현재 페이지 URL const currentUrl = window.location.href; return ( - {topThreeEmojis.map((emojiData, index) => ( - - {emojiData.emoji} - {emojiData.count} - - ))} + {/* 상위 3개 이모지 표시 */} + + {/* 더 많은 이모지가 있을 경우 드롭다운 */} {hasMoreEmojis && ( - - - {isEmojiDropdownOpen && ( - <> - - - - {sortedEmojis.map((emojiData, index) => ( - - {emojiData.emoji} - {emojiData.count} - - ))} - - - - )} - - )} - - - - - - 추가 - - - - - + )} - + } /> ); diff --git a/src/pages/rolling-page.jsx b/src/pages/rolling-page.jsx index 749a8c8..5524288 100644 --- a/src/pages/rolling-page.jsx +++ b/src/pages/rolling-page.jsx @@ -1,23 +1,27 @@ -import React from 'react'; +import React, { useState } from 'react'; import { RollingHeaderContainer, RollingHeaderUserInfo, RollingHeaderRightContainer, - RollingHeaderUserPeopleContainer, - RollingHeaderUserPeopleImages, - RollingHeaderUserPeopleImage, - RollingHeaderUserDefaultImage, - RollingHeaderUserPeopleState, PerpendicularLineFirst, RollingPageContainer, } from "@/styles/rolling-page-styles"; import RollingPageHeader from "@/pages/rolling-page-head"; +import ParticipantSection from "@/components/rolling/participant-section"; import ArrowDownIcon from "@/assets/icons/arrow-down.svg"; import AddEmojiIcon from "@/assets/icons/add-emoji.svg"; import ShareIcon from "@/assets/icons/share.svg"; - export default function RollingPage() { + // TODO: 실제로는 API에서 받아올 데이터 + // 임시 데이터 (나중에 API 호출로 대체) + const [profiles] = useState([ + { id: 1, name: '김철수', profileImageURL: 'https://via.placeholder.com/28' }, + { id: 2, name: '이영희', profileImageURL: 'https://via.placeholder.com/28' }, + { id: 3, name: '박민수', profileImageURL: 'https://via.placeholder.com/28' }, + + ]); + return ( <> @@ -26,35 +30,23 @@ export default function RollingPage() { - - {/* //여기에서 함수를 불러와서 처리해야함 */} - - - - - - - - - 23명이 작성 했어요! - - - + {/* 참여자 프로필 섹션 */} + + + {/* 이모지 및 공유 헤더 */} - - + - + {/* 롤링 페이퍼 컨텐츠가 들어갈 영역 */} - ); } diff --git a/src/styles/rolling-page-styles.js b/src/styles/rolling-page-styles.js index 34ccb73..f29f391 100644 --- a/src/styles/rolling-page-styles.js +++ b/src/styles/rolling-page-styles.js @@ -22,8 +22,7 @@ export const RollingHeaderContainer = styled.div` height: 68px; margin: 0 auto; padding: 13px 20px; - overflow-x: auto; - overflow-y: hidden; + gap: 20px; `} @@ -32,8 +31,7 @@ export const RollingHeaderContainer = styled.div` height: 68px; margin: 0; padding: 13px 20px; - overflow-x: auto; - overflow-y: hidden; + gap: 10px; `} @@ -123,9 +121,11 @@ export const RollingHeaderUserPeopleContainer = styled.div` //유저 이미지 프로필 사진들 export const RollingHeaderUserPeopleImages = styled.div` + display: flex; width: 76px; height: 28px; position: relative; + cursor: pointer; ${media.medium` @@ -143,7 +143,7 @@ export const RollingHeaderUserPeopleImage = styled.img` width: 28px; height: 28px; border-radius: 140px; - border: 1.4px solid #000; + border: 1.4px solid #fff; position: relative; margin-left: -10px; `; From 48720865ce7ff95e5a291d93e839d1d302624f33 Mon Sep 17 00:00:00 2001 From: BZzzzi Date: Tue, 11 Nov 2025 04:46:11 +0900 Subject: [PATCH 22/91] =?UTF-8?q?Refactor:=20=ED=86=A0=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=20=EC=8B=9C=EC=8A=A4=ED=85=9C=20=EA=B5=AC=EC=A1=B0=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Context 파일들을 toast-provider.jsx로 통합하여 구조 단순화 - 토스트 Provider를 contexts에서 components/common으로 이동 - useToast 훅을 named export로 변경하여 일관성 개선 - 자동 닫기 및 애니메이션 타이머 관리 로직 개선 - ToastProvider import 경로를 components/common으로 변경 --- src/components/common/toast-provider.jsx | 80 ++++++++++++++++++++++++ src/contexts/toast-context-state.js | 3 - src/contexts/toast-context.jsx | 52 --------------- src/hooks/use-toast.js | 7 ++- src/main.jsx | 2 +- src/pages/toast-test-page.jsx | 2 +- 6 files changed, 86 insertions(+), 60 deletions(-) create mode 100644 src/components/common/toast-provider.jsx delete mode 100644 src/contexts/toast-context-state.js delete mode 100644 src/contexts/toast-context.jsx diff --git a/src/components/common/toast-provider.jsx b/src/components/common/toast-provider.jsx new file mode 100644 index 0000000..662e0e2 --- /dev/null +++ b/src/components/common/toast-provider.jsx @@ -0,0 +1,80 @@ +import { useState, useRef } from "react"; +import { createPortal } from "react-dom"; +import Toast from "@/components/common/toast"; +import { ToastContext } from "@/hooks/use-toast"; + +export function ToastProvider({ children }) { + const [toast, setToast] = useState(null); + + const autoCloseTimerRef = useRef(null); + const closeAnimTimerRef = useRef(null); + + const hideToast = () => { + if (autoCloseTimerRef.current) { + clearTimeout(autoCloseTimerRef.current); + autoCloseTimerRef.current = null; + } + + if (closeAnimTimerRef.current) { + clearTimeout(closeAnimTimerRef.current); + closeAnimTimerRef.current = null; + } + + setToast((prev) => { + if (!prev || prev.isClosing) return prev; + return { ...prev, isClosing: true }; + }); + + closeAnimTimerRef.current = setTimeout(() => { + setToast(null); + closeAnimTimerRef.current = null; + }, 3000); + }; + + const showToast = (message, type = "success") => { + if (autoCloseTimerRef.current) clearTimeout(autoCloseTimerRef.current); + if (closeAnimTimerRef.current) { + clearTimeout(closeAnimTimerRef.current); + } + + setToast({ + message, + type, + key: Date.now(), + isClosing: false, + }); + + autoCloseTimerRef.current = setTimeout(() => { + hideToast(); + }, 3000); + }; + + const contextValue = { + success: (message) => showToast(message, "success"), + delete: (message) => showToast(message, "delete"), + }; + + const toastContainer = document.getElementById("toast"); + + return ( + + {children} + {toastContainer && + createPortal( + <> + {toast && ( + + {toast.message} + + )} + , + toastContainer + )} + + ); +} diff --git a/src/contexts/toast-context-state.js b/src/contexts/toast-context-state.js deleted file mode 100644 index 2056dd1..0000000 --- a/src/contexts/toast-context-state.js +++ /dev/null @@ -1,3 +0,0 @@ -import { createContext } from "react"; - -export const ToastContext = createContext(null); diff --git a/src/contexts/toast-context.jsx b/src/contexts/toast-context.jsx deleted file mode 100644 index 4e764b3..0000000 --- a/src/contexts/toast-context.jsx +++ /dev/null @@ -1,52 +0,0 @@ -import { useState, useCallback } from "react"; -import { createPortal } from "react-dom"; -import Toast from "@/components/common/toast"; -import { ToastContext } from "./toast-context-state"; - -export default function ToastProvider({ children }) { - const [toasts, setToasts] = useState([]); - - const removeToast = useCallback((id) => { - setToasts((prev) => prev.filter((t) => t.id !== id)); - }, []); - - const addToast = useCallback( - (type, message) => { - const id = Date.now(); - setToasts((prev) => [...prev, { id, type, message, isClosing: false }]); - setTimeout(() => { - setToasts((prev) => - prev.map((t) => (t.id === id ? { ...t, isClosing: true } : t)) - ); - setTimeout(() => removeToast(id), 300); - }, 4700); - }, - [removeToast] - ); - - const toast = { - success: (msg) => addToast("success", msg), - delete: (msg) => addToast("delete", msg), - }; - - return ( - - {children} - {createPortal( - <> - {toasts.map((t) => ( - removeToast(t.id)} - > - {t.message} - - ))} - , - document.getElementById("toast") - )} - - ); -} diff --git a/src/hooks/use-toast.js b/src/hooks/use-toast.js index 47c8088..0d60146 100644 --- a/src/hooks/use-toast.js +++ b/src/hooks/use-toast.js @@ -1,6 +1,7 @@ -import { useContext } from "react"; -import { ToastContext } from "@/contexts/toast-context-state"; +import { createContext, useContext } from "react"; -export default function useToast() { +export const ToastContext = createContext(null); + +export function useToast() { return useContext(ToastContext); } diff --git a/src/main.jsx b/src/main.jsx index 84fb027..0a09416 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -1,7 +1,7 @@ import { createRoot } from "react-dom/client"; import { BrowserRouter } from "react-router"; import App from "./App.jsx"; -import ToastProvider from "@/contexts/toast-context"; +import { ToastProvider } from "@/components/common/toast-provider.jsx"; createRoot(document.getElementById("root")).render( diff --git a/src/pages/toast-test-page.jsx b/src/pages/toast-test-page.jsx index b1ae12d..bbe8e3b 100644 --- a/src/pages/toast-test-page.jsx +++ b/src/pages/toast-test-page.jsx @@ -1,6 +1,6 @@ import styled from "styled-components"; import Button from "@/components/common/button"; -import useToast from "@/hooks/use-toast"; +import { useToast } from "@/hooks/use-toast"; const Container = styled.div` padding: 40px; From 54f229d49e4264660b324e58329c1396a581e87a Mon Sep 17 00:00:00 2001 From: apple Date: Tue, 11 Nov 2025 07:32:36 +0900 Subject: [PATCH 23/91] =?UTF-8?q?Feat:=20message=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20UI=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.jsx | 2 + src/pages/message-page.jsx | 250 +++++++++++++++++++++++++++++++++++++ src/styles/message-page.js | 173 +++++++++++++++++++++++++ 3 files changed, 425 insertions(+) create mode 100644 src/pages/message-page.jsx create mode 100644 src/styles/message-page.js diff --git a/src/App.jsx b/src/App.jsx index e7b73a5..b8f39d1 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -2,6 +2,7 @@ import { BrowserRouter, Routes, Route } from "react-router"; import { GlobalStyle } from "@/styles/global-style"; import GlobalLayout from "@/components/global-layout"; import TestPage from "@/pages/test-page"; +import MessagePage from "@/pages/message-page"; function App() { return ( @@ -11,6 +12,7 @@ function App() { }> } /> + } /> diff --git a/src/pages/message-page.jsx b/src/pages/message-page.jsx new file mode 100644 index 0000000..0c9ae52 --- /dev/null +++ b/src/pages/message-page.jsx @@ -0,0 +1,250 @@ +import React from "react"; +import styled, { css } from "styled-components"; + +const DEFAULT_ICON_URL = "/assets/default-user.svg"; +const TEMP_IMAGE_URL = "/assets/temp-profile.jpg"; + +const selectableImages = [ + { id: 1, url: TEMP_IMAGE_URL, isSelected: true }, + { id: 2, url: TEMP_IMAGE_URL, isSelected: false }, + { id: 3, url: TEMP_IMAGE_URL, isSelected: false }, + { id: 4, url: TEMP_IMAGE_URL, isSelected: false }, + { id: 5, url: TEMP_IMAGE_URL, isSelected: false }, + { id: 6, url: TEMP_IMAGE_URL, isSelected: false }, + { id: 7, url: TEMP_IMAGE_URL, isSelected: false }, +]; + +export const PageContainer = styled.div` + max-width: 720px; + margin: 0 auto; + padding: 60px 24px; +`; + +export const MessageFormBox = styled.form` + display: flex; + flex-direction: column; + gap: 20px; +`; + +export const FormField = styled.div` + display: flex; + flex-direction: column; + gap: 12px; +`; + +export const FormLabel = styled.label` + font-size: 16px; + font-weight: 700; + line-height: 26px; /* 162.5% */ + color: #181818; +`; + +export const InputField = styled.input` + width: 100%; + padding: 12px 16px; + border-radius: 8px; + border: 1px solid #ccc; + font-size: 16px; + outline: none; + + &:focus { + border-color: #555; + } +`; + +export const ErrorMessage = styled.p` + color: #dc3545; /* 빨간색 계열 */ + font-size: 14px; + margin-top: -8px; /* 위쪽 갭 조정 */ +`; + +export const ProfileWrapper = styled.div` + display: flex; + flex-direction: column; + gap: 12px; +`; + +export const ProfileSelectorContainer = styled.div` + display: flex; + align-items: center; + gap: 32px; +`; + +export const ProfileDefaultBox = styled.div` + width: 70px; + height: 70px; + border-radius: 50%; + overflow: hidden; + flex-shrink: 0; /* 크기 고정 */ + border: 1px solid #ccc; + + img { + width: 100%; + height: 100%; + object-fit: cover; + } +`; + +export const SelectableImagesList = styled.ul` + display: flex; + gap: 4px; + overflow-x: auto; + padding: 4px 0; /* 스크롤바를 위한 패딩 */ + -webkit-overflow-scrolling: touch; /* iOS에서 부드러운 스크롤 */ + list-style: none; + margin: 0; +`; + +export const SelectableImageItem = styled.li` + width: 56px; + height: 56px; + border-radius: 50%; + overflow: hidden; + cursor: pointer; + flex-shrink: 0; /* 크기 고정 */ + transition: transform 0.2s, border 0.2s; + + border: 2px solid transparent; + ${({ isSelected }) => + isSelected && + css` + border-color: #3f60ff; /* 선택된 이미지 하이라이트 색상 */ + transform: scale(1.05); + `} + + &:hover { + opacity: 0.8; + } + + img { + width: 100%; + height: 100%; + object-fit: cover; + } +`; + +export const SelectField = styled.select` + width: 100%; + padding: 12px 16px; + border-radius: 8px; + border: 1px solid #ccc; + font-size: 16px; + background-color: #fff; + appearance: none; /* 기본 드롭다운 화살표 숨기기 */ + outline: none; +`; + +export const EditorPlaceholder = styled.div` + min-height: 200px; + padding: 16px; + border-radius: 8px; + border: 1px solid #ccc; + background-color: #f9f9f9; + font-size: 16px; + color: #777; +`; + +export const SubmitButton = styled.button` + width: 100%; + padding: 14px 0; + margin-top: 20px; + border-radius: 12px; + background-color: #3f60ff; /* Primary Color */ + color: #fff; + font-size: 18px; + font-weight: 700; + border: none; + cursor: pointer; + transition: background-color 0.3s; + + &:disabled { + background-color: #ccc; + cursor: not-allowed; + } + + &:not(:disabled):hover { + background-color: #2e4bc0; + } +`; + +function MessagePage() { + const isFormValid = false; + const hasError = true; + + return ( + + + {/* From. 입력 필드 */} + + From. + + {/* 에러 메시지 표시 */} + {hasError && "값을 입력해 주세요."} + + {/* 프로필 이미지 선택창 */} + + 프로필 이미지 + + + 기본 프로필 이미지 + + + + {selectableImages.map((image) => ( + + {`프로필 + + ))} + + + + + {/* 상대와의 관계 드롭다운*/} + + 상대와의 관계 + + + + + + + + + {/* 내용 입력 (Rich Text Editor 사용) */} + + 내용을 입력해 주세요 + +

I am your reach text editor.

+
+
+ + {/* 폰트 선택 드롭다운 */} + + 폰트 선택 + + + {/* 추가 폰트 옵션 추가 예정 */} + + + + {/* 생성하기 버튼 */} + + 생성하기 + +
+
+ ); +} + +export default MessagePage; diff --git a/src/styles/message-page.js b/src/styles/message-page.js new file mode 100644 index 0000000..70fc663 --- /dev/null +++ b/src/styles/message-page.js @@ -0,0 +1,173 @@ +import React from "react"; +import styled from "styled-components"; +import { colors } from "@/styles/colors"; +import { font } from "@/styles/font"; +import media from "@/styles/media"; + +const COLOR_PRIMARY = "#954aff"; +const COLOR_GRAY_BORDER = "#ddd"; + +// 페이지 전체 컨테이너 +export const PageContainer = styled.div` + display: flex; + justify-content: center; + padding: 80px 0; + min-height: 100vh; + background-color: white; +`; + +// 폼 박스 (720px 너비) +export const MessageFormBox = styled.form` + width: 720px; + padding: 40px; + background-color: white; +`; + +// 개별 폼 필드 섹션 (From, 관계, 폰트 등) +export const FormField = styled.div` + margin-bottom: 32px; +`; + +// 필드 제목 (Label 역할) +export const FormLabel = styled.label` + display: block; + margin-bottom: 12px; + font-size: 16px; + font-weight: bold; + color: #1c1c1c; +`; + +// 일반 입력 필드 (From. Input) +export const InputField = styled.input` + width: 100%; + padding: 12px 16px; + border: 1px solid ${COLOR_GRAY_BORDER}; + border-radius: 8px; + font-size: 16px; + &:focus { + outline: none; + border-color: ${COLOR_PRIMARY}; + } +`; + +// 에러 메시지 스타일 (2단계에서 사용) +export const ErrorMessage = styled.p` + margin-top: 8px; + font-size: 14px; + color: #ff5050; /* 예시 에러 색상 */ +`; + +// --- 프로필 이미지 섹션 스타일 --- + +export const ProfileWrapper = styled(FormField)` + /* FormField 스타일 상속 */ +`; + +export const ProfileSelectorContainer = styled.div` + display: flex; + align-items: center; +`; + +// 기본 프로필 이미지 박스 +export const ProfileDefaultBox = styled.div` + width: 56px; + height: 56px; + border-radius: 50%; + background-color: #eee; + display: flex; + justify-content: center; + align-items: center; + overflow: hidden; + margin-right: 16px; + border: 1px solid ${COLOR_GRAY_BORDER}; + cursor: pointer; + & img { + width: 100%; + height: 100%; + object-fit: cover; + } +`; + +// 선택 가능한 이미지 목록 컨테이너 +export const SelectableImagesList = styled.ul` + display: flex; + list-style: none; + padding: 0; + margin: 0; + overflow-x: auto; + gap: 8px; +`; + +// 개별 선택 이미지 아이템 +export const SelectableImageItem = styled.li` + width: 40px; + height: 40px; + border-radius: 50%; + overflow: hidden; + cursor: pointer; + border: 2px solid + ${(props) => (props.isSelected ? COLOR_PRIMARY : "transparent")}; + transition: border 0.2s; + + & img { + width: 100%; + height: 100%; + object-fit: cover; + } +`; + +// --- 드롭다운 (Select) 스타일 --- + +export const SelectField = styled.select` + width: 100%; + padding: 12px 16px; + border: 1px solid ${COLOR_GRAY_BORDER}; + border-radius: 8px; + font-size: 16px; + appearance: none; /* 기본 화살표 제거 */ + background-image: url("data:image/svg+xml;utf8,"); /* 커스텀 화살표 SVG */ + background-repeat: no-repeat; + background-position: right 16px center; + &:focus { + outline: none; + border-color: ${COLOR_PRIMARY}; + } +`; + +// --- 텍스트 에디터 섹션 --- +// (실제 에디터 라이브러리 통합 시 대체될 부분) + +export const EditorPlaceholder = styled.div` + min-height: 200px; /* 에디터가 차지할 최소 공간 */ + border: 1px solid ${COLOR_GRAY_BORDER}; + border-radius: 8px; + padding: 16px; + line-height: 1.5; + color: #888; + /* 실제 에디터 스타일을 여기서 오버라이드할 수 있습니다 */ +`; + +// --- 버튼 스타일 --- + +export const SubmitButton = styled.button` + width: 100%; + padding: 18px 0; + margin-top: 40px; + background-color: ${COLOR_PRIMARY}; + color: white; + font-size: 18px; + font-weight: bold; + border: none; + border-radius: 12px; + cursor: pointer; + transition: background-color 0.2s; + + &:hover:not(:disabled) { + background-color: #8335f0; /* 살짝 어두운 색 */ + } + + &:disabled { + background-color: #cccccc; /* 비활성화 색상 */ + cursor: not-allowed; + } +`; From 2ac1c50f27e353fa270b260b4af045f689bee288 Mon Sep 17 00:00:00 2001 From: BZzzzi Date: Thu, 6 Nov 2025 19:39:22 +0900 Subject: [PATCH 24/91] =?UTF-8?q?Chore:=20Global=20=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=95=84=EC=9B=83=20=ED=8F=B4=EB=8D=94=20=EC=9C=84=EC=B9=98=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/common/global-layout.jsx | 16 ++++++++++++++++ src/components/global-layout.jsx | 9 --------- 2 files changed, 16 insertions(+), 9 deletions(-) create mode 100644 src/components/common/global-layout.jsx delete mode 100644 src/components/global-layout.jsx diff --git a/src/components/common/global-layout.jsx b/src/components/common/global-layout.jsx new file mode 100644 index 0000000..332c319 --- /dev/null +++ b/src/components/common/global-layout.jsx @@ -0,0 +1,16 @@ +import { Outlet, useLocation } from "react-router"; +import Header from "@/components/common/header"; + +export default function GlobalLayout() { + const location = useLocation(); + const showButton = + location.pathname.includes("main-page") || + location.pathname.includes("list-page"); + + return ( + <> +
+ + + ); +} diff --git a/src/components/global-layout.jsx b/src/components/global-layout.jsx deleted file mode 100644 index 7fd6d55..0000000 --- a/src/components/global-layout.jsx +++ /dev/null @@ -1,9 +0,0 @@ -import { Outlet } from "react-router"; - -export default function GlobalLayout() { - return ( -
- -
- ); -} From 1dffa4943bed7ae6a09825c84dbf7ab12007b2be Mon Sep 17 00:00:00 2001 From: BZzzzi Date: Thu, 6 Nov 2025 19:43:56 +0900 Subject: [PATCH 25/91] =?UTF-8?q?Feat:=20=EA=B3=B5=ED=86=B5=20Header=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 반응형 레이아웃 적용 - 로고 클릭 시 루트 페이지로 이동 - useLocation()을 활용해 페이지별 버튼 노출 조건 분기 - useLocation() 사용을 위해 BrowserRouter를 main에서 App을 감싸도록 구조 변경 --- src/App.jsx | 5 ++- src/components/common/header.jsx | 59 ++++++++++++++++++++++++++++++++ src/main.jsx | 7 +++- src/pages/test-page.jsx | 51 ++++++++++++++++++++++++++- 4 files changed, 117 insertions(+), 5 deletions(-) create mode 100644 src/components/common/header.jsx diff --git a/src/App.jsx b/src/App.jsx index b8f39d1..3920ba7 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,6 +1,6 @@ -import { BrowserRouter, Routes, Route } from "react-router"; +import { Routes, Route } from "react-router"; import { GlobalStyle } from "@/styles/global-style"; -import GlobalLayout from "@/components/global-layout"; +import GlobalLayout from "@/components/common/global-layout"; import TestPage from "@/pages/test-page"; import MessagePage from "@/pages/message-page"; @@ -12,7 +12,6 @@ function App() { }> } /> - } /> diff --git a/src/components/common/header.jsx b/src/components/common/header.jsx new file mode 100644 index 0000000..455b475 --- /dev/null +++ b/src/components/common/header.jsx @@ -0,0 +1,59 @@ +import { Link } from "react-router"; +import styled from "styled-components"; +import logo from "@/assets/icons/logo.svg"; + +const ContainWrapper = styled.div` + position: sticky; + top: 0; + background-color: white; + border-bottom: 1px solid #ededed; +`; + +const Contain = styled.div` + max-width: 1248px; + margin: 0 auto; +`; + +const HeaderWrapper = styled.div` + display: flex; + justify-content: start; + align-items: center; + gap: 8px; + margin: 0 24px; +`; + +const LogoWrapper = styled.div` + display: flex; + align-items: center; + gap: 8px; +`; + +const ButtonWrapper = styled.div` + margin-left: auto; +`; + +export default function Header({ showButton }) { + return ( + <> + + + + + + 로고 +

Rolling

+
+ + {showButton && ( + + + + + + )} +
+
+
+ + ); +} diff --git a/src/main.jsx b/src/main.jsx index 12d36b1..e50388c 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -1,4 +1,9 @@ import { createRoot } from "react-dom/client"; +import { BrowserRouter } from "react-router"; import App from "./App.jsx"; -createRoot(document.getElementById("root")).render(); +createRoot(document.getElementById("root")).render( + + + +); diff --git a/src/pages/test-page.jsx b/src/pages/test-page.jsx index c03cb51..9a5e662 100644 --- a/src/pages/test-page.jsx +++ b/src/pages/test-page.jsx @@ -5,7 +5,6 @@ import media from "@/styles/media"; const Container = styled.div` padding: 40px 20px; - max-width: 800px; margin: 0 auto; `; @@ -51,6 +50,56 @@ export default function TestPage() {
데스크톱(1024px~): 보라색 배경, 큰 폰트 + + 창 크기를 조절해보세요! +
+
+ 모바일(~599px): 파란색 배경, 작은 폰트 +
+ 태블릿(600~1023px): 초록색 배경, 중간 폰트 +
+ 데스크톱(1024px~): 보라색 배경, 큰 폰트 +
+ + 창 크기를 조절해보세요! +
+
+ 모바일(~599px): 파란색 배경, 작은 폰트 +
+ 태블릿(600~1023px): 초록색 배경, 중간 폰트 +
+ 데스크톱(1024px~): 보라색 배경, 큰 폰트 +
+ + 창 크기를 조절해보세요! +
+
+ 모바일(~599px): 파란색 배경, 작은 폰트 +
+ 태블릿(600~1023px): 초록색 배경, 중간 폰트 +
+ 데스크톱(1024px~): 보라색 배경, 큰 폰트 +
+ + 창 크기를 조절해보세요! +
+
+ 모바일(~599px): 파란색 배경, 작은 폰트 +
+ 태블릿(600~1023px): 초록색 배경, 중간 폰트 +
+ 데스크톱(1024px~): 보라색 배경, 큰 폰트 +
+ + 창 크기를 조절해보세요! +
+
+ 모바일(~599px): 파란색 배경, 작은 폰트 +
+ 태블릿(600~1023px): 초록색 배경, 중간 폰트 +
+ 데스크톱(1024px~): 보라색 배경, 큰 폰트 +
); } From 300642f863a8ef300274d24f197f87dc76ce2dd5 Mon Sep 17 00:00:00 2001 From: BZzzzi Date: Thu, 6 Nov 2025 19:49:04 +0900 Subject: [PATCH 26/91] =?UTF-8?q?Refactor:=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/test-page.jsx | 50 ----------------------------------------- 1 file changed, 50 deletions(-) diff --git a/src/pages/test-page.jsx b/src/pages/test-page.jsx index 9a5e662..8a39db3 100644 --- a/src/pages/test-page.jsx +++ b/src/pages/test-page.jsx @@ -50,56 +50,6 @@ export default function TestPage() {
데스크톱(1024px~): 보라색 배경, 큰 폰트 - - 창 크기를 조절해보세요! -
-
- 모바일(~599px): 파란색 배경, 작은 폰트 -
- 태블릿(600~1023px): 초록색 배경, 중간 폰트 -
- 데스크톱(1024px~): 보라색 배경, 큰 폰트 -
- - 창 크기를 조절해보세요! -
-
- 모바일(~599px): 파란색 배경, 작은 폰트 -
- 태블릿(600~1023px): 초록색 배경, 중간 폰트 -
- 데스크톱(1024px~): 보라색 배경, 큰 폰트 -
- - 창 크기를 조절해보세요! -
-
- 모바일(~599px): 파란색 배경, 작은 폰트 -
- 태블릿(600~1023px): 초록색 배경, 중간 폰트 -
- 데스크톱(1024px~): 보라색 배경, 큰 폰트 -
- - 창 크기를 조절해보세요! -
-
- 모바일(~599px): 파란색 배경, 작은 폰트 -
- 태블릿(600~1023px): 초록색 배경, 중간 폰트 -
- 데스크톱(1024px~): 보라색 배경, 큰 폰트 -
- - 창 크기를 조절해보세요! -
-
- 모바일(~599px): 파란색 배경, 작은 폰트 -
- 태블릿(600~1023px): 초록색 배경, 중간 폰트 -
- 데스크톱(1024px~): 보라색 배경, 큰 폰트 -
); } From 4dc228651dda6e52adb79402719e68ebc8ad4cd0 Mon Sep 17 00:00:00 2001 From: BZzzzi Date: Fri, 7 Nov 2025 18:43:37 +0900 Subject: [PATCH 27/91] =?UTF-8?q?Feat:=20=EA=B3=B5=ED=86=B5=20=EB=B2=84?= =?UTF-8?q?=ED=8A=BC=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Button 컴포넌트 생성 및 variant별 스타일 정의 - primary, secondary, outlined, plus, delete variant 지원 - large, medium, small, tiny, plus, delete 사이즈 옵션 제공 - plus, delete variant는 아이콘 전용 버튼으로 구현 - outlined variant에서 emoji prop으로 이모지 아이콘 선택적 추가 지원 - 아이콘과 텍스트를 함께 표시할 수 있는 레이아웃 구현 (gap: 10px) - 각 아이콘에 className 적용으로 variant별 스타일 분리 - hover, active, focus 상태별 인터랙션 스타일 적용 - CSS transition으로 부드러운 상태 전환 효과 추가 - styled-components 기반 동적 스타일 시스템 구성 - 테스트 페이지에 버튼 컴포넌트 사용 예시 추가 --- src/components/common/button.jsx | 191 +++++++++++++++++++++++++++++++ src/pages/test-page.jsx | 31 ++++- 2 files changed, 221 insertions(+), 1 deletion(-) create mode 100644 src/components/common/button.jsx diff --git a/src/components/common/button.jsx b/src/components/common/button.jsx new file mode 100644 index 0000000..a9fb0c8 --- /dev/null +++ b/src/components/common/button.jsx @@ -0,0 +1,191 @@ +import styled, { css } from "styled-components"; +import { colors } from "@/styles/colors"; +import { font } from "@/styles/font"; +import plusIcon from "@/assets/icons/plus.svg"; +import deleteIcon from "@/assets/icons/deleted.svg"; +import addEmojiIcon from "@/assets/icons/add-emoji.svg"; + +const SIZES = { + large: css` + padding: 14px 24px; + border-radius: 12px; + ${font.bold18} + `, + medium: css` + padding: 7px 16px; + border-radius: 6px; + ${font.regular16} + `, + small: css` + padding: 6px 16px; + border-radius: 6px; + ${font.regular16} + `, + tiny: css` + padding: 2px 16px; + border-radius: 6px; + ${font.regular14} + `, + plus: css` + padding: 16px; + border-radius: 9999px; + `, + delete: css` + padding: 6px; + border-radius: 6px; + `, +}; + +const VARIANT_STYLES = { + primary: css` + background-color: ${colors.purple[600]}; + color: white; + border: none; + + &:hover { + background-color: ${colors.purple[700]}; + } + + &:active { + background-color: ${colors.purple[800]}; + } + + &:focus { + background-color: ${colors.purple[900]}; + } + `, + + secondary: css` + background-color: white; + color: ${colors.purple[700]}; + border: 1px solid ${colors.purple[600]}; + + &:hover { + background-color: ${colors.purple[100]}; + color: ${colors.purple[600]}; + border-color: ${colors.purple[700]}; + } + + &:active { + background-color: ${colors.purple[100]}; + color: ${colors.purple[600]}; + border-color: ${colors.purple[800]}; + } + + &:focus { + color: ${colors.purple[600]}; + border-color: ${colors.purple[800]}; + } + `, + + outlined: css` + background-color: white; + color: ${colors.gray[900]}; + border: 1px solid ${colors.gray[300]}; + + &:hover { + background-color: ${colors.gray[100]}; + } + + &:active { + background-color: ${colors.gray[100]}; + } + + &:focus { + border-color: ${colors.gray[500]}; + } + + .emoji-icon { + width: 24px; + height: 24px; + } + `, + + plus: css` + background-color: ${colors.gray[500]}; + border: 1px solid transparent; + + &:hover { + background-color: ${colors.gray[600]}; + } + + &:active { + background-color: ${colors.gray[700]}; + } + + &:focus { + background-color: ${colors.gray[700]}; + border: 1px solid ${colors.gray[800]}; + } + + .plus-icon { + width: 24px; + height: 24px; + } + `, + + delete: css` + background-color: white; + border: 1px solid ${colors.gray[300]}; + + &:hover { + background-color: ${colors.gray[100]}; + } + + &:active { + background-color: ${colors.gray[100]}; + } + + &:focus { + border-color: 1px solid ${colors.gray[500]}; + } + + .delete-icon { + width: 24px; + height: 24px; + } + `, +}; + +const CustomButton = styled.button` + display: flex; + justify-content: center; + align-items: center; + gap: 10px; + cursor: pointer; + transition: all 0.2s ease-in-out; + ${({ variant }) => VARIANT_STYLES[variant] || VARIANT_STYLES.primary} + ${({ size }) => SIZES[size] || SIZES.medium} + + &:disabled { + background-color: ${colors.gray[300]}; + color: white; + border: transparent; + cursor: not-allowed; + } +`; + +export default function Button({ + children, + variant = "primary", + size = "medium", + emoji = "", + ...props +}) { + return ( + + {variant === "plus" ? ( + 추가 + ) : variant === "delete" ? ( + 삭제 + ) : variant === "outlined" && emoji ? ( + <> + 이모지 + {children} + + ) : ( + children + )} + + ); +} diff --git a/src/pages/test-page.jsx b/src/pages/test-page.jsx index 8a39db3..0f345bb 100644 --- a/src/pages/test-page.jsx +++ b/src/pages/test-page.jsx @@ -2,6 +2,7 @@ import styled from "styled-components"; import { colors } from "@/styles/colors"; import { font } from "@/styles/font"; import media from "@/styles/media"; +import Button from "@/components/common/button"; const Container = styled.div` padding: 40px 20px; @@ -36,10 +37,15 @@ const ResponsiveBox = styled.div` `} `; +const CustomButton = styled(Button)` + width: 100%; + padding: 30px; +`; + export default function TestPage() { return ( - 스타일 테스트 + 공용 스타일 & 컴포넌트 테스트 창 크기를 조절해보세요!
@@ -50,6 +56,29 @@ export default function TestPage() {
데스크톱(1024px~): 보라색 배경, 큰 폰트
+ + + + + + + + custom button + custom button
); } From 9c1c7ba4f8c7c8e7fe7400ef86e70783a073b81a Mon Sep 17 00:00:00 2001 From: BZzzzi Date: Fri, 7 Nov 2025 18:44:46 +0900 Subject: [PATCH 28/91] =?UTF-8?q?Feat:=20Header=EC=97=90=20=EA=B3=B5?= =?UTF-8?q?=ED=86=B5=20Button=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 기존 HTML button 태그를 Button 컴포넌트로 교체 - 일관된 디자인 시스템 적용을 위한 컴포넌트 통합 --- src/components/common/header.jsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/common/header.jsx b/src/components/common/header.jsx index 455b475..7d0e16d 100644 --- a/src/components/common/header.jsx +++ b/src/components/common/header.jsx @@ -1,6 +1,7 @@ import { Link } from "react-router"; import styled from "styled-components"; import logo from "@/assets/icons/logo.svg"; +import Button from "@/components/common/button"; const ContainWrapper = styled.div` position: sticky; @@ -47,7 +48,9 @@ export default function Header({ showButton }) { {showButton && ( - + )} From ffd7c4577245f8a576d56f537d55ea513fa4e336 Mon Sep 17 00:00:00 2001 From: BZzzzi Date: Fri, 7 Nov 2025 18:46:15 +0900 Subject: [PATCH 29/91] =?UTF-8?q?Refactor:=20GlobalLayout=20=EB=B2=84?= =?UTF-8?q?=ED=8A=BC=20=ED=91=9C=EC=8B=9C=20=EB=A1=9C=EC=A7=81=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 버튼 표시 페이지 목록을 PAGES_WITH_BUTTON 상수로 분리 - 새로운 페이지 추가 시 배열에 문자열만 추가하면 되도록 개선 --- src/components/common/global-layout.jsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/components/common/global-layout.jsx b/src/components/common/global-layout.jsx index 332c319..db2b1ed 100644 --- a/src/components/common/global-layout.jsx +++ b/src/components/common/global-layout.jsx @@ -1,11 +1,13 @@ import { Outlet, useLocation } from "react-router"; import Header from "@/components/common/header"; +const PAGES_WITH_BUTTON = ["main-page", "list-page"]; + export default function GlobalLayout() { const location = useLocation(); - const showButton = - location.pathname.includes("main-page") || - location.pathname.includes("list-page"); + const showButton = PAGES_WITH_BUTTON.some((page) => + location.pathname.includes(page) + ); return ( <> From cd13719d2fbde8e7b60912c0cfca5cf4ef9e12c6 Mon Sep 17 00:00:00 2001 From: BZzzzi Date: Fri, 7 Nov 2025 19:59:47 +0900 Subject: [PATCH 30/91] =?UTF-8?q?Feat:=20=EC=9E=84=EC=8B=9C=20=EB=84=A4?= =?UTF-8?q?=EB=B9=84=EA=B2=8C=EC=9D=B4=EC=85=98=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.jsx | 14 ++++++++++++ src/pages/temp-page.jsx | 48 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 src/pages/temp-page.jsx diff --git a/src/App.jsx b/src/App.jsx index 3920ba7..5369fae 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -3,6 +3,8 @@ import { GlobalStyle } from "@/styles/global-style"; import GlobalLayout from "@/components/common/global-layout"; import TestPage from "@/pages/test-page"; import MessagePage from "@/pages/message-page"; +import TempPage from "@/pages/temp-page"; +import ToastTestPage from "@/pages/toast-test-page"; function App() { return ( @@ -15,6 +17,18 @@ function App() {
+ + }> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + ); } diff --git a/src/pages/temp-page.jsx b/src/pages/temp-page.jsx new file mode 100644 index 0000000..fd8990b --- /dev/null +++ b/src/pages/temp-page.jsx @@ -0,0 +1,48 @@ +import Button from "@/components/common/button"; +import { Link } from "react-router"; +import styled from "styled-components"; + +const Contain = styled.div` + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 10px; + margin: 5% auto; +`; + +const CustomButton = styled(Button)` + width: 200px; + padding: 10%; +`; +export default function TempPage() { + return ( + + + 메인 페이지 + + + 리스트 페이지 + + + 롤페 페이지 + + + 롤페 생성 페이지 + + + 롤페 메시지 페이지 + + + + 테스트 페이지 + + + + + toast 테스트 페이지 + + + + ); +} From 90a718ae30b60dce26bdf9ae9391eebb7e4bf029 Mon Sep 17 00:00:00 2001 From: BZzzzi Date: Sat, 8 Nov 2025 02:28:53 +0900 Subject: [PATCH 31/91] =?UTF-8?q?Feat:=20=EA=B3=B5=ED=86=B5=20Toast=20?= =?UTF-8?q?=EC=95=8C=EB=A6=BC=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 컴포넌트 생성 및 success/delete 타입 지원 - fadeIn/fadeOut 애니메이션 적용 (0.3s) - media breakpoint 기반 반응형 디자인 적용 (small/medium/large) - ToastContext 및 ToastProvider를 통한 전역 토스트 상태 관리 - useToast 커스텀 훅 제공 (success, delete 메서드) - createPortal을 활용한 토스트 렌더링 구조 - 5초 자동 사라짐 및 수동 닫기 기능 - Fast Refresh 호환을 위한 Context 파일 분리 - 삭제 에러용 빨간색 아이콘 추가 - 토스트 테스트 페이지 추가 --- index.html | 1 + src/assets/icons/deleted-red.svg | 3 ++ src/components/common/toast.jsx | 83 +++++++++++++++++++++++++++++ src/contexts/toast-context-state.js | 3 ++ src/contexts/toast-context.jsx | 52 ++++++++++++++++++ src/hooks/use-toast.js | 6 +++ src/main.jsx | 5 +- src/pages/toast-test-page.jsx | 26 +++++++++ 8 files changed, 178 insertions(+), 1 deletion(-) create mode 100644 src/assets/icons/deleted-red.svg create mode 100644 src/components/common/toast.jsx create mode 100644 src/contexts/toast-context-state.js create mode 100644 src/contexts/toast-context.jsx create mode 100644 src/hooks/use-toast.js create mode 100644 src/pages/toast-test-page.jsx diff --git a/index.html b/index.html index d4c7e2a..cd6c3dd 100644 --- a/index.html +++ b/index.html @@ -14,6 +14,7 @@
+
diff --git a/src/assets/icons/deleted-red.svg b/src/assets/icons/deleted-red.svg new file mode 100644 index 0000000..e051b72 --- /dev/null +++ b/src/assets/icons/deleted-red.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/common/toast.jsx b/src/components/common/toast.jsx new file mode 100644 index 0000000..09ef5b2 --- /dev/null +++ b/src/components/common/toast.jsx @@ -0,0 +1,83 @@ +import styled, { keyframes } from "styled-components"; +import completedIcon from "@/assets/icons/completed.svg"; +import deletedRedIcon from "@/assets/icons/deleted-red.svg"; +import closeIcon from "@/assets/icons/close.svg"; +import { font } from "@/styles/font"; +import media from "@/styles/media"; + +const fadeIn = keyframes` + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +`; + +const fadeOut = keyframes` + from { + opacity: 1; + transform: translateY(0); + } + to { + opacity: 0; + transform: translateY(20px); + } +`; + +const ToastStyle = styled.div` + display: flex; + justify-content: start; + align-items: center; + gap: 12px; + position: fixed; + background-color: rgba(0, 0, 0, 0.8); + color: white; + padding: 19px 30px; + border-radius: 8px; + animation: ${({ isClosing }) => (isClosing ? fadeOut : fadeIn)} 0.3s + ease-in-out forwards; + ${font.regular16} + + button { + background-color: transparent; + border: none; + cursor: pointer; + margin-left: auto; + } + + ${media.large` + width: 524px; + left: calc(50% - 262px); + bottom: 70px; + `} + + ${media.medium` + width: 524px; + left: calc(50% - 262px); + bottom: 50px; + `} + + ${media.small` + width: 320px; + left: calc(50% - 160px); + bottom: 88px; + `} +`; + +export default function Toast({ children, type, isClosing, onClose }) { + return ( + + {type + {children} + + + ); +} diff --git a/src/contexts/toast-context-state.js b/src/contexts/toast-context-state.js new file mode 100644 index 0000000..2056dd1 --- /dev/null +++ b/src/contexts/toast-context-state.js @@ -0,0 +1,3 @@ +import { createContext } from "react"; + +export const ToastContext = createContext(null); diff --git a/src/contexts/toast-context.jsx b/src/contexts/toast-context.jsx new file mode 100644 index 0000000..4e764b3 --- /dev/null +++ b/src/contexts/toast-context.jsx @@ -0,0 +1,52 @@ +import { useState, useCallback } from "react"; +import { createPortal } from "react-dom"; +import Toast from "@/components/common/toast"; +import { ToastContext } from "./toast-context-state"; + +export default function ToastProvider({ children }) { + const [toasts, setToasts] = useState([]); + + const removeToast = useCallback((id) => { + setToasts((prev) => prev.filter((t) => t.id !== id)); + }, []); + + const addToast = useCallback( + (type, message) => { + const id = Date.now(); + setToasts((prev) => [...prev, { id, type, message, isClosing: false }]); + setTimeout(() => { + setToasts((prev) => + prev.map((t) => (t.id === id ? { ...t, isClosing: true } : t)) + ); + setTimeout(() => removeToast(id), 300); + }, 4700); + }, + [removeToast] + ); + + const toast = { + success: (msg) => addToast("success", msg), + delete: (msg) => addToast("delete", msg), + }; + + return ( + + {children} + {createPortal( + <> + {toasts.map((t) => ( + removeToast(t.id)} + > + {t.message} + + ))} + , + document.getElementById("toast") + )} + + ); +} diff --git a/src/hooks/use-toast.js b/src/hooks/use-toast.js new file mode 100644 index 0000000..47c8088 --- /dev/null +++ b/src/hooks/use-toast.js @@ -0,0 +1,6 @@ +import { useContext } from "react"; +import { ToastContext } from "@/contexts/toast-context-state"; + +export default function useToast() { + return useContext(ToastContext); +} diff --git a/src/main.jsx b/src/main.jsx index e50388c..84fb027 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -1,9 +1,12 @@ import { createRoot } from "react-dom/client"; import { BrowserRouter } from "react-router"; import App from "./App.jsx"; +import ToastProvider from "@/contexts/toast-context"; createRoot(document.getElementById("root")).render( - + + + ); diff --git a/src/pages/toast-test-page.jsx b/src/pages/toast-test-page.jsx new file mode 100644 index 0000000..b1ae12d --- /dev/null +++ b/src/pages/toast-test-page.jsx @@ -0,0 +1,26 @@ +import styled from "styled-components"; +import Button from "@/components/common/button"; +import useToast from "@/hooks/use-toast"; + +const Container = styled.div` + padding: 40px; + display: flex; + flex-direction: column; + gap: 20px; +`; + +export default function ToastTestPage() { + const toast = useToast(); + + return ( + +

Toast 테스트 페이지 (5초 후 자동 사라짐)

+ + +
+ ); +} From 9179789835445a30aa5b79ad207c2a289919481d Mon Sep 17 00:00:00 2001 From: BZzzzi Date: Sat, 8 Nov 2025 02:31:04 +0900 Subject: [PATCH 32/91] =?UTF-8?q?Refactor:=20Button=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EC=8A=A4=ED=83=80=EC=9D=BC=EB=AA=85=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20=EB=B0=8F=20=EB=9D=BC=EC=9A=B0=ED=8A=B8=20?= =?UTF-8?q?=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CustomButton을 ButtonStyle로 네이밍 변경 - App.jsx의 미구현 라우트 주석 처리 --- src/App.jsx | 10 +++++----- src/components/common/button.jsx | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index 5369fae..94f8b09 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -20,11 +20,11 @@ function App() { }> } /> - } /> - } /> - } /> - } /> - } /> + {/* } /> + } /> + } /> + } /> + } /> */} } /> } /> diff --git a/src/components/common/button.jsx b/src/components/common/button.jsx index a9fb0c8..f37e20a 100644 --- a/src/components/common/button.jsx +++ b/src/components/common/button.jsx @@ -147,7 +147,7 @@ const VARIANT_STYLES = { `, }; -const CustomButton = styled.button` +const ButtonStyle = styled.button` display: flex; justify-content: center; align-items: center; @@ -173,7 +173,7 @@ export default function Button({ ...props }) { return ( - + {variant === "plus" ? ( 추가 ) : variant === "delete" ? ( @@ -186,6 +186,6 @@ export default function Button({ ) : ( children )} - + ); } From f12b9b282c69c05c7803d0dce9d3894f5fafdf32 Mon Sep 17 00:00:00 2001 From: apple Date: Tue, 11 Nov 2025 07:32:36 +0900 Subject: [PATCH 33/91] =?UTF-8?q?Feat:=20message=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20UI=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.jsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/App.jsx b/src/App.jsx index 94f8b09..ea89a9b 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -5,6 +5,7 @@ import TestPage from "@/pages/test-page"; import MessagePage from "@/pages/message-page"; import TempPage from "@/pages/temp-page"; import ToastTestPage from "@/pages/toast-test-page"; +import MessagePage from "@/pages/message-page"; function App() { return ( @@ -29,6 +30,14 @@ function App() { } /> + + + }> + } /> + } /> + + + ); } From 5511ae10d4aca5145b1d1b51a9a8c8002b2024a8 Mon Sep 17 00:00:00 2001 From: yeseung00 Date: Tue, 11 Nov 2025 12:04:13 +0900 Subject: [PATCH 34/91] =?UTF-8?q?Refactor:=20=EB=9D=BC=EC=9A=B0=ED=8A=B8?= =?UTF-8?q?=20=EA=B2=BD=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.jsx | 22 +++------------------- src/pages/temp-page.jsx | 2 +- 2 files changed, 4 insertions(+), 20 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index ea89a9b..6296bc0 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -5,39 +5,23 @@ import TestPage from "@/pages/test-page"; import MessagePage from "@/pages/message-page"; import TempPage from "@/pages/temp-page"; import ToastTestPage from "@/pages/toast-test-page"; -import MessagePage from "@/pages/message-page"; function App() { return ( <> - - - }> - } /> - - - }> } /> - {/* } /> + {/*} /> } /> } /> - } /> - } /> */} + } /> */} + } /> } /> } /> - - - }> - } /> - } /> - - - ); } diff --git a/src/pages/temp-page.jsx b/src/pages/temp-page.jsx index fd8990b..cb732a2 100644 --- a/src/pages/temp-page.jsx +++ b/src/pages/temp-page.jsx @@ -30,7 +30,7 @@ export default function TempPage() { 롤페 생성 페이지 - + 롤페 메시지 페이지 From 4362b70c00aaf30695eecab116f5ec1f3363bf78 Mon Sep 17 00:00:00 2001 From: yeseung00 Date: Tue, 11 Nov 2025 12:15:29 +0900 Subject: [PATCH 35/91] =?UTF-8?q?Fix:=20=EB=B9=8C=EB=93=9C=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/styles/message-page.js | 26 ++------------------------ 1 file changed, 2 insertions(+), 24 deletions(-) diff --git a/src/styles/message-page.js b/src/styles/message-page.js index 70fc663..dafb8ad 100644 --- a/src/styles/message-page.js +++ b/src/styles/message-page.js @@ -1,13 +1,9 @@ import React from "react"; import styled from "styled-components"; -import { colors } from "@/styles/colors"; -import { font } from "@/styles/font"; -import media from "@/styles/media"; const COLOR_PRIMARY = "#954aff"; const COLOR_GRAY_BORDER = "#ddd"; -// 페이지 전체 컨테이너 export const PageContainer = styled.div` display: flex; justify-content: center; @@ -16,19 +12,16 @@ export const PageContainer = styled.div` background-color: white; `; -// 폼 박스 (720px 너비) export const MessageFormBox = styled.form` width: 720px; padding: 40px; background-color: white; `; -// 개별 폼 필드 섹션 (From, 관계, 폰트 등) export const FormField = styled.div` margin-bottom: 32px; `; -// 필드 제목 (Label 역할) export const FormLabel = styled.label` display: block; margin-bottom: 12px; @@ -37,7 +30,6 @@ export const FormLabel = styled.label` color: #1c1c1c; `; -// 일반 입력 필드 (From. Input) export const InputField = styled.input` width: 100%; padding: 12px 16px; @@ -50,15 +42,12 @@ export const InputField = styled.input` } `; -// 에러 메시지 스타일 (2단계에서 사용) export const ErrorMessage = styled.p` margin-top: 8px; font-size: 14px; color: #ff5050; /* 예시 에러 색상 */ `; -// --- 프로필 이미지 섹션 스타일 --- - export const ProfileWrapper = styled(FormField)` /* FormField 스타일 상속 */ `; @@ -68,7 +57,6 @@ export const ProfileSelectorContainer = styled.div` align-items: center; `; -// 기본 프로필 이미지 박스 export const ProfileDefaultBox = styled.div` width: 56px; height: 56px; @@ -88,7 +76,6 @@ export const ProfileDefaultBox = styled.div` } `; -// 선택 가능한 이미지 목록 컨테이너 export const SelectableImagesList = styled.ul` display: flex; list-style: none; @@ -98,7 +85,6 @@ export const SelectableImagesList = styled.ul` gap: 8px; `; -// 개별 선택 이미지 아이템 export const SelectableImageItem = styled.li` width: 40px; height: 40px; @@ -116,8 +102,6 @@ export const SelectableImageItem = styled.li` } `; -// --- 드롭다운 (Select) 스타일 --- - export const SelectField = styled.select` width: 100%; padding: 12px 16px; @@ -134,21 +118,15 @@ export const SelectField = styled.select` } `; -// --- 텍스트 에디터 섹션 --- -// (실제 에디터 라이브러리 통합 시 대체될 부분) - export const EditorPlaceholder = styled.div` - min-height: 200px; /* 에디터가 차지할 최소 공간 */ + min-height: 200px; border: 1px solid ${COLOR_GRAY_BORDER}; border-radius: 8px; padding: 16px; line-height: 1.5; color: #888; - /* 실제 에디터 스타일을 여기서 오버라이드할 수 있습니다 */ `; -// --- 버튼 스타일 --- - export const SubmitButton = styled.button` width: 100%; padding: 18px 0; @@ -163,7 +141,7 @@ export const SubmitButton = styled.button` transition: background-color 0.2s; &:hover:not(:disabled) { - background-color: #8335f0; /* 살짝 어두운 색 */ + background-color: #8335f0; } &:disabled { From 6fddc155bed7b8d0b276640e4e55040a512ca3cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A0=95=EC=9B=90?= Date: Wed, 12 Nov 2025 11:17:27 +0900 Subject: [PATCH 36/91] =?UTF-8?q?Feat:=20=EB=A6=AC=EB=B7=B0=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EB=B0=8F,=20=EC=B9=B4=EB=93=9C=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=20UI=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.jsx | 20 ++- src/components/rolling/card-contents.jsx | 59 +++++++ src/hooks/use-cards.js | 36 +++++ src/pages/rolling-page-edit.jsx | 58 +++++++ src/pages/rolling-page.jsx | 6 +- src/styles/rolling-page-styles.js | 190 ++++++++++++++++++++++- 6 files changed, 349 insertions(+), 20 deletions(-) create mode 100644 src/components/rolling/card-contents.jsx create mode 100644 src/hooks/use-cards.js create mode 100644 src/pages/rolling-page-edit.jsx diff --git a/src/App.jsx b/src/App.jsx index 1a6ba7d..397b8e8 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -11,21 +11,19 @@ function App() { return ( <> - - - }> - } /> - {/* } /> + + }> + } /> + {/* } /> } /> } /> } /> } /> */} - } /> - } /> - } /> - - - + } /> + } /> + } /> + + ); } diff --git a/src/components/rolling/card-contents.jsx b/src/components/rolling/card-contents.jsx new file mode 100644 index 0000000..4c1336b --- /dev/null +++ b/src/components/rolling/card-contents.jsx @@ -0,0 +1,59 @@ +import { + CardContainer, + Card, + CardEditButton, + CardContentContainer, + CardContentStatus, + CardContentStatusContainer, + CardContentStatusProfileImage, + CardContentStatusProfileName, + CardContentStatusRelationship, + CardContentText, + CardContentDate, + CardContentStatusProfileContainer, + CardContentDeleteButton, +} from "@/styles/rolling-page-styles"; +import { useState } from "react"; +import useCards from "@/hooks/use-cards"; + + +export default function CardContents({ maxVisible = 6 }) { + const [cards] = useState([ + { id: 1, name: '김철수', profileImageURL: 'https://via.placeholder.com/28', relationship: 'friend' }, + { id: 2, name: '이영희', profileImageURL: 'https://via.placeholder.com/28', relationship: 'family' }, + { id: 3, name: '박민수', profileImageURL: 'https://via.placeholder.com/28', relationship: 'colleague' }, + { id: 4, name: '최영희', profileImageURL: 'https://via.placeholder.com/28', relationship: 'acquaintance' }, + { id: 5, name: 'dsadsa', profileImageURL: 'https://via.placeholder.com/28', relationship: 'friend' }, + { id: 6, name: 'qweqwe', profileImageURL: 'https://via.placeholder.com/28', relationship: 'family' }, + { id: 7, name: 'zxczxc', profileImageURL: 'https://via.placeholder.com/28', relationship: 'colleague' }, + { id: 8, name: 'm,nmnb', profileImageURL: 'https://via.placeholder.com/28', relationship: 'acquaintance' } + ]); + const { visibleCards } = useCards(cards, maxVisible); + + return ( + <> + + + {visibleCards.map((card) => ( + + + + + + + From. {card.name} + {card.relationship} + + + + + sdasdsa + 2025.11.12 + + + + ))} + + + ); +} \ No newline at end of file diff --git a/src/hooks/use-cards.js b/src/hooks/use-cards.js new file mode 100644 index 0000000..dcf0bfc --- /dev/null +++ b/src/hooks/use-cards.js @@ -0,0 +1,36 @@ +import { useMemo } from 'react'; + + +/** + * 카드 섹션 컴포넌트 + * 책임: 카드 데이터를 처리하고 표시 + */ +export default function useCards(cards, maxVisible = 6) { + const processedData = useMemo(() => { + if (!cards || cards.length === 0) { + return { + visibleCards: [], + overflowCount: 0, + totalCount: 0, + hasOverflow: false, + }; + } + + const totalCount = cards.length; + const hasOverflow = totalCount > maxVisible; + + const visibleCount = hasOverflow ? maxVisible - 1 : maxVisible; + const visibleCards = cards.slice(0, visibleCount); + const overflowCount = hasOverflow ? totalCount - visibleCount : 0; + + return { + visibleCards, + overflowCount, + totalCount, + hasOverflow, + }; + }, [cards, maxVisible]); + + return processedData; +} + diff --git a/src/pages/rolling-page-edit.jsx b/src/pages/rolling-page-edit.jsx new file mode 100644 index 0000000..8cea62b --- /dev/null +++ b/src/pages/rolling-page-edit.jsx @@ -0,0 +1,58 @@ +import React, { useState } from 'react'; +import { + RollingHeaderContainer, + RollingHeaderUserInfo, + RollingHeaderRightContainer, + PerpendicularLineFirst, + RollingPageContainer, + + +} from "@/styles/rolling-page-styles"; +import RollingPageHeader from "@/pages/rolling-page-head"; +import ParticipantSection from "@/components/rolling/participant-section"; +import ArrowDownIcon from "@/assets/icons/arrow-down.svg"; +import AddEmojiIcon from "@/assets/icons/add-emoji.svg"; +import ShareIcon from "@/assets/icons/share.svg"; +import CardContents from "@/components/rolling/card-contents"; + +export default function RollingPage() { + // TODO: 실제로는 API에서 받아올 데이터 + // 임시 데이터 (나중에 API 호출로 대체) + const [profiles] = useState([ + { id: 1, name: '김철수', profileImageURL: 'https://via.placeholder.com/28' }, + { id: 2, name: '이영희', profileImageURL: 'https://via.placeholder.com/28' }, + { id: 3, name: '박민수', profileImageURL: 'https://via.placeholder.com/28' }, + + ]); + + + return ( + <> + + + To. Ashley Kim + + + + {/* 참여자 프로필 섹션 */} + + + + + {/* 이모지 및 공유 헤더 */} + + + + + + + + + ); +} + + diff --git a/src/pages/rolling-page.jsx b/src/pages/rolling-page.jsx index 5524288..9c43cba 100644 --- a/src/pages/rolling-page.jsx +++ b/src/pages/rolling-page.jsx @@ -5,12 +5,15 @@ import { RollingHeaderRightContainer, PerpendicularLineFirst, RollingPageContainer, + + } from "@/styles/rolling-page-styles"; import RollingPageHeader from "@/pages/rolling-page-head"; import ParticipantSection from "@/components/rolling/participant-section"; import ArrowDownIcon from "@/assets/icons/arrow-down.svg"; import AddEmojiIcon from "@/assets/icons/add-emoji.svg"; import ShareIcon from "@/assets/icons/share.svg"; +import CardContents from "@/components/rolling/card-contents"; export default function RollingPage() { // TODO: 실제로는 API에서 받아올 데이터 @@ -22,6 +25,7 @@ export default function RollingPage() { ]); + return ( <> @@ -45,7 +49,7 @@ export default function RollingPage() { - {/* 롤링 페이퍼 컨텐츠가 들어갈 영역 */} + ); diff --git a/src/styles/rolling-page-styles.js b/src/styles/rolling-page-styles.js index f29f391..d2162db 100644 --- a/src/styles/rolling-page-styles.js +++ b/src/styles/rolling-page-styles.js @@ -3,6 +3,8 @@ import { colors } from "@/styles/colors"; import { font } from "@/styles/font"; import ShareIcon from "@/assets/icons/share.svg"; import media from "@/styles/media"; +import EditIcon from "@/assets/icons/plus.svg"; +import DeleteIcon from "@/assets/icons/deleted.svg"; //최상단헤더 컨테이너 @@ -278,13 +280,6 @@ export const RollingHeaderArrowDown = styled.img` -export const RollingPageContainer = styled.div` -background-color: ${colors.blue[100]}; -width: 100%; -margin: 0 auto; -padding: 20px; -height: 100vh; -`; @@ -303,4 +298,183 @@ export const PerpendicularLineFirst = styled(PerpendicularLine)` `} `; -export const PerpendicularLineSecond = styled(PerpendicularLine)``; \ No newline at end of file +export const PerpendicularLineSecond = styled(PerpendicularLine)``; + + + +export const RollingPageContainer = styled.div` +display: flex; +justify-content: center; +align-items: center; +background-color: ${colors.blue[100]}; +width: 100%; +margin: 0 auto; +padding: 20px; + +`; + + + +export const CardContainer = styled.div` + display: grid; + grid-template-columns: repeat(3, 1fr); + grid-template-rows: repeat(2, 1fr); + + gap: 20px; + ${media.medium` + grid-template-columns: repeat(2, 1fr); + grid-template-rows: repeat(3, 1fr); + `} + ${media.small` + grid-template-columns: repeat(1, 1fr); + grid-template-rows: repeat(6, 1fr); + `} +`; + +export const Card = styled.div` + display: flex; + justify-content: center; + align-items: center; + width: 384px; + height: 280px; + border-radius: 16px; + background-color: #fff; + position: relative; +`; + +export const CardEditButton = styled.button` + width: 56px; + height: 56px; + background-image: url("${EditIcon}"); + background-color: ${colors.gray[500]}; + border-radius: 100px; + border: none; + padding: 20px; + background-size: 24px 24px; + background-repeat: no-repeat; + background-position: center; + cursor: pointer; + &:hover { + background-color: ${colors.gray[400]}; + color: ${colors.gray[100]}; + } +`; + +export const CardContentContainer = styled.div` + display: flex; + flex-direction: column; + justify-content: center; + width: 100%; + height: 100%; + padding: 16px 24px; +`; + +export const CardContentStatus = styled.div` + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + height: auto; + gap: 14px; + border-bottom: 1px solid ${colors.gray[200]}; + padding-bottom: 16px; +`; + +export const CardContentStatusContainer = styled.div` + display: flex; + align-items: center; + gap: 14px; +`; + +export const CardContentStatusProfileContainer = styled.div` + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 6px; +`; + +export const CardContentFrom = styled.div` + + display: flex; + justify-content: center; + align-items: center; + width: 100%; + height: 100%; +`; + +export const CardContentStatusProfileImage = styled.img` + width: 56px; + height: 56px; + border-radius: 100px; + border: 1px solid ${colors.gray[300]}; +`; + +export const CardContentStatusProfileName = styled.div` + ${font.regular16} + color: ${colors.gray[900]}; +`; + +const relationshipColors = { + friend: colors.blue[100], + family: colors.green[100], + colleague: colors.purple[100], + acquaintance: colors.beige[100], +}; + +const relationshipTextColors = { + friend: colors.blue[500], + family: colors.green[500], + colleague: colors.purple[600], + acquaintance: colors.beige[500], +}; + +// const relationshipLabels = { +// friend: '친구', +// family: '가족', +// colleague: '동료', +// acquaintance: '지인', +// }; + +export const CardContentStatusRelationship = styled.div` + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0 8px; + height: 20px; + border-radius: 4px; + ${font.regular14} + color: ${props => relationshipTextColors[props.$relationship] || colors.gray[500]}; + + background-color: ${props => relationshipColors[props.$relationship] || colors.gray[500]}; +`; + +export const CardContentText = styled.div` + width: 100%; + height: 100%; + ${font.regular16} + color: ${colors.gray[600]}; + padding-top: 16px; + cursor: pointer; +`; + +export const CardContentDate = styled.div` + ${font.regular12} + color: ${colors.gray[400]}; +`; + +export const CardContentDeleteButton = styled.div` + width: 40px; + height: 40px; + background-image: url("${DeleteIcon}"); + border-radius: 6px; + border: 1px solid ${colors.gray[300]}; + padding: 20px; + background-size: 24px 24px; + background-repeat: no-repeat; + background-position: center; + cursor: pointer; + &:hover { + background-color: ${colors.gray[200]}; + color: ${colors.gray[100]}; + } +`; \ No newline at end of file From 829f9c7fde90008e1826be59d035fc4ec913e385 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A0=95=EC=9B=90?= Date: Wed, 12 Nov 2025 11:53:27 +0900 Subject: [PATCH 37/91] =?UTF-8?q?Chore:=20PR=20=EB=B9=8C=EB=93=9C=20?= =?UTF-8?q?=EC=98=A4=EB=A5=98=EB=A1=9C=20=EC=9D=B8=ED=95=9C=20=EC=9E=AC?= =?UTF-8?q?=ED=91=B8=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.jsx | 10 +++++----- src/hooks/use-kakao-sdk.js | 2 +- src/hooks/use-share-actions.js | 11 +++++------ 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index 165e987..42e8d58 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,14 +1,13 @@ import { Routes, Route } from "react-router"; import { GlobalStyle } from "@/styles/global-style"; -import ToastProvider from "@/contexts/toast-context"; import GlobalLayout from "@/components/common/global-layout"; -import RollingPage from "@/pages/rolling-page"; import TestPage from "@/pages/test-page"; import MessagePage from "@/pages/message-page"; import MainPage from "@/pages/main-page"; import PostPage from "@/pages/post-page"; import TempPage from "@/pages/temp-page"; import ToastTestPage from "@/pages/toast-test-page"; +import RollingPage from "@/pages/rolling-page"; function App() { return ( @@ -18,15 +17,16 @@ function App() { }> } /> } /> - } /> + {/* } /> */} } /> } /> } /> - + } /> + } /> ); } -export default App; +export default App; \ No newline at end of file diff --git a/src/hooks/use-kakao-sdk.js b/src/hooks/use-kakao-sdk.js index 54f240c..7a8ac58 100644 --- a/src/hooks/use-kakao-sdk.js +++ b/src/hooks/use-kakao-sdk.js @@ -7,6 +7,7 @@ const KAKAO_KEY = import.meta.env.VITE_KAKAO_KEY; * 책임: 카카오 SDK 스크립트 로드 및 초기화 */ export default function useKakaoSdk() { + useEffect(() => { // 이미 SDK가 로드되어 있으면 초기화만 수행 if (window.Kakao) { @@ -31,7 +32,6 @@ export default function useKakaoSdk() { document.head.appendChild(script); - // 클린업 함수는 필요시 추가 (일반적으로는 SDK를 제거하지 않음) }, []); return { diff --git a/src/hooks/use-share-actions.js b/src/hooks/use-share-actions.js index fb57522..03bd9ea 100644 --- a/src/hooks/use-share-actions.js +++ b/src/hooks/use-share-actions.js @@ -1,12 +1,12 @@ import { useCallback } from 'react'; -import useToast from '@/hooks/use-toast'; +import { useToast } from '@/hooks/use-toast'; /** * 공유 기능 커스텀 훅 * 책임: URL 복사 및 카카오톡 공유 비즈니스 로직 처리 */ export default function useShareActions() { - const { showToast } = useToast(); + const showToast = useToast(); /** * URL을 클립보드에 복사 @@ -14,11 +14,10 @@ export default function useShareActions() { const copyToClipboard = useCallback(async (url) => { try { await navigator.clipboard.writeText(url); - showToast('URL이 복사되었습니다.', 'success'); + showToast.success('URL이 복사되었습니다.'); return true; } catch (err) { console.error('URL 복사 실패:', err); - showToast('URL 복사에 실패했습니다.', 'delete'); return false; } }, [showToast]); @@ -28,7 +27,6 @@ export default function useShareActions() { */ const shareToKakao = useCallback((url) => { if (!window.Kakao) { - showToast('카카오톡 SDK가 로드되지 않았습니다.', 'delete'); return false; } @@ -36,10 +34,11 @@ export default function useShareActions() { window.Kakao.Share.sendScrap({ requestUrl: url, }); + showToast.success('카카오톡으로 공유되었습니다.'); + return true; } catch (err) { console.error('카카오톡 공유 실패:', err); - showToast('카카오톡 공유에 실패했습니다.', 'delete'); return false; } }, [showToast]); From 1ae4424d0e74724d8b695b25147cdad24dff65c0 Mon Sep 17 00:00:00 2001 From: BZzzzi Date: Wed, 12 Nov 2025 14:37:22 +0900 Subject: [PATCH 38/91] =?UTF-8?q?Refactor:=20ToastProvider=20=EC=84=B1?= =?UTF-8?q?=EB=8A=A5=20=EC=B5=9C=EC=A0=81=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - hideToast, showToast 함수를 useCallback으로 메모이제이션하여 불필요한 리렌더링 방지 - 닫기 애니메이션 타이밍 3000ms에서 300ms로 조정 - 자동 닫기 타이밍을 3000ms에서 5000ms로 조정 --- src/components/common/toast-provider.jsx | 41 +++++++++++++----------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/src/components/common/toast-provider.jsx b/src/components/common/toast-provider.jsx index 662e0e2..a54e466 100644 --- a/src/components/common/toast-provider.jsx +++ b/src/components/common/toast-provider.jsx @@ -1,4 +1,4 @@ -import { useState, useRef } from "react"; +import { useState, useRef, useCallback } from "react"; import { createPortal } from "react-dom"; import Toast from "@/components/common/toast"; import { ToastContext } from "@/hooks/use-toast"; @@ -9,7 +9,7 @@ export function ToastProvider({ children }) { const autoCloseTimerRef = useRef(null); const closeAnimTimerRef = useRef(null); - const hideToast = () => { + const hideToast = useCallback(() => { if (autoCloseTimerRef.current) { clearTimeout(autoCloseTimerRef.current); autoCloseTimerRef.current = null; @@ -28,26 +28,29 @@ export function ToastProvider({ children }) { closeAnimTimerRef.current = setTimeout(() => { setToast(null); closeAnimTimerRef.current = null; - }, 3000); - }; + }, 300); + }, []); - const showToast = (message, type = "success") => { - if (autoCloseTimerRef.current) clearTimeout(autoCloseTimerRef.current); - if (closeAnimTimerRef.current) { - clearTimeout(closeAnimTimerRef.current); - } + const showToast = useCallback( + (message, type = "success") => { + if (autoCloseTimerRef.current) clearTimeout(autoCloseTimerRef.current); + if (closeAnimTimerRef.current) { + clearTimeout(closeAnimTimerRef.current); + } - setToast({ - message, - type, - key: Date.now(), - isClosing: false, - }); + setToast({ + message, + type, + key: Date.now(), + isClosing: false, + }); - autoCloseTimerRef.current = setTimeout(() => { - hideToast(); - }, 3000); - }; + autoCloseTimerRef.current = setTimeout(() => { + hideToast(); + }, 5000); + }, + [hideToast] + ); const contextValue = { success: (message) => showToast(message, "success"), From 1f8cf5c5f30939cbae9361178f2cf68c95a0c2d5 Mon Sep 17 00:00:00 2001 From: BZzzzi Date: Wed, 12 Nov 2025 14:38:30 +0900 Subject: [PATCH 39/91] =?UTF-8?q?Feat:=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EB=9D=BC=EC=9A=B0=ED=8C=85=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - /list 경로에 ListPage 컴포넌트 라우팅 설정 - App.jsx에 ListPage import 및 Route 추가 - list-page.jsx 기본 컴포넌트 생성 --- src/App.jsx | 3 ++- src/pages/list-page.jsx | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 src/pages/list-page.jsx diff --git a/src/App.jsx b/src/App.jsx index bacecaa..c242f0c 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -4,6 +4,7 @@ import GlobalLayout from "@/components/common/global-layout"; import TestPage from "@/pages/test-page"; import MessagePage from "@/pages/message-page"; import MainPage from "@/pages/main-page"; +import ListPage from "@/pages/list-page"; import PostPage from "@/pages/post-page"; import TempPage from "@/pages/temp-page"; import ToastTestPage from "@/pages/toast-test-page"; @@ -19,7 +20,7 @@ function App() { } /> } /> } /> - } /> + } /> } /> } /> diff --git a/src/pages/list-page.jsx b/src/pages/list-page.jsx new file mode 100644 index 0000000..ca3e299 --- /dev/null +++ b/src/pages/list-page.jsx @@ -0,0 +1,3 @@ +export default function ListPage() { + return <>Hello; +} From 9433c2d511ec4f4f26417642e203221a3ea8642e Mon Sep 17 00:00:00 2001 From: BZzzzi Date: Wed, 12 Nov 2025 14:53:12 +0900 Subject: [PATCH 40/91] =?UTF-8?q?Refactor:=20=EC=9E=84=EC=8B=9C=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EB=9D=BC=EC=9A=B0=ED=8A=B8=20?= =?UTF-8?q?=EA=B2=BD=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.jsx | 4 ++-- src/pages/temp-page.jsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index 43beb32..57d237c 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -18,7 +18,7 @@ function App() { }> } /> } /> - {/* } /> */} + } /> } /> } /> } /> @@ -30,4 +30,4 @@ function App() { ); } -export default App; \ No newline at end of file +export default App; diff --git a/src/pages/temp-page.jsx b/src/pages/temp-page.jsx index cb732a2..9fd7121 100644 --- a/src/pages/temp-page.jsx +++ b/src/pages/temp-page.jsx @@ -24,7 +24,7 @@ export default function TempPage() { 리스트 페이지 - + 롤페 페이지 From 9224fe3a5c89d2f59649bfce040e56e74efbafb3 Mon Sep 17 00:00:00 2001 From: BZzzzi Date: Wed, 12 Nov 2025 16:43:08 +0900 Subject: [PATCH 41/91] =?UTF-8?q?Refactor:=20=EC=9E=84=EC=8B=9C=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EB=B0=8F=20=ED=97=A4=EB=8D=94=20?= =?UTF-8?q?=EB=B2=84=ED=8A=BC=20=EB=A7=81=ED=81=AC=20=EA=B2=BD=EB=A1=9C=20?= =?UTF-8?q?=EC=9E=AC=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.jsx | 6 +++--- src/components/common/global-layout.jsx | 2 +- src/components/common/header.jsx | 2 +- src/contexts/toast-context-state.jsx | 5 ----- src/pages/temp-page.jsx | 6 +++--- 5 files changed, 8 insertions(+), 13 deletions(-) delete mode 100644 src/contexts/toast-context-state.jsx diff --git a/src/App.jsx b/src/App.jsx index 57d237c..928e238 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -21,9 +21,9 @@ function App() { } /> } /> } /> - } /> - } /> - } /> + } /> + } /> + } /> diff --git a/src/components/common/global-layout.jsx b/src/components/common/global-layout.jsx index db2b1ed..143890f 100644 --- a/src/components/common/global-layout.jsx +++ b/src/components/common/global-layout.jsx @@ -1,7 +1,7 @@ import { Outlet, useLocation } from "react-router"; import Header from "@/components/common/header"; -const PAGES_WITH_BUTTON = ["main-page", "list-page"]; +const PAGES_WITH_BUTTON = ["main", "list"]; export default function GlobalLayout() { const location = useLocation(); diff --git a/src/components/common/header.jsx b/src/components/common/header.jsx index 44210f8..ccb1485 100644 --- a/src/components/common/header.jsx +++ b/src/components/common/header.jsx @@ -48,7 +48,7 @@ export default function Header({ showButton }) { {showButton && ( - + diff --git a/src/contexts/toast-context-state.jsx b/src/contexts/toast-context-state.jsx deleted file mode 100644 index a5eb1e9..0000000 --- a/src/contexts/toast-context-state.jsx +++ /dev/null @@ -1,5 +0,0 @@ -import React, { createContext } from 'react'; - -export const ToastContext = createContext(); - - diff --git a/src/pages/temp-page.jsx b/src/pages/temp-page.jsx index 9fd7121..a605c6a 100644 --- a/src/pages/temp-page.jsx +++ b/src/pages/temp-page.jsx @@ -30,15 +30,15 @@ export default function TempPage() { 롤페 생성 페이지 - + 롤페 메시지 페이지 - + 테스트 페이지 - + toast 테스트 페이지 From 31b9b83340a06a36da850b927d9f98374e1efb67 Mon Sep 17 00:00:00 2001 From: yeseung00 Date: Wed, 12 Nov 2025 20:16:46 +0900 Subject: [PATCH 42/91] =?UTF-8?q?Style:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=EC=BD=94=EB=93=9C=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/message-page.jsx | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/pages/message-page.jsx b/src/pages/message-page.jsx index 0c9ae52..1f146da 100644 --- a/src/pages/message-page.jsx +++ b/src/pages/message-page.jsx @@ -105,12 +105,6 @@ export const SelectableImageItem = styled.li` transition: transform 0.2s, border 0.2s; border: 2px solid transparent; - ${({ isSelected }) => - isSelected && - css` - border-color: #3f60ff; /* 선택된 이미지 하이라이트 색상 */ - transform: scale(1.05); - `} &:hover { opacity: 0.8; @@ -195,10 +189,7 @@ function MessagePage() { {selectableImages.map((image) => ( - + {`프로필 ))} From aa9fdeed067060f83ed5d010cffb0ce5adf093bd Mon Sep 17 00:00:00 2001 From: BZzzzi Date: Thu, 13 Nov 2025 01:57:55 +0900 Subject: [PATCH 43/91] =?UTF-8?q?Refactor:=20=EA=B3=B5=ED=86=B5=20?= =?UTF-8?q?=ED=97=A4=EB=8D=94=20=EB=B2=84=ED=8A=BC=20=EB=A7=81=ED=81=AC=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=EB=B0=8F=20GlobalStyle=20=ED=8F=B0?= =?UTF-8?q?=ED=8A=B8=20=EC=A0=81=EC=9A=A9=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/common/header.jsx | 2 +- src/styles/global-style.js | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/components/common/header.jsx b/src/components/common/header.jsx index ccb1485..572036b 100644 --- a/src/components/common/header.jsx +++ b/src/components/common/header.jsx @@ -48,7 +48,7 @@ export default function Header({ showButton }) { {showButton && ( - + diff --git a/src/styles/global-style.js b/src/styles/global-style.js index 44bbb3d..8f272f2 100644 --- a/src/styles/global-style.js +++ b/src/styles/global-style.js @@ -25,6 +25,13 @@ export const GlobalStyle = createGlobalStyle`${css` -moz-osx-font-smoothing: grayscale; } + button, + input, + textarea, + select { + font-family: var(--font-family); + } + a { text-decoration: none; color: inherit; From cbc19b243cef9ed62ebc479c8372129d6e917640 Mon Sep 17 00:00:00 2001 From: BZzzzi Date: Thu, 13 Nov 2025 02:00:05 +0900 Subject: [PATCH 44/91] =?UTF-8?q?Feat:=20=EB=A1=A4=EB=A7=81=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=ED=8D=BC=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?getRecipients=20API=20=ED=95=A8=EC=88=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/list-user-api.js | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 src/api/list-user-api.js diff --git a/src/api/list-user-api.js b/src/api/list-user-api.js new file mode 100644 index 0000000..a1b511d --- /dev/null +++ b/src/api/list-user-api.js @@ -0,0 +1,11 @@ +// 카드 리스트 유저 GET +import axios from "axios"; + +const baseURL = import.meta.env.VITE_API_BASE_URL; + +export async function getRecipients({ limit, offset, sort }) { + const res = await axios.get( + `${baseURL}/recipients/?limit=${limit}&offset=${offset}&sort=${sort}` + ); + return res.data; +} From dd7a890009fe4aebff69b21cd14dfa5c91248252 Mon Sep 17 00:00:00 2001 From: BZzzzi Date: Thu, 13 Nov 2025 02:02:17 +0900 Subject: [PATCH 45/91] =?UTF-8?q?Feat:=20=EB=A1=A4=EB=A7=81=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=ED=8D=BC=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CardList 컴포넌트 생성 및 카드 리스트 표시 기능 추가 - CardList 레이아웃 및 스타일 정의 - useState를 통해 인기/최근 롤링페이퍼 상태 관리 - useEffect로 getRecipients API 호출하여 데이터 fetching - CardWrapper에 배경색/배경이미지 prop 조건부 적용 - 프로필 이미지, 메시지 수, 이모지 표시 기능 포함 --- src/components/list/card-list.jsx | 44 +++++++++++++++++++++ src/pages/list-page.jsx | 52 ++++++++++++++++++++++++- src/styles/list-page-styles.js | 65 +++++++++++++++++++++++++++++++ 3 files changed, 160 insertions(+), 1 deletion(-) create mode 100644 src/components/list/card-list.jsx create mode 100644 src/styles/list-page-styles.js diff --git a/src/components/list/card-list.jsx b/src/components/list/card-list.jsx new file mode 100644 index 0000000..d3807c2 --- /dev/null +++ b/src/components/list/card-list.jsx @@ -0,0 +1,44 @@ +import EmojiDisplayList from "@/components/rolling/emoji-display-list"; +import { + CardImgWrapper, + CardListLayout, + CardListWrapper, + CardWrapper, + EmojiWrapper, +} from "@/styles/list-page-styles"; + +export function CardList({ title, userList }) { + return ( + <> + +
{title}
+ + {userList.length === 0 ? ( +
작성된 롤링페이퍼가 없습니다! 직접 만들어 보세요.
+ ) : ( + userList.map((it) => { + return ( + +

To. {it.name}

+ + {it.recentMessages.map((it) => ( + + ))} + +

{it.messageCount}명이 작성했어요!

+ + + +
+ ); + }) + )} +
+
+ + ); +} diff --git a/src/pages/list-page.jsx b/src/pages/list-page.jsx index ca3e299..b6a2eb5 100644 --- a/src/pages/list-page.jsx +++ b/src/pages/list-page.jsx @@ -1,3 +1,53 @@ +import { Link } from "react-router"; +import { useEffect, useState } from "react"; +import { getRecipients } from "@/api/list-user-api"; + +import { + BottomWrapper, + CustomButton, + PageContainer, +} from "@/styles/list-page-styles"; +import { CardList } from "@/components/list/card-list"; + +const UI_PAGE_SIZE = 4; + export default function ListPage() { - return <>Hello; + const [likePaper, setLikePaper] = useState([]); + const [recentPaper, setRecentPaper] = useState([]); + + useEffect(() => { + const fetchRecipients = async () => { + try { + const likeData = await getRecipients({ + limit: 4, + offset: 0, + sort: "like", + }); + const recentData = await getRecipients({ + limit: 4, + offset: 0, + sort: "", + }); + console.log("likeData:", likeData); + setLikePaper(likeData.results); + setRecentPaper(recentData.results); + } catch (error) { + console.error("recipients 가져오기 실패:", error); + } + }; + + fetchRecipients(); + }, []); + + return ( + + + + + + 나도 만들어보기 + + + + ); } diff --git a/src/styles/list-page-styles.js b/src/styles/list-page-styles.js new file mode 100644 index 0000000..caa2da4 --- /dev/null +++ b/src/styles/list-page-styles.js @@ -0,0 +1,65 @@ +import styled from "styled-components"; +import Button from "@/components/common/button"; +import { RollingHeaderImojiContainer } from "@/styles/rolling-page-styles"; + +// list-page +export const PageContainer = styled.div` + max-width: 1200px; + margin: 0 auto; +`; + +export const BottomWrapper = styled.div` + display: flex; + justify-content: center; +`; + +export const CustomButton = styled(Button)` + padding: 14px 60px; +`; + +// card-list +export const CardListLayout = styled.div` + display: flex; + flex-direction: column; + justify-content: center; + align-items: start; + background-color: rgba(0, 0, 0, 0.5); + margin-top: 50px; +`; + +export const CardListWrapper = styled.div` + display: flex; + justify-content: center; + align-items: start; + background-color: rgba(0, 0, 0, 0.5); + margin-top: 50px; +`; + +export const CardWrapper = styled.div` + background-color: ${(props) => { + if (props.bgImg) return "transparent"; + if (props.bg) return props.bg; + return "#ffffff"; + }}; + background-image: ${(props) => { + if (props.bgImg) return `url(${props.bgImg})`; + return "none"; + }}; + background-size: cover; + background-position: center; + margin-top: 50px; +`; + +export const CardImgWrapper = styled.div` + display: flex; + justify-content: center; + align-items: start; + img { + width: 20px; + } +`; + +export const EmojiWrapper = styled(RollingHeaderImojiContainer)` + border-top: 1px solid rgba(0, 0, 0, 0.12); + padding: 17px 28px 0 0; +`; From 4746248442b822db73c83e301555d56ceb2aec70 Mon Sep 17 00:00:00 2001 From: BZzzzi Date: Thu, 13 Nov 2025 15:23:13 +0900 Subject: [PATCH 46/91] =?UTF-8?q?Chore:=20Swiper=20=EB=9D=BC=EC=9D=B4?= =?UTF-8?q?=EB=B8=8C=EB=9F=AC=EB=A6=AC=20=EC=84=A4=EC=B9=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 22 +++++++++++++++++++++- package.json | 3 ++- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1cb617d..0ccae41 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,8 @@ "react": "^19.1.1", "react-dom": "^19.1.1", "react-router": "^7.9.5", - "styled-components": "^6.1.19" + "styled-components": "^6.1.19", + "swiper": "^12.0.3" }, "devDependencies": { "@eslint/js": "^9.36.0", @@ -3096,6 +3097,25 @@ "node": ">=8" } }, + "node_modules/swiper": { + "version": "12.0.3", + "resolved": "https://registry.npmjs.org/swiper/-/swiper-12.0.3.tgz", + "integrity": "sha512-BHd6U1VPEIksrXlyXjMmRWO0onmdNPaTAFduzqR3pgjvi7KfmUCAm/0cj49u2D7B0zNjMw02TSeXfinC1hDCXg==", + "funding": [ + { + "type": "patreon", + "url": "https://www.patreon.com/swiperjs" + }, + { + "type": "open_collective", + "url": "http://opencollective.com/swiper" + } + ], + "license": "MIT", + "engines": { + "node": ">= 4.7.0" + } + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", diff --git a/package.json b/package.json index 4af7841..ab3788a 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,8 @@ "react": "^19.1.1", "react-dom": "^19.1.1", "react-router": "^7.9.5", - "styled-components": "^6.1.19" + "styled-components": "^6.1.19", + "swiper": "^12.0.3" }, "devDependencies": { "@eslint/js": "^9.36.0", From ba8d292bc92e1c3770dd1c5910d182c43a61e167 Mon Sep 17 00:00:00 2001 From: BZzzzi Date: Thu, 13 Nov 2025 15:25:02 +0900 Subject: [PATCH 47/91] =?UTF-8?q?Feat:=20UI=20=EC=84=B8=EB=B6=80=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20=EB=B0=8F=20=ED=8E=98=EC=9D=B4=EC=A7=80?= =?UTF-8?q?=EB=84=A4=EC=9D=B4=EC=85=98=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 페이지네이션을 위한 API prop 형식 변경 - Swiper로 페이지네이션 UI 적용 - 카드 스타일 디테일 적용 --- src/api/list-user-api.js | 8 +-- src/components/list/card-list.jsx | 92 +++++++++++++++++++++---------- src/pages/list-page.jsx | 9 +-- src/styles/list-page-styles.js | 39 +++++++++---- 4 files changed, 96 insertions(+), 52 deletions(-) diff --git a/src/api/list-user-api.js b/src/api/list-user-api.js index a1b511d..27a28db 100644 --- a/src/api/list-user-api.js +++ b/src/api/list-user-api.js @@ -1,11 +1,7 @@ // 카드 리스트 유저 GET import axios from "axios"; -const baseURL = import.meta.env.VITE_API_BASE_URL; - -export async function getRecipients({ limit, offset, sort }) { - const res = await axios.get( - `${baseURL}/recipients/?limit=${limit}&offset=${offset}&sort=${sort}` - ); +export async function getRecipients({ url }) { + const res = await axios.get(url); return res.data; } diff --git a/src/components/list/card-list.jsx b/src/components/list/card-list.jsx index d3807c2..4f5dc85 100644 --- a/src/components/list/card-list.jsx +++ b/src/components/list/card-list.jsx @@ -1,9 +1,14 @@ +import { Swiper, SwiperSlide } from "swiper/react"; +import { Navigation } from "swiper/modules"; +import "swiper/css"; +import "swiper/css/navigation"; import EmojiDisplayList from "@/components/rolling/emoji-display-list"; import { CardImgWrapper, CardListLayout, - CardListWrapper, + SwiperWrapper, CardWrapper, + CustomH3, EmojiWrapper, } from "@/styles/list-page-styles"; @@ -11,33 +16,64 @@ export function CardList({ title, userList }) { return ( <> -
{title}
- - {userList.length === 0 ? ( -
작성된 롤링페이퍼가 없습니다! 직접 만들어 보세요.
- ) : ( - userList.map((it) => { - return ( - -

To. {it.name}

- - {it.recentMessages.map((it) => ( - - ))} - -

{it.messageCount}명이 작성했어요!

- - - -
- ); - }) - )} -
+ {title} + {userList.length === 0 ? ( +
작성된 롤링페이퍼가 없습니다! 직접 만들어 보세요.
+ ) : ( + + + {userList.map((it) => { + return ( + + + To. {it.name} + + {it.recentMessages.map((it) => ( + + ))} + +

{it.messageCount}명이 작성했어요!

+ + + +
+
+ ); + })} +
+
+ )}
); diff --git a/src/pages/list-page.jsx b/src/pages/list-page.jsx index b6a2eb5..a60ac7a 100644 --- a/src/pages/list-page.jsx +++ b/src/pages/list-page.jsx @@ -10,6 +10,7 @@ import { import { CardList } from "@/components/list/card-list"; const UI_PAGE_SIZE = 4; +const baseURL = import.meta.env.VITE_API_BASE_URL; export default function ListPage() { const [likePaper, setLikePaper] = useState([]); @@ -19,14 +20,10 @@ export default function ListPage() { const fetchRecipients = async () => { try { const likeData = await getRecipients({ - limit: 4, - offset: 0, - sort: "like", + url: `${baseURL}/recipients/?sort=like`, }); const recentData = await getRecipients({ - limit: 4, - offset: 0, - sort: "", + url: `${baseURL}/recipients/`, }); console.log("likeData:", likeData); setLikePaper(likeData.results); diff --git a/src/styles/list-page-styles.js b/src/styles/list-page-styles.js index caa2da4..8d82c83 100644 --- a/src/styles/list-page-styles.js +++ b/src/styles/list-page-styles.js @@ -1,6 +1,7 @@ import styled from "styled-components"; import Button from "@/components/common/button"; import { RollingHeaderImojiContainer } from "@/styles/rolling-page-styles"; +import { font } from "@/styles/font"; // list-page export const PageContainer = styled.div` @@ -11,6 +12,8 @@ export const PageContainer = styled.div` export const BottomWrapper = styled.div` display: flex; justify-content: center; + padding: 24px 0; + margin-top: 40px; `; export const CustomButton = styled(Button)` @@ -18,36 +21,48 @@ export const CustomButton = styled(Button)` `; // card-list +export const CustomH3 = styled.div` + ${font.bold24} +`; + export const CardListLayout = styled.div` display: flex; flex-direction: column; justify-content: center; align-items: start; - background-color: rgba(0, 0, 0, 0.5); margin-top: 50px; + padding: 0 20px; `; -export const CardListWrapper = styled.div` - display: flex; - justify-content: center; - align-items: start; - background-color: rgba(0, 0, 0, 0.5); - margin-top: 50px; +export const SwiperWrapper = styled.div` + width: 100%; + margin-top: 16px; + + .swiper-button-next, + .swiper-button-prev { + color: #000; + } `; export const CardWrapper = styled.div` + width: 275px; + height: 260px; background-color: ${(props) => { - if (props.bgImg) return "transparent"; + // if (props.bgImg) return "transparent"; if (props.bg) return props.bg; return "#ffffff"; }}; - background-image: ${(props) => { + /* background-image: ${(props) => { if (props.bgImg) return `url(${props.bgImg})`; return "none"; - }}; - background-size: cover; + }}; */ + background-repeat: no-repeat; background-position: center; - margin-top: 50px; + background-size: cover; + border: 1px solid rgba(0, 0, 0, 0.1); + border-radius: 16px; + box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.08); + padding: 30px 24px 20px; `; export const CardImgWrapper = styled.div` From ea60b5211e5fbe865680db2fede1d32b6c3166f8 Mon Sep 17 00:00:00 2001 From: summerlane Date: Thu, 13 Nov 2025 15:35:31 +0900 Subject: [PATCH 48/91] =?UTF-8?q?Feat:=20=ED=86=A0=EA=B8=80=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=83=9D=EC=84=B1=ED=95=B4?= =?UTF-8?q?=EC=84=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/common/toggle.jsx | 168 +++++++++++++++++++++++++++++++ src/pages/post-page.jsx | 158 +++-------------------------- 2 files changed, 181 insertions(+), 145 deletions(-) create mode 100644 src/components/common/toggle.jsx diff --git a/src/components/common/toggle.jsx b/src/components/common/toggle.jsx new file mode 100644 index 0000000..547ddd4 --- /dev/null +++ b/src/components/common/toggle.jsx @@ -0,0 +1,168 @@ +import styled from "styled-components"; +import { colors } from "@/styles/colors"; +import { font } from "@/styles/font"; +import media from "@/styles/media"; +import { useState } from "react"; + +const ToggleSection = styled.div` + width: 100%; + min-width: 360px; + display: flex; + flex-direction: column; + gap: 17px; +`; + +const ToggleTitle = styled.p` + ${font.bold24}; + color: ${colors.gray[900]}; + margin: 0; +`; + +const ToggleTitleSmall = styled.p` + ${font.regular16}; + color: ${colors.gray[600]}; + margin: 0; +`; + +const ToggleButtonContainer = styled.div` + width: 224px; + height: 40px; + background-color: ${colors.gray[100]}; + border-radius: 6px; + display: flex; + flex-direction: row; + margin-top: 20px; +`; + +const ToggleButton = styled.div` + width: 122px; + height: 40px; + background-color: ${(props) => (props.$active ? "transparent" : "#ffffff")}; + color: ${(props) => (props.$active ? colors.gray[900] : colors.purple[600])}; + border: ${(props) => + props.$active ? null : `2px solid ${colors.purple[600]}`}; + ${(props) => (props.$active ? `${font.regular16}` : `${font.bold16}`)}; + border-radius: 6px; + display: flex; + justify-content: center; + align-items: center; + cursor: pointer; +`; + +const ToggleButtonDisable = styled.div` + width: 122px; + height: 40px; + background-color: ${(props) => (props.$active ? "transparent" : "#ffffff")}; + color: ${(props) => (props.$active ? colors.gray[900] : colors.purple[600])}; + border: ${(props) => + props.$active ? null : `2px solid ${colors.purple[600]}`}; + ${(props) => (props.$active ? `${font.regular16}` : `${font.bold16}`)}; + border-radius: 6px; + display: flex; + justify-content: center; + align-items: center; + cursor: pointer; +`; + +const ToggleDivContainer = styled.div` + display: flex; + flex-direction: row; + flex-wrap: wrap; + gap: 16px; + margin-top: 40px; +`; + +const ToggleDiv = styled.div` + width: 168px; + height: 168px; + border: 1px solid rgba(0, 0, 0, 0.08); + border-radius: 16px; + background-color: ${(props) => props.$bgColor}; + cursor: pointer; + + ${media.small` + flex: 1 1 40%; + `} + + ${media.medium` + flex: 1 1 40%; + `} +`; + +const ToggleImgContainer = styled.div` + display: flex; + flex-direction: row; + flex-wrap: wrap; + gap: 16px; + margin-top: 40px; +`; + +const ToggleImg = styled.div` + width: 168px; + height: 168px; + border: 1px solid rgba(0, 0, 0, 0.08); + border-radius: 16px; + background-image: url(./src/assets/images/img-car.webp); + background-repeat: no-repeat; + background-size: 100%; + background-position: center; + cursor: pointer; + + ${media.small` + flex: 1 1 40%; + `} + + ${media.medium` + flex: 1 1 40%; + `} +`; + +const ImgSelect = styled.div` + width: 100%; + height: 100%; + background-image: url(./src/assets/images/select-circle.webp); + background-repeat: no-repeat; + background-size: 44px 44px; + background-position: center; +`; + +export default function Toggle() { + const [toggle, setToggle] = useState(false); + + const handleToggle = () => { + setToggle(!toggle); + }; + return ( + <> + + 배경화면을 선택해 주세요. + + 컬러를 선택하거나, 이미지를 선택할 수 있습니다. + + + 컬러 + 이미지 + + {toggle === false ? ( + + + + + + + + + ) : ( + + + + + + + + + )} + + + ); +} diff --git a/src/pages/post-page.jsx b/src/pages/post-page.jsx index b505096..bb8a312 100644 --- a/src/pages/post-page.jsx +++ b/src/pages/post-page.jsx @@ -2,6 +2,8 @@ import styled from "styled-components"; import { colors } from "@/styles/colors"; import { font } from "@/styles/font"; import media from "@/styles/media"; +import Toggle from "@/components/common/toggle"; +import { useState } from "react"; const Container = styled.div` max-width: 720px; @@ -48,124 +50,6 @@ const Input = styled.input` padding: 12px 16px; `; -const ToggleSection = styled.div` - width: 100%; - min-width: 360px; - display: flex; - flex-direction: column; - gap: 17px; -`; - -const ToggleTitle = styled.p` - ${font.bold24}; - color: ${colors.gray[900]}; - margin: 0; -`; - -const ToggleTitleSmall = styled.p` - ${font.regular16}; - color: ${colors.gray[600]}; - margin: 0; -`; - -const ToggleButtonContainer = styled.div` - width: 224px; - height: 40px; - background-color: ${colors.gray[100]}; - border-radius: 6px; - display: flex; - flex-direction: row; - margin-top: 20px; -`; - -const ToggleButton = styled.div` - width: 122px; - height: 40px; - background-color: #ffffff; - color: ${colors.purple[600]}; - ${font.bold16}; - border: 2px solid ${colors.purple[600]}; - border-radius: 6px; - display: flex; - justify-content: center; - align-items: center; - cursor: pointer; -`; - -const ToggleButtonDisable = styled.div` - width: 122px; - height: 40px; - background-color: transparent; - color: ${colors.gray[900]}; - ${font.regular16}; - display: flex; - justify-content: center; - align-items: center; - cursor: pointer; -`; - -const ToggleDivContainer = styled.div` - display: flex; - flex-direction: row; - flex-wrap: wrap; - gap: 16px; - margin-top: 40px; -`; - -const ToggleDiv = styled.div` - width: 168px; - height: 168px; - border: 1px solid rgba(0, 0, 0, 0.08); - border-radius: 16px; - background-color: ${(props) => props.$bgColor}; - cursor: pointer; - - ${media.small` - flex: 1 1 40%; - `} - - ${media.medium` - flex: 1 1 40%; - `} -`; - -const ToggleImgContainer = styled.div` - display: flex; - flex-direction: row; - flex-wrap: wrap; - gap: 16px; - margin-top: 40px; -`; - -const ToggleImg = styled.div` - width: 168px; - height: 168px; - border: 1px solid rgba(0, 0, 0, 0.08); - border-radius: 16px; - background-image: url(./src/assets/images/img-car.webp); - background-repeat: no-repeat; - background-size: 100%; - background-position: center; - cursor: pointer; - - ${media.small` - flex: 1 1 40%; - `} - - ${media.medium` - flex: 1 1 40%; - `} -`; - -const ImgSelect = styled.div` - width: 100%; - height: 100%; - background-image: url(./src/assets/images/select-circle.webp); - background-repeat: no-repeat; - background-size: 44px 44px; - background-position: center; -`; - const Button = styled.button` width: 100%; height: 56px; @@ -190,38 +74,22 @@ const Button = styled.button` `; export default function PostPage() { + const [name, setName] = useState(""); + + const handleInputName = (e) => { + setName(e.target.value); + }; return ( To. - + - - 배경화면을 선택해 주세요. - - 컬러를 선택하거나, 이미지를 선택할 수 있습니다. - - - 컬러 - 이미지 - - - - - - - - - - - - - - - - - - + ); From cee8084b1d167a18fceae7b2635494707f017bb4 Mon Sep 17 00:00:00 2001 From: BZzzzi Date: Thu, 13 Nov 2025 17:44:43 +0900 Subject: [PATCH 49/91] =?UTF-8?q?Refactor:=20RollingHeaderImojiIconContain?= =?UTF-8?q?er=20=EC=8A=A4=ED=83=80=EC=9D=BC=20=EC=BB=B4=ED=8F=AC=EB=84=8C?= =?UTF-8?q?=ED=8A=B8=20=EB=B0=B0=EA=B2=BD=EC=83=89=20=EC=A1=B0=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 검은색에 투명도 조절한 색상으로 변경했습니다. --- src/styles/rolling-page-styles.js | 72 +++++++++++-------------------- 1 file changed, 25 insertions(+), 47 deletions(-) diff --git a/src/styles/rolling-page-styles.js b/src/styles/rolling-page-styles.js index d2162db..2e8a89c 100644 --- a/src/styles/rolling-page-styles.js +++ b/src/styles/rolling-page-styles.js @@ -6,9 +6,8 @@ import media from "@/styles/media"; import EditIcon from "@/assets/icons/plus.svg"; import DeleteIcon from "@/assets/icons/deleted.svg"; - //최상단헤더 컨테이너 -export const RollingHeaderContainer = styled.div` +export const RollingHeaderContainer = styled.div` display: flex; justify-content: space-between; align-items: center; @@ -18,7 +17,7 @@ export const RollingHeaderContainer = styled.div` height: 68px; background-color: rgba(255, 255, 255, 1); gap: 20px; - + ${media.large` width: 1200px; height: 68px; @@ -46,10 +45,8 @@ export const RollingHeaderContainer = styled.div` gap: 0px; `} - `; - //유저 정보 컨테이너 TO. Ashley Kim export const RollingHeaderUserInfo = styled.div` display: flex; @@ -61,7 +58,7 @@ export const RollingHeaderUserInfo = styled.div` ${font.bold28} color: ${colors.gray[800]}; flex-shrink: 0; - + ${media.medium` min-width: 150px; height: 42px; @@ -79,7 +76,6 @@ export const RollingHeaderUserInfo = styled.div` `} `; - export const RollingHeaderRightContainer = styled.div` display: flex; align-items: center; @@ -100,19 +96,15 @@ export const RollingHeaderRightContainer = styled.div` `} - - `; - - //유저 이미지 컨테이너 프로필 사진들과, 몇명이 작성중인지 표시 export const RollingHeaderUserPeopleContainer = styled.div` width: 228px; display: flex; align-items: center; - - ${media.medium` + + ${media.medium` display: none; `} @@ -129,7 +121,7 @@ export const RollingHeaderUserPeopleImages = styled.div` position: relative; cursor: pointer; - ${media.medium` + ${media.medium` display: none; `} @@ -138,7 +130,6 @@ export const RollingHeaderUserPeopleImages = styled.div` display: none; `} - `; export const RollingHeaderUserPeopleImage = styled.img` @@ -150,7 +141,9 @@ export const RollingHeaderUserPeopleImage = styled.img` margin-left: -10px; `; -export const RollingHeaderUserDefaultImage = styled(RollingHeaderUserPeopleImage)``; +export const RollingHeaderUserDefaultImage = styled( + RollingHeaderUserPeopleImage +)``; //몇명이 작성중인지 export const RollingHeaderUserPeopleState = styled.div` @@ -160,7 +153,7 @@ export const RollingHeaderUserPeopleState = styled.div` ${font.bold18} color: ${colors.gray[900]}; text-align: center; - ${media.medium` + ${media.medium` display: none; `} @@ -175,7 +168,6 @@ export const RollingHeaderImojiContainer = styled.div` display: flex; align-items: center; gap: 8px; - `; export const RollingHeaderImojiIconContainer = styled.div` @@ -187,17 +179,15 @@ export const RollingHeaderImojiIconContainer = styled.div` padding: 8px 12px; text-align: center; border-radius: 32px; - background: rgba(153, 153, 153, 1); + background: rgba(0, 0, 0, 0.54); gap: 2px; ${media.small` padding: 4px 8px; `} - `; export const RollingHeaderImojiIcon = styled.div` - width: 24px; height: 24px; color: rgba(255, 255, 255, 1); @@ -206,7 +196,6 @@ export const RollingHeaderImojiIcon = styled.div` width: 20px; height: 24px; `} - `; export const RollingHeaderImojiText = styled.span` @@ -268,7 +257,6 @@ export const RollingHeaderLinkShareButton = styled.div` padding: 8px 8px; `} - `; export const RollingHeaderArrowDown = styled.img` @@ -277,12 +265,6 @@ export const RollingHeaderArrowDown = styled.img` cursor: pointer; `; - - - - - - export const PerpendicularLine = styled.div` border-left: 1px solid ${colors.gray[200]}; height: 28px; @@ -300,21 +282,16 @@ export const PerpendicularLineFirst = styled(PerpendicularLine)` export const PerpendicularLineSecond = styled(PerpendicularLine)``; - - export const RollingPageContainer = styled.div` -display: flex; -justify-content: center; -align-items: center; -background-color: ${colors.blue[100]}; -width: 100%; -margin: 0 auto; -padding: 20px; - + display: flex; + justify-content: center; + align-items: center; + background-color: ${colors.blue[100]}; + width: 100%; + margin: 0 auto; + padding: 20px; `; - - export const CardContainer = styled.div` display: grid; grid-template-columns: repeat(3, 1fr); @@ -394,7 +371,6 @@ export const CardContentStatusProfileContainer = styled.div` `; export const CardContentFrom = styled.div` - display: flex; justify-content: center; align-items: center; @@ -443,9 +419,11 @@ export const CardContentStatusRelationship = styled.div` height: 20px; border-radius: 4px; ${font.regular14} - color: ${props => relationshipTextColors[props.$relationship] || colors.gray[500]}; - - background-color: ${props => relationshipColors[props.$relationship] || colors.gray[500]}; + color: ${(props) => + relationshipTextColors[props.$relationship] || colors.gray[500]}; + + background-color: ${(props) => + relationshipColors[props.$relationship] || colors.gray[500]}; `; export const CardContentText = styled.div` @@ -473,8 +451,8 @@ export const CardContentDeleteButton = styled.div` background-repeat: no-repeat; background-position: center; cursor: pointer; - &:hover { + &:hover { background-color: ${colors.gray[200]}; color: ${colors.gray[100]}; } -`; \ No newline at end of file +`; From 8c54e26d2b5a190852415b4e482687a14277d985 Mon Sep 17 00:00:00 2001 From: BZzzzi Date: Thu, 13 Nov 2025 17:45:24 +0900 Subject: [PATCH 50/91] =?UTF-8?q?Refactor:=20p=20=ED=83=9C=EA=B7=B8=20?= =?UTF-8?q?=EC=8A=A4=ED=83=80=EC=9D=BC=20reset=EC=9D=84=20GlobalStyle?= =?UTF-8?q?=EC=97=90=20=ED=8F=AC=ED=95=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/styles/global-style.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/styles/global-style.js b/src/styles/global-style.js index 8f272f2..b57f026 100644 --- a/src/styles/global-style.js +++ b/src/styles/global-style.js @@ -9,6 +9,7 @@ export const GlobalStyle = createGlobalStyle`${css` html, body, + p, #root { margin: 0; padding: 0; From 538437321a111c15922799afcf51812bbc2e9562 Mon Sep 17 00:00:00 2001 From: BZzzzi Date: Thu, 13 Nov 2025 17:46:15 +0900 Subject: [PATCH 51/91] =?UTF-8?q?Feat:=20=EC=B9=B4=EB=93=9C=20=EB=82=B4?= =?UTF-8?q?=EB=B6=80=20=EC=8A=A4=ED=83=80=EC=9D=BC=20=EA=B5=AC=ED=98=84=20?= =?UTF-8?q?=EB=B0=8F=20=EC=8A=A4=EC=99=80=EC=9D=B4=ED=8D=BC=20=EC=A1=B0?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/list/card-list.jsx | 24 +++++++--- src/styles/list-page-styles.js | 79 +++++++++++++++++++++++++++---- 2 files changed, 87 insertions(+), 16 deletions(-) diff --git a/src/components/list/card-list.jsx b/src/components/list/card-list.jsx index 4f5dc85..db3d95a 100644 --- a/src/components/list/card-list.jsx +++ b/src/components/list/card-list.jsx @@ -10,6 +10,8 @@ import { CardWrapper, CustomH3, EmojiWrapper, + WriterCountText, + ProfileCount, } from "@/styles/list-page-styles"; export function CardList({ title, userList }) { @@ -27,6 +29,7 @@ export function CardList({ title, userList }) { slidesPerView={4} slidesPerGroup={4} navigation + allowTouchMove={false} breakpoints={{ 320: { slidesPerView: 1, @@ -57,13 +60,20 @@ export function CardList({ title, userList }) { bg={it.backgroundColor} bgImg={it.backgroundImageURL} > - To. {it.name} - - {it.recentMessages.map((it) => ( - - ))} - -

{it.messageCount}명이 작성했어요!

+
+ To. {it.name} + + {it.recentMessages.slice(0, 3).map((it) => ( + + ))} + {it.messageCount > 3 && ( + +{it.messageCount - 3} + )} + + + {it.messageCount}명이 작성했어요! + +
diff --git a/src/styles/list-page-styles.js b/src/styles/list-page-styles.js index 8d82c83..57afe10 100644 --- a/src/styles/list-page-styles.js +++ b/src/styles/list-page-styles.js @@ -2,6 +2,7 @@ import styled from "styled-components"; import Button from "@/components/common/button"; import { RollingHeaderImojiContainer } from "@/styles/rolling-page-styles"; import { font } from "@/styles/font"; +import { colors } from "@/styles/colors"; // list-page export const PageContainer = styled.div` @@ -37,28 +38,62 @@ export const CardListLayout = styled.div` export const SwiperWrapper = styled.div` width: 100%; margin-top: 16px; + position: relative; + + .swiper-navigation-icon { + width: 14px; + height: 14px; + } + + .swiper { + position: static; + overflow: hidden; + } .swiper-button-next, .swiper-button-prev { - color: #000; + width: 40px; + height: 40px; + background-color: rgba(255, 255, 255, 0.9); + border: 1px solid #dadcdf; + border-radius: 50%; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.08); + color: #000000; + } + + .swiper-button-next { + right: -20px; + } + + .swiper-button-prev { + left: -20px; + } + + .swiper-button-disabled { + opacity: 0; + cursor: none; } `; export const CardWrapper = styled.div` width: 275px; height: 260px; - background-color: ${(props) => { - // if (props.bgImg) return "transparent"; + display: flex; + flex-direction: column; + justify-content: space-between; + /* background-color: ${(props) => { + if (props.bgImg) return "transparent"; if (props.bg) return props.bg; return "#ffffff"; }}; - /* background-image: ${(props) => { + background-image: ${(props) => { if (props.bgImg) return `url(${props.bgImg})`; return "none"; - }}; */ + }}; background-repeat: no-repeat; background-position: center; - background-size: cover; + background-size: cover; */ + background-color: ${colors.purple[200]}; border: 1px solid rgba(0, 0, 0, 0.1); border-radius: 16px; box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.08); @@ -67,10 +102,36 @@ export const CardWrapper = styled.div` export const CardImgWrapper = styled.div` display: flex; - justify-content: center; - align-items: start; + align-items: center; + margin-top: 12px; + img { - width: 20px; + width: 28px; + height: 28px; + border: 1.5px solid #ffffff; + border-radius: 50%; + &:not(:first-child) { + margin-left: -14px; + } + } +`; + +export const ProfileCount = styled.div` + display: flex; + justify-content: center; + align-items: center; + border-radius: 30px; + background-color: #ffffff; + margin-left: -14px; + padding: 5px 6px; +`; + +export const WriterCountText = styled.div` + margin-top: 12px; + ${font.regular16} + + span { + ${font.bold16} } `; From 476bd04e303eb807fbf686ca10eecb80693eced2 Mon Sep 17 00:00:00 2001 From: BZzzzi Date: Fri, 14 Nov 2025 02:05:24 +0900 Subject: [PATCH 52/91] =?UTF-8?q?Refactor:=20RollingHeaderImojiContainer?= =?UTF-8?q?=20=EB=B0=98=EC=9D=91=ED=98=95=20=EC=8A=A4=ED=83=80=EC=9D=BC=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/styles/rolling-page-styles.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/styles/rolling-page-styles.js b/src/styles/rolling-page-styles.js index 2e8a89c..a75de70 100644 --- a/src/styles/rolling-page-styles.js +++ b/src/styles/rolling-page-styles.js @@ -168,6 +168,9 @@ export const RollingHeaderImojiContainer = styled.div` display: flex; align-items: center; gap: 8px; + ${media.small` + gap: 4px; + `} `; export const RollingHeaderImojiIconContainer = styled.div` @@ -200,7 +203,11 @@ export const RollingHeaderImojiIcon = styled.div` export const RollingHeaderImojiText = styled.span` ${font.regular16} - color: rgba(255, 255, 255, 1) + color: rgba(255, 255, 255, 1); + + ${media.small` + ${font.regular14} + `} `; export const RollingHeaderImojiEditButton = styled.button` From 6f343babe995eec0d6c4bfdcc91e86c6a7b61e1a Mon Sep 17 00:00:00 2001 From: BZzzzi Date: Fri, 14 Nov 2025 02:06:56 +0900 Subject: [PATCH 53/91] =?UTF-8?q?Refactor:=20GlobalStyle=20h1,=20h2,=20h3?= =?UTF-8?q?=20reset=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/styles/global-style.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/styles/global-style.js b/src/styles/global-style.js index b57f026..426aa27 100644 --- a/src/styles/global-style.js +++ b/src/styles/global-style.js @@ -10,6 +10,9 @@ export const GlobalStyle = createGlobalStyle`${css` html, body, p, + h1, + h2, + h3, #root { margin: 0; padding: 0; From d0dcd58ff718d9f7c97825abfde8da273a6d606e Mon Sep 17 00:00:00 2001 From: BZzzzi Date: Fri, 14 Nov 2025 02:13:06 +0900 Subject: [PATCH 54/91] =?UTF-8?q?Feat:=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EB=B0=98=EC=9D=91=ED=98=95=20=EA=B5=AC=ED=98=84=20=EB=B0=8F=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=EB=84=A4=EC=9D=B4=EC=85=98=20prop?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 반응형 UI 스타일 구현 - 반응형에 따른 Swiper 구현 - 빈 배열 내려주면 Empty Section 보여주도록 구현 - onLoadMore prop으로 페이지네이션 Url 연결 --- src/components/list/card-list.jsx | 47 ++++++++++++--------- src/pages/list-page.jsx | 33 ++++++++++++--- src/styles/list-page-styles.js | 70 +++++++++++++++++++++++++++++-- 3 files changed, 121 insertions(+), 29 deletions(-) diff --git a/src/components/list/card-list.jsx b/src/components/list/card-list.jsx index db3d95a..f577f5e 100644 --- a/src/components/list/card-list.jsx +++ b/src/components/list/card-list.jsx @@ -8,48 +8,55 @@ import { CardListLayout, SwiperWrapper, CardWrapper, - CustomH3, EmojiWrapper, WriterCountText, ProfileCount, + EmptySection, + Title, + ReceiverName, } from "@/styles/list-page-styles"; -export function CardList({ title, userList }) { +export function CardList({ title, userList, onLoadMore }) { return ( <> - {title} + {title} {userList.length === 0 ? ( -
작성된 롤링페이퍼가 없습니다! 직접 만들어 보세요.
+ + 아직 작성된 롤링 페이퍼가 없습니다. +
+ 새로운 롤링 페이퍼를 만들어 보세요! +
) : ( { + if (onLoadMore) onLoadMore(); + }} + navigation={true} + allowTouchMove={true} + slidesPerView="auto" + slidesPerGroup={1} + spaceBetween={12} + slidesOffsetBefore={20} + slidesOffsetAfter={20} breakpoints={{ - 320: { - slidesPerView: 1, - slidesPerGroup: 1, - spaceBetween: 10, - }, - 640: { - slidesPerView: 2, - slidesPerGroup: 2, - spaceBetween: 15, - }, 1024: { slidesPerView: 3, slidesPerGroup: 3, spaceBetween: 20, + allowTouchMove: false, + slidesOffsetBefore: 0, + slidesOffsetAfter: 0, }, 1200: { slidesPerView: 4, slidesPerGroup: 4, spaceBetween: 20, + allowTouchMove: false, + slidesOffsetBefore: 0, + slidesOffsetAfter: 0, }, }} > @@ -61,7 +68,7 @@ export function CardList({ title, userList }) { bgImg={it.backgroundImageURL} >
- To. {it.name} + To. {it.name} {it.recentMessages.slice(0, 3).map((it) => ( diff --git a/src/pages/list-page.jsx b/src/pages/list-page.jsx index a60ac7a..dac7855 100644 --- a/src/pages/list-page.jsx +++ b/src/pages/list-page.jsx @@ -9,12 +9,13 @@ import { } from "@/styles/list-page-styles"; import { CardList } from "@/components/list/card-list"; -const UI_PAGE_SIZE = 4; const baseURL = import.meta.env.VITE_API_BASE_URL; export default function ListPage() { const [likePaper, setLikePaper] = useState([]); const [recentPaper, setRecentPaper] = useState([]); + const [likeNextUrl, setLikeNextUrl] = useState(null); + const [recentNextUrl, setRecentNextUrl] = useState(null); useEffect(() => { const fetchRecipients = async () => { @@ -25,21 +26,43 @@ export default function ListPage() { const recentData = await getRecipients({ url: `${baseURL}/recipients/`, }); - console.log("likeData:", likeData); setLikePaper(likeData.results); + setLikeNextUrl(likeData.next); setRecentPaper(recentData.results); + setRecentNextUrl(recentData.next); } catch (error) { console.error("recipients 가져오기 실패:", error); } }; - fetchRecipients(); }, []); + const fetchMoreLink = async () => { + if (!likeNextUrl) return; + const nextData = await getRecipients({ url: likeNextUrl }); + setLikePaper((prev) => [...prev, ...nextData.results]); + setLikeNextUrl(nextData.next); + }; + + const fetchMoreRecent = async () => { + if (!recentNextUrl) return; + const nextData = await getRecipients({ url: recentNextUrl }); + setRecentPaper((prev) => [...prev, ...nextData.results]); + setRecentNextUrl(nextData.next); + }; + return ( - - + + 나도 만들어보기 diff --git a/src/styles/list-page-styles.js b/src/styles/list-page-styles.js index 57afe10..cd450ed 100644 --- a/src/styles/list-page-styles.js +++ b/src/styles/list-page-styles.js @@ -3,6 +3,7 @@ import Button from "@/components/common/button"; import { RollingHeaderImojiContainer } from "@/styles/rolling-page-styles"; import { font } from "@/styles/font"; import { colors } from "@/styles/colors"; +import media from "@/styles/media"; // list-page export const PageContainer = styled.div` @@ -21,9 +22,41 @@ export const CustomButton = styled(Button)` padding: 14px 60px; `; +// card-list 비었을 때 +export const EmptySection = styled.div` + width: 100%; + text-align: center; + background-color: rgba(0, 0, 0, 0.05); + border: 1px solid rgba(0, 0, 0, 0.1); + border-radius: 16px; + box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.08); + ${font.regular20}; + margin-top: 12px; + padding: 80px; + ${media.small` + padding: 80px 30px; + ${font.regular14} + `} +`; + // card-list -export const CustomH3 = styled.div` - ${font.bold24} +export const Title = styled.h3` + ${font.bold24}; + + ${media.medium` + padding-left: 20px; + `} + + ${media.small` + padding-left: 20px; + `} +`; + +export const ReceiverName = styled.h3` + ${font.bold24}; + ${media.small` + ${font.bold18}; + `} `; export const CardListLayout = styled.div` @@ -33,6 +66,14 @@ export const CardListLayout = styled.div` align-items: start; margin-top: 50px; padding: 0 20px; + + ${media.small` + padding: 0; + `} + + ${media.medium` + padding: 0; + `} `; export const SwiperWrapper = styled.div` @@ -50,6 +91,18 @@ export const SwiperWrapper = styled.div` overflow: hidden; } + .swiper-slide { + ${media.medium` + width: 30%; + min-width: 275px; + `} + + ${media.small` + width: 50%; + min-width: 208px; + `} + } + .swiper-button-next, .swiper-button-prev { width: 40px; @@ -59,6 +112,14 @@ export const SwiperWrapper = styled.div` border-radius: 50%; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.08); color: #000000; + + ${media.medium` + display: none; + `} + + ${media.small` + display: none; + `} } .swiper-button-next { @@ -76,7 +137,7 @@ export const SwiperWrapper = styled.div` `; export const CardWrapper = styled.div` - width: 275px; + width: 100%; height: 260px; display: flex; flex-direction: column; @@ -137,5 +198,6 @@ export const WriterCountText = styled.div` export const EmojiWrapper = styled(RollingHeaderImojiContainer)` border-top: 1px solid rgba(0, 0, 0, 0.12); - padding: 17px 28px 0 0; + padding-top: 17px; + font-size: 14px; `; From 2baa39e35fd41aa35aed30b1a0109b566b4556c9 Mon Sep 17 00:00:00 2001 From: BZzzzi Date: Fri, 14 Nov 2025 02:13:44 +0900 Subject: [PATCH 55/91] =?UTF-8?q?Refactor:=20GlobalStyle=20h3=20reset?= =?UTF-8?q?=EC=97=90=20=EB=94=B0=EB=A5=B8=20=ED=97=A4=EB=8D=94=20=EC=8A=A4?= =?UTF-8?q?=ED=83=80=EC=9D=BC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/common/header.jsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/common/header.jsx b/src/components/common/header.jsx index 572036b..9e60117 100644 --- a/src/components/common/header.jsx +++ b/src/components/common/header.jsx @@ -30,6 +30,10 @@ const LogoWrapper = styled.div` gap: 8px; `; +const Title = styled.h3` + padding: 20px 0; +`; + const ButtonWrapper = styled.div` margin-left: auto; `; @@ -43,7 +47,7 @@ export default function Header({ showButton }) { 로고 -

Rolling

+ Rolling
{showButton && ( From de72ae19b05b26a85968aee8eb90d768ec1cc90e Mon Sep 17 00:00:00 2001 From: yeseung00 Date: Fri, 14 Nov 2025 17:19:11 +0900 Subject: [PATCH 56/91] =?UTF-8?q?Feat:=20=EB=A9=94=EC=8B=9C=EC=A7=80=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20UI=20=EB=94=94=EC=9E=90=EC=9D=B8,?= =?UTF-8?q?=20=EB=93=9C=EB=A1=AD=EB=8B=A4=EC=9A=B4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 생성하기 버튼에 공용 컴포넌트 사용 - 폼 인풋 스타일 적용 - ‘상대와의 관계’, ‘폰트선택’ 드롭다운 컴포넌트 구현# --- src/App.jsx | 6 +- src/components/message/drop-down.jsx | 159 +++++++++++++++++++++++++++ src/hooks/use-dropdown.js | 16 +++ src/pages/message-page.jsx | 126 ++++++++++----------- 4 files changed, 242 insertions(+), 65 deletions(-) create mode 100644 src/components/message/drop-down.jsx create mode 100644 src/hooks/use-dropdown.js diff --git a/src/App.jsx b/src/App.jsx index bacecaa..f9403a6 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -15,11 +15,11 @@ function App() { }> } /> - } /> + {/*} /> } /> } /> - } /> - } /> + } /> */} + } /> } /> } /> diff --git a/src/components/message/drop-down.jsx b/src/components/message/drop-down.jsx new file mode 100644 index 0000000..26512e0 --- /dev/null +++ b/src/components/message/drop-down.jsx @@ -0,0 +1,159 @@ +import React, { useState, useRef, useEffect, useCallback } from "react"; +import styled, { css } from "styled-components"; +import { colors } from "@/styles/colors"; +import { font } from "@/styles/font"; + +import ARROW_ICON from "@/assets/icons/arrow-right.svg"; + +const DropDownWrapper = styled.div` + position: relative; + width: 320px; +`; + +const DropDownTrigger = styled.button` + width: 100%; + height: 50px; + display: flex; + justify-content: space-between; + align-items: center; + + padding: 12px 16px; + border-radius: 8px; + border: 1px solid ${colors.gray[300]}; + background-color: #fff; + + ${font.regular16} + text-align: left; + + outline: none; + + color: ${({ currentValue, defaultValue, $isInitialLoad }) => { + if ($isInitialLoad && currentValue === defaultValue) { + return colors.gray[500]; + } + return colors.gray[900]; + }}; + + ${({ $isOpen }) => + $isOpen && + css` + border: 2px solid ${colors.gray[500]}; + padding: 11px 15px; + `} +`; + +const ArrowImage = styled.img` + width: 16px; + height: 16px; + transform: rotate(${({ $isOpen }) => ($isOpen ? "270deg" : "90deg")}); + transition: transform 0.2s; +`; + +const DropDownMenuContainer = styled.ul` + list-style: none; + margin: 10px 1px; + padding: 1px; + + position: absolute; + top: 100%; + left: 0; + z-index: 10; + width: 320px; + max-height: 220px; + overflow-y: auto; + + background-color: #ffffff; + border: 1px solid ${colors.gray[300]}; + border-radius: 8px; + + box-shadow: 0px 2px 12px 0px rgba(0, 0, 0, 0.08); +`; + +const DropDownItem = styled.li` + height: 50px; + display: flex; + align-items: center; + padding: 12px 16px; + + ${font.regular16} + color: ${colors.gray[900]}; + + &:hover { + background-color: ${colors.gray[100]}; + } +`; + +function DropDown({ id, name, defaultValue, value, onChange, options }) { + const [isOpen, setIsOpen] = useState(false); + const dropdownRef = useRef(null); + const currentValue = value; + const [isInitialLoad, setIsInitialLoad] = useState(true); + + const handleClickOutside = useCallback((event) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target)) { + setIsOpen(false); + } + }, []); + + useEffect(() => { + document.addEventListener("mousedown", handleClickOutside); + return () => { + document.removeEventListener("mousedown", handleClickOutside); + }; + }, [handleClickOutside]); + + const handleItemClick = (optionValue) => { + onChange({ target: { name: name, value: optionValue } }); + setIsOpen(false); + setIsInitialLoad(false); + }; + + const handleTriggerClick = () => { + setIsOpen((prev) => !prev); + if (isInitialLoad) { + setIsInitialLoad(false); + } + }; + + const selectedOption = options.find((opt) => opt.value === currentValue) || { + label: defaultValue, + value: defaultValue, + }; + + return ( + + + {selectedOption.label} + + + + {isOpen && ( + + {options.map((option) => ( + handleItemClick(option.value)} + > + {option.label} + + ))} + + )} + + ); +} + +export default DropDown; diff --git a/src/hooks/use-dropdown.js b/src/hooks/use-dropdown.js new file mode 100644 index 0000000..c4ed4b7 --- /dev/null +++ b/src/hooks/use-dropdown.js @@ -0,0 +1,16 @@ +import { useState } from "react"; + +const useDropdown = (initialValue) => { + const [value, setValue] = useState(initialValue); + + const handleChange = (e) => { + setValue(e.target.value); + }; + + return { + value, + handleChange, + }; +}; + +export default useDropdown; diff --git a/src/pages/message-page.jsx b/src/pages/message-page.jsx index 1f146da..9e42d09 100644 --- a/src/pages/message-page.jsx +++ b/src/pages/message-page.jsx @@ -1,9 +1,21 @@ import React from "react"; import styled, { css } from "styled-components"; +import { font } from "@/styles/font"; +import Button from "@/components/common/button"; +import DropDown from "@/components/message/drop-down"; +import useDropdown from "@/hooks/use-dropdown"; + +const RELATIONSHIP_OPTIONS = [ + { label: "지인", value: "지인" }, + { label: "친구", value: "친구" }, + { label: "동료", value: "동료" }, + { label: "가족", value: "가족" }, +]; + +const FONT_OPTIONS = [{ label: "Noto Sans", value: "Noto Sans" }]; const DEFAULT_ICON_URL = "/assets/default-user.svg"; const TEMP_IMAGE_URL = "/assets/temp-profile.jpg"; - const selectableImages = [ { id: 1, url: TEMP_IMAGE_URL, isSelected: true }, { id: 2, url: TEMP_IMAGE_URL, isSelected: false }, @@ -14,6 +26,21 @@ const selectableImages = [ { id: 7, url: TEMP_IMAGE_URL, isSelected: false }, ]; +const FormInputStyle = css` + width: 100%; + padding: 12px 16px; + border-radius: 8px; + border: 1px solid #ccc; + font-size: 16px; + outline: none; + color: #181818; + background-color: #fff; + + &:focus { + border-color: #555; + } +`; + export const PageContainer = styled.div` max-width: 720px; margin: 0 auto; @@ -33,19 +60,14 @@ export const FormField = styled.div` `; export const FormLabel = styled.label` - font-size: 16px; - font-weight: 700; - line-height: 26px; /* 162.5% */ + ${font.bold24} + line-height: 36px; + letter-spacing: -0.01em; color: #181818; `; export const InputField = styled.input` - width: 100%; - padding: 12px 16px; - border-radius: 8px; - border: 1px solid #ccc; - font-size: 16px; - outline: none; + ${FormInputStyle} &:focus { border-color: #555; @@ -53,9 +75,9 @@ export const InputField = styled.input` `; export const ErrorMessage = styled.p` - color: #dc3545; /* 빨간색 계열 */ + color: #dc3545; font-size: 14px; - margin-top: -8px; /* 위쪽 갭 조정 */ + margin-top: -8px; `; export const ProfileWrapper = styled.div` @@ -75,7 +97,7 @@ export const ProfileDefaultBox = styled.div` height: 70px; border-radius: 50%; overflow: hidden; - flex-shrink: 0; /* 크기 고정 */ + flex-shrink: 0; border: 1px solid #ccc; img { @@ -89,8 +111,8 @@ export const SelectableImagesList = styled.ul` display: flex; gap: 4px; overflow-x: auto; - padding: 4px 0; /* 스크롤바를 위한 패딩 */ - -webkit-overflow-scrolling: touch; /* iOS에서 부드러운 스크롤 */ + padding: 4px 0; + -webkit-overflow-scrolling: touch; list-style: none; margin: 0; `; @@ -101,11 +123,10 @@ export const SelectableImageItem = styled.li` border-radius: 50%; overflow: hidden; cursor: pointer; - flex-shrink: 0; /* 크기 고정 */ + flex-shrink: 0; transition: transform 0.2s, border 0.2s; border: 2px solid transparent; - &:hover { opacity: 0.8; } @@ -117,17 +138,6 @@ export const SelectableImageItem = styled.li` } `; -export const SelectField = styled.select` - width: 100%; - padding: 12px 16px; - border-radius: 8px; - border: 1px solid #ccc; - font-size: 16px; - background-color: #fff; - appearance: none; /* 기본 드롭다운 화살표 숨기기 */ - outline: none; -`; - export const EditorPlaceholder = styled.div` min-height: 200px; padding: 16px; @@ -138,33 +148,18 @@ export const EditorPlaceholder = styled.div` color: #777; `; -export const SubmitButton = styled.button` +const FullWidthButton = styled(Button)` width: 100%; - padding: 14px 0; margin-top: 20px; - border-radius: 12px; - background-color: #3f60ff; /* Primary Color */ - color: #fff; - font-size: 18px; - font-weight: 700; - border: none; - cursor: pointer; - transition: background-color 0.3s; - - &:disabled { - background-color: #ccc; - cursor: not-allowed; - } - - &:not(:disabled):hover { - background-color: #2e4bc0; - } `; function MessagePage() { const isFormValid = false; const hasError = true; + const relationshipDropdown = useDropdown("지인"); + const fontDropdown = useDropdown("Noto Sans"); + return ( @@ -179,14 +174,14 @@ function MessagePage() { {/* 에러 메시지 표시 */} {hasError && "값을 입력해 주세요."} - {/* 프로필 이미지 선택창 */} + + {/* 프로필 이미지 선택창 (생략) */} 프로필 이미지 기본 프로필 이미지 - {selectableImages.map((image) => ( @@ -200,16 +195,14 @@ function MessagePage() { {/* 상대와의 관계 드롭다운*/} 상대와의 관계 - - - - - - + options={RELATIONSHIP_OPTIONS} + value={relationshipDropdown.value} + onChange={relationshipDropdown.handleChange} + /> {/* 내용 입력 (Rich Text Editor 사용) */} @@ -223,16 +216,25 @@ function MessagePage() { {/* 폰트 선택 드롭다운 */} 폰트 선택 - - - {/* 추가 폰트 옵션 추가 예정 */} - + {/* 생성하기 버튼 */} - + 생성하기 - + ); From f3a8d7d005b7b6367daa81fd3796573bcde06e6f Mon Sep 17 00:00:00 2001 From: yeseung00 Date: Fri, 14 Nov 2025 20:25:53 +0900 Subject: [PATCH 57/91] =?UTF-8?q?Feat:=20from.=20input=20=EB=B0=98?= =?UTF-8?q?=EC=9D=91=ED=98=95=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/message/drop-down.jsx | 7 ++-- src/components/message/from-input.jsx | 47 +++++++++++++++++++++++++++ src/hooks/use-from-input.js | 24 ++++++++++++++ src/pages/message-page.jsx | 23 ++++++++----- 4 files changed, 88 insertions(+), 13 deletions(-) create mode 100644 src/components/message/from-input.jsx create mode 100644 src/hooks/use-from-input.js diff --git a/src/components/message/drop-down.jsx b/src/components/message/drop-down.jsx index 26512e0..37d34df 100644 --- a/src/components/message/drop-down.jsx +++ b/src/components/message/drop-down.jsx @@ -12,7 +12,6 @@ const DropDownWrapper = styled.div` const DropDownTrigger = styled.button` width: 100%; - height: 50px; display: flex; justify-content: space-between; align-items: center; @@ -27,8 +26,8 @@ const DropDownTrigger = styled.button` outline: none; - color: ${({ currentValue, defaultValue, $isInitialLoad }) => { - if ($isInitialLoad && currentValue === defaultValue) { + color: ${({ $currentValue, defaultValue, $isInitialLoad }) => { + if ($isInitialLoad && $currentValue === defaultValue) { return colors.gray[500]; } return colors.gray[900]; @@ -128,7 +127,7 @@ function DropDown({ id, name, defaultValue, value, onChange, options }) { type="button" onClick={handleTriggerClick} $isOpen={isOpen} - currentValue={currentValue} + $currentValue={currentValue} defaultValue={defaultValue} $isInitialLoad={isInitialLoad} aria-haspopup="listbox" diff --git a/src/components/message/from-input.jsx b/src/components/message/from-input.jsx new file mode 100644 index 0000000..16a1e58 --- /dev/null +++ b/src/components/message/from-input.jsx @@ -0,0 +1,47 @@ +import React from "react"; +import styled, { css } from "styled-components"; +import { colors } from "@/styles/colors"; +import { + FormInputStyle, + ErrorMessage, + FormField, +} from "@/pages/message-page.jsx"; + +const ErrorOverrideStyle = css` + border-color: ${colors.error}; + + &:focus { + border-color: ${colors.error}; + } +`; +const StyledInput = styled.input` + ${() => FormInputStyle} + ${(props) => props.$hasError && ErrorOverrideStyle} +`; + +export default function FromInput({ + id, + name, + placeholder, + hasError, + errorMessage, + onBlur, + onChange, + value, +}) { + return ( + + + {/* 에러 메시지 표시 */} + {hasError && {errorMessage}} + + ); +} diff --git a/src/hooks/use-from-input.js b/src/hooks/use-from-input.js new file mode 100644 index 0000000..f1c5118 --- /dev/null +++ b/src/hooks/use-from-input.js @@ -0,0 +1,24 @@ +import { useState } from "react"; + +export default function useFormInput(initialValue = "") { + const [value, setValue] = useState(initialValue); + const [isTouched, setIsTouched] = useState(false); + + // 에러 상태 계산 빙법: 포커스 아웃되었고, 값이 비어있을 때 에러 + const hasError = isTouched && value.trim() === ""; + + const handleChange = (e) => { + setValue(e.target.value); + }; + + const handleBlur = () => { + setIsTouched(true); + }; + + return { + value, + hasError, + handleChange, + handleBlur, + }; +} diff --git a/src/pages/message-page.jsx b/src/pages/message-page.jsx index 9e42d09..421d11c 100644 --- a/src/pages/message-page.jsx +++ b/src/pages/message-page.jsx @@ -1,9 +1,12 @@ import React from "react"; import styled, { css } from "styled-components"; +import { colors } from "@/styles/colors"; import { font } from "@/styles/font"; import Button from "@/components/common/button"; import DropDown from "@/components/message/drop-down"; +import FromInput from "@/components/message/from-input"; import useDropdown from "@/hooks/use-dropdown"; +import useFormInput from "@/hooks/use-from-input"; const RELATIONSHIP_OPTIONS = [ { label: "지인", value: "지인" }, @@ -26,14 +29,14 @@ const selectableImages = [ { id: 7, url: TEMP_IMAGE_URL, isSelected: false }, ]; -const FormInputStyle = css` +export const FormInputStyle = css` width: 100%; padding: 12px 16px; border-radius: 8px; border: 1px solid #ccc; - font-size: 16px; + ${font.regular16}; outline: none; - color: #181818; + ${colors.gray[900]}; background-color: #fff; &:focus { @@ -63,7 +66,7 @@ export const FormLabel = styled.label` ${font.bold24} line-height: 36px; letter-spacing: -0.01em; - color: #181818; + ${colors.gray[900]}; `; export const InputField = styled.input` @@ -155,8 +158,7 @@ const FullWidthButton = styled(Button)` function MessagePage() { const isFormValid = false; - const hasError = true; - + const fromInput = useFormInput(""); const relationshipDropdown = useDropdown("지인"); const fontDropdown = useDropdown("Noto Sans"); @@ -166,13 +168,16 @@ function MessagePage() { {/* From. 입력 필드 */} From. - - {/* 에러 메시지 표시 */} - {hasError && "값을 입력해 주세요."} {/* 프로필 이미지 선택창 (생략) */} From 1757ec93da8bcb07a51d6d8df9532946ac23e41f Mon Sep 17 00:00:00 2001 From: BZzzzi Date: Fri, 14 Nov 2025 20:49:41 +0900 Subject: [PATCH 58/91] =?UTF-8?q?Feat:=20=EC=B9=B4=EB=93=9C=20=EC=8A=A4?= =?UTF-8?q?=ED=83=80=EC=9D=BC=20=EB=94=94=ED=85=8C=EC=9D=BC=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20=EB=B0=8F=20=EB=A7=81=ED=81=AC=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 카드 클릭 시, rolling 페이지로 연결(id 연결은 추후 예정) - 카드 배경 색상/이미지 상태에 따른 디자인 세부 조정 --- src/components/list/card-list.jsx | 27 +++++++++++--- src/pages/list-page.jsx | 11 ++---- src/styles/list-page-styles.js | 62 ++++++++++++++++++++++++++----- 3 files changed, 78 insertions(+), 22 deletions(-) diff --git a/src/components/list/card-list.jsx b/src/components/list/card-list.jsx index f577f5e..61d1b51 100644 --- a/src/components/list/card-list.jsx +++ b/src/components/list/card-list.jsx @@ -15,8 +15,17 @@ import { Title, ReceiverName, } from "@/styles/list-page-styles"; +import { useNavigate } from "react-router"; export function CardList({ title, userList, onLoadMore }) { + const navigate = useNavigate(); + + const handleCardClick = () => { + if (userList) { + navigate("/rolling"); + } + }; + return ( <> @@ -42,6 +51,10 @@ export function CardList({ title, userList, onLoadMore }) { slidesOffsetBefore={20} slidesOffsetAfter={20} breakpoints={{ + 600: { + slidesOffsetBefore: 24, + slidesOffsetAfter: 24, + }, 1024: { slidesPerView: 3, slidesPerGroup: 3, @@ -62,22 +75,26 @@ export function CardList({ title, userList, onLoadMore }) { > {userList.map((it) => { return ( - +
- To. {it.name} + + To. {it.name} + + {/* 프로필 이미지 map */} {it.recentMessages.slice(0, 3).map((it) => ( ))} {it.messageCount > 3 && ( +{it.messageCount - 3} )} + {/* -------------- */} - + {it.messageCount}명이 작성했어요!
diff --git a/src/pages/list-page.jsx b/src/pages/list-page.jsx index dac7855..3e17783 100644 --- a/src/pages/list-page.jsx +++ b/src/pages/list-page.jsx @@ -1,9 +1,8 @@ -import { Link } from "react-router"; import { useEffect, useState } from "react"; import { getRecipients } from "@/api/list-user-api"; import { - BottomWrapper, + ButtonLink, CustomButton, PageContainer, } from "@/styles/list-page-styles"; @@ -63,11 +62,9 @@ export default function ListPage() { userList={recentPaper} onLoadMore={fetchMoreRecent} /> - - - 나도 만들어보기 - - + + 나도 만들어보기 +
); } diff --git a/src/styles/list-page-styles.js b/src/styles/list-page-styles.js index cd450ed..bd4a4f2 100644 --- a/src/styles/list-page-styles.js +++ b/src/styles/list-page-styles.js @@ -1,5 +1,6 @@ import styled from "styled-components"; import Button from "@/components/common/button"; +import { Link } from "react-router"; import { RollingHeaderImojiContainer } from "@/styles/rolling-page-styles"; import { font } from "@/styles/font"; import { colors } from "@/styles/colors"; @@ -11,15 +12,33 @@ export const PageContainer = styled.div` margin: 0 auto; `; -export const BottomWrapper = styled.div` +export const ButtonLink = styled(Link)` + width: 100%; display: flex; justify-content: center; + align-items: center; padding: 24px 0; margin-top: 40px; + + ${media.medium` + padding: 24px; + `} + + ${media.small` + padding: 24px 20px; + `} `; export const CustomButton = styled(Button)` padding: 14px 60px; + + ${media.medium` + width: 100%; + `} + + ${media.small` + width: 100%; + `} `; // card-list 비었을 때 @@ -54,6 +73,14 @@ export const Title = styled.h3` export const ReceiverName = styled.h3` ${font.bold24}; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + + color: ${(props) => { + return props.$bgImg ? "white" : "black"; + }}; + ${media.small` ${font.bold18}; `} @@ -65,7 +92,7 @@ export const CardListLayout = styled.div` justify-content: center; align-items: start; margin-top: 50px; - padding: 0 20px; + padding: 0 24px; ${media.small` padding: 0; @@ -142,23 +169,35 @@ export const CardWrapper = styled.div` display: flex; flex-direction: column; justify-content: space-between; - /* background-color: ${(props) => { - if (props.bgImg) return "transparent"; - if (props.bg) return props.bg; + + // 배경 color일 때 적용(이미지 같이 내려올 시, 이미지 우선) + background-color: ${(props) => { + if (props.$bgImg) return "transparent"; + if (props.$bg) { + if (props.$bg === "beige") return `${colors.beige[200]}`; + if (props.$bg === "purple") return `${colors.purple[200]}`; + if (props.$bg === "blue") return `${colors.blue[200]}`; + if (props.$bg === "green") return `${colors.green[200]}`; + } return "#ffffff"; }}; + + // 배경 이미지, null이면 color 적용 background-image: ${(props) => { - if (props.bgImg) return `url(${props.bgImg})`; + if (props.$bgImg) { + return `linear-gradient(rgba(0,0,0,0.45), rgba(0,0,0,0.45)), url(${props.$bgImg})`; + } return "none"; }}; + background-repeat: no-repeat; background-position: center; - background-size: cover; */ - background-color: ${colors.purple[200]}; + background-size: cover; border: 1px solid rgba(0, 0, 0, 0.1); border-radius: 16px; box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.08); padding: 30px 24px 20px; + cursor: pointer; `; export const CardImgWrapper = styled.div` @@ -189,9 +228,12 @@ export const ProfileCount = styled.div` export const WriterCountText = styled.div` margin-top: 12px; - ${font.regular16} - span { + color: ${(props) => { + return props.$bgImg ? "white" : "black"; + }}; + + ${font.regular16} span { ${font.bold16} } `; From 83a51ce6aa065b4e3c3393f1e6c17935ccf0b081 Mon Sep 17 00:00:00 2001 From: BZzzzi Date: Fri, 14 Nov 2025 22:39:18 +0900 Subject: [PATCH 59/91] =?UTF-8?q?Refactor:=20=EC=B9=B4=EB=93=9C=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20API=20=EC=9A=94=EC=B2=AD=20?= =?UTF-8?q?=EB=A1=9C=EB=94=A9=20=EB=B0=8F=20=EC=97=90=EB=9F=AC=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 중복 요청 방지를 위한 로딩 처리와 에러 발생 시 상태 관리 - 오타 수정 --- src/pages/list-page.jsx | 45 +++++++++++++++++++++++++++++++---------- 1 file changed, 34 insertions(+), 11 deletions(-) diff --git a/src/pages/list-page.jsx b/src/pages/list-page.jsx index 3e17783..33dc972 100644 --- a/src/pages/list-page.jsx +++ b/src/pages/list-page.jsx @@ -1,6 +1,5 @@ import { useEffect, useState } from "react"; import { getRecipients } from "@/api/list-user-api"; - import { ButtonLink, CustomButton, @@ -15,10 +14,13 @@ export default function ListPage() { const [recentPaper, setRecentPaper] = useState([]); const [likeNextUrl, setLikeNextUrl] = useState(null); const [recentNextUrl, setRecentNextUrl] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); useEffect(() => { const fetchRecipients = async () => { try { + setIsLoading(true); const likeData = await getRecipients({ url: `${baseURL}/recipients/?sort=like`, }); @@ -29,38 +31,59 @@ export default function ListPage() { setLikeNextUrl(likeData.next); setRecentPaper(recentData.results); setRecentNextUrl(recentData.next); - } catch (error) { - console.error("recipients 가져오기 실패:", error); + } catch (err) { + console.error("recipients 가져오기 실패:", err); + setError("데이터를 불러오는데 실패했습니다."); + } finally { + setIsLoading(false); } }; fetchRecipients(); }, []); - const fetchMoreLink = async () => { + const fetchMoreLike = async () => { if (!likeNextUrl) return; - const nextData = await getRecipients({ url: likeNextUrl }); - setLikePaper((prev) => [...prev, ...nextData.results]); - setLikeNextUrl(nextData.next); + try { + const nextData = await getRecipients({ url: likeNextUrl }); + setLikePaper((prev) => [...prev, ...nextData.results]); + setLikeNextUrl(nextData.next); + } catch (err) { + console.error("추가 데이터 로드 실패:", err); + } }; const fetchMoreRecent = async () => { if (!recentNextUrl) return; - const nextData = await getRecipients({ url: recentNextUrl }); - setRecentPaper((prev) => [...prev, ...nextData.results]); - setRecentNextUrl(nextData.next); + try { + const nextData = await getRecipients({ url: recentNextUrl }); + setRecentPaper((prev) => [...prev, ...nextData.results]); + setRecentNextUrl(nextData.next); + } catch (err) { + console.error("추가 데이터 로드 실패:", err); + } }; + if (isLoading) { + return 로딩 중...; + } + + if (error) { + return {error}; + } + return ( 나도 만들어보기 From 41a544c48c3612506790c8bd626d3ae2b7f27eab Mon Sep 17 00:00:00 2001 From: BZzzzi Date: Fri, 14 Nov 2025 22:42:49 +0900 Subject: [PATCH 60/91] =?UTF-8?q?Refactor:=20Swiper=20=EB=8F=99=EC=9E=91?= =?UTF-8?q?=20=EB=B0=8F=20=ED=8E=98=EC=9D=B4=EC=A7=80=EB=84=A4=EC=9D=B4?= =?UTF-8?q?=EC=85=98=20=EC=82=AC=EC=9A=A9=EC=9E=90=20=EA=B2=BD=ED=97=98=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 추가 로드 시 발생하던 Swiper 버벅임 개선 - null 체크 로직 보강 및 중복 코드 공통 함수로 통합 - 카드 클릭 시 올바른 ID 기반 롤링 상세 페이지로 이동하도록 처리 개선 --- src/components/list/card-list.jsx | 36 ++++++++++++++++++++++--------- src/styles/list-page-styles.js | 13 ++++++++++- 2 files changed, 38 insertions(+), 11 deletions(-) diff --git a/src/components/list/card-list.jsx b/src/components/list/card-list.jsx index 61d1b51..323db3c 100644 --- a/src/components/list/card-list.jsx +++ b/src/components/list/card-list.jsx @@ -1,5 +1,6 @@ import { Swiper, SwiperSlide } from "swiper/react"; import { Navigation } from "swiper/modules"; +import { useNavigate } from "react-router"; import "swiper/css"; import "swiper/css/navigation"; import EmojiDisplayList from "@/components/rolling/emoji-display-list"; @@ -15,15 +16,18 @@ import { Title, ReceiverName, } from "@/styles/list-page-styles"; -import { useNavigate } from "react-router"; -export function CardList({ title, userList, onLoadMore }) { +export function CardList({ title, userList, onLoadMore, nextCheck }) { + const isDesktop = window.innerWidth >= 1024; const navigate = useNavigate(); - const handleCardClick = () => { - if (userList) { - navigate("/rolling"); - } + const handleCardClick = (id) => { + navigate(`/rolling/${id}`); + }; + + const handleLoadMore = async () => { + if (!onLoadMore || !nextCheck) return; + await onLoadMore(); }; return ( @@ -37,14 +41,23 @@ export function CardList({ title, userList, onLoadMore }) { 새로운 롤링 페이퍼를 만들어 보세요! ) : ( - + { - if (onLoadMore) onLoadMore(); + if (!isDesktop) return; + handleLoadMore(); + }} + onSlideChange={(swiper) => { + if (isDesktop) return; + const current = swiper.activeIndex; + const last = swiper.slides.length - 1; + if (current >= last - 2) { + handleLoadMore(); + } }} navigation={true} - allowTouchMove={true} + allowTouchMove={!isDesktop} slidesPerView="auto" slidesPerGroup={1} spaceBetween={12} @@ -75,7 +88,10 @@ export function CardList({ title, userList, onLoadMore }) { > {userList.map((it) => { return ( - + handleCardClick(it.id)} + > Date: Sat, 15 Nov 2025 00:59:47 +0900 Subject: [PATCH 61/91] =?UTF-8?q?Feat:=20=EC=B9=B4=EB=93=9C=20=EB=B0=B0?= =?UTF-8?q?=EA=B2=BD=20=EC=83=89=EC=83=81=EB=B3=84=20=EB=8F=84=ED=98=95=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - bg 색상별 svg 도형 적용 - 반응형 구현 --- src/assets/images/bg-pattern-beige.svg | 3 ++ src/assets/images/bg-pattern-blue.svg | 3 ++ src/assets/images/bg-pattern-green.svg | 3 ++ src/assets/images/bg-pattern-purple.svg | 3 ++ src/styles/list-page-styles.js | 53 ++++++++++++++++++++----- 5 files changed, 55 insertions(+), 10 deletions(-) create mode 100644 src/assets/images/bg-pattern-beige.svg create mode 100644 src/assets/images/bg-pattern-blue.svg create mode 100644 src/assets/images/bg-pattern-green.svg create mode 100644 src/assets/images/bg-pattern-purple.svg diff --git a/src/assets/images/bg-pattern-beige.svg b/src/assets/images/bg-pattern-beige.svg new file mode 100644 index 0000000..e1b9e8d --- /dev/null +++ b/src/assets/images/bg-pattern-beige.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/images/bg-pattern-blue.svg b/src/assets/images/bg-pattern-blue.svg new file mode 100644 index 0000000..37b626c --- /dev/null +++ b/src/assets/images/bg-pattern-blue.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/images/bg-pattern-green.svg b/src/assets/images/bg-pattern-green.svg new file mode 100644 index 0000000..59c771b --- /dev/null +++ b/src/assets/images/bg-pattern-green.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/images/bg-pattern-purple.svg b/src/assets/images/bg-pattern-purple.svg new file mode 100644 index 0000000..6d22b58 --- /dev/null +++ b/src/assets/images/bg-pattern-purple.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/styles/list-page-styles.js b/src/styles/list-page-styles.js index 5bec928..5e0b627 100644 --- a/src/styles/list-page-styles.js +++ b/src/styles/list-page-styles.js @@ -5,6 +5,10 @@ import { RollingHeaderImojiContainer } from "@/styles/rolling-page-styles"; import { font } from "@/styles/font"; import { colors } from "@/styles/colors"; import media from "@/styles/media"; +import bgPatternBeige from "@/assets/images/bg-pattern-beige.svg"; +import bgPatternPurple from "@/assets/images/bg-pattern-purple.svg"; +import bgPatternBlue from "@/assets/images/bg-pattern-blue.svg"; +import bgPatternGreen from "@/assets/images/bg-pattern-green.svg"; // list-page export const PageContainer = styled.div` @@ -184,31 +188,60 @@ export const CardWrapper = styled.div` // 배경 color일 때 적용(이미지 같이 내려올 시, 이미지 우선) background-color: ${(props) => { if (props.$bgImg) return "transparent"; - if (props.$bg) { - if (props.$bg === "beige") return `${colors.beige[200]}`; - if (props.$bg === "purple") return `${colors.purple[200]}`; - if (props.$bg === "blue") return `${colors.blue[200]}`; - if (props.$bg === "green") return `${colors.green[200]}`; - } - return "#ffffff"; + const colorMap = { + beige: colors.beige[200], + purple: colors.purple[200], + blue: colors.blue[200], + green: colors.green[200], + }; + return colorMap[props.$bg] || "#ffffff"; }}; // 배경 이미지, null이면 color 적용 background-image: ${(props) => { if (props.$bgImg) { - return `linear-gradient(rgba(0,0,0,0.45), rgba(0,0,0,0.45)), url(${props.$bgImg})`; + return `linear-gradient(rgba(0,0,0,0.45), rgba(0,0,0,0.45)), url("${props.$bgImg}")`; + } + const patternMap = { + beige: bgPatternBeige, + purple: bgPatternPurple, + blue: bgPatternBlue, + green: bgPatternGreen, + }; + if (props.$bg && patternMap[props.$bg]) { + return `url("${patternMap[props.$bg]}")`; } return "none"; }}; + background-size: ${(props) => { + if (props.$bgImg) return "cover"; + if (props.$bg === "beige") return "130px"; + if (props.$bg) return "150px"; + return "cover"; + }}; + + background-position: ${(props) => { + if (props.$bgImg) return "center"; + if (props.$bg) return "bottom right"; + return "center"; + }}; + background-repeat: no-repeat; - background-position: center; - background-size: cover; border: 1px solid rgba(0, 0, 0, 0.1); border-radius: 16px; box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.08); padding: 30px 24px 20px; cursor: pointer; + + ${media.small` + background-size: ${(props) => { + if (props.$bgImg) return "cover"; + if (props.$bg === "beige") return "110px"; + if (props.$bg) return "130px"; + return "cover"; + }}; + `} `; export const CardImgWrapper = styled.div` From 18d966145fa43e6e5b5eb401c872cf09729fbc57 Mon Sep 17 00:00:00 2001 From: yeseung00 Date: Sat, 15 Nov 2025 04:00:12 +0900 Subject: [PATCH 62/91] =?UTF-8?q?Feat:=20reach=20text=20editor=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 89 +++++++++++++++++- package.json | 2 + src/components/message/reach-text-editor.jsx | 95 ++++++++++++++++++++ src/hooks/use-message-form.js | 55 ++++++++++++ src/pages/message-page.jsx | 63 ++++++------- 5 files changed, 263 insertions(+), 41 deletions(-) create mode 100644 src/components/message/reach-text-editor.jsx create mode 100644 src/hooks/use-message-form.js diff --git a/package-lock.json b/package-lock.json index 0982c14..8b81a7d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,8 +9,10 @@ "version": "0.0.0", "dependencies": { "axios": "^1.13.2", + "quill": "^2.0.3", "react": "^19.1.1", "react-dom": "^19.1.1", + "react-quill-new": "^3.6.0", "react-router": "^7.9.5", "styled-components": "^6.1.19" }, @@ -2101,6 +2103,12 @@ "node": ">=0.10.0" } }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -2108,6 +2116,12 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "license": "Apache-2.0" + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -2460,9 +2474,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "license": "MIT", "dependencies": { @@ -2559,6 +2573,25 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", + "license": "MIT" + }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", + "license": "MIT" + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -2708,6 +2741,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parchment": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/parchment/-/parchment-3.0.0.tgz", + "integrity": "sha512-HUrJFQ/StvgmXRcQ1ftY6VEZUq3jA2t9ncFN4F84J/vN0/FPpQF+8FKXb3l6fLces6q0uOHj6NJn+2xvZnxO6A==", + "license": "BSD-3-Clause" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -2821,6 +2860,35 @@ "node": ">=6" } }, + "node_modules/quill": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/quill/-/quill-2.0.3.tgz", + "integrity": "sha512-xEYQBqfYx/sfb33VJiKnSJp8ehloavImQ2A6564GAbqG55PGw1dAWUn1MUbQB62t0azawUS2CZZhWCjO8gRvTw==", + "license": "BSD-3-Clause", + "dependencies": { + "eventemitter3": "^5.0.1", + "lodash-es": "^4.17.21", + "parchment": "^3.0.0", + "quill-delta": "^5.1.0" + }, + "engines": { + "npm": ">=8.2.3" + } + }, + "node_modules/quill-delta": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-5.1.0.tgz", + "integrity": "sha512-X74oCeRI4/p0ucjb5Ma8adTXd9Scumz367kkMK5V/IatcX6A0vlgLgKbzXWy5nZmCGeNJm2oQX0d2Eqj+ZIlCA==", + "license": "MIT", + "dependencies": { + "fast-diff": "^1.3.0", + "lodash.clonedeep": "^4.5.0", + "lodash.isequal": "^4.5.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, "node_modules/react": { "version": "19.2.0", "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", @@ -2842,6 +2910,21 @@ "react": "^19.2.0" } }, + "node_modules/react-quill-new": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/react-quill-new/-/react-quill-new-3.6.0.tgz", + "integrity": "sha512-weU6YfB2+7Cujw5Hjgmi0aN/qJd3B6ADWrxgUJMp2MO3tEvKX5kfB0sg3P0UdOVfU0z8icsKFzlnEIpeW1mLhw==", + "license": "MIT", + "dependencies": { + "lodash-es": "^4.17.21", + "quill": "~2.0.2" + }, + "peerDependencies": { + "quill-delta": "^5.1.0", + "react": "^16 || ^17 || ^18 || ^19", + "react-dom": "^16 || ^17 || ^18 || ^19" + } + }, "node_modules/react-refresh": { "version": "0.18.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", diff --git a/package.json b/package.json index 8045356..fc182a8 100644 --- a/package.json +++ b/package.json @@ -11,8 +11,10 @@ }, "dependencies": { "axios": "^1.13.2", + "quill": "^2.0.3", "react": "^19.1.1", "react-dom": "^19.1.1", + "react-quill-new": "^3.6.0", "react-router": "^7.9.5", "styled-components": "^6.1.19" }, diff --git a/src/components/message/reach-text-editor.jsx b/src/components/message/reach-text-editor.jsx new file mode 100644 index 0000000..069e683 --- /dev/null +++ b/src/components/message/reach-text-editor.jsx @@ -0,0 +1,95 @@ +import React from "react"; +import ReactQuill from "react-quill-new"; +import "react-quill-new/dist/quill.snow.css"; + +import styled from "styled-components"; +import { font } from "@/styles/font"; +import { colors } from "@/styles/colors"; +import Quill from "quill"; + +const toolbar = Quill.import("modules/toolbar"); +const list = Quill.import("formats/list"); + +if (list) { + Quill.register(list, true); +} +if (toolbar) { +} + +const EditorContainer = styled.div` + min-height: 243px; + border-radius: 8px; + border: 1px solid #ccc; + overflow: hidden; + + .ql-toolbar.ql-snow { + background-color: #eee; + border: none; + border-bottom: 1px solid #ccc; + padding: 14px 16px; + line-height: 1; + + .ql-formats { + margin-right: 12px; + } + .ql-formats button, + .ql-formats select { + width: 24px; + height: 24px; + padding: 0; + margin-right: 8px; + } + } + + .ql-container.ql-snow { + border: none; + ${font.regular16}; + color: ${colors.gray[900]}; + } + + .ql-editor { + min-height: 200px; + padding: 16px; + } +`; + +function RichTextEditor({ value, onChange }) { + const modules = { + toolbar: [ + ["bold", "italic", "underline"], + [ + { align: "" }, + { align: "center" }, + { align: "right" }, + { align: "justify" }, + ], + [{ list: "ordered" }, { list: "bullet" }], + ["link", "image"], + ], + }; + + const formats = [ + "bold", + "italic", + "underline", + "align", + "list", + "bullet", + "link", + "image", + ]; + + return ( + + + + ); +} + +export default RichTextEditor; diff --git a/src/hooks/use-message-form.js b/src/hooks/use-message-form.js new file mode 100644 index 0000000..07a9250 --- /dev/null +++ b/src/hooks/use-message-form.js @@ -0,0 +1,55 @@ +import { useState } from "react"; +import useDropdown from "@/hooks/use-dropdown"; +import useFormInput from "@/hooks/use-from-input"; + +const RELATIONSHIP_OPTIONS = [ + { label: "지인", value: "지인" }, + { label: "친구", value: "친구" }, + { label: "동료", value: "동료" }, + { label: "가족", value: "가족" }, +]; +const FONT_OPTIONS = [{ label: "Noto Sans", value: "Noto Sans" }]; + +export function useMessageForm() { + const fromInput = useFormInput(""); + const relationshipDropdown = useDropdown(RELATIONSHIP_OPTIONS[0].value); + const fontDropdown = useDropdown(FONT_OPTIONS[0].value); + const [editorContent, setEditorContent] = useState(""); + + const isContentValid = + editorContent.trim().length > 0 && editorContent !== "


"; + const isFormValid = fromInput.value.trim().length > 0 && isContentValid; + + const handleSubmit = (e) => { + e.preventDefault(); + + if (!isFormValid) { + alert("모든 필수 항목을 입력해 주세요."); + return; + } + + const formData = { + from: fromInput.value, + relationship: relationshipDropdown.value, + content: editorContent, + font: fontDropdown.value, + }; + + console.log("✅ 폼 데이터 제출 성공:", formData); + alert(` 메시지가 성공적으로 생성되었습니다.`); + + // TODO: 서버 API 호출 로직 구현 + }; + + return { + fromInput, + relationshipDropdown, + fontDropdown, + editorContent, + setEditorContent, + isFormValid, + handleSubmit, + RELATIONSHIP_OPTIONS, + FONT_OPTIONS, + }; +} diff --git a/src/pages/message-page.jsx b/src/pages/message-page.jsx index 421d11c..153e0ae 100644 --- a/src/pages/message-page.jsx +++ b/src/pages/message-page.jsx @@ -5,17 +5,8 @@ import { font } from "@/styles/font"; import Button from "@/components/common/button"; import DropDown from "@/components/message/drop-down"; import FromInput from "@/components/message/from-input"; -import useDropdown from "@/hooks/use-dropdown"; -import useFormInput from "@/hooks/use-from-input"; - -const RELATIONSHIP_OPTIONS = [ - { label: "지인", value: "지인" }, - { label: "친구", value: "친구" }, - { label: "동료", value: "동료" }, - { label: "가족", value: "가족" }, -]; - -const FONT_OPTIONS = [{ label: "Noto Sans", value: "Noto Sans" }]; +import { useMessageForm } from "@/hooks/use-message-form"; +import RichTextEditor from "@/components/message/reach-text-editor"; const DEFAULT_ICON_URL = "/assets/default-user.svg"; const TEMP_IMAGE_URL = "/assets/temp-profile.jpg"; @@ -33,27 +24,27 @@ export const FormInputStyle = css` width: 100%; padding: 12px 16px; border-radius: 8px; - border: 1px solid #ccc; + border: 1px solid ${colors.gray[300]}; ${font.regular16}; outline: none; ${colors.gray[900]}; background-color: #fff; &:focus { - border-color: #555; + border-color: ${colors.gray[500]}; } `; export const PageContainer = styled.div` max-width: 720px; margin: 0 auto; - padding: 60px 24px; + padding: 47px 24px 60px 24px; `; export const MessageFormBox = styled.form` display: flex; flex-direction: column; - gap: 20px; + gap: 50px; `; export const FormField = styled.div` @@ -67,18 +58,20 @@ export const FormLabel = styled.label` line-height: 36px; letter-spacing: -0.01em; ${colors.gray[900]}; + margin: 0; + padding: 0; `; export const InputField = styled.input` ${FormInputStyle} &:focus { - border-color: #555; + border-color: ${colors.gray[500]}; } `; export const ErrorMessage = styled.p` - color: #dc3545; + color: ${colors.error}; font-size: 14px; margin-top: -8px; `; @@ -101,7 +94,7 @@ export const ProfileDefaultBox = styled.div` border-radius: 50%; overflow: hidden; flex-shrink: 0; - border: 1px solid #ccc; + border: 1px solid ${colors.gray[300]}; img { width: 100%; @@ -141,30 +134,27 @@ export const SelectableImageItem = styled.li` } `; -export const EditorPlaceholder = styled.div` - min-height: 200px; - padding: 16px; - border-radius: 8px; - border: 1px solid #ccc; - background-color: #f9f9f9; - font-size: 16px; - color: #777; -`; - const FullWidthButton = styled(Button)` width: 100%; margin-top: 20px; `; function MessagePage() { - const isFormValid = false; - const fromInput = useFormInput(""); - const relationshipDropdown = useDropdown("지인"); - const fontDropdown = useDropdown("Noto Sans"); + const { + fromInput, + relationshipDropdown, + fontDropdown, + editorContent, + setEditorContent, + isFormValid, + handleSubmit, + RELATIONSHIP_OPTIONS, + FONT_OPTIONS, + } = useMessageForm(); return ( - + {/* From. 입력 필드 */} From. @@ -180,7 +170,7 @@ function MessagePage() { /> - {/* 프로필 이미지 선택창 (생략) */} + {/* 프로필 이미지 선택창 */} 프로필 이미지 @@ -210,12 +200,9 @@ function MessagePage() { /> - {/* 내용 입력 (Rich Text Editor 사용) */} 내용을 입력해 주세요 - -

I am your reach text editor.

-
+
{/* 폰트 선택 드롭다운 */} From 52ada7741dd02fadf67d4298323651cfcd3b98be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A0=95=EC=9B=90?= Date: Sat, 15 Nov 2025 05:53:48 +0900 Subject: [PATCH 63/91] =?UTF-8?q?Feat:=20=EC=82=AD=EC=A0=9C=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EA=B5=AC=ED=98=84,=20=EC=9C=A0=EC=A0=80=EA=B0=80?= =?UTF-8?q?=20=EC=84=A0=ED=83=9D=ED=95=9C=20=EB=B0=B0=EA=B2=BD=ED=99=94?= =?UTF-8?q?=EB=A9=B4=20=EB=B0=98=EC=98=81,=20=EB=94=B0=EC=98=B4=ED=91=9C?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 22 + package.json | 1 + src/App.jsx | 8 +- src/api/client.js | 22 + src/api/rolling-page-api.js | 86 ++ src/components/common/global-layout.jsx | 4 +- src/components/common/modal-layout.jsx | 166 +-- src/components/common/toast-provider.jsx | 2 +- src/components/common/toast.jsx | 3 +- src/components/rolling/card-contents.jsx | 253 ++++- src/components/rolling/card-detail-modal.jsx | 137 +++ .../rolling/delete-confirm-modal.jsx | 52 + src/components/rolling/emoji-display-list.jsx | 47 +- src/components/rolling/emoji-dropdown.jsx | 208 ++-- .../rolling/emoji-picker-component.jsx | 123 +- .../rolling/header-action-buttons.jsx | 109 +- .../rolling/participant-section.jsx | 70 +- src/components/rolling/participant-stats.jsx | 41 +- src/components/rolling/profile-image-list.jsx | 49 +- .../rolling/profile-overflow-badge.jsx | 63 +- src/components/rolling/rolling-page-head.jsx | 135 +++ src/components/rolling/share-button-group.jsx | 138 ++- src/components/rolling/share-modal.jsx | 109 +- src/contexts/toast-context-state.jsx | 8 +- src/hooks/use-cards.js | 65 +- src/hooks/use-delete-actions.js | 68 ++ src/hooks/use-edit-mode.js | 14 + src/hooks/use-emoji-manager.js | 81 +- src/hooks/use-infinite-recipients.js | 81 ++ src/hooks/use-kakao-sdk.js | 79 +- src/hooks/use-profile-images.js | 71 +- src/hooks/use-reactions.js | 69 ++ src/hooks/use-recipients.js | 51 + src/hooks/use-share-actions.js | 107 +- src/main.jsx | 2 +- src/pages/main-page.jsx | 8 +- src/pages/message-page.jsx | 21 +- src/pages/post-page.jsx | 4 +- src/pages/rolling-page-edit.jsx | 58 - src/pages/rolling-page-head.jsx | 106 -- src/pages/rolling-page.jsx | 187 ++- src/pages/toast-test-page.jsx | 8 +- src/styles/global-style.js | 3 +- src/styles/head-nav-style.js | 34 +- src/styles/message-page.js | 3 +- src/styles/rolling-page-styles.js | 1008 +++++++++-------- 46 files changed, 2382 insertions(+), 1602 deletions(-) create mode 100644 src/api/client.js create mode 100644 src/api/rolling-page-api.js create mode 100644 src/components/rolling/card-detail-modal.jsx create mode 100644 src/components/rolling/delete-confirm-modal.jsx create mode 100644 src/components/rolling/rolling-page-head.jsx create mode 100644 src/hooks/use-delete-actions.js create mode 100644 src/hooks/use-edit-mode.js create mode 100644 src/hooks/use-infinite-recipients.js create mode 100644 src/hooks/use-reactions.js create mode 100644 src/hooks/use-recipients.js delete mode 100644 src/pages/rolling-page-edit.jsx delete mode 100644 src/pages/rolling-page-head.jsx diff --git a/package-lock.json b/package-lock.json index 1cb617d..f26ea30 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "emoji-picker-react": "^4.15.0", "react": "^19.1.1", "react-dom": "^19.1.1", + "react-infinite-scroll-component": "^6.1.0", "react-router": "^7.9.5", "styled-components": "^6.1.19" }, @@ -2864,6 +2865,18 @@ "react": "^19.2.0" } }, + "node_modules/react-infinite-scroll-component": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/react-infinite-scroll-component/-/react-infinite-scroll-component-6.1.0.tgz", + "integrity": "sha512-SQu5nCqy8DxQWpnUVLx7V7b7LcA37aM7tvoWjTLZp1dk6EJibM5/4EJKzOnl07/BsM1Y40sKLuqjCwwH/xV0TQ==", + "license": "MIT", + "dependencies": { + "throttle-debounce": "^2.1.0" + }, + "peerDependencies": { + "react": ">=16.0.0" + } + }, "node_modules/react-refresh": { "version": "0.18.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", @@ -3096,6 +3109,15 @@ "node": ">=8" } }, + "node_modules/throttle-debounce": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-2.3.0.tgz", + "integrity": "sha512-H7oLPV0P7+jgvrk+6mwwwBDmxTaxnu9HMXmloNLXwnNO0ZxZ31Orah2n8lU1eMPvsaowP2CX+USCgyovXfdOFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", diff --git a/package.json b/package.json index 4af7841..a88f007 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "emoji-picker-react": "^4.15.0", "react": "^19.1.1", "react-dom": "^19.1.1", + "react-infinite-scroll-component": "^6.1.0", "react-router": "^7.9.5", "styled-components": "^6.1.19" }, diff --git a/src/App.jsx b/src/App.jsx index 42e8d58..e02de75 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -18,7 +18,11 @@ function App() { } /> } /> {/* } /> */} - } /> + + {/* 롤링 페이퍼 뷰어/편집 모드 */} + } /> + } /> + } /> } /> } /> @@ -29,4 +33,4 @@ function App() { ); } -export default App; \ No newline at end of file +export default App; diff --git a/src/api/client.js b/src/api/client.js new file mode 100644 index 0000000..4dcc951 --- /dev/null +++ b/src/api/client.js @@ -0,0 +1,22 @@ +import axios from "axios"; + +// 기수-팀 번호 설정 (환경변수로 관리 가능) +const TEAM_CODE = "2-1"; // 추후 환경변수로 변경 가능 + +// API 기본 설정 +const BASE_URL = `https://rolling-api.vercel.app/${TEAM_CODE}`; + +/** + * API 클라이언트 + * 책임: axios 인스턴스 생성 및 기본 설정 + */ +const apiClient = axios.create({ + baseURL: BASE_URL, + timeout: 10000, + headers: { + "Content-Type": "application/json", + }, +}); + +export default apiClient; +export { TEAM_CODE }; diff --git a/src/api/rolling-page-api.js b/src/api/rolling-page-api.js new file mode 100644 index 0000000..e23567b --- /dev/null +++ b/src/api/rolling-page-api.js @@ -0,0 +1,86 @@ +import apiClient from "./client"; + +/** + * Recipients API 함수들 + * 책임: Recipients 관련 API 호출 + */ + +// 유저 상세 조회 + +export const getRecipientById = async (recipientId) => { + try { + const response = await apiClient.get(`/recipients/${recipientId}/`); + return response.data; + } catch (error) { + console.error(`Failed to fetch recipient ${recipientId}:`, error); + throw error; + } +}; + +// 롤링 페이퍼 전체 삭제 + +export const deleteRecipient = async (recipientId) => { + try { + const response = await apiClient.delete(`/recipients/${recipientId}/`); + return response.data; + } catch (error) { + console.error(`Failed to delete recipient ${recipientId}:`, error); + throw error; + } +}; + + +// 유저의 모든 리액션 조회 + +export const getReactions = async (recipientId, params = {}) => { + try { + const response = await apiClient.get(`/recipients/${recipientId}/reactions/`, { + params, + }); + return response.data; + } catch (error) { + console.error(`Failed to fetch reactions for recipient ${recipientId}:`, error); + throw error; + } +}; + + +// 수신자에게 리액션 추가/감소 + +export const addReaction = async (recipientId, data) => { + try { + const response = await apiClient.post(`/recipients/${recipientId}/reactions/`, data); + return response.data; + } catch (error) { + console.error(`Failed to add reaction to recipient ${recipientId}:`, error); + throw error; + } +}; + + +// 수신자의 메시지 목록 조회 + +export const getRecipientMessages = async (recipientId, { limit = 6, offset = 0 } = {}) => { + try { + const response = await apiClient.get(`/recipients/${recipientId}/messages/`, { + params: { limit, offset }, + }); + return response.data; + } catch (error) { + console.error(`Failed to fetch messages for recipient ${recipientId}:`, error); + throw error; + } +}; + +// 메시지 삭제 + +export const deleteMessage = async (messageId) => { + try { + const response = await apiClient.delete(`/messages/${messageId}/`); + return response.data; + } catch (error) { + console.error(`Failed to delete message ${messageId}:`, error); + throw error; + } +}; + diff --git a/src/components/common/global-layout.jsx b/src/components/common/global-layout.jsx index db2b1ed..3e783aa 100644 --- a/src/components/common/global-layout.jsx +++ b/src/components/common/global-layout.jsx @@ -5,9 +5,7 @@ const PAGES_WITH_BUTTON = ["main-page", "list-page"]; export default function GlobalLayout() { const location = useLocation(); - const showButton = PAGES_WITH_BUTTON.some((page) => - location.pathname.includes(page) - ); + const showButton = PAGES_WITH_BUTTON.some((page) => location.pathname.includes(page)); return ( <> diff --git a/src/components/common/modal-layout.jsx b/src/components/common/modal-layout.jsx index af54b7a..e6b032d 100644 --- a/src/components/common/modal-layout.jsx +++ b/src/components/common/modal-layout.jsx @@ -1,82 +1,84 @@ -import React from 'react'; -import styled from 'styled-components'; -import { colors } from '@/styles/colors'; -import { font } from '@/styles/font'; -import media from '@/styles/media'; - -const Overlay = styled.div` - position: fixed; - top: 0; - left: 0; - width: 100vw; - height: 100vh; - background: transparent; - z-index: 999; -`; - -const ModalContainer = styled.div` - background: white; - border-radius: 16px; - padding: 40px; - width: 480px; - box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15); - - ${media.medium` - width: 400px; - padding: 30px; - `} - - ${media.small` - width: 320px; - padding: 24px; - `} -`; - -const ModalTitle = styled.h2` - ${font.bold24} - color: ${colors.gray[900]}; - margin-bottom: 24px; - text-align: center; -`; - -const ModalContent = styled.div` - width: 100%; -`; - -const CloseButton = styled.button` - width: 100%; - margin-top: 16px; - padding: 6px; - background: transparent; - border: 1px solid ${colors.gray[300]}; - border-radius: 8px; - cursor: pointer; - ${font.regular16} - color: ${colors.gray[700]}; - transition: all 0.2s; - - &:hover { - background: ${colors.gray[50]}; - } -`; - -/** - * 공통 모달 레이아웃 컴포넌트 - * 책임: 모달의 기본 구조와 레이아웃 제공 - */ -export default function ModalLayout({ isOpen, onClose, title, children, showCloseButton = true }) { - if (!isOpen) return null; - - return ( - - e.stopPropagation()}> - {title && {title}} - {children} - {showCloseButton && ( - 닫기 - )} - - - ); -} - +import React from "react"; +import styled from "styled-components"; +import { colors } from "@/styles/colors"; +import { font } from "@/styles/font"; +import media from "@/styles/media"; + +const Overlay = styled.div` + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background: rgba(0, 0, 0, 0.6); + display: flex; + align-items: center; + justify-content: center; + z-index: 999; +`; + +const ModalContainer = styled.div` + background: white; + border-radius: 16px; + padding: 40px; + width: 600px; + max-height: 90vh; + overflow-y: auto; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15); + + ${media.medium` + width: 600px; + padding: 30px; + `} + + ${media.small` + width: 320px; + padding: 24px; + `} +`; + +const ModalTitle = styled.h2` + ${font.bold24} + color: ${colors.gray[900]}; + margin-bottom: 24px; + text-align: center; +`; + +const ModalContent = styled.div` + width: 100%; +`; + +const CloseButton = styled.button` + width: 100%; + margin-top: 16px; + padding: 6px; + background: transparent; + border: 1px solid ${colors.gray[300]}; + border-radius: 8px; + cursor: pointer; + ${font.regular16} + color: ${colors.gray[700]}; + transition: all 0.2s; + + &:hover { + background: ${colors.gray[50]}; + } +`; + +/** + * 공통 모달 레이아웃 컴포넌트 + * 책임: 모달의 기본 구조와 레이아웃 제공 + */ +export default function ModalLayout({ isOpen, onClose, title, children, showCloseButton = true }) { + if (!isOpen) return null; + + return ( + + e.stopPropagation()}> + {title && {title}} + {children} + {showCloseButton && 닫기} + + + ); +} diff --git a/src/components/common/toast-provider.jsx b/src/components/common/toast-provider.jsx index 662e0e2..13f4939 100644 --- a/src/components/common/toast-provider.jsx +++ b/src/components/common/toast-provider.jsx @@ -73,7 +73,7 @@ export function ToastProvider({ children }) { )} , - toastContainer + toastContainer, )} ); diff --git a/src/components/common/toast.jsx b/src/components/common/toast.jsx index 023441f..08a58a5 100644 --- a/src/components/common/toast.jsx +++ b/src/components/common/toast.jsx @@ -37,8 +37,7 @@ const ToastStyle = styled.div` color: white; padding: 19px 30px; border-radius: 8px; - animation: ${({ $isClosing }) => ($isClosing ? fadeOut : fadeIn)} 0.3s - ease-in-out forwards; + animation: ${({ $isClosing }) => ($isClosing ? fadeOut : fadeIn)} 0.3s ease-in-out forwards; ${font.regular16} button { diff --git a/src/components/rolling/card-contents.jsx b/src/components/rolling/card-contents.jsx index 4c1336b..c76a890 100644 --- a/src/components/rolling/card-contents.jsx +++ b/src/components/rolling/card-contents.jsx @@ -1,59 +1,194 @@ -import { - CardContainer, - Card, - CardEditButton, - CardContentContainer, - CardContentStatus, - CardContentStatusContainer, - CardContentStatusProfileImage, - CardContentStatusProfileName, - CardContentStatusRelationship, - CardContentText, - CardContentDate, - CardContentStatusProfileContainer, - CardContentDeleteButton, -} from "@/styles/rolling-page-styles"; -import { useState } from "react"; -import useCards from "@/hooks/use-cards"; - - -export default function CardContents({ maxVisible = 6 }) { - const [cards] = useState([ - { id: 1, name: '김철수', profileImageURL: 'https://via.placeholder.com/28', relationship: 'friend' }, - { id: 2, name: '이영희', profileImageURL: 'https://via.placeholder.com/28', relationship: 'family' }, - { id: 3, name: '박민수', profileImageURL: 'https://via.placeholder.com/28', relationship: 'colleague' }, - { id: 4, name: '최영희', profileImageURL: 'https://via.placeholder.com/28', relationship: 'acquaintance' }, - { id: 5, name: 'dsadsa', profileImageURL: 'https://via.placeholder.com/28', relationship: 'friend' }, - { id: 6, name: 'qweqwe', profileImageURL: 'https://via.placeholder.com/28', relationship: 'family' }, - { id: 7, name: 'zxczxc', profileImageURL: 'https://via.placeholder.com/28', relationship: 'colleague' }, - { id: 8, name: 'm,nmnb', profileImageURL: 'https://via.placeholder.com/28', relationship: 'acquaintance' } - ]); - const { visibleCards } = useCards(cards, maxVisible); - - return ( - <> - - - {visibleCards.map((card) => ( - - - - - - - From. {card.name} - {card.relationship} - - - - - sdasdsa - 2025.11.12 - - - - ))} - - - ); -} \ No newline at end of file +import React, { useEffect, useState } from "react"; +import { useNavigate } from "react-router"; +import InfiniteScroll from "react-infinite-scroll-component"; +import { + CardContainer, + Card, + CardEditButton, + CardContentContainer, + CardContentStatus, + CardContentStatusContainer, + CardContentStatusProfileImage, + CardContentStatusProfileName, + CardContentStatusRelationship, + CardContentText, + CardContentDate, + CardContentStatusProfileContainer, + CardContentDeleteButton, +} from "@/styles/rolling-page-styles"; +import { useInfiniteRecipientMessages } from "@/hooks/use-infinite-recipients"; +import { useDeleteActions } from "@/hooks/use-delete-actions"; +import CardDetailModal from "./card-detail-modal"; +import DeleteConfirmModal from "./delete-confirm-modal"; + +/** + * 카드 컨텐츠 컴포넌트 (무한 스크롤) + * 책임: 메시지 카드 목록 표시 및 무한 스크롤 처리 + * @param {number} recipientId - 수신자 ID + * @param {boolean} isEditMode - 편집 모드 여부 (true: 편집 가능, false: 뷰어) + */ +export default function CardContents({ recipientId, isEditMode = false }) { + const navigate = useNavigate(); + const { messages, loading, hasMore, fetchInitialData, fetchMoreData, refresh } = + useInfiniteRecipientMessages(recipientId, isEditMode); + + // 삭제 액션 훅 + const { handleDeleteMessage } = useDeleteActions(); + + // 모달 상태 관리 + const [selectedMessage, setSelectedMessage] = useState(null); + const [isDetailModalOpen, setIsDetailModalOpen] = useState(false); + const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); + const [messageToDelete, setMessageToDelete] = useState(null); + + // 초기 데이터 로드 + useEffect(() => { + fetchInitialData(); + }, [fetchInitialData]); + + // 카드 클릭 핸들러 + const handleCardClick = (message) => { + setSelectedMessage(message); + setIsDetailModalOpen(true); + }; + + const handleCardEditClick = () => { + navigate(`/post/${recipientId}/message`); + }; + + // 상세 모달 닫기 + const handleCloseDetailModal = () => { + setIsDetailModalOpen(false); + setSelectedMessage(null); + }; + + // 삭제 확인 모달 열기 + const handleOpenDeleteModal = (message) => { + setMessageToDelete(message); + setIsDeleteModalOpen(true); + }; + + // 삭제 확인 모달 닫기 + const handleCloseDeleteModal = () => { + setIsDeleteModalOpen(false); + setMessageToDelete(null); + }; + + // 메시지 삭제 실행 + const handleConfirmDelete = async () => { + if (!messageToDelete) return; + + const success = await handleDeleteMessage(messageToDelete.id, () => { + refresh(); // 목록 갱신 + }); + + if (success) { + handleCloseDeleteModal(); + } + }; + + const formatDate = (dateString) => { + const date = new Date(dateString); + return date.toLocaleDateString("ko-KR", { + year: "numeric", + month: "2-digit", + day: "2-digit", + }).replace(/\. /g, ".").replace(/\.$/, ""); // 마지막 점만 제거 + }; + + + // 관계 라벨 매핑 + const relationshipMap = { + 친구: "friend", + 가족: "family", + 동료: "colleague", + 지인: "acquaintance", + }; + + if (loading && messages.length === 0) { + return ( + +
로딩 중...
+
+ ); + } + + return ( + <> + 로딩 중...} + endMessage={ +

+ 모든 메시지를 확인했습니다 +

+ } + > + + {/* 뷰어 모드일 때만 카드 추가 버튼 표시 */} + {!isEditMode && ( + + handleCardEditClick(recipientId)} /> + + )} + + {messages.map((message) => ( + handleCardClick(message)}> + + + + + + + From. {message.sender} + + + {message.relationship} + + + + + {/* 편집 모드일 때만 카드 삭제 버튼 표시 */} + {isEditMode && ( + { + e.stopPropagation(); // 카드 클릭 이벤트 방지 + handleOpenDeleteModal(message); + }} + /> + )} + + {message.content} + {formatDate(message.createdAt)} + + + ))} + +
+ + {/* 카드 상세 모달 */} + < CardDetailModal + isOpen={isDetailModalOpen} + onClose={handleCloseDetailModal} + message={selectedMessage} + /> + + {/* 삭제 확인 모달 */} + < DeleteConfirmModal + isOpen={isDeleteModalOpen} + onClose={handleCloseDeleteModal} + onConfirm={handleConfirmDelete} + title="메시지 삭제" + message={`${messageToDelete?.sender}님의 메시지를 삭제하시겠습니까?` + } + /> + + ); +} diff --git a/src/components/rolling/card-detail-modal.jsx b/src/components/rolling/card-detail-modal.jsx new file mode 100644 index 0000000..b3e353c --- /dev/null +++ b/src/components/rolling/card-detail-modal.jsx @@ -0,0 +1,137 @@ +import React from "react"; +import styled from "styled-components"; +import ModalLayout from "@/components/common/modal-layout"; +import Button from "@/components/common/button"; +import { colors } from "@/styles/colors"; +import { font } from "@/styles/font"; + +const ProfileSection = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + gap: 16px; + padding-bottom: 20px; + border-bottom: 1px solid ${colors.gray[200]}; +`; + +const ProfileContainer = styled.div` + display: flex; + align-items: center; + gap: 16px; + +`; +const ProfileImage = styled.img` + width: 56px; + height: 56px; + border-radius: 100px; + border: 1px solid ${colors.gray[300]}; +`; + +const ProfileInfo = styled.div` + display: flex; + flex-direction: column; + gap: 6px; +`; + +const ProfileName = styled.div` + ${font.regular16} + color: ${colors.gray[900]}; +`; + +const RelationshipBadge = styled.div` + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0 8px; + height: 20px; + border-radius: 4px; + ${font.regular14} + width: fit-content; +`; + +const MessageContent = styled.div` + ${font.regular18} + color: #5A5A5A; + line-height: 1.6; + white-space: pre-wrap; + word-break: break-word; + overflow-y: auto; + margin-bottom: 24px; + padding-top: 16px; +`; + +const MessageDate = styled.div` + ${font.regular14} + color: ${colors.gray[400]}; +`; + +const ButtonWrapper = styled.div` + display: flex; + justify-content: center; +`; + +// 관계별 배경색 및 텍스트 색상 +const relationshipColors = { + 친구: { bg: colors.blue[100], text: colors.blue[500] }, + 가족: { bg: colors.green[100], text: colors.green[500] }, + 동료: { bg: colors.purple[100], text: colors.purple[600] }, + 지인: { bg: colors.beige[100], text: colors.beige[500] }, +}; + +/** + * 카드 상세 모달 컴포넌트 + * 책임: 메시지 전체 내용을 모달로 표시 + */ +export default function CardDetailModal({ isOpen, onClose, message }) { + if (!message) return null; + + const relationshipStyle = relationshipColors[message.relationship] || { + bg: colors.gray[100], + text: colors.gray[500], + }; + + // 날짜 포맷팅 + const formatDate = (dateString) => { + const date = new Date(dateString); + return date.toLocaleDateString("ko-KR", { + year: "numeric", + month: "long", + day: "numeric", + }); + }; + + return ( + + + + + + + From. {message.sender} + + + {message.relationship} + + + + {formatDate(message.createdAt)} + + + + {message.content} + + + + + + + ); +} + diff --git a/src/components/rolling/delete-confirm-modal.jsx b/src/components/rolling/delete-confirm-modal.jsx new file mode 100644 index 0000000..f0b2887 --- /dev/null +++ b/src/components/rolling/delete-confirm-modal.jsx @@ -0,0 +1,52 @@ +import React from "react"; +import styled from "styled-components"; +import ModalLayout from "@/components/common/modal-layout"; +import Button from "@/components/common/button"; +import { colors } from "@/styles/colors"; +import { font } from "@/styles/font"; + +const ModalContent = styled.div` + text-align: center; +`; + +const ModalMessage = styled.p` + ${font.regular18} + color: ${colors.gray[700]}; + margin-bottom: 32px; + line-height: 1.6; + white-space: pre-wrap; +`; + +const ButtonGroup = styled.div` + display: flex; + gap: 12px; + justify-content: center; +`; + +/** + * 삭제 확인 모달 컴포넌트 + * 책임: 삭제 전 사용자 확인 받기 + */ +export default function DeleteConfirmModal({ isOpen, onClose, onConfirm, title, message }) { + const handleConfirm = () => { + onConfirm(); + onClose(); + }; + + return ( + + + {message} + + + + + + + ); +} + diff --git a/src/components/rolling/emoji-display-list.jsx b/src/components/rolling/emoji-display-list.jsx index 3e1492f..2aa45fe 100644 --- a/src/components/rolling/emoji-display-list.jsx +++ b/src/components/rolling/emoji-display-list.jsx @@ -1,24 +1,23 @@ -import React from 'react'; -import { - RollingHeaderImojiIconContainer, - RollingHeaderImojiText, - RollingHeaderImojiIcon, -} from '@/styles/rolling-page-styles'; - -/** - * 이모지 표시 리스트 컴포넌트 - * 책임: 상위 N개의 이모지를 화면에 표시 - */ -export default function EmojiDisplayList({ emojis }) { - return ( - <> - {emojis.map((emojiData, index) => ( - - {emojiData.emoji} - {emojiData.count} - - ))} - - ); -} - +import React from "react"; +import { + RollingHeaderEmojiIconContainer, + RollingHeaderEmojiText, + RollingHeaderEmojiIcon, +} from "@/styles/rolling-page-styles"; + +/** + * 이모지 표시 리스트 컴포넌트 + * 책임: 상위 N개의 이모지를 화면에 표시 + */ +export default function EmojiDisplayList({ emojis }) { + return ( + <> + {emojis.map((emojiData, index) => ( + + {emojiData.emoji} + {emojiData.count} + + ))} + + ); +} diff --git a/src/components/rolling/emoji-dropdown.jsx b/src/components/rolling/emoji-dropdown.jsx index b5bcc45..6b73e43 100644 --- a/src/components/rolling/emoji-dropdown.jsx +++ b/src/components/rolling/emoji-dropdown.jsx @@ -1,108 +1,100 @@ -import React from 'react'; -import styled from 'styled-components'; -import { colors } from '@/styles/colors'; -import media from '@/styles/media'; -import { font } from '@/styles/font'; -import { RollingHeaderArrowDown } from '@/styles/rolling-page-styles'; - -const EmojiDropdownContainer = styled.div` - position: relative; - display: inline-block; -`; - -const EmojiDropdownWrapper = styled.div` - position: fixed; - transform: translate(-80%, 10%); - z-index: 1000; - background: white; - border-radius: 8px; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); - border: 1px solid ${colors.gray[300]}; - padding: 24px; - width: auto; - max-height: 300px; - overflow-y: auto; -`; - -const EmojiDropdownGrid = styled.div` - display: grid; - grid-template-columns: repeat(4, 1fr); - gap: 8px; - ${media.medium` - grid-template-columns: repeat(3, 1fr); - `} - ${media.small` - grid-template-columns: repeat(3, 1fr); - `} -`; - -const EmojiDropdownItem = styled.div` - display: flex; - align-items: center; - justify-content: center; - width: auto; - height: auto; - padding: 8px 12px; - text-align: center; - border-radius: 32px; - background: rgba(153, 153, 153, 1); - gap: 2px; - - ${media.small` - padding: 4px 8px; - `} -`; - -const EmojiDropdownIcon = styled.div``; - -const EmojiDropdownCount = styled.span` - ${font.regular16} - color: rgba(255, 255, 255, 1); -`; - -const Overlay = styled.div` - position: fixed; - top: 0; - left: 0; - width: 100vw; - height: 100vh; - background: transparent; - z-index: 999; -`; - -/** - * 이모지 드롭다운 컴포넌트 - * 책임: 모든 이모지 목록을 드롭다운 형태로 표시 - */ -export default function EmojiDropdown({ - emojis, - isOpen, - onToggle, - onClose, - arrowDownIcon -}) { - return ( - - - {isOpen && ( - <> - - - - {emojis.map((emojiData, index) => ( - - {emojiData.emoji} - {emojiData.count} - - ))} - - - - )} - - ); -} - +import React from "react"; +import styled from "styled-components"; +import { colors } from "@/styles/colors"; +import media from "@/styles/media"; +import { font } from "@/styles/font"; +import { RollingHeaderArrowDown } from "@/styles/rolling-page-styles"; + +const EmojiDropdownContainer = styled.div` + position: relative; + display: inline-block; +`; + +const EmojiDropdownWrapper = styled.div` + position: fixed; + transform: translate(-80%, 10%); + z-index: 1000; + background: white; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + border: 1px solid ${colors.gray[300]}; + padding: 24px; + width: auto; + max-height: 300px; + overflow-y: auto; +`; + +const EmojiDropdownGrid = styled.div` + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 8px; + ${media.medium` + grid-template-columns: repeat(3, 1fr); + `} + ${media.small` + grid-template-columns: repeat(3, 1fr); + `} +`; + +const EmojiDropdownItem = styled.div` + display: flex; + align-items: center; + justify-content: center; + width: auto; + height: auto; + padding: 8px 12px; + text-align: center; + border-radius: 32px; + background: rgba(153, 153, 153, 1); + gap: 2px; + + ${media.small` + padding: 4px 8px; + `} +`; + +const EmojiDropdownIcon = styled.div``; + +const EmojiDropdownCount = styled.span` + ${font.regular16} + color: rgba(255, 255, 255, 1); +`; + +const Overlay = styled.div` + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background: transparent; + z-index: 999; +`; + +/** + * 이모지 드롭다운 컴포넌트 + * 책임: 이모지 목록을 드롭다운 형태로 표시 (API에서 이미 정렬된 상위 8개) + */ +export default function EmojiDropdown({ emojis, isOpen, onToggle, onClose, arrowDownIcon }) { + // API에서 이미 카운트 순으로 정렬되어 최대 8개만 제공됨 + const topEmojis = emojis; + return ( + + + {isOpen && ( + <> + + + + {topEmojis.map((emojiData, index) => ( + + {emojiData.emoji} + {emojiData.count} + + ))} + + + + )} + + ); +} diff --git a/src/components/rolling/emoji-picker-component.jsx b/src/components/rolling/emoji-picker-component.jsx index fc13037..ad9d4b3 100644 --- a/src/components/rolling/emoji-picker-component.jsx +++ b/src/components/rolling/emoji-picker-component.jsx @@ -1,62 +1,61 @@ -import React from 'react'; -import EmojiPicker from 'emoji-picker-react'; -import styled from 'styled-components'; -import { colors } from '@/styles/colors'; - -const EmojiPickerContainer = styled.div` - position: relative; - display: inline-block; -`; - -const EmojiPickerWrapper = styled.div` - position: fixed; - transform: translate(-60%, 2%); - z-index: 1000; - background: white; - border-radius: 8px; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); - border: 1px solid ${colors.gray[300]}; -`; - -const Overlay = styled.div` - position: fixed; - top: 0; - left: 0; - width: 100vw; - height: 100vh; - background: transparent; - z-index: 999; -`; - -/** - * 이모지 선택기 컴포넌트 - * 책임: 이모지 피커 UI 렌더링 및 이모지 선택 이벤트 처리 - */ -export default function EmojiPickerComponent({ isOpen, onClose, onEmojiSelect, children }) { - const handleEmojiClick = (emojiData) => { - onEmojiSelect(emojiData.emoji); - onClose(); - }; - - return ( - - {children} - {isOpen && ( - <> - - - - - - )} - - ); -} - +import React from "react"; +import EmojiPicker from "emoji-picker-react"; +import styled from "styled-components"; +import { colors } from "@/styles/colors"; + +const EmojiPickerContainer = styled.div` + position: relative; + display: inline-block; +`; + +const EmojiPickerWrapper = styled.div` + position: fixed; + transform: translate(-60%, 2%); + z-index: 1000; + background: white; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + border: 1px solid ${colors.gray[300]}; +`; + +const Overlay = styled.div` + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background: transparent; + z-index: 999; +`; + +/** + * 이모지 선택기 컴포넌트 + * 책임: 이모지 피커 UI 렌더링 및 이모지 선택 이벤트 처리 + */ +export default function EmojiPickerComponent({ isOpen, onClose, onEmojiSelect, children }) { + const handleEmojiClick = (emojiData) => { + onEmojiSelect(emojiData.emoji); + onClose(); + }; + + return ( + + {children} + {isOpen && ( + <> + + + + + + )} + + ); +} diff --git a/src/components/rolling/header-action-buttons.jsx b/src/components/rolling/header-action-buttons.jsx index b00531d..49b5695 100644 --- a/src/components/rolling/header-action-buttons.jsx +++ b/src/components/rolling/header-action-buttons.jsx @@ -1,55 +1,54 @@ -import React from 'react'; -import styled from 'styled-components'; -import EmojiPickerComponent from './emoji-picker-component'; -import { - RollingHeaderImojiEditButtonContainer, - RollingHeaderImojiEditButton, - RollingHeaderImojiEditButtonIcon, - RollingHeaderImojiEditButtonText, - PerpendicularLineSecond, - RollingHeaderLinkShareButton, -} from '@/styles/rolling-page-styles'; - -const ShareButtonWrapper = styled.div` - position: relative; -`; - -/** - * 헤더 액션 버튼 컴포넌트 - * 책임: 이모지 추가 버튼과 공유 버튼 렌더링 - */ -export default function HeaderActionButtons({ - isEmojiPickerOpen, - onToggleEmojiPicker, - onCloseEmojiPicker, - onEmojiSelect, - onShareClick, - addEmojiIcon, - shareIcon, - shareModalComponent, // ShareModal 컴포넌트를 props로 받음 -}) { - return ( - - - - - 추가 - - - - - - {shareModalComponent} - - - ); -} - +import React from "react"; +import styled from "styled-components"; +import EmojiPickerComponent from "./emoji-picker-component"; +import { + RollingHeaderEmojiEditButtonContainer, + RollingHeaderEmojiEditButton, + RollingHeaderEmojiEditButtonIcon, + RollingHeaderEmojiEditButtonText, + PerpendicularLineSecond, + RollingHeaderLinkShareButton, +} from "@/styles/rolling-page-styles"; + +const ShareButtonWrapper = styled.div` + position: relative; +`; + +/** + * 헤더 액션 버튼 컴포넌트 + * 책임: 이모지 추가 버튼과 공유 버튼 렌더링 + */ +export default function HeaderActionButtons({ + isEmojiPickerOpen, + onToggleEmojiPicker, + onCloseEmojiPicker, + onEmojiSelect, + onShareClick, + addEmojiIcon, + shareIcon, + shareModalComponent, // ShareModal 컴포넌트를 props로 받음 +}) { + return ( + + + + + 추가 + + + + + + {shareModalComponent} + + + ); +} diff --git a/src/components/rolling/participant-section.jsx b/src/components/rolling/participant-section.jsx index 7d27ee6..4dbb7b3 100644 --- a/src/components/rolling/participant-section.jsx +++ b/src/components/rolling/participant-section.jsx @@ -1,34 +1,36 @@ -import React from 'react'; -import { - RollingHeaderUserPeopleContainer, - RollingHeaderUserPeopleImages, -} from '@/styles/rolling-page-styles'; -import ProfileImageList from './profile-image-list'; -import ProfileOverflowBadge from './profile-overflow-badge'; -import ParticipantStats from './participant-stats'; -import useProfileImages from '@/hooks/use-profile-images'; - -/** - * 참여자 섹션 컴포넌트 - * 책임: 프로필 이미지와 참여자 통계를 조합하여 표시 - */ -export default function ParticipantSection({ profiles, maxVisible = 3 }) { - const { visibleProfiles, overflowCount, totalCount, hasOverflow } = - useProfileImages(profiles, maxVisible); - - return ( - - - {/* 보이는 프로필 이미지들 */} - - - {/* 오버플로우 뱃지 (+N) */} - {hasOverflow && } - - - {/* 참여자 통계 텍스트 */} - - - ); -} - +import React from "react"; +import { + RollingHeaderUserPeopleContainer, + RollingHeaderUserPeopleImages, +} from "@/styles/rolling-page-styles"; +import ProfileImageList from "./profile-image-list"; +import ProfileOverflowBadge from "./profile-overflow-badge"; +import ParticipantStats from "./participant-stats"; +import useProfileImages from "@/hooks/use-profile-images"; + +/** + * 참여자 섹션 컴포넌트 + * 책임: 프로필 이미지와 참여자 통계를 조합하여 표시 + * @param {Array} profiles - 표시할 프로필 목록 + * @param {number} totalCount - 전체 참여자 수 (messageCount) + * @param {number} maxVisible - 최대 표시 개수 + */ +export default function ParticipantSection({ profiles, totalCount, maxVisible = 3 }) { + const { overflowCount, hasOverflow } = useProfileImages(totalCount, profiles, maxVisible); + + + return ( + + + {/* 보이는 프로필 이미지들 */} + + + {/* 오버플로우 뱃지 (+N) */} + {hasOverflow && } + + + {/* 참여자 통계 텍스트 - API의 messageCount 사용 */} + + + ); +} diff --git a/src/components/rolling/participant-stats.jsx b/src/components/rolling/participant-stats.jsx index e52c89c..39c2b56 100644 --- a/src/components/rolling/participant-stats.jsx +++ b/src/components/rolling/participant-stats.jsx @@ -1,23 +1,18 @@ -import React from 'react'; -import { RollingHeaderUserPeopleState } from '@/styles/rolling-page-styles'; - -/** - * 참여자 통계 컴포넌트 - * 책임: 참여자 수 텍스트 표시 - */ -export default function ParticipantStats({ count }) { - if (count === 0) { - return ( - - 아직 작성한 사람이 없어요 - - ); - } - - return ( - - {count}명이 작성했어요! - - ); -} - +import React from "react"; +import { RollingHeaderUserPeopleState } from "@/styles/rolling-page-styles"; + +/** + * 참여자 통계 컴포넌트 + * 책임: 참여자 수 텍스트 표시 + */ +export default function ParticipantStats({ count }) { + if (count === 0) { + return 아직 작성한 사람이 없어요; + } + + return ( + + {count}명이 작성했어요! + + ); +} diff --git a/src/components/rolling/profile-image-list.jsx b/src/components/rolling/profile-image-list.jsx index 22e5551..a4e5921 100644 --- a/src/components/rolling/profile-image-list.jsx +++ b/src/components/rolling/profile-image-list.jsx @@ -1,25 +1,24 @@ -import React from 'react'; -import { RollingHeaderUserPeopleImage } from '@/styles/rolling-page-styles'; - -/** - * 프로필 이미지 리스트 컴포넌트 - * 책임: 프로필 이미지들을 렌더링 (래퍼 없이 순수 이미지만) - */ -export default function ProfileImageList({ profiles }) { - if (!profiles || profiles.length === 0) { - return null; - } - - return ( - <> - {profiles.map((profile, index) => ( - - ))} - - ); -} - +import React from "react"; +import { RollingHeaderUserPeopleImage } from "@/styles/rolling-page-styles"; + +/** + * 프로필 이미지 리스트 컴포넌트 + * 책임: 프로필 이미지들을 렌더링 (래퍼 없이 순수 이미지만) + */ +export default function ProfileImageList({ profiles }) { + if (!profiles || profiles.length === 0) { + return null; + } + + return ( + <> + {profiles.map((profile, index) => ( + + ))} + + ); +} diff --git a/src/components/rolling/profile-overflow-badge.jsx b/src/components/rolling/profile-overflow-badge.jsx index 2fe5249..9eb2d39 100644 --- a/src/components/rolling/profile-overflow-badge.jsx +++ b/src/components/rolling/profile-overflow-badge.jsx @@ -1,32 +1,31 @@ -import React from 'react'; -import styled from 'styled-components'; -import { colors } from '@/styles/colors'; -import { font } from '@/styles/font'; - -const OverflowBadge = styled.div` - width: 28px; - height: 28px; - border-radius: 140px; - border: 1.4px solid ${colors.gray[900]}; - background: ${colors.gray[200]}; - display: flex; - align-items: center; - justify-content: center; - position: relative; - margin-left: -10px; - ${font.regular12} - color: ${colors.gray[700]}; -`; - -/** - * 프로필 오버플로우 뱃지 컴포넌트 - * 책임: 추가 인원 수를 표시 (+N) - */ -export default function ProfileOverflowBadge({ count }) { - if (count <= 0) { - return null; - } - - return +{count}; -} - +import React from "react"; +import styled from "styled-components"; +import { colors } from "@/styles/colors"; +import { font } from "@/styles/font"; + +const OverflowBadge = styled.div` + width: 28px; + height: 28px; + border-radius: 140px; + border: 1.4px solid ${colors.gray[300]}; + background: #fff; + display: flex; + align-items: center; + justify-content: center; + position: relative; + margin-left: -10px; + ${font.regular12} + color: ${colors.gray[700]}; +`; + +/** + * 프로필 오버플로우 뱃지 컴포넌트 + * 책임: 추가 인원 수를 표시 (+N) + */ +export default function ProfileOverflowBadge({ count }) { + if (count <= 0) { + return null; + } + + return +{count}; +} diff --git a/src/components/rolling/rolling-page-head.jsx b/src/components/rolling/rolling-page-head.jsx new file mode 100644 index 0000000..90a6431 --- /dev/null +++ b/src/components/rolling/rolling-page-head.jsx @@ -0,0 +1,135 @@ +import React, { useState, useEffect } from "react"; +import ShareModal from "@/components/rolling/share-modal"; +import EmojiDisplayList from "@/components/rolling/emoji-display-list"; +import EmojiDropdown from "@/components/rolling/emoji-dropdown"; +import HeaderActionButtons from "@/components/rolling/header-action-buttons"; +import useEmojiManager from "@/hooks/use-emoji-manager"; +import { useReactions } from "@/hooks/use-reactions"; +import { RollingHeaderEmojiContainer } from "@/styles/rolling-page-styles"; + +/** + * 롤링 페이지 헤더 컴포넌트 + * 책임: 전체 헤더 구성 요소 조합 및 상태 관리 + */ +export default function RollingPageHeader({ + recipientId, + topReactions = [], + ArrowDownIcon, + AddEmojiIcon, + ShareIcon, +}) { + // UI 상태 관리 + const [isEmojiPickerOpen, setIsEmojiPickerOpen] = useState(false); + const [isEmojiDropdownOpen, setIsEmojiDropdownOpen] = useState(false); + const [isShareModalOpen, setIsShareModalOpen] = useState(false); + const [allReactions, setAllReactions] = useState([]); + + // API의 topReactions를 이모지 형태로 변환 + const initialEmojis = topReactions.map((reaction) => ({ + emoji: reaction.emoji, + count: reaction.count, + })); + + const { handleEmojiSelect, getSortedEmojis, getTopEmojis } = useEmojiManager(initialEmojis); + + // 리액션 API 훅 + const { reactions, fetchReactions, toggleReaction } = useReactions(recipientId); + + // 드롭다운 열릴 때 전체 리액션 불러오기 + useEffect(() => { + if (isEmojiDropdownOpen && reactions.length === 0) { + fetchReactions(); + } + }, [isEmojiDropdownOpen, reactions.length, fetchReactions]); + + // 전체 리액션 데이터 변환 + useEffect(() => { + if (reactions.length > 0) { + const converted = reactions.map((r) => ({ + emoji: r.emoji, + count: r.count, + })); + setAllReactions(converted); + } + }, [reactions]); + + // 이모지 선택 시 API 호출 + const handleEmojiAdd = async (emoji) => { + try { + await toggleReaction(emoji, "increase"); + handleEmojiSelect(emoji); + } catch (err) { + console.error("이모지 추가 실패", err); + } + }; + + // 이모지 피커 핸들러 + const toggleEmojiPicker = () => { + setIsEmojiPickerOpen(!isEmojiPickerOpen); + }; + + const closeEmojiPicker = () => { + setIsEmojiPickerOpen(false); + }; + + // 이모지 드롭다운 핸들러 + const toggleEmojiDropdown = () => { + setIsEmojiDropdownOpen(!isEmojiDropdownOpen); + }; + + const closeEmojiDropdown = () => { + setIsEmojiDropdownOpen(false); + }; + + // 공유 모달 핸들러 + const openShareModal = () => { + setIsShareModalOpen(true); + }; + + const closeShareModal = () => { + setIsShareModalOpen(false); + }; + + // 정렬된 이모지 및 상위 3개 추출 + const sortedEmojis = getSortedEmojis(); + const topThreeEmojis = getTopEmojis(3); + + // 드롭다운에 표시할 데이터: allReactions가 있으면 사용, 없으면 sortedEmojis 사용 + const dropdownEmojis = allReactions.length > 0 ? allReactions : sortedEmojis; + + // 현재 페이지 URL + const currentUrl = window.location.href; + + return ( + + {/* 상위 3개 이모지 표시 */} +
+ + {dropdownEmojis.length > 0 && ( + + )} +
+ + + {/* 이모지 추가 및 공유 버튼 */} + + } + /> +
+ ); +} diff --git a/src/components/rolling/share-button-group.jsx b/src/components/rolling/share-button-group.jsx index dddc833..b6c30c9 100644 --- a/src/components/rolling/share-button-group.jsx +++ b/src/components/rolling/share-button-group.jsx @@ -1,70 +1,68 @@ -import React from 'react'; -import styled from 'styled-components'; -import { colors } from '@/styles/colors'; -import { font } from '@/styles/font'; - -const ButtonGroup = styled.div` - position: absolute; - top: calc(100% + 8px); - right: 0; - width: 140px; - height: auto; - display: flex; - flex-direction: column; - background: white; - border-radius: 8px; - border: 1px solid ${colors.gray[300]}; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); - z-index: 1001; - padding: 10px 0px; -`; - -const ShareButton = styled.button` - display: flex; - align-items: center; - justify-content: center; - gap: 12px; - background: transparent; - width: 100%; - height: 50px; - border: none; - cursor: pointer; - transition: all 0.2s; - ${font.regular16} - color: ${colors.gray[900]}; - - &:hover { - background: ${colors.gray[200]}; - border-color: ${colors.gray[400]}; - - } - - &:active { - transform: scale(0.98); - } -`; - -const KakaoButton = styled(ShareButton)` - background: #fee500; - border-color: #fee500; - color: #000000; - - &:hover { - background: #fdd835; - border-color: #fdd835; - } -`; - -/** - * 공유 버튼 그룹 컴포넌트 - * 책임: 공유 방법별 버튼 UI 렌더링 - */ -export default function ShareButtonGroup({ onKakaoShare, onCopyUrl }) { - return ( - - 카카오톡 공유 - URL 복사 - - ); -} - +import React from "react"; +import styled from "styled-components"; +import { colors } from "@/styles/colors"; +import { font } from "@/styles/font"; + +const ButtonGroup = styled.div` + position: absolute; + top: calc(100% + 8px); + right: 0; + width: 140px; + height: auto; + display: flex; + flex-direction: column; + background: white; + border-radius: 8px; + border: 1px solid ${colors.gray[300]}; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + z-index: 1001; + padding: 10px 0px; +`; + +const ShareButton = styled.button` + display: flex; + align-items: center; + justify-content: center; + gap: 12px; + background: transparent; + width: 100%; + height: 50px; + border: none; + cursor: pointer; + transition: all 0.2s; + ${font.regular16} + color: ${colors.gray[900]}; + + &:hover { + background: ${colors.gray[200]}; + border-color: ${colors.gray[400]}; + } + + &:active { + transform: scale(0.98); + } +`; + +const KakaoButton = styled(ShareButton)` + background: #fee500; + border-color: #fee500; + color: #000000; + + &:hover { + background: #fdd835; + border-color: #fdd835; + } +`; + +/** + * 공유 버튼 그룹 컴포넌트 + * 책임: 공유 방법별 버튼 UI 렌더링 + */ +export default function ShareButtonGroup({ onKakaoShare, onCopyUrl }) { + return ( + + 카카오톡 공유 + URL 복사 + + ); +} diff --git a/src/components/rolling/share-modal.jsx b/src/components/rolling/share-modal.jsx index a1a18dc..74c5fd0 100644 --- a/src/components/rolling/share-modal.jsx +++ b/src/components/rolling/share-modal.jsx @@ -1,55 +1,54 @@ -import React from 'react'; -import styled from 'styled-components'; - -import ShareButtonGroup from './share-button-group'; -import useKakaoSdk from '@/hooks/use-kakao-sdk'; -import useShareActions from '@/hooks/use-share-actions'; - -const Overlay = styled.div` - position: fixed; - top: 0; - left: 0; - width: 100vw; - height: 100vh; - background: transparent; - z-index: 999; -`; - -/** - * 공유 모달 컴포넌트 - * 책임: 공유 모달 UI 및 공유 액션 연결 - */ -export default function ShareModal({ isOpen, onClose, shareUrl }) { - // 카카오 SDK 초기화 - useKakaoSdk(); - - // 공유 기능 훅 - const { copyToClipboard, shareToKakao } = useShareActions(); - - // URL 복사 핸들러 - const handleCopyUrl = async () => { - const success = await copyToClipboard(shareUrl); - if (success) { - onClose(); - } - }; - - // 카카오톡 공유 핸들러 - const handleKakaoShare = () => { - shareToKakao(shareUrl); - }; - - if (!isOpen) return null; - - return ( - <> - - e.stopPropagation()} - onKakaoShare={handleKakaoShare} - onCopyUrl={handleCopyUrl} - /> - - ); -} - +import React from "react"; +import styled from "styled-components"; + +import ShareButtonGroup from "./share-button-group"; +import useKakaoSdk from "@/hooks/use-kakao-sdk"; +import useShareActions from "@/hooks/use-share-actions"; + +const Overlay = styled.div` + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background: transparent; + z-index: 999; +`; + +/** + * 공유 모달 컴포넌트 + * 책임: 공유 모달 UI 및 공유 액션 연결 + */ +export default function ShareModal({ isOpen, onClose, shareUrl }) { + // 카카오 SDK 초기화 + useKakaoSdk(); + + // 공유 기능 훅 + const { copyToClipboard, shareToKakao } = useShareActions(); + + // URL 복사 핸들러 + const handleCopyUrl = async () => { + const success = await copyToClipboard(shareUrl); + if (success) { + onClose(); + } + }; + + // 카카오톡 공유 핸들러 + const handleKakaoShare = () => { + shareToKakao(shareUrl); + }; + + if (!isOpen) return null; + + return ( + <> + + e.stopPropagation()} + onKakaoShare={handleKakaoShare} + onCopyUrl={handleCopyUrl} + /> + + ); +} diff --git a/src/contexts/toast-context-state.jsx b/src/contexts/toast-context-state.jsx index a5eb1e9..ab40623 100644 --- a/src/contexts/toast-context-state.jsx +++ b/src/contexts/toast-context-state.jsx @@ -1,5 +1,3 @@ -import React, { createContext } from 'react'; - -export const ToastContext = createContext(); - - +import React, { createContext } from "react"; + +export const ToastContext = createContext(); diff --git a/src/hooks/use-cards.js b/src/hooks/use-cards.js index dcf0bfc..f9885aa 100644 --- a/src/hooks/use-cards.js +++ b/src/hooks/use-cards.js @@ -1,36 +1,29 @@ -import { useMemo } from 'react'; - - -/** - * 카드 섹션 컴포넌트 - * 책임: 카드 데이터를 처리하고 표시 - */ -export default function useCards(cards, maxVisible = 6) { - const processedData = useMemo(() => { - if (!cards || cards.length === 0) { - return { - visibleCards: [], - overflowCount: 0, - totalCount: 0, - hasOverflow: false, - }; - } - - const totalCount = cards.length; - const hasOverflow = totalCount > maxVisible; - - const visibleCount = hasOverflow ? maxVisible - 1 : maxVisible; - const visibleCards = cards.slice(0, visibleCount); - const overflowCount = hasOverflow ? totalCount - visibleCount : 0; - - return { - visibleCards, - overflowCount, - totalCount, - hasOverflow, - }; - }, [cards, maxVisible]); - - return processedData; -} - +import { useMemo } from "react"; + +/** + * 카드 데이터 처리 커스텀 훅 + * 책임: 카드 목록 데이터 가공 및 표시 개수 제한 + */ +export default function useCards(cards, maxVisible = 6) { + const processedData = useMemo(() => { + if (!cards || cards.length === 0) { + return { + visibleCards: [], + totalCount: 0, + hasMore: false, + }; + } + + const totalCount = cards.length; + const hasMore = totalCount > maxVisible; + const visibleCards = cards.slice(0, maxVisible); + + return { + visibleCards, + totalCount, + hasMore, + }; + }, [cards, maxVisible]); + + return processedData; +} diff --git a/src/hooks/use-delete-actions.js b/src/hooks/use-delete-actions.js new file mode 100644 index 0000000..b463770 --- /dev/null +++ b/src/hooks/use-delete-actions.js @@ -0,0 +1,68 @@ +import { useCallback } from "react"; +import { useNavigate } from "react-router"; +import { deleteRecipient } from "@/api/rolling-page-api"; +import { deleteMessage } from "@/api/rolling-page-api"; +import { useToast } from "@/hooks/use-toast"; + +/** + * 삭제 액션 관리 커스텀 훅 + * 책임: 수신자 및 메시지 삭제 로직 처리 + */ +export function useDeleteActions() { + const navigate = useNavigate(); + const showToast = useToast(); + + /** + * 롤링 페이퍼 전체 삭제 + */ + const handleDeleteRecipient = useCallback( + async (recipientId) => { + try { + await deleteRecipient(recipientId); + showToast.delete("롤링 페이퍼가 삭제되었습니다."); + + // 삭제 후 메인 페이지로 이동 + setTimeout(() => { + navigate("/"); + }, 1000); + + return true; + } catch (err) { + console.error("롤링 페이퍼 삭제 실패:", err); + return false; + } + }, + [navigate, showToast], + ); + + /** + * 개별 메시지 삭제 + */ + const handleDeleteMessage = useCallback( + async (messageId, onSuccess) => { + try { + await deleteMessage(messageId); + showToast.delete("메시지가 삭제되었습니다."); + + // 삭제 성공 시 콜백 실행 (목록 갱신 등) + if (onSuccess) { + onSuccess(); + } + + return true; + } catch (err) { + console.error("메시지 삭제 실패:", err); + return false; + } + }, + [showToast], + ); + + return { + handleDeleteRecipient, + handleDeleteMessage, + }; +} + +export default useDeleteActions; + diff --git a/src/hooks/use-edit-mode.js b/src/hooks/use-edit-mode.js new file mode 100644 index 0000000..1048f93 --- /dev/null +++ b/src/hooks/use-edit-mode.js @@ -0,0 +1,14 @@ +import { useLocation } from "react-router"; + +/** + * 편집 모드 확인 커스텀 훅 + * 책임: URL 경로를 확인하여 편집 모드 여부 판단 + */ +export default function useEditMode() { + const location = useLocation(); + + // URL이 /edit으로 끝나면 편집 모드 + const isEditMode = location.pathname.endsWith("/edit"); + + return isEditMode; +} diff --git a/src/hooks/use-emoji-manager.js b/src/hooks/use-emoji-manager.js index c47018f..34553ae 100644 --- a/src/hooks/use-emoji-manager.js +++ b/src/hooks/use-emoji-manager.js @@ -1,41 +1,40 @@ -import { useState } from 'react'; - -/** - * 이모지 관리 커스텀 훅 - * 책임: 이모지 상태 관리 및 비즈니스 로직 처리 - */ -export default function useEmojiManager(initialEmojis = []) { - const [selectedEmojis, setSelectedEmojis] = useState(initialEmojis); - - const handleEmojiSelect = (emoji) => { - const existingEmojiIndex = selectedEmojis.findIndex(item => item.emoji === emoji); - - if (existingEmojiIndex !== -1) { - // 이미 존재하는 이모지면 카운트 증가 - const updatedEmojis = [...selectedEmojis]; - updatedEmojis[existingEmojiIndex].count += 1; - setSelectedEmojis(updatedEmojis); - } else { - // 새로운 이모지면 추가 - setSelectedEmojis([...selectedEmojis, { emoji, count: 1 }]); - } - }; - - // 카운트 순으로 정렬 - const getSortedEmojis = () => { - return [...selectedEmojis].sort((a, b) => b.count - a.count); - }; - - // 상위 N개 추출 - const getTopEmojis = (count) => { - return getSortedEmojis().slice(0, count); - }; - - return { - selectedEmojis, - handleEmojiSelect, - getSortedEmojis, - getTopEmojis, - }; -} - +import { useState } from "react"; + +/** + * 이모지 관리 커스텀 훅 + * 책임: 이모지 상태 관리 및 비즈니스 로직 처리 + */ +export default function useEmojiManager(initialEmojis = []) { + const [selectedEmojis, setSelectedEmojis] = useState(initialEmojis); + + const handleEmojiSelect = (emoji) => { + const existingEmojiIndex = selectedEmojis.findIndex((item) => item.emoji === emoji); + + if (existingEmojiIndex !== -1) { + // 이미 존재하는 이모지면 카운트 증가 + const updatedEmojis = [...selectedEmojis]; + updatedEmojis[existingEmojiIndex].count += 1; + setSelectedEmojis(updatedEmojis); + } else { + // 새로운 이모지면 추가 + setSelectedEmojis([...selectedEmojis, { emoji, count: 1 }]); + } + }; + + // 카운트 순으로 정렬 + const getSortedEmojis = () => { + return [...selectedEmojis].sort((a, b) => b.count - a.count); + }; + + // 상위 N개 추출 + const getTopEmojis = (count) => { + return getSortedEmojis().slice(0, count); + }; + + return { + selectedEmojis, + handleEmojiSelect, + getSortedEmojis, + getTopEmojis, + }; +} diff --git a/src/hooks/use-infinite-recipients.js b/src/hooks/use-infinite-recipients.js new file mode 100644 index 0000000..f0dfc87 --- /dev/null +++ b/src/hooks/use-infinite-recipients.js @@ -0,0 +1,81 @@ +import { useState, useCallback } from "react"; +import { getRecipientMessages } from "@/api/rolling-page-api"; + +/** + * 무한 스크롤을 위한 수신자 메시지 조회 커스텀 훅 + * 책임: 메시지 무한 스크롤 데이터 관리 + */ +export function useInfiniteRecipientMessages(recipientId, isEditMode = false) { + const [messages, setMessages] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [hasMore, setHasMore] = useState(true); + const [offset, setOffset] = useState(0); + const limit = 6; // 이후 로드할 메시지 개수 + const initialLimit = isEditMode ? 6 : 5; // 뷰어 모드는 추가하기 카드 때문에 5개 + + // 초기 데이터 로드 + const fetchInitialData = useCallback(async () => { + if (!recipientId) return; + + setLoading(true); + setError(null); + setOffset(0); + + try { + const data = await getRecipientMessages(recipientId, { + limit: initialLimit, + offset: 0 + }); + // results는 이미 최신순으로 정렬되어 있음 + setMessages(data.results || []); + setHasMore(data.next !== null); + setOffset(initialLimit); + } catch (err) { + setError(err.message || "메시지를 불러오는데 실패했습니다."); + setHasMore(false); + } finally { + setLoading(false); + } + }, [recipientId, initialLimit]); + + // 더 많은 데이터 로드 (무한 스크롤) + const fetchMoreData = useCallback(async () => { + if (loading || !hasMore) return; + + setLoading(true); + + try { + const data = await getRecipientMessages(recipientId, { limit, offset }); + + // 기존 메시지에 새 메시지 추가 + setMessages((prev) => [...prev, ...(data.results || [])]); + setHasMore(data.next !== null); + setOffset((prev) => prev + limit); + } catch (err) { + setError(err.message || "추가 메시지를 불러오는데 실패했습니다."); + setHasMore(false); + } finally { + setLoading(false); + } + }, [loading, hasMore, offset, recipientId]); + + const refresh = useCallback(() => { + setMessages([]); + setOffset(0); + setHasMore(true); + fetchInitialData(); + }, [fetchInitialData]); + + return { + messages, + loading, + error, + hasMore, + fetchInitialData, + fetchMoreData, + refresh, + }; +} + +export default useInfiniteRecipientMessages; diff --git a/src/hooks/use-kakao-sdk.js b/src/hooks/use-kakao-sdk.js index 7a8ac58..18d0104 100644 --- a/src/hooks/use-kakao-sdk.js +++ b/src/hooks/use-kakao-sdk.js @@ -1,41 +1,38 @@ -import { useEffect } from 'react'; - -const KAKAO_KEY = import.meta.env.VITE_KAKAO_KEY; - -/** - * 카카오 SDK 초기화 커스텀 훅 - * 책임: 카카오 SDK 스크립트 로드 및 초기화 - */ -export default function useKakaoSdk() { - - useEffect(() => { - // 이미 SDK가 로드되어 있으면 초기화만 수행 - if (window.Kakao) { - if (!window.Kakao.isInitialized()) { - window.Kakao.init(KAKAO_KEY); - } - return; - } - - // SDK 스크립트 동적 로드 - const script = document.createElement('script'); - script.src = 'https://t1.kakaocdn.net/kakao_js_sdk/2.7.7/kakao.min.js'; - script.integrity = 'sha384-tJkjbtDbvoxO+diRuDtwRO9JXR7pjWnfjfRn5ePUpl7e7RJCxKCwwnfqUAdXh53p'; - script.crossOrigin = 'anonymous'; - script.async = true; - - script.onload = () => { - if (window.Kakao && !window.Kakao.isInitialized()) { - window.Kakao.init(KAKAO_KEY); - } - }; - - document.head.appendChild(script); - - }, []); - - return { - isKakaoReady: window.Kakao?.isInitialized() || false, - }; -} - +import { useEffect } from "react"; + +const KAKAO_KEY = import.meta.env.VITE_KAKAO_KEY; + +/** + * 카카오 SDK 초기화 커스텀 훅 + * 책임: 카카오 SDK 스크립트 로드 및 초기화 + */ +export default function useKakaoSdk() { + useEffect(() => { + // 이미 SDK가 로드되어 있으면 초기화만 수행 + if (window.Kakao) { + if (!window.Kakao.isInitialized()) { + window.Kakao.init(KAKAO_KEY); + } + return; + } + + // SDK 스크립트 동적 로드 + const script = document.createElement("script"); + script.src = "https://t1.kakaocdn.net/kakao_js_sdk/2.7.7/kakao.min.js"; + script.integrity = "sha384-tJkjbtDbvoxO+diRuDtwRO9JXR7pjWnfjfRn5ePUpl7e7RJCxKCwwnfqUAdXh53p"; + script.crossOrigin = "anonymous"; + script.async = true; + + script.onload = () => { + if (window.Kakao && !window.Kakao.isInitialized()) { + window.Kakao.init(KAKAO_KEY); + } + }; + + document.head.appendChild(script); + }, []); + + return { + isKakaoReady: window.Kakao?.isInitialized() || false, + }; +} diff --git a/src/hooks/use-profile-images.js b/src/hooks/use-profile-images.js index 8e83833..0a886c9 100644 --- a/src/hooks/use-profile-images.js +++ b/src/hooks/use-profile-images.js @@ -1,36 +1,35 @@ -import { useMemo } from 'react'; - -/** - * 프로필 이미지 데이터 처리 커스텀 훅 - * 책임: 프로필 이미지 데이터 가공 및 오버플로우 계산 - */ -export default function useProfileImages(profiles, maxVisible = 3) { - const processedData = useMemo(() => { - if (!profiles || profiles.length === 0) { - return { - visibleProfiles: [], - overflowCount: 0, - totalCount: 0, - hasOverflow: false, - }; - } - - const totalCount = profiles.length; - const hasOverflow = totalCount > maxVisible; - - // 오버플로우가 있으면 마지막 자리는 +N 표시용으로 비움 - const visibleCount = hasOverflow ? maxVisible - 1 : maxVisible; - const visibleProfiles = profiles.slice(0, visibleCount); - const overflowCount = hasOverflow ? totalCount - visibleCount : 0; - - return { - visibleProfiles, - overflowCount, - totalCount, - hasOverflow, - }; - }, [profiles, maxVisible]); - - return processedData; -} - +import { useMemo } from "react"; + +/** + * 프로필 이미지 데이터 처리 커스텀 훅 + * 책임: 프로필 이미지 데이터 가공 및 오버플로우 계산 + */ +export default function useProfileImages(totalCount, profiles, maxVisible = 3) { + const processedData = useMemo(() => { + if (!profiles || profiles.length === 0) { + return { + visibleProfiles: [], + overflowCount: 0, + totalCount: 0, + hasOverflow: false, + }; + } + + + const hasOverflow = totalCount > maxVisible; + + // 오버플로우가 있으면 마지막 자리는 +N 표시용으로 비움 + const visibleCount = hasOverflow ? maxVisible - 1 : maxVisible; + const visibleProfiles = profiles.slice(0, visibleCount); + const overflowCount = hasOverflow ? totalCount - visibleCount : 0; + + return { + visibleProfiles, + overflowCount, + totalCount, + hasOverflow, + }; + }, [totalCount, profiles, maxVisible]); + + return processedData; +} diff --git a/src/hooks/use-reactions.js b/src/hooks/use-reactions.js new file mode 100644 index 0000000..3e6d932 --- /dev/null +++ b/src/hooks/use-reactions.js @@ -0,0 +1,69 @@ +import { useState, useCallback } from "react"; +import { getReactions, addReaction } from "@/api/rolling-page-api"; + +/** + * 리액션 관리 커스텀 훅 + * 책임: 리액션 데이터 관리 및 API 호출 + */ +export function useReactions(recipientId) { + const [reactions, setReactions] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + // 모든 리액션 조회 (최대 8개) + const fetchReactions = useCallback(async () => { + if (!recipientId) return; + + setLoading(true); + setError(null); + + try { + const data = await getReactions(recipientId, { limit: 8, offset: 0 }); + setReactions(data.results || []); + } catch (err) { + setError(err.message || "리액션을 불러오는데 실패했습니다."); + } finally { + setLoading(false); + } + }, [recipientId]); + + // 리액션 추가/감소 + const toggleReaction = useCallback( + async (emoji, type = "increase") => { + if (!recipientId) return; + + try { + const result = await addReaction(recipientId, { emoji, type }); + + // 로컬 상태 업데이트 + setReactions((prev) => { + const existing = prev.find((r) => r.emoji === emoji); + if (existing) { + return prev.map((r) => + r.emoji === emoji ? { ...r, count: result.count } : r, + ); + } else { + return [...prev, result]; + } + }); + + return result; + } catch (err) { + setError(err.message || "리액션 추가에 실패했습니다."); + throw err; + } + }, + [recipientId], + ); + + return { + reactions, + loading, + error, + fetchReactions, + toggleReaction, + }; +} + +export default useReactions; + diff --git a/src/hooks/use-recipients.js b/src/hooks/use-recipients.js new file mode 100644 index 0000000..da78758 --- /dev/null +++ b/src/hooks/use-recipients.js @@ -0,0 +1,51 @@ +import { useState, useEffect, useCallback } from "react"; +import { getRecipientById } from "@/api/rolling-page-api"; + + +/** + * 특정 유저 상세 조회 커스텀 훅 + * 책임: 단일 유저 데이터 관리 및 API 호출 + */ +export function useRecipient(recipientId, autoFetch = true) { + const [recipient, setRecipient] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const fetchRecipient = useCallback(async () => { + if (!recipientId) return; + + setLoading(true); + setError(null); + + try { + const data = await getRecipientById(recipientId); + setRecipient(data); + } catch (err) { + setError(err.message || "수신자 정보를 불러오는데 실패했습니다."); + } finally { + setLoading(false); + } + }, [recipientId]); + + useEffect(() => { + if (autoFetch && recipientId) { + fetchRecipient(); + } + }, [autoFetch, recipientId, fetchRecipient]); + + const refresh = useCallback(() => { + fetchRecipient(); + }, [fetchRecipient]); + + return { + recipient, + loading, + error, + refresh, + fetchRecipient, + }; +} + + + + diff --git a/src/hooks/use-share-actions.js b/src/hooks/use-share-actions.js index 03bd9ea..a175c2f 100644 --- a/src/hooks/use-share-actions.js +++ b/src/hooks/use-share-actions.js @@ -1,51 +1,56 @@ -import { useCallback } from 'react'; -import { useToast } from '@/hooks/use-toast'; - -/** - * 공유 기능 커스텀 훅 - * 책임: URL 복사 및 카카오톡 공유 비즈니스 로직 처리 - */ -export default function useShareActions() { - const showToast = useToast(); - - /** - * URL을 클립보드에 복사 - */ - const copyToClipboard = useCallback(async (url) => { - try { - await navigator.clipboard.writeText(url); - showToast.success('URL이 복사되었습니다.'); - return true; - } catch (err) { - console.error('URL 복사 실패:', err); - return false; - } - }, [showToast]); - - /** - * 카카오톡으로 공유 - */ - const shareToKakao = useCallback((url) => { - if (!window.Kakao) { - return false; - } - - try { - window.Kakao.Share.sendScrap({ - requestUrl: url, - }); - showToast.success('카카오톡으로 공유되었습니다.'); - - return true; - } catch (err) { - console.error('카카오톡 공유 실패:', err); - return false; - } - }, [showToast]); - - return { - copyToClipboard, - shareToKakao, - }; -} - +import { useCallback } from "react"; +import { useToast } from "@/hooks/use-toast"; + +/** + * 공유 기능 커스텀 훅 + * 책임: URL 복사 및 카카오톡 공유 비즈니스 로직 처리 + */ +export default function useShareActions() { + const showToast = useToast(); + + /** + * URL을 클립보드에 복사 + */ + const copyToClipboard = useCallback( + async (url) => { + try { + await navigator.clipboard.writeText(url); + showToast.success("URL이 복사되었습니다."); + return true; + } catch (err) { + console.error("URL 복사 실패:", err); + return false; + } + }, + [showToast], + ); + + /** + * 카카오톡으로 공유 + */ + const shareToKakao = useCallback( + (url) => { + if (!window.Kakao) { + return false; + } + + try { + window.Kakao.Share.sendScrap({ + requestUrl: url, + }); + showToast.success("카카오톡으로 공유되었습니다."); + + return true; + } catch (err) { + console.error("카카오톡 공유 실패:", err); + return false; + } + }, + [showToast], + ); + + return { + copyToClipboard, + shareToKakao, + }; +} diff --git a/src/main.jsx b/src/main.jsx index 0a09416..66e917e 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -8,5 +8,5 @@ createRoot(document.getElementById("root")).render( - + , ); diff --git a/src/pages/main-page.jsx b/src/pages/main-page.jsx index de50804..63594a3 100644 --- a/src/pages/main-page.jsx +++ b/src/pages/main-page.jsx @@ -176,17 +176,13 @@ export default function MainPage() { Point. 01 - - 누구나 손쉽게, 온라인 롤링 페이퍼를 만들 수 있어요 - + 누구나 손쉽게, 온라인 롤링 페이퍼를 만들 수 있어요 로그인 없이 자유롭게 만들어요. Point. 02 서로에게 이모지로 감정을 표현해보세요 - - 롤링 페이퍼에 이모지를 추가할 수 있어요. - + 롤링 페이퍼에 이모지를 추가할 수 있어요. diff --git a/src/pages/message-page.jsx b/src/pages/message-page.jsx index 0c9ae52..066eced 100644 --- a/src/pages/message-page.jsx +++ b/src/pages/message-page.jsx @@ -102,7 +102,9 @@ export const SelectableImageItem = styled.li` overflow: hidden; cursor: pointer; flex-shrink: 0; /* 크기 고정 */ - transition: transform 0.2s, border 0.2s; + transition: + transform 0.2s, + border 0.2s; border: 2px solid transparent; ${({ isSelected }) => @@ -177,11 +179,7 @@ function MessagePage() { {/* From. 입력 필드 */} From. - + {/* 에러 메시지 표시 */} {hasError && "값을 입력해 주세요."} @@ -195,10 +193,7 @@ function MessagePage() { {selectableImages.map((image) => ( - + {`프로필 ))} @@ -209,11 +204,7 @@ function MessagePage() { {/* 상대와의 관계 드롭다운*/} 상대와의 관계 - + diff --git a/src/pages/post-page.jsx b/src/pages/post-page.jsx index b505096..b0446ac 100644 --- a/src/pages/post-page.jsx +++ b/src/pages/post-page.jsx @@ -198,9 +198,7 @@ export default function PostPage() { 배경화면을 선택해 주세요. - - 컬러를 선택하거나, 이미지를 선택할 수 있습니다. - + 컬러를 선택하거나, 이미지를 선택할 수 있습니다. 컬러 이미지 diff --git a/src/pages/rolling-page-edit.jsx b/src/pages/rolling-page-edit.jsx deleted file mode 100644 index 8cea62b..0000000 --- a/src/pages/rolling-page-edit.jsx +++ /dev/null @@ -1,58 +0,0 @@ -import React, { useState } from 'react'; -import { - RollingHeaderContainer, - RollingHeaderUserInfo, - RollingHeaderRightContainer, - PerpendicularLineFirst, - RollingPageContainer, - - -} from "@/styles/rolling-page-styles"; -import RollingPageHeader from "@/pages/rolling-page-head"; -import ParticipantSection from "@/components/rolling/participant-section"; -import ArrowDownIcon from "@/assets/icons/arrow-down.svg"; -import AddEmojiIcon from "@/assets/icons/add-emoji.svg"; -import ShareIcon from "@/assets/icons/share.svg"; -import CardContents from "@/components/rolling/card-contents"; - -export default function RollingPage() { - // TODO: 실제로는 API에서 받아올 데이터 - // 임시 데이터 (나중에 API 호출로 대체) - const [profiles] = useState([ - { id: 1, name: '김철수', profileImageURL: 'https://via.placeholder.com/28' }, - { id: 2, name: '이영희', profileImageURL: 'https://via.placeholder.com/28' }, - { id: 3, name: '박민수', profileImageURL: 'https://via.placeholder.com/28' }, - - ]); - - - return ( - <> - - - To. Ashley Kim - - - - {/* 참여자 프로필 섹션 */} - - - - - {/* 이모지 및 공유 헤더 */} - - - - - - - - - ); -} - - diff --git a/src/pages/rolling-page-head.jsx b/src/pages/rolling-page-head.jsx deleted file mode 100644 index f165398..0000000 --- a/src/pages/rolling-page-head.jsx +++ /dev/null @@ -1,106 +0,0 @@ -import React, { useState } from 'react'; -import ShareModal from '@/components/rolling/share-modal'; -import EmojiDisplayList from '@/components/rolling/emoji-display-list'; -import EmojiDropdown from '@/components/rolling/emoji-dropdown'; -import HeaderActionButtons from '@/components/rolling/header-action-buttons'; -import useEmojiManager from '@/hooks/use-emoji-manager'; -import { RollingHeaderImojiContainer } from '@/styles/rolling-page-styles'; - -/** - * 롤링 페이지 헤더 컴포넌트 - * 책임: 전체 헤더 구성 요소 조합 및 상태 관리 - */ -export default function RollingPageHeader({ - ArrowDownIcon, - AddEmojiIcon, - ShareIcon -}) { - // UI 상태 관리 - const [isEmojiPickerOpen, setIsEmojiPickerOpen] = useState(false); - const [isEmojiDropdownOpen, setIsEmojiDropdownOpen] = useState(false); - const [isShareModalOpen, setIsShareModalOpen] = useState(false); - - // 이모지 상태 및 로직 관리 - const initialEmojis = [ - { emoji: '😘', count: 12 }, - { emoji: '😍', count: 8 }, - { emoji: '👍', count: 15 }, - { emoji: '🎉', count: 5 }, - { emoji: '❤️', count: 20 }, - { emoji: '😂', count: 3 }, - { emoji: '🔥', count: 7 } - ]; - - const { handleEmojiSelect, getSortedEmojis, getTopEmojis } = useEmojiManager(initialEmojis); - - // 이모지 피커 핸들러 - const toggleEmojiPicker = () => { - setIsEmojiPickerOpen(!isEmojiPickerOpen); - }; - - const closeEmojiPicker = () => { - setIsEmojiPickerOpen(false); - }; - - // 이모지 드롭다운 핸들러 - const toggleEmojiDropdown = () => { - setIsEmojiDropdownOpen(!isEmojiDropdownOpen); - }; - - const closeEmojiDropdown = () => { - setIsEmojiDropdownOpen(false); - }; - - // 공유 모달 핸들러 - const openShareModal = () => { - setIsShareModalOpen(true); - }; - - const closeShareModal = () => { - setIsShareModalOpen(false); - }; - - // 정렬된 이모지 및 상위 3개 추출 - const sortedEmojis = getSortedEmojis(); - const topThreeEmojis = getTopEmojis(3); - const hasMoreEmojis = sortedEmojis.length > 3; - - // 현재 페이지 URL - const currentUrl = window.location.href; - - return ( - - {/* 상위 3개 이모지 표시 */} - - - {/* 더 많은 이모지가 있을 경우 드롭다운 */} - {hasMoreEmojis && ( - - )} - - {/* 이모지 추가 및 공유 버튼 */} - - } - /> - - ); -} \ No newline at end of file diff --git a/src/pages/rolling-page.jsx b/src/pages/rolling-page.jsx index 9c43cba..98c1592 100644 --- a/src/pages/rolling-page.jsx +++ b/src/pages/rolling-page.jsx @@ -1,58 +1,129 @@ -import React, { useState } from 'react'; -import { - RollingHeaderContainer, - RollingHeaderUserInfo, - RollingHeaderRightContainer, - PerpendicularLineFirst, - RollingPageContainer, - - -} from "@/styles/rolling-page-styles"; -import RollingPageHeader from "@/pages/rolling-page-head"; -import ParticipantSection from "@/components/rolling/participant-section"; -import ArrowDownIcon from "@/assets/icons/arrow-down.svg"; -import AddEmojiIcon from "@/assets/icons/add-emoji.svg"; -import ShareIcon from "@/assets/icons/share.svg"; -import CardContents from "@/components/rolling/card-contents"; - -export default function RollingPage() { - // TODO: 실제로는 API에서 받아올 데이터 - // 임시 데이터 (나중에 API 호출로 대체) - const [profiles] = useState([ - { id: 1, name: '김철수', profileImageURL: 'https://via.placeholder.com/28' }, - { id: 2, name: '이영희', profileImageURL: 'https://via.placeholder.com/28' }, - { id: 3, name: '박민수', profileImageURL: 'https://via.placeholder.com/28' }, - - ]); - - - return ( - <> - - - To. Ashley Kim - - - - {/* 참여자 프로필 섹션 */} - - - - - {/* 이모지 및 공유 헤더 */} - - - - - - - - - ); -} - - +import React, { useState } from "react"; +import { useParams } from "react-router"; +import { + RollingHeaderContainer, + RollingHeaderUserInfo, + RollingHeaderRightContainer, + PerpendicularLineFirst, + RollingPageContainer, + CardPageDeleteButton, + CardContainerWrapper, +} from "@/styles/rolling-page-styles"; +import RollingPageHeader from "@/components/rolling/rolling-page-head"; +import ParticipantSection from "@/components/rolling/participant-section"; +import CardContents from "@/components/rolling/card-contents"; +import DeleteConfirmModal from "@/components/rolling/delete-confirm-modal"; +import useEditMode from "@/hooks/use-edit-mode"; +import { useRecipient } from "@/hooks/use-recipients"; +import { useDeleteActions } from "@/hooks/use-delete-actions"; +import ArrowDownIcon from "@/assets/icons/arrow-down.svg"; +import AddEmojiIcon from "@/assets/icons/add-emoji.svg"; +import ShareIcon from "@/assets/icons/share.svg"; + +export default function RollingPage() { + // URL 파라미터에서 recipientId 가져오기 (/post/:id) + const { id } = useParams(); + const recipientId = Number(id); + + // 편집 모드 확인 (URL이 /edit으로 끝나는지) + const isEditMode = useEditMode(); + + // API에서 수신자 데이터 가져오기 + const { recipient, loading, error } = useRecipient(recipientId); + + // 삭제 액션 훅 + const { handleDeleteRecipient } = useDeleteActions(); + + // 페이지 삭제 확인 모달 상태 + const [isDeletePageModalOpen, setIsDeletePageModalOpen] = useState(false); + + // recipientId가 없거나 유효하지 않은 경우 + if (!recipientId || isNaN(recipientId)) { + return
잘못된 페이지 주소입니다.
; + } + + if (loading) { + return
로딩 중123...
; + } + + if (error) { + return
에러가 발생했습니다: {error}
; + } + + if (!recipient) { + return
데이터를 불러올 수 없습니다.
; + } + + // recentMessages에서 프로필 데이터 추출 (최신순 3개) + const profiles = recipient.recentMessages?.map((msg, index) => ({ + id: msg.id || index, + name: msg.sender, + profileImageURL: msg.profileImageURL, + })) || []; + + // 페이지 삭제 핸들러 + const handleOpenDeletePageModal = () => { + setIsDeletePageModalOpen(true); + }; + + const handleCloseDeletePageModal = () => { + setIsDeletePageModalOpen(false); + }; + + const handleConfirmDeletePage = () => { + handleDeleteRecipient(recipientId); + }; + + return ( + <> + + To. {recipient.name} + + + {/* 참여자 프로필 섹션 - messageCount 전달 */} + + + + + {/* 이모지 및 공유 헤더 - topReactions 전달 */} + + + + + + + {/* 편집 모드일 때만 페이지 삭제 버튼 표시 */} + + {isEditMode && ( + + 삭제하기 + + )} + + + + + + {/* 페이지 전체 삭제 확인 모달 */} + + + ); +} diff --git a/src/pages/toast-test-page.jsx b/src/pages/toast-test-page.jsx index bbe8e3b..6333b29 100644 --- a/src/pages/toast-test-page.jsx +++ b/src/pages/toast-test-page.jsx @@ -15,12 +15,8 @@ export default function ToastTestPage() { return (

Toast 테스트 페이지 (5초 후 자동 사라짐)

- - + +
); } diff --git a/src/styles/global-style.js b/src/styles/global-style.js index 44bbb3d..7086845 100644 --- a/src/styles/global-style.js +++ b/src/styles/global-style.js @@ -15,8 +15,7 @@ export const GlobalStyle = createGlobalStyle`${css` } :root { - --font-family: "Pretendard", -apple-system, BlinkMacSystemFont, system-ui, - Roboto, sans-serif; + --font-family: "Pretendard", -apple-system, BlinkMacSystemFont, system-ui, Roboto, sans-serif; } body { diff --git a/src/styles/head-nav-style.js b/src/styles/head-nav-style.js index 17d8de0..ebb611d 100644 --- a/src/styles/head-nav-style.js +++ b/src/styles/head-nav-style.js @@ -1,18 +1,16 @@ -import styled from "styled-components"; -import { colors } from "@/styles/colors"; -import media from "@/styles/media"; - - -export const HeadNavContainer = styled.div` - display: flex; - justify-content: space-between; - align-items: center; - height: 65px; - background-color: #fff; - border-bottom: 1px solid ${colors.gray[200]}; - - ${media.small` - display: none; - `} - -`; +import styled from "styled-components"; +import { colors } from "@/styles/colors"; +import media from "@/styles/media"; + +export const HeadNavContainer = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + height: 65px; + background-color: #fff; + border-bottom: 1px solid ${colors.gray[200]}; + + ${media.small` + display: none; + `} +`; diff --git a/src/styles/message-page.js b/src/styles/message-page.js index dafb8ad..493b0eb 100644 --- a/src/styles/message-page.js +++ b/src/styles/message-page.js @@ -91,8 +91,7 @@ export const SelectableImageItem = styled.li` border-radius: 50%; overflow: hidden; cursor: pointer; - border: 2px solid - ${(props) => (props.isSelected ? COLOR_PRIMARY : "transparent")}; + border: 2px solid ${(props) => (props.isSelected ? COLOR_PRIMARY : "transparent")}; transition: border 0.2s; & img { diff --git a/src/styles/rolling-page-styles.js b/src/styles/rolling-page-styles.js index d2162db..157847d 100644 --- a/src/styles/rolling-page-styles.js +++ b/src/styles/rolling-page-styles.js @@ -1,480 +1,528 @@ -import styled from "styled-components"; -import { colors } from "@/styles/colors"; -import { font } from "@/styles/font"; -import ShareIcon from "@/assets/icons/share.svg"; -import media from "@/styles/media"; -import EditIcon from "@/assets/icons/plus.svg"; -import DeleteIcon from "@/assets/icons/deleted.svg"; - - -//최상단헤더 컨테이너 -export const RollingHeaderContainer = styled.div` - display: flex; - justify-content: space-between; - align-items: center; - width: 1200px; - margin: 0 auto; - padding: 13px 20px; - height: 68px; - background-color: rgba(255, 255, 255, 1); - gap: 20px; - - ${media.large` - width: 1200px; - height: 68px; - margin: 0 auto; - padding: 13px 20px; - - gap: 20px; - `} - - ${media.medium` - width: 100%; - height: 68px; - margin: 0; - padding: 13px 20px; - - gap: 10px; - `} - - ${media.small` - flex-direction: column; - align-items: center; - height: auto; - width: 100%; - padding: 0px; - gap: 0px; - - `} - -`; - - -//유저 정보 컨테이너 TO. Ashley Kim -export const RollingHeaderUserInfo = styled.div` - display: flex; - align-items: center; - min-width: 227px; - height: 42px; - line-height: 42px; - letter-spacing: -1%; - ${font.bold28} - color: ${colors.gray[800]}; - flex-shrink: 0; - - ${media.medium` - min-width: 150px; - height: 42px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - `} - - ${media.small` - width: 100%; - min-width: auto; - height: auto; - padding: 12px 20px; - border-bottom: 1px solid ${colors.gray[200]}; - `} -`; - - -export const RollingHeaderRightContainer = styled.div` - display: flex; - align-items: center; - gap: 20px; - flex-shrink: 1; - min-width: 0; - - ${media.medium` - gap: 8px; - flex-shrink: 1; - min-width: 0; - - `} - - ${media.small` - width: 100%; - padding: 8px 20px; - - - `} - - -`; - - - -//유저 이미지 컨테이너 프로필 사진들과, 몇명이 작성중인지 표시 -export const RollingHeaderUserPeopleContainer = styled.div` - width: 228px; - display: flex; - align-items: center; - - ${media.medium` - display: none; - `} - - ${media.small` - display: none; - `} -`; - -//유저 이미지 프로필 사진들 -export const RollingHeaderUserPeopleImages = styled.div` - display: flex; - width: 76px; - height: 28px; - position: relative; - - cursor: pointer; - ${media.medium` - - display: none; - `} - - ${media.small` - - display: none; - `} - -`; - -export const RollingHeaderUserPeopleImage = styled.img` - width: 28px; - height: 28px; - border-radius: 140px; - border: 1.4px solid #fff; - position: relative; - margin-left: -10px; -`; - -export const RollingHeaderUserDefaultImage = styled(RollingHeaderUserPeopleImage)``; - -//몇명이 작성중인지 -export const RollingHeaderUserPeopleState = styled.div` - width: 160px; - height: 27px; - line-height: 27px; - ${font.bold18} - color: ${colors.gray[900]}; - text-align: center; - ${media.medium` - - display: none; - `} - - ${media.small` - display: none; - `} -`; - -//이모지 컨테이너 드롭박스 포함, 추가 버튼 포함 -export const RollingHeaderImojiContainer = styled.div` - display: flex; - align-items: center; - gap: 8px; - -`; - -export const RollingHeaderImojiIconContainer = styled.div` - display: flex; - align-items: center; - justify-content: center; - width: auto; - height: auto; - padding: 8px 12px; - text-align: center; - border-radius: 32px; - background: rgba(153, 153, 153, 1); - gap: 2px; - - ${media.small` - padding: 4px 8px; - `} - -`; - -export const RollingHeaderImojiIcon = styled.div` - - width: 24px; - height: 24px; - color: rgba(255, 255, 255, 1); - - ${media.small` - width: 20px; - height: 24px; - `} - -`; - -export const RollingHeaderImojiText = styled.span` - ${font.regular16} - color: rgba(255, 255, 255, 1) -`; - -export const RollingHeaderImojiEditButton = styled.button` - display: flex; - align-items: center; - justify-content: center; - gap: 7px; - width: 88px; - height: 36px; - border-radius: 6px; - background: #fff; - border: 1px solid ${colors.gray[300]}; - cursor: pointer; - ${media.small` - width: 36px; - height: 32px; - `} -`; - -export const RollingHeaderImojiEditButtonContainer = styled.div` - display: flex; - align-items: center; - gap: 15px; -`; - -export const RollingHeaderImojiEditButtonIcon = styled.img` - width: 20px; - height: 20px; -`; - -export const RollingHeaderImojiEditButtonText = styled.span` - ${font.regular16} - color: ${colors.gray[900]}; - ${media.small` - display: none; - `} -`; - -export const RollingHeaderLinkShareButton = styled.div` - width: 56px; - height: 36px; - border-radius: 6px; - background-image: url("${ShareIcon}"); - background-size: 24px 24px; - background-repeat: no-repeat; - background-position: center; - padding: 12px 32px; - cursor: pointer; - border: 1px solid ${colors.gray[300]}; - ${media.small` - width: 36px; - height: 32px; - background-size: 20px 20px; - padding: 8px 8px; - - `} - -`; - -export const RollingHeaderArrowDown = styled.img` - width: 24px; - height: 24px; - cursor: pointer; -`; - - - - - - - -export const PerpendicularLine = styled.div` - border-left: 1px solid ${colors.gray[200]}; - height: 28px; -`; - -export const PerpendicularLineFirst = styled(PerpendicularLine)` - ${media.medium` - display: none; - `} - - ${media.small` - display: none; - `} -`; - -export const PerpendicularLineSecond = styled(PerpendicularLine)``; - - - -export const RollingPageContainer = styled.div` -display: flex; -justify-content: center; -align-items: center; -background-color: ${colors.blue[100]}; -width: 100%; -margin: 0 auto; -padding: 20px; - -`; - - - -export const CardContainer = styled.div` - display: grid; - grid-template-columns: repeat(3, 1fr); - grid-template-rows: repeat(2, 1fr); - - gap: 20px; - ${media.medium` - grid-template-columns: repeat(2, 1fr); - grid-template-rows: repeat(3, 1fr); - `} - ${media.small` - grid-template-columns: repeat(1, 1fr); - grid-template-rows: repeat(6, 1fr); - `} -`; - -export const Card = styled.div` - display: flex; - justify-content: center; - align-items: center; - width: 384px; - height: 280px; - border-radius: 16px; - background-color: #fff; - position: relative; -`; - -export const CardEditButton = styled.button` - width: 56px; - height: 56px; - background-image: url("${EditIcon}"); - background-color: ${colors.gray[500]}; - border-radius: 100px; - border: none; - padding: 20px; - background-size: 24px 24px; - background-repeat: no-repeat; - background-position: center; - cursor: pointer; - &:hover { - background-color: ${colors.gray[400]}; - color: ${colors.gray[100]}; - } -`; - -export const CardContentContainer = styled.div` - display: flex; - flex-direction: column; - justify-content: center; - width: 100%; - height: 100%; - padding: 16px 24px; -`; - -export const CardContentStatus = styled.div` - display: flex; - align-items: center; - justify-content: space-between; - width: 100%; - height: auto; - gap: 14px; - border-bottom: 1px solid ${colors.gray[200]}; - padding-bottom: 16px; -`; - -export const CardContentStatusContainer = styled.div` - display: flex; - align-items: center; - gap: 14px; -`; - -export const CardContentStatusProfileContainer = styled.div` - display: flex; - flex-direction: column; - align-items: flex-start; - gap: 6px; -`; - -export const CardContentFrom = styled.div` - - display: flex; - justify-content: center; - align-items: center; - width: 100%; - height: 100%; -`; - -export const CardContentStatusProfileImage = styled.img` - width: 56px; - height: 56px; - border-radius: 100px; - border: 1px solid ${colors.gray[300]}; -`; - -export const CardContentStatusProfileName = styled.div` - ${font.regular16} - color: ${colors.gray[900]}; -`; - -const relationshipColors = { - friend: colors.blue[100], - family: colors.green[100], - colleague: colors.purple[100], - acquaintance: colors.beige[100], -}; - -const relationshipTextColors = { - friend: colors.blue[500], - family: colors.green[500], - colleague: colors.purple[600], - acquaintance: colors.beige[500], -}; - -// const relationshipLabels = { -// friend: '친구', -// family: '가족', -// colleague: '동료', -// acquaintance: '지인', -// }; - -export const CardContentStatusRelationship = styled.div` - display: inline-flex; - align-items: center; - justify-content: center; - padding: 0 8px; - height: 20px; - border-radius: 4px; - ${font.regular14} - color: ${props => relationshipTextColors[props.$relationship] || colors.gray[500]}; - - background-color: ${props => relationshipColors[props.$relationship] || colors.gray[500]}; -`; - -export const CardContentText = styled.div` - width: 100%; - height: 100%; - ${font.regular16} - color: ${colors.gray[600]}; - padding-top: 16px; - cursor: pointer; -`; - -export const CardContentDate = styled.div` - ${font.regular12} - color: ${colors.gray[400]}; -`; - -export const CardContentDeleteButton = styled.div` - width: 40px; - height: 40px; - background-image: url("${DeleteIcon}"); - border-radius: 6px; - border: 1px solid ${colors.gray[300]}; - padding: 20px; - background-size: 24px 24px; - background-repeat: no-repeat; - background-position: center; - cursor: pointer; - &:hover { - background-color: ${colors.gray[200]}; - color: ${colors.gray[100]}; - } -`; \ No newline at end of file +import React from "react"; + +import styled from "styled-components"; +import { colors } from "@/styles/colors"; +import { font } from "@/styles/font"; +import ShareIcon from "@/assets/icons/share.svg"; +import media from "@/styles/media"; +import EditIcon from "@/assets/icons/plus.svg"; +import DeleteIcon from "@/assets/icons/deleted.svg"; + +//최상단헤더 컨테이너 +export const RollingHeaderContainer = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + width: 1200px; + margin: 0 auto; + height: 68px; + background-color: rgba(255, 255, 255, 1); + gap: 20px; + + ${media.large` + width: 1200px; + height: 68px; + margin: 0 auto; + padding: 13px 0px; + + gap: 20px; + `} + + ${media.medium` + width: 100%; + height: 68px; + margin: 0; + padding: 13px 20px; + + gap: 10px; + `} + + ${media.small` + flex-direction: column; + align-items: center; + + height: auto; + width: 100%; + padding: 0px; + gap: 0px; + + `} +`; + +//유저 정보 컨테이너 TO. Ashley Kim +export const RollingHeaderUserInfo = styled.div` + display: flex; + align-items: center; + min-width: 227px; + height: 42px; + line-height: 42px; + letter-spacing: -1%; + ${font.bold28} + color: ${colors.gray[800]}; + flex-shrink: 0; + + ${media.medium` + min-width: 150px; + height: 42px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + `} + + ${media.small` + width: 100%; + min-width: auto; + height: auto; + padding: 12px 20px; + border-bottom: 1px solid ${colors.gray[200]}; + `} +`; + +export const RollingHeaderRightContainer = styled.div` + display: flex; + align-items: center; + gap: 20px; + flex-shrink: 1; + min-width: 0; + + ${media.medium` + gap: 8px; + flex-shrink: 1; + min-width: 0; + + `} + + ${media.small` + width: 100%; + padding: 8px 20px; + + + `} +`; + +//유저 이미지 컨테이너 프로필 사진들과, 몇명이 작성중인지 표시 +export const RollingHeaderUserPeopleContainer = styled.div` + width: 228px; + display: flex; + align-items: center; + + ${media.medium` + display: none; + `} + + ${media.small` + display: none; + `} +`; + +//유저 이미지 프로필 사진들 +export const RollingHeaderUserPeopleImages = styled.div` + display: flex; + width: 76px; + height: 28px; + position: relative; + + cursor: pointer; + ${media.medium` + + display: none; + `} + + ${media.small` + + display: none; + `} +`; + +export const RollingHeaderUserPeopleImage = styled.img` + width: 28px; + height: 28px; + border-radius: 140px; + border: 1.4px solid #fff; + position: relative; + margin-left: -10px; +`; + +export const RollingHeaderUserDefaultImage = styled(RollingHeaderUserPeopleImage)``; + +//몇명이 작성중인지 +export const RollingHeaderUserPeopleState = styled.div` + width: 160px; + height: 27px; + line-height: 27px; + ${font.bold18} + color: ${colors.gray[900]}; + text-align: center; + ${media.medium` + + display: none; + `} + + ${media.small` + display: none; + `} +`; + +//이모지 컨테이너 드롭박스 포함, 추가 버튼 포함 +export const RollingHeaderEmojiContainer = styled.div` + display: flex; + align-items: center; + gap: 8px; +`; + +export const RollingHeaderEmojiIconContainer = styled.div` + display: flex; + align-items: center; + justify-content: center; + width: auto; + height: auto; + padding: 8px 12px; + text-align: center; + border-radius: 32px; + background: rgba(153, 153, 153, 1); + gap: 6px; + + ${media.small` + padding: 6px 10px; + `} +`; + +export const RollingHeaderEmojiIcon = styled.div` + width: 24px; + height: 24px; + color: rgba(255, 255, 255, 1); + + ${media.small` + width: 20px; + height: 24px; + + `} +`; + +export const RollingHeaderEmojiText = styled.span` + ${font.regular16} + color: rgba(255, 255, 255, 1) +`; + +export const RollingHeaderEmojiEditButton = styled.button` + display: flex; + align-items: center; + justify-content: center; + gap: 4px; + width: 88px; + height: 36px; + border-radius: 6px; + background: #fff; + border: 1px solid ${colors.gray[300]}; + cursor: pointer; + ${media.small` + width: 36px; + height: 32px; + `} + &:hover { + background-color: ${colors.gray[100]}; + } +`; + +export const RollingHeaderEmojiEditButtonContainer = styled.div` + display: flex; + align-items: center; + gap: 15px; +`; + +export const RollingHeaderEmojiEditButtonIcon = styled.img` + width: 20px; + height: 20px; +`; + +export const RollingHeaderEmojiEditButtonText = styled.span` + ${font.regular16} + color: ${colors.gray[900]}; + ${media.small` + display: none; + `} +`; + +export const RollingHeaderLinkShareButton = styled.div` + width: 56px; + height: 36px; + border-radius: 6px; + background-image: url("${ShareIcon}"); + background-size: 24px 24px; + background-repeat: no-repeat; + background-position: center; + padding: 12px 32px; + cursor: pointer; + border: 1px solid ${colors.gray[300]}; + &:hover { + background-color: ${colors.gray[100]}; + } + ${media.small` + width: 36px; + height: 32px; + background-size: 20px 20px; + padding: 8px 8px; + + `} +`; + +export const RollingHeaderArrowDown = styled.img` + width: 24px; + height: 24px; + cursor: pointer; +`; + +export const PerpendicularLine = styled.div` + border-left: 1px solid ${colors.gray[200]}; + height: 28px; +`; + +export const PerpendicularLineFirst = styled(PerpendicularLine)` + ${media.medium` + display: none; + `} + + ${media.small` + display: none; + `} +`; + +export const PerpendicularLineSecond = styled(PerpendicularLine)``; + + +const RollingPageWrapper = ({ $backgroundcolor, $backgroundimage, ...rest }) => { + return React.createElement('div', { $backgroundcolor, $backgroundimage, ...rest }); +}; +export const RollingPageContainer = styled(RollingPageWrapper)` + display: flex; + justify-content: center; + align-items: center; + background-color: ${(props) => props.$backgroundcolor || colors.blue[100]}; + background-image: ${(props) => + props.$backgroundimage ? `url(${props.$backgroundimage})` : "none"}; + background-size: cover; + background-repeat: no-repeat; + width: 100%; + height: 100%; + margin: 0 auto; + padding: 63px 216px 113px 216px; + gap: 11px; + + + ${media.medium` + padding: 5% 2%; + `} + + ${media.small` + padding: 63px 20px 113px 20px; + `} +`; + + + +export const CardContainerWrapper = styled.div` + display: flex; + flex-direction: column; + gap: 11px; + + & > :first-child { + align-self: flex-end; + } + + ${media.medium` + gap: 7px; + width: 100%; + `} + + ${media.small` + gap: 4px; + `} +`; + +export const CardContainer = styled.div` + display: grid; + width: 100%; + height: 100%; + grid-template-columns: repeat(3, 1fr); + grid-template-rows: repeat(2, 1fr); + + gap: 20px; + ${media.medium` + grid-template-columns: repeat(2, 1fr); + grid-template-rows: repeat(3, 1fr); + gap: 16px; + `} + ${media.small` + grid-template-columns: repeat(1, 1fr); + grid-template-rows: repeat(6, 1fr); + gap: 16px; + + `} +`; + +export const Card = styled.div` + display: flex; + justify-content: center; + align-items: center; + width: 384px; + height: 280px; + border-radius: 16px; + background-color: #fff; + position: relative; + + ${media.medium` + width: 100%; + height: auto; + `} + + ${media.small` + width: 100%; + `} +`; + +export const CardEditButton = styled.button` + width: 56px; + height: 56px; + background-image: url("${EditIcon}"); + background-color: ${colors.gray[500]}; + border-radius: 100px; + border: none; + padding: 20px; + background-size: 24px 24px; + background-repeat: no-repeat; + background-position: center; + cursor: pointer; + &:hover { + background-color: ${colors.gray[400]}; + color: ${colors.gray[100]}; + } +`; + +export const CardContentContainer = styled.div` + display: flex; + flex-direction: column; + justify-content: center; + width: 100%; + height: 100%; + padding: 16px 24px; +`; + +export const CardContentStatus = styled.div` + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + height: auto; + gap: 14px; + border-bottom: 1px solid ${colors.gray[200]}; + padding-bottom: 16px; +`; + +export const CardContentStatusContainer = styled.div` + display: flex; + align-items: center; + gap: 14px; +`; + +export const CardContentStatusProfileContainer = styled.div` + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 6px; +`; + +export const CardContentFrom = styled.div` + display: flex; + justify-content: center; + align-items: center; + width: 100%; + height: 100%; +`; + +export const CardContentStatusProfileImage = styled.img` + width: 56px; + height: 56px; + border-radius: 100px; + border: 1px solid ${colors.gray[300]}; +`; + +export const CardContentStatusProfileName = styled.div` + ${font.regular16} + color: ${colors.gray[900]}; +`; + +const relationshipColors = { + friend: colors.blue[100], + family: colors.green[100], + colleague: colors.purple[100], + acquaintance: colors.beige[100], +}; + +const relationshipTextColors = { + friend: colors.blue[500], + family: colors.green[500], + colleague: colors.purple[600], + acquaintance: colors.beige[500], +}; + + + +export const CardContentStatusRelationship = styled.div` + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0 8px; + height: 20px; + border-radius: 4px; + ${font.regular14} + color: ${(props) => relationshipTextColors[props.$relationship] || colors.gray[500]}; + + background-color: ${(props) => relationshipColors[props.$relationship] || colors.gray[500]}; +`; + +export const CardContentText = styled.div` + width: 100%; + height: 100%; + ${font.regular16} + color: ${colors.gray[600]}; + padding-top: 16px; + cursor: pointer; +`; + +export const CardContentDate = styled.div` + ${font.regular12} + color: ${colors.gray[400]}; +`; + +export const CardContentDeleteButton = styled.div` + width: 40px; + height: 40px; + background-image: url("${DeleteIcon}"); + border-radius: 6px; + border: 1px solid ${colors.gray[300]}; + padding: 20px; + background-size: 24px 24px; + background-repeat: no-repeat; + background-position: center; + cursor: pointer; + &:hover { + background-color: ${colors.gray[200]}; + color: ${colors.gray[100]}; + } +`; + +export const CardPageDeleteButton = styled.div` + display: flex; + justify-content: center; + align-items: center; + width: 92px; + height: 39px; + border-radius: 6px; + border: 1px solid ${colors.gray[300]}; + background-color: ${colors.purple[600]}; + cursor: pointer; + color: #fff; + padding: 7px 16px; + ${font.regular16} + text-align: center; +`; From 157ad78addc0c75cd9b9da7f4813d4e9eb0230f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A0=95=EC=9B=90?= Date: Sat, 15 Nov 2025 07:04:00 +0900 Subject: [PATCH 64/91] =?UTF-8?q?Refactor:=20=EC=B9=B4=EB=93=9C=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=20=EB=B0=98=EC=9D=91=ED=98=95=20=EB=A6=AC?= =?UTF-8?q?=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/rolling/card-contents.jsx | 11 ++------- src/pages/rolling-page.jsx | 5 +--- src/styles/global-style.js | 1 + src/styles/rolling-page-styles.js | 29 ++++++++++++++++++++++-- 4 files changed, 31 insertions(+), 15 deletions(-) diff --git a/src/components/rolling/card-contents.jsx b/src/components/rolling/card-contents.jsx index c76a890..c6c3361 100644 --- a/src/components/rolling/card-contents.jsx +++ b/src/components/rolling/card-contents.jsx @@ -29,7 +29,7 @@ import DeleteConfirmModal from "./delete-confirm-modal"; */ export default function CardContents({ recipientId, isEditMode = false }) { const navigate = useNavigate(); - const { messages, loading, hasMore, fetchInitialData, fetchMoreData, refresh } = + const { messages, hasMore, fetchInitialData, fetchMoreData, refresh } = useInfiniteRecipientMessages(recipientId, isEditMode); // 삭제 액션 훅 @@ -105,13 +105,7 @@ export default function CardContents({ recipientId, isEditMode = false }) { 지인: "acquaintance", }; - if (loading && messages.length === 0) { - return ( - -
로딩 중...
-
- ); - } + return ( <> @@ -119,7 +113,6 @@ export default function CardContents({ recipientId, isEditMode = false }) { dataLength={messages.length} next={fetchMoreData} hasMore={hasMore} - loader={

로딩 중...

} endMessage={

모든 메시지를 확인했습니다 diff --git a/src/pages/rolling-page.jsx b/src/pages/rolling-page.jsx index 98c1592..00826ac 100644 --- a/src/pages/rolling-page.jsx +++ b/src/pages/rolling-page.jsx @@ -29,7 +29,7 @@ export default function RollingPage() { const isEditMode = useEditMode(); // API에서 수신자 데이터 가져오기 - const { recipient, loading, error } = useRecipient(recipientId); + const { recipient, error } = useRecipient(recipientId); // 삭제 액션 훅 const { handleDeleteRecipient } = useDeleteActions(); @@ -42,9 +42,6 @@ export default function RollingPage() { return

잘못된 페이지 주소입니다.
; } - if (loading) { - return
로딩 중123...
; - } if (error) { return
에러가 발생했습니다: {error}
; diff --git a/src/styles/global-style.js b/src/styles/global-style.js index 7086845..19ad8d1 100644 --- a/src/styles/global-style.js +++ b/src/styles/global-style.js @@ -12,6 +12,7 @@ export const GlobalStyle = createGlobalStyle`${css` #root { margin: 0; padding: 0; + } :root { diff --git a/src/styles/rolling-page-styles.js b/src/styles/rolling-page-styles.js index 157847d..3bb176b 100644 --- a/src/styles/rolling-page-styles.js +++ b/src/styles/rolling-page-styles.js @@ -296,14 +296,13 @@ const RollingPageWrapper = ({ $backgroundcolor, $backgroundimage, ...rest }) => export const RollingPageContainer = styled(RollingPageWrapper)` display: flex; justify-content: center; - align-items: center; background-color: ${(props) => props.$backgroundcolor || colors.blue[100]}; background-image: ${(props) => props.$backgroundimage ? `url(${props.$backgroundimage})` : "none"}; background-size: cover; background-repeat: no-repeat; width: 100%; - height: 100%; + min-height: 100vh; margin: 0 auto; padding: 63px 216px 113px 216px; gap: 11px; @@ -525,4 +524,30 @@ export const CardPageDeleteButton = styled.div` padding: 7px 16px; ${font.regular16} text-align: center; + + ${media.medium` + position: fixed; + bottom: 24px; + left: 50%; + transform: translateX(-50%); + width: calc(100% - 40px); + max-width: 768px; + height: 56px; + padding: 12px 16px; + z-index: 1003; + box-shadow: 0 -4px 16px rgba(0, 0, 0, 0.2); + `} + + ${media.small` + position: fixed; + bottom: 20px; + left: 50%; + transform: translateX(-50%); + width: calc(100% - 40px); + max-width: 384px; + height: 56px; + padding: 12px 16px; + z-index: 1003; + box-shadow: 0 -4px 16px rgba(0, 0, 0, 0.2); + `} `; From fefbd77a293ed816ad58327d6c9fbc9af45b0319 Mon Sep 17 00:00:00 2001 From: summerlane Date: Sat, 15 Nov 2025 21:34:55 +0900 Subject: [PATCH 65/91] =?UTF-8?q?Feat:=20=EC=BB=AC=EB=9F=AC,=20=EC=9D=B4?= =?UTF-8?q?=EB=AF=B8=EC=A7=80=20=EC=85=80=EB=A0=89=ED=8A=B8=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EA=B5=AC=ED=98=84=20=EB=B0=8F=20api=20post=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/common/toggle.jsx | 90 ++++++++++++++++++++----- src/pages/main-page.jsx | 29 ++++---- src/pages/post-page.jsx | 111 ++++++++++++++++++++++--------- 3 files changed, 166 insertions(+), 64 deletions(-) diff --git a/src/components/common/toggle.jsx b/src/components/common/toggle.jsx index 547ddd4..8f8f442 100644 --- a/src/components/common/toggle.jsx +++ b/src/components/common/toggle.jsx @@ -2,7 +2,8 @@ import styled from "styled-components"; import { colors } from "@/styles/colors"; import { font } from "@/styles/font"; import media from "@/styles/media"; -import { useState } from "react"; +import { useEffect, useState } from "react"; +import axios from "axios"; const ToggleSection = styled.div` width: 100%; @@ -79,6 +80,11 @@ const ToggleDiv = styled.div` border-radius: 16px; background-color: ${(props) => props.$bgColor}; cursor: pointer; + background-image: ${(props) => + props.$active ? "url(./src/assets/images/select-circle.webp)" : null}; + background-repeat: no-repeat; + background-size: 44px 44px; + background-position: center; ${media.small` flex: 1 1 40%; @@ -86,7 +92,7 @@ const ToggleDiv = styled.div` ${media.medium` flex: 1 1 40%; - `} + `}; `; const ToggleImgContainer = styled.div` @@ -102,9 +108,13 @@ const ToggleImg = styled.div` height: 168px; border: 1px solid rgba(0, 0, 0, 0.08); border-radius: 16px; - background-image: url(./src/assets/images/img-car.webp); + background-image: ${(props) => + props.$active + ? "url(./src/assets/images/select-circle.webp)" + : "url(${$bgImgs})"}, + url(${(props) => props.$bgImgs}); background-repeat: no-repeat; - background-size: 100%; + background-size: 44px 44px, 180%; background-position: center; cursor: pointer; @@ -126,12 +136,52 @@ const ImgSelect = styled.div` background-position: center; `; -export default function Toggle() { +export default function Toggle({ + bgColors, + isSelectDiv, + setIsSelectDiv, + isSelectImg, + setIsSelectImg, +}) { + // const bgColors = [ + // colors.beige[200], + // colors.purple[200], + // colors.blue[200], + // colors.green[200], + // ]; + + const [imgs, setImgs] = useState([]); const [toggle, setToggle] = useState(false); + // const [isSelectDiv, setIsSelectDiv] = useState(bgColors[0]); + // const [isSelectImg, setIsSelectImg] = useState(imgs[0]); + + useEffect(() => { + axios + .get("https://rolling-api.vercel.app/background-images/") + .then((response) => { + setImgs(response.data.imageUrls); + + if (response.data.imageUrls.length > 0) { + setIsSelectImg(response.data.imageUrls[0]); + } + }) + .catch((error) => { + console.error("배경 이미지 가져오기에 실패했습니다.", error); + }); + }, [setIsSelectImg]); const handleToggle = () => { setToggle(!toggle); }; + + const handleClickDiv = (bgColor) => { + setIsSelectDiv(bgColor); + }; + + const handleClickImg = (bgImg) => { + setIsSelectImg(bgImg); + }; + return ( <> @@ -145,21 +195,27 @@ export default function Toggle() {
{toggle === false ? ( - - - - - - + {bgColors.map((bgColor) => ( + handleClickDiv(bgColor)} + $active={isSelectDiv === bgColor} + $bgColor={bgColor} + /> + ))} ) : ( - - - - - - + {imgs.map((bgImg) => ( + handleClickImg(bgImg)} + $active={isSelectImg === bgImg} + $bgImgs={bgImg} + /> + ))} + {/* {isSelectImg === 1 && } + */} )}
diff --git a/src/pages/main-page.jsx b/src/pages/main-page.jsx index de50804..fe4e5f4 100644 --- a/src/pages/main-page.jsx +++ b/src/pages/main-page.jsx @@ -2,6 +2,8 @@ import styled from "styled-components"; import { colors } from "@/styles/colors"; import { font } from "@/styles/font"; import media from "@/styles/media"; +import Button from "@/components/common/button"; +import { Link } from "react-router"; const Container = styled.div` max-width: 1200px; @@ -147,28 +149,17 @@ const MainTitleSmall = styled.p` `} `; -const Button = styled.button` - width: 280px; +const CustomButton = styled(Button)` + width: 286px; height: 56px; - display: flex; - justify-content: center; - align-items: center; - margin: 0 auto; - border-radius: 12px; - background-color: ${colors.purple[600]}; - color: #ffffff; - ${font.bold18}; - margin-top: 28px; - border: none; - cursor: pointer; ${media.small` width: calc(100% - 40px); - `} + `} ${media.medium` - width: calc(100% - 48px); - `} + width: calc(100% - 48px); + `} `; export default function MainPage() { @@ -188,7 +179,11 @@ export default function MainPage() { 롤링 페이퍼에 이모지를 추가할 수 있어요. - + + + 구경해보기 + + ); } diff --git a/src/pages/post-page.jsx b/src/pages/post-page.jsx index bb8a312..197089b 100644 --- a/src/pages/post-page.jsx +++ b/src/pages/post-page.jsx @@ -4,6 +4,8 @@ import { font } from "@/styles/font"; import media from "@/styles/media"; import Toggle from "@/components/common/toggle"; import { useState } from "react"; +import Button from "@/components/common/button"; +import axios from "axios"; const Container = styled.div` max-width: 720px; @@ -50,47 +52,96 @@ const Input = styled.input` padding: 12px 16px; `; -const Button = styled.button` +const CustomButton = styled(Button)` width: 100%; height: 56px; - ${font.bold18}; - background-color: ${colors.purple[600]}; - color: #ffffff; - display: flex; - justify-content: center; - align-items: center; - margin: 0 auto; - border-radius: 12px; - border: 0; - cursor: pointer; - - ${media.small` - width: 100%; - `} - - ${media.medium` - width: 100%; - `} `; export default function PostPage() { const [name, setName] = useState(""); + const [buttonActive, setButtonActive] = useState(false); + + const bgColors = [ + colors.beige[200], + colors.purple[200], + colors.blue[200], + colors.green[200], + ]; + + const [isSelectDiv, setIsSelectDiv] = useState(bgColors[0]); + const [isSelectImg, setIsSelectImg] = useState(null); const handleInputName = (e) => { setName(e.target.value); + + setButtonActive(e.target.value.length > 0 ? true : false); + }; + + const handleSubmit = (e) => { + e.preventDefault(); + + const colorChangeToString = () => { + if (isSelectDiv === "#FFE2AD") { + return "beige"; + } + + if (isSelectDiv === "#ECD9FF") { + return "purple"; + } + + if (isSelectDiv === "#B1E4FF") { + return "blue"; + } + + if (isSelectDiv === "#D0F5C3") { + return "green"; + } + }; + + axios + .post("https://rolling-api.vercel.app/20-1/recipients/", { + name: name, + backgroundColor: colorChangeToString(), + backgroundImageURL: isSelectImg, + }) + .then((response) => { + console.log(response.data); + alert("전송을 성공하였습니다."); + }) + .catch((error) => { + console.error("전송에 실패하였습니다.", error); + }); + + setName(""); }; + return ( - - - To. - + + + To. + + + - - - - + + 생성하기 + + + ); } From 3d0ccea1163223a922410bd64fbfb96c3fffd0e3 Mon Sep 17 00:00:00 2001 From: yeseung00 Date: Sun, 16 Nov 2025 03:00:43 +0900 Subject: [PATCH 66/91] =?UTF-8?q?=20Feat:=20=ED=94=84=EB=A1=9C=ED=95=84=20?= =?UTF-8?q?=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=84=A0=ED=83=9D=ED=95=98?= =?UTF-8?q?=EA=B8=B0=20UI=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 프로필 이미지 파일을 선택하지 않으면 기본 이미지를 넣도록 함 - 프로필을 선택시 colors.purple[700] 색의 테두리가 보이도록 함 --- .../message/profile-image-selector.jsx | 148 ++++++++++++++++++ src/hooks/use-message-form.js | 128 +++++++++++---- src/hooks/use-profile-image.js | 61 ++++++++ src/pages/message-page.jsx | 99 ++---------- 4 files changed, 319 insertions(+), 117 deletions(-) create mode 100644 src/components/message/profile-image-selector.jsx create mode 100644 src/hooks/use-profile-image.js diff --git a/src/components/message/profile-image-selector.jsx b/src/components/message/profile-image-selector.jsx new file mode 100644 index 0000000..b387e85 --- /dev/null +++ b/src/components/message/profile-image-selector.jsx @@ -0,0 +1,148 @@ +import React from "react"; +import styled from "styled-components"; +import { colors } from "@/styles/colors"; +import { font } from "@/styles/font"; +import { DEFAULT_IMAGE_ID } from "@/hooks/use-profile-image"; +import defaultIcon from "@/assets/icons/person.svg"; + +const DEFAULT_ICON_URL = defaultIcon; + +const FormLabel = styled.label` + ${font.bold24} + line-height: 36px; + letter-spacing: -0.01em; + color: ${colors.gray[900]}; + margin: 0; + padding: 0; +`; + +const ProfileWrapper = styled.div` + display: flex; + flex-direction: column; + gap: 12px; +`; + +const SelectorPrompt = styled.p` + ${font.regular16}; + color: ${colors.gray[500]}; + margin: 0; + padding: 0; +`; + +const ProfileSelectorContainer = styled.div` + display: flex; + flex-direction: row; + align-items: flex-start; + gap: 32px; + padding: 4px 0; +`; + +const SelectorRightBlock = styled.div` + display: flex; + flex-direction: column; + gap: 12px; +`; + +const ProfileDefaultBox = styled.div` + width: 80px; + height: 80px; + border-radius: 50%; + overflow: hidden; + flex-shrink: 0; + background-color: ${colors.gray[300]}; + + border: 1px solid + ${(props) => (props.$isSelected ? colors.purple[700] : colors.gray[200])}; + ${(props) => props.$isSelected && `border-width: 2px;`} + + cursor: pointer; + transition: border 0.2s; + + display: flex; + justify-content: center; + align-items: center; + + img { + width: 32px; + height: 32px; + object-fit: contain; + border: none; + } +`; + +const SelectableImagesList = styled.ul` + display: flex; + flex-wrap: wrap; + gap: 0px; + + list-style: none; + margin: 0; + padding: 0; +`; + +const SelectableImageItem = styled.li` + width: 56px; + height: 56px; + border-radius: 50%; + overflow: hidden; + cursor: pointer; + flex-shrink: 0; + transition: border 0.2s; + + border: 2px solid + ${(props) => (props.$isSelected ? colors.purple[700] : "transparent")}; + + &:hover { + opacity: 0.8; + } + + img { + width: 100%; + height: 100%; + object-fit: cover; + border: none; + } +`; + +function ProfileImageSelector({ + selectedId, + onImageSelect, + selectableImages, + isLoading, + error, +}) { + return ( + + 프로필 이미지 + + onImageSelect(DEFAULT_IMAGE_ID)} + > + 기본 프로필 아이콘 + + + + 프로필 이미지를 선택해주세요! + + {isLoading &&

이미지를 불러오는 중입니다...

} + {error &&

이미지를 불러오는 데 실패했습니다.

} + {!isLoading && + !error && + selectableImages.map((image) => ( + onImageSelect(image.id)} + > + {`프로필 + + ))} +
+
+
+
+ ); +} + +export default ProfileImageSelector; diff --git a/src/hooks/use-message-form.js b/src/hooks/use-message-form.js index 07a9250..fa5f3ae 100644 --- a/src/hooks/use-message-form.js +++ b/src/hooks/use-message-form.js @@ -1,44 +1,103 @@ -import { useState } from "react"; -import useDropdown from "@/hooks/use-dropdown"; -import useFormInput from "@/hooks/use-from-input"; - -const RELATIONSHIP_OPTIONS = [ - { label: "지인", value: "지인" }, - { label: "친구", value: "친구" }, - { label: "동료", value: "동료" }, - { label: "가족", value: "가족" }, +import { useState, useMemo } from "react"; +import { useProfileImage } from "./use-profile-image"; + +export const RELATIONSHIP_OPTIONS = [ + { value: "지인", label: "지인" }, + { value: "친구", label: "친구" }, + { value: "가족", label: "가족" }, + { value: "동료", label: "동료" }, ]; -const FONT_OPTIONS = [{ label: "Noto Sans", value: "Noto Sans" }]; -export function useMessageForm() { - const fromInput = useFormInput(""); - const relationshipDropdown = useDropdown(RELATIONSHIP_OPTIONS[0].value); - const fontDropdown = useDropdown(FONT_OPTIONS[0].value); +export const FONT_OPTIONS = [{ value: "Noto Sans", label: "Noto Sans" }]; + +const useInput = (initialValue) => { + const [value, setValue] = useState(initialValue); + const [hasError, setHasError] = useState(false); + const [isTouched, setIsTouched] = useState(false); + + const handleChange = (e) => { + setValue(e.target.value); + if (isTouched && e.target.value.trim() !== "") { + setHasError(false); + } + }; + + const handleBlur = () => { + setIsTouched(true); + if (value.trim() === "") { + setHasError(true); + } else { + setHasError(false); + } + }; + + return { + value, + hasError, + handleChange, + handleBlur, + isTouched, + isValid: value.trim() !== "", + reset: () => { + setValue(initialValue); + setHasError(false); + setIsTouched(false); + }, + }; +}; + +export const useMessageForm = () => { + const fromInput = useInput(""); + + const [relationship, setRelationship] = useState( + RELATIONSHIP_OPTIONS[0].value + ); + const relationshipDropdown = { + value: relationship, + handleChange: (e) => setRelationship(e.target.value), + }; + + const [font, setFont] = useState(FONT_OPTIONS[0].value); + const fontDropdown = { + value: font, + handleChange: (e) => setFont(e.target.value), + }; + const [editorContent, setEditorContent] = useState(""); - const isContentValid = - editorContent.trim().length > 0 && editorContent !== "


"; - const isFormValid = fromInput.value.trim().length > 0 && isContentValid; + const { + selectedProfileImageId, + handleImageSelect, + selectableImages, + isLoading, + error, + } = useProfileImage(); + + const isFormValid = useMemo(() => { + const isFromValid = fromInput.isValid; + const isContentValid = editorContent.trim().length > 0; + + return isFromValid && isContentValid; + }, [fromInput.isValid, editorContent]); const handleSubmit = (e) => { e.preventDefault(); - if (!isFormValid) { - alert("모든 필수 항목을 입력해 주세요."); - return; - } + fromInput.handleBlur(); - const formData = { - from: fromInput.value, - relationship: relationshipDropdown.value, - content: editorContent, - font: fontDropdown.value, - }; + if (isFormValid) { + const formData = { + from: fromInput.value, + relationship: relationship, + font: font, + content: editorContent, + profileImageId: selectedProfileImageId, + }; - console.log("✅ 폼 데이터 제출 성공:", formData); - alert(` 메시지가 성공적으로 생성되었습니다.`); - - // TODO: 서버 API 호출 로직 구현 + console.log("Form Submitted:", formData); + } else { + console.log("Form validation failed."); + } }; return { @@ -47,9 +106,14 @@ export function useMessageForm() { fontDropdown, editorContent, setEditorContent, + selectedProfileImageId, + handleImageSelect, + selectableImages, + isLoading, + error, isFormValid, handleSubmit, RELATIONSHIP_OPTIONS, FONT_OPTIONS, }; -} +}; diff --git a/src/hooks/use-profile-image.js b/src/hooks/use-profile-image.js new file mode 100644 index 0000000..57ab31f --- /dev/null +++ b/src/hooks/use-profile-image.js @@ -0,0 +1,61 @@ +import { useState, useEffect } from "react"; + +import img1 from "@/assets/images/profile-img-01.webp"; +import img2 from "@/assets/images/profile-img-02.webp"; + +export const DEFAULT_IMAGE_ID = 0; + +/** + * 프로필 이미지 선택 상태와 로직 및 API 통신을 관리하는 커스텀 훅 + */ +export const useProfileImage = () => { + const [selectedProfileImageId, setSelectedProfileImageId] = + useState(DEFAULT_IMAGE_ID); + + const [selectableImages, setSelectableImages] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + + const handleImageSelect = (id) => { + setSelectedProfileImageId(id); + }; + + useEffect(() => { + const API_ENDPOINT = "/api/profile-images"; + + async function fetchImages() { + try { + const MOCK_IMAGES = [ + { id: 1, url: img2 }, + { id: 2, url: img1 }, + { id: 3, url: img2 }, + { id: 4, url: img1 }, + { id: 5, url: img2 }, + { id: 6, url: img1 }, + { id: 7, url: img2 }, + { id: 8, url: img1 }, + { id: 9, url: img2 }, + { id: 10, url: img1 }, + ]; + + setSelectableImages(MOCK_IMAGES); + setError(null); + } catch (err) { + setError(err.message); + console.error("Failed to fetch profile images:", err); + } finally { + setIsLoading(false); + } + } + fetchImages(); + }, []); + + return { + selectedProfileImageId, + handleImageSelect, + selectableImages, + DEFAULT_IMAGE_ID, + isLoading, + error, + }; +}; diff --git a/src/pages/message-page.jsx b/src/pages/message-page.jsx index 153e0ae..09ed23a 100644 --- a/src/pages/message-page.jsx +++ b/src/pages/message-page.jsx @@ -8,17 +8,7 @@ import FromInput from "@/components/message/from-input"; import { useMessageForm } from "@/hooks/use-message-form"; import RichTextEditor from "@/components/message/reach-text-editor"; -const DEFAULT_ICON_URL = "/assets/default-user.svg"; -const TEMP_IMAGE_URL = "/assets/temp-profile.jpg"; -const selectableImages = [ - { id: 1, url: TEMP_IMAGE_URL, isSelected: true }, - { id: 2, url: TEMP_IMAGE_URL, isSelected: false }, - { id: 3, url: TEMP_IMAGE_URL, isSelected: false }, - { id: 4, url: TEMP_IMAGE_URL, isSelected: false }, - { id: 5, url: TEMP_IMAGE_URL, isSelected: false }, - { id: 6, url: TEMP_IMAGE_URL, isSelected: false }, - { id: 7, url: TEMP_IMAGE_URL, isSelected: false }, -]; +import ProfileImageSelector from "@/components/message/profile-image-selector"; export const FormInputStyle = css` width: 100%; @@ -76,64 +66,6 @@ export const ErrorMessage = styled.p` margin-top: -8px; `; -export const ProfileWrapper = styled.div` - display: flex; - flex-direction: column; - gap: 12px; -`; - -export const ProfileSelectorContainer = styled.div` - display: flex; - align-items: center; - gap: 32px; -`; - -export const ProfileDefaultBox = styled.div` - width: 70px; - height: 70px; - border-radius: 50%; - overflow: hidden; - flex-shrink: 0; - border: 1px solid ${colors.gray[300]}; - - img { - width: 100%; - height: 100%; - object-fit: cover; - } -`; - -export const SelectableImagesList = styled.ul` - display: flex; - gap: 4px; - overflow-x: auto; - padding: 4px 0; - -webkit-overflow-scrolling: touch; - list-style: none; - margin: 0; -`; - -export const SelectableImageItem = styled.li` - width: 56px; - height: 56px; - border-radius: 50%; - overflow: hidden; - cursor: pointer; - flex-shrink: 0; - transition: transform 0.2s, border 0.2s; - - border: 2px solid transparent; - &:hover { - opacity: 0.8; - } - - img { - width: 100%; - height: 100%; - object-fit: cover; - } -`; - const FullWidthButton = styled(Button)` width: 100%; margin-top: 20px; @@ -150,6 +82,11 @@ function MessagePage() { handleSubmit, RELATIONSHIP_OPTIONS, FONT_OPTIONS, + selectedProfileImageId, + handleImageSelect, + selectableImages, + isLoading, + error, } = useMessageForm(); return ( @@ -170,22 +107,14 @@ function MessagePage() { />
- {/* 프로필 이미지 선택창 */} - - 프로필 이미지 - - - 기본 프로필 이미지 - - - {selectableImages.map((image) => ( - - {`프로필 - - ))} - - - + {/* 프로필 이미지 선택 */} + {/* 상대와의 관계 드롭다운*/} From cc249f38fabf734e695cef7d76689805d408dced Mon Sep 17 00:00:00 2001 From: yeseung00 Date: Sun, 16 Nov 2025 03:23:06 +0900 Subject: [PATCH 67/91] =?UTF-8?q?Fix:=20Quill=20=EC=9E=84=ED=8F=AC?= =?UTF-8?q?=ED=8A=B8=20=EB=B0=8F=20=EB=93=B1=EB=A1=9D=20=EB=AC=B8=EC=A0=9C?= =?UTF-8?q?=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/message/reach-text-editor.jsx | 64 ++++++++++---------- 1 file changed, 31 insertions(+), 33 deletions(-) diff --git a/src/components/message/reach-text-editor.jsx b/src/components/message/reach-text-editor.jsx index 069e683..27abe38 100644 --- a/src/components/message/reach-text-editor.jsx +++ b/src/components/message/reach-text-editor.jsx @@ -1,20 +1,12 @@ -import React from "react"; +import React, { useMemo } from "react"; import ReactQuill from "react-quill-new"; + import "react-quill-new/dist/quill.snow.css"; import styled from "styled-components"; + import { font } from "@/styles/font"; import { colors } from "@/styles/colors"; -import Quill from "quill"; - -const toolbar = Quill.import("modules/toolbar"); -const list = Quill.import("formats/list"); - -if (list) { - Quill.register(list, true); -} -if (toolbar) { -} const EditorContainer = styled.div` min-height: 243px; @@ -54,30 +46,36 @@ const EditorContainer = styled.div` `; function RichTextEditor({ value, onChange }) { - const modules = { - toolbar: [ - ["bold", "italic", "underline"], - [ - { align: "" }, - { align: "center" }, - { align: "right" }, - { align: "justify" }, + const modules = useMemo( + () => ({ + toolbar: [ + ["bold", "italic", "underline"], + [ + { align: "" }, + { align: "center" }, + { align: "right" }, + { align: "justify" }, + ], + [{ list: "ordered" }, { list: "bullet" }], + ["link", "image"], ], - [{ list: "ordered" }, { list: "bullet" }], - ["link", "image"], - ], - }; + }), + [] + ); - const formats = [ - "bold", - "italic", - "underline", - "align", - "list", - "bullet", - "link", - "image", - ]; + const formats = useMemo( + () => [ + "bold", + "italic", + "underline", + "align", + "list", + "bullet", + "link", + "image", + ], + [] + ); return ( From 84d0ba3a71f012519322cb09cc2a2299fd175ba6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A0=95=EC=9B=90?= Date: Mon, 17 Nov 2025 11:14:51 +0900 Subject: [PATCH 68/91] =?UTF-8?q?Feat:=20=EC=B9=B4=EB=93=9C=20=ED=85=8D?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=203=EC=A4=84=20=EC=9D=B4=EC=83=81=20?= =?UTF-8?q?=EB=84=98=EC=96=B4=EA=B0=88=20=EA=B2=BD=EC=9A=B0=20...=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/common/header.jsx | 12 ++++++++++-- src/components/common/toast.jsx | 4 +++- src/components/rolling/card-detail-modal.jsx | 4 ++-- src/components/rolling/participant-stats.jsx | 2 +- ...ge-head.jsx => rolling-head-emoji-share.jsx} | 0 src/hooks/use-profile-images.js | 2 +- src/styles/rolling-page-styles.js | 17 +++++++++++------ 7 files changed, 28 insertions(+), 13 deletions(-) rename src/components/rolling/{rolling-page-head.jsx => rolling-head-emoji-share.jsx} (100%) diff --git a/src/components/common/header.jsx b/src/components/common/header.jsx index ccb1485..a4ef1c8 100644 --- a/src/components/common/header.jsx +++ b/src/components/common/header.jsx @@ -1,7 +1,8 @@ -import { Link } from "react-router"; +import { Link, useLocation } from "react-router"; import styled from "styled-components"; import logo from "@/assets/icons/logo.svg"; import Button from "@/components/common/button"; +import media from "@/styles/media"; const ContainWrapper = styled.div` position: sticky; @@ -9,6 +10,10 @@ const ContainWrapper = styled.div` background-color: white; border-bottom: 1px solid #ededed; z-index: 1003; + + ${props => props.$isRollingPage && media.small` + display: none; + `} `; const Contain = styled.div` @@ -35,9 +40,12 @@ const ButtonWrapper = styled.div` `; export default function Header({ showButton }) { + const location = useLocation(); + const isRollingPage = location.pathname.startsWith('/post/'); + return ( <> - + diff --git a/src/components/common/toast.jsx b/src/components/common/toast.jsx index 08a58a5..7ab8c90 100644 --- a/src/components/common/toast.jsx +++ b/src/components/common/toast.jsx @@ -55,8 +55,10 @@ const ToastStyle = styled.div` ${media.medium` width: 524px; + position: absolute; left: calc(50% - 262px); - bottom: 50px; + bottom: 10%; + transform: translate(-50%, 50%); `} ${media.small` diff --git a/src/components/rolling/card-detail-modal.jsx b/src/components/rolling/card-detail-modal.jsx index b3e353c..735d98a 100644 --- a/src/components/rolling/card-detail-modal.jsx +++ b/src/components/rolling/card-detail-modal.jsx @@ -51,8 +51,8 @@ const RelationshipBadge = styled.div` const MessageContent = styled.div` ${font.regular18} - color: #5A5A5A; - line-height: 1.6; + color:${colors.gray[600]}; + line-height: 24px; white-space: pre-wrap; word-break: break-word; overflow-y: auto; diff --git a/src/components/rolling/participant-stats.jsx b/src/components/rolling/participant-stats.jsx index 39c2b56..d6f0f7a 100644 --- a/src/components/rolling/participant-stats.jsx +++ b/src/components/rolling/participant-stats.jsx @@ -7,7 +7,7 @@ import { RollingHeaderUserPeopleState } from "@/styles/rolling-page-styles"; */ export default function ParticipantStats({ count }) { if (count === 0) { - return 아직 작성한 사람이 없어요; + return 작성한 사람이 없어요!; } return ( diff --git a/src/components/rolling/rolling-page-head.jsx b/src/components/rolling/rolling-head-emoji-share.jsx similarity index 100% rename from src/components/rolling/rolling-page-head.jsx rename to src/components/rolling/rolling-head-emoji-share.jsx diff --git a/src/hooks/use-profile-images.js b/src/hooks/use-profile-images.js index 0a886c9..34f011d 100644 --- a/src/hooks/use-profile-images.js +++ b/src/hooks/use-profile-images.js @@ -19,7 +19,7 @@ export default function useProfileImages(totalCount, profiles, maxVisible = 3) { const hasOverflow = totalCount > maxVisible; // 오버플로우가 있으면 마지막 자리는 +N 표시용으로 비움 - const visibleCount = hasOverflow ? maxVisible - 1 : maxVisible; + const visibleCount = 3 const visibleProfiles = profiles.slice(0, visibleCount); const overflowCount = hasOverflow ? totalCount - visibleCount : 0; diff --git a/src/styles/rolling-page-styles.js b/src/styles/rolling-page-styles.js index 3bb176b..4f201e5 100644 --- a/src/styles/rolling-page-styles.js +++ b/src/styles/rolling-page-styles.js @@ -403,7 +403,8 @@ export const CardContentContainer = styled.div` justify-content: center; width: 100%; height: 100%; - padding: 16px 24px; + gap: 15px; + padding: 28px 24px; `; export const CardContentStatus = styled.div` @@ -481,11 +482,17 @@ export const CardContentStatusRelationship = styled.div` export const CardContentText = styled.div` width: 100%; - height: 100%; - ${font.regular16} + height: 40%; + ${font.regular18} color: ${colors.gray[600]}; - padding-top: 16px; cursor: pointer; + line-height: 28px; + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + `; export const CardContentDate = styled.div` @@ -531,7 +538,6 @@ export const CardPageDeleteButton = styled.div` left: 50%; transform: translateX(-50%); width: calc(100% - 40px); - max-width: 768px; height: 56px; padding: 12px 16px; z-index: 1003; @@ -544,7 +550,6 @@ export const CardPageDeleteButton = styled.div` left: 50%; transform: translateX(-50%); width: calc(100% - 40px); - max-width: 384px; height: 56px; padding: 12px 16px; z-index: 1003; From f1318cb62a10473b4e09be258517fb44fa5fd79d Mon Sep 17 00:00:00 2001 From: yeseung00 Date: Mon, 17 Nov 2025 11:21:20 +0900 Subject: [PATCH 69/91] =?UTF-8?q?Feat:=20=ED=94=84=EB=A1=9C=ED=95=84=20?= =?UTF-8?q?=EC=9D=B4=EB=AF=B8=EC=A7=80=20API=20=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/use-message-form.js | 218 ++++++++++++++++++++++----------- src/hooks/use-profile-image.js | 4 +- 2 files changed, 145 insertions(+), 77 deletions(-) diff --git a/src/hooks/use-message-form.js b/src/hooks/use-message-form.js index fa5f3ae..64f6073 100644 --- a/src/hooks/use-message-form.js +++ b/src/hooks/use-message-form.js @@ -1,119 +1,189 @@ -import { useState, useMemo } from "react"; -import { useProfileImage } from "./use-profile-image"; +import { useState, useMemo, useEffect } from "react"; +import axios from "axios"; -export const RELATIONSHIP_OPTIONS = [ +// API 호스트 루트 +const BASE_URL = "https://rolling-api.vercel.app"; + +const FONT_OPTIONS = [{ value: "Noto Sans", label: "Noto Sans" }]; + +const RELATIONSHIP_OPTIONS = [ { value: "지인", label: "지인" }, { value: "친구", label: "친구" }, { value: "가족", label: "가족" }, { value: "동료", label: "동료" }, ]; -export const FONT_OPTIONS = [{ value: "Noto Sans", label: "Noto Sans" }]; - -const useInput = (initialValue) => { +// 유효성 검사 훅 (이전과 동일) +const useInput = (initialValue, validate) => { const [value, setValue] = useState(initialValue); - const [hasError, setHasError] = useState(false); const [isTouched, setIsTouched] = useState(false); + const isValid = validate(value); + const hasError = isTouched && !isValid; + const handleChange = (e) => { setValue(e.target.value); - if (isTouched && e.target.value.trim() !== "") { - setHasError(false); - } }; const handleBlur = () => { setIsTouched(true); - if (value.trim() === "") { - setHasError(true); - } else { - setHasError(false); - } }; - return { - value, - hasError, - handleChange, - handleBlur, - isTouched, - isValid: value.trim() !== "", - reset: () => { - setValue(initialValue); - setHasError(false); - setIsTouched(false); - }, - }; + return { value, isValid, hasError, handleChange, handleBlur }; }; +// 메시지 폼 및 Axios API 로직 + export const useMessageForm = () => { - const fromInput = useInput(""); + // 상태 정의 - const [relationship, setRelationship] = useState( - RELATIONSHIP_OPTIONS[0].value + const fromInput = useInput("", (val) => val.trim().length > 0); + const relationshipDropdown = useInput( + RELATIONSHIP_OPTIONS[0].value, + (val) => val.trim().length > 0 + ); + const fontDropdown = useInput( + FONT_OPTIONS[0].value, + (val) => val.trim().length > 0 ); - const relationshipDropdown = { - value: relationship, - handleChange: (e) => setRelationship(e.target.value), - }; - - const [font, setFont] = useState(FONT_OPTIONS[0].value); - const fontDropdown = { - value: font, - handleChange: (e) => setFont(e.target.value), - }; - const [editorContent, setEditorContent] = useState(""); - const { - selectedProfileImageId, - handleImageSelect, - selectableImages, - isLoading, - error, - } = useProfileImage(); - - const isFormValid = useMemo(() => { - const isFromValid = fromInput.isValid; - const isContentValid = editorContent.trim().length > 0; - - return isFromValid && isContentValid; - }, [fromInput.isValid, editorContent]); + const [selectedProfileImageId, setSelectedProfileImageId] = useState(0); + const [isSubmitting, setIsSubmitting] = useState(false); // POST 요청 상태 + + // 이미지 GET 요청 상태 + const [selectableImages, setSelectableImages] = useState([]); + const [isImagesLoading, setIsImagesLoading] = useState(true); + const [imagesError, setImagesError] = useState(null); + + const isEditorContentValid = editorContent.trim().length > 0; + + const isFormValid = useMemo( + () => + fromInput.isValid && + relationshipDropdown.isValid && + fontDropdown.isValid && + isEditorContentValid, + [ + fromInput.isValid, + relationshipDropdown.isValid, + fontDropdown.isValid, + isEditorContentValid, + ] + ); - const handleSubmit = (e) => { + // [GET 요청] 프로필 이미지 목록 가져오기 + + useEffect(() => { + const PROFILE_API_URL = `${BASE_URL}/profile-images/`; + + const fetchImages = async () => { + setIsImagesLoading(true); + setImagesError(null); + + try { + console.log(`[API CALL] GET 요청 시작: ${PROFILE_API_URL}`); + + // axios.get 사용 + const response = await axios.get(PROFILE_API_URL); + const data = response.data; + + // 응답 데이터 (imageUrls 배열)를 변환하여 상태에 저장 + const images = data.imageUrls.map((url, index) => ({ + id: index + 1, + url: url, + })); + + setSelectableImages(images); + // 첫 번째 이미지를 기본값으로 선택 + if (images.length > 0 && selectedProfileImageId === 0) { + setSelectedProfileImageId(images[0].id); + } + } catch (err) { + console.error("프로필 이미지 GET 요청 실패:", err); + // Axios 에러 객체에서 메시지 추출 + const errorMessage = err.response?.data?.message || err.message; + setImagesError(new Error(errorMessage)); + setSelectableImages([]); + } finally { + setIsImagesLoading(false); + } + }; + + fetchImages(); + }, []); + + // [POST 요청] 폼 데이터 전송 (Axios 사용) + + const handleSubmit = async (e) => { e.preventDefault(); - fromInput.handleBlur(); + if (!isFormValid || isSubmitting) { + fromInput.handleBlur(); + return false; + } - if (isFormValid) { - const formData = { - from: fromInput.value, - relationship: relationship, - font: font, - content: editorContent, - profileImageId: selectedProfileImageId, - }; + // 프로필 이미지가 선택되었는지 확인 + if (selectedProfileImageId === 0) { + alert("프로필 이미지를 선택해 주세요."); + return false; + } - console.log("Form Submitted:", formData); - } else { - console.log("Form validation failed."); + setIsSubmitting(true); + + // 선택된 이미지의 URL을 찾습니다. + const selectedImage = selectableImages.find( + (img) => img.id === selectedProfileImageId + ); + + const formData = { + // API 요구사항에 맞춰 필드 이름을 구성합니다. + sender: fromInput.value, + relationship: relationshipDropdown.value, + font: fontDropdown.value, + content: editorContent, + profileImageURL: selectedImage?.url || null, + createdAt: new Date().toISOString(), + }; + + // 롤링페이퍼 ID가 필요하다면 이 훅을 사용하는 컴포넌트에서 recipientId를 주입해야 합니다. + const POST_API_URL = `${BASE_URL}/messages/`; + + try { + console.log(`[API CALL] POST 요청 시작: ${POST_API_URL}`); + + // axios.post 사용 + const response = await axios.post(POST_API_URL, formData); + + console.log("롤링페이퍼 생성 성공. 서버 응답:", response.data); + return true; + } catch (error) { + console.error("롤링페이퍼 생성 중 API 오류 발생:", error); + // Axios 에러 객체에서 메시지 추출 + const errorMessage = error.response?.data?.message || error.message; + alert(`롤링페이퍼 생성에 실패했습니다: ${errorMessage}`); + return false; + } finally { + setIsSubmitting(false); } }; + // 반환 값 return { fromInput, relationshipDropdown, fontDropdown, editorContent, setEditorContent, - selectedProfileImageId, - handleImageSelect, - selectableImages, - isLoading, - error, isFormValid, handleSubmit, RELATIONSHIP_OPTIONS, FONT_OPTIONS, + selectedProfileImageId, + handleImageSelect: setSelectedProfileImageId, + selectableImages, + // 이미지 로딩 중이거나 제출 로딩 중이라면 true + isLoading: isImagesLoading || isSubmitting, + error: imagesError, }; }; diff --git a/src/hooks/use-profile-image.js b/src/hooks/use-profile-image.js index 57ab31f..dcc8917 100644 --- a/src/hooks/use-profile-image.js +++ b/src/hooks/use-profile-image.js @@ -5,9 +5,7 @@ import img2 from "@/assets/images/profile-img-02.webp"; export const DEFAULT_IMAGE_ID = 0; -/** - * 프로필 이미지 선택 상태와 로직 및 API 통신을 관리하는 커스텀 훅 - */ +/* 프로필 이미지 선택 상태와 로직 및 API 통신을 관리하는 커스텀 훅 */ export const useProfileImage = () => { const [selectedProfileImageId, setSelectedProfileImageId] = useState(DEFAULT_IMAGE_ID); From 7118beb10ef0c07605deb9e3a043c07825a64f8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A0=95=EC=9B=90?= Date: Fri, 7 Nov 2025 05:47:37 +0900 Subject: [PATCH 70/91] =?UTF-8?q?Feat:=20=EB=A1=A4=EB=A7=81=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=ED=8D=BC=20=ED=97=A4=EB=8D=94=20UI=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .vscode/settings.json | 9 +- src/App.jsx | 2 +- src/components/head-nav.jsx | 9 ++ src/pages/rolling-page.jsx | 87 ++++++++++++++++ src/styles/head-nav-style.js | 12 +++ src/styles/rolling-page-styles.js | 167 ++++++++++++++++++++++++++++++ 6 files changed, 281 insertions(+), 5 deletions(-) create mode 100644 src/components/head-nav.jsx create mode 100644 src/pages/rolling-page.jsx create mode 100644 src/styles/head-nav-style.js create mode 100644 src/styles/rolling-page-styles.js diff --git a/.vscode/settings.json b/.vscode/settings.json index c86e487..62d54bc 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,11 +5,12 @@ "editor.codeActionsOnSave": { "source.fixAll.eslint": "explicit" }, - // 린트 적용 - "eslint.validate": ["javascript", "javascriptreact"], - + "eslint.validate": [ + "javascript", + "javascriptreact" + ], // JavaScript 관련 "javascript.preferences.importModuleSpecifier": "non-relative", "javascript.updateImportsOnFileMove.enabled": "always" -} +} \ No newline at end of file diff --git a/src/App.jsx b/src/App.jsx index f9403a6..08b96e5 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -17,7 +17,7 @@ function App() { } /> {/*} /> } /> - } /> + } /> } /> */} } /> } /> diff --git a/src/components/head-nav.jsx b/src/components/head-nav.jsx new file mode 100644 index 0000000..7f22bb5 --- /dev/null +++ b/src/components/head-nav.jsx @@ -0,0 +1,9 @@ +import { HeadNavContainer } from "@/styles/head-nav-style"; + +export default function HeadNav() { + return ( + +

navigation

+
+ ); +} \ No newline at end of file diff --git a/src/pages/rolling-page.jsx b/src/pages/rolling-page.jsx new file mode 100644 index 0000000..fa357c8 --- /dev/null +++ b/src/pages/rolling-page.jsx @@ -0,0 +1,87 @@ +import { + RollingHeaderContainer, + RollingHeaderUserInfo, + RollingHeaderRightContainer, + RollingHeaderUserPeopleContainer, + RollingHeaderUserPeopleImages, + RollingHeaderUserPeopleImage, + RollingHeaderUserDefaultImage, + RollingHeaderUserPeopleState, + RollingHeaderImojiContainer, + RollingHeaderArrowDown, + PerpendicularLine, + RollingHeaderImojiIconContainer, + RollingHeaderImojiText, + RollingHeaderImojiIcon, + RollingHeaderImojiEditButton, + RollingHeaderImojiEditButtonIcon, + RollingHeaderImojiEditButtonText, + RollingHeaderLinkShareButton, + RollingPageContainer, +} from "@/styles/rolling-page-styles"; +import HeadNav from "@/components/head-nav"; +import ArrowDownIcon from "@/assets/icons/arrow-down.svg"; +import AddEmojiIcon from "@/assets/icons/add-emoji.svg"; +import ShareIcon from "@/assets/icons/share.svg"; + +export default function RollingPage() { + return ( + <> + + + + To. Ashley Kim + + + + + {/* //여기에서 함수를 불러와서 처리해야함 */} + + + + + + + + + 23명이 작성 했어요! + + + + + + + {/* //여기에서 함수를 불러와서 처리해야함 */} + + 😘 + 12 + + + 😘 + 12 + + + 😘 + 12 + + + + + 추가 + + + + + + + + + + + + + + ); +} + + diff --git a/src/styles/head-nav-style.js b/src/styles/head-nav-style.js new file mode 100644 index 0000000..e952219 --- /dev/null +++ b/src/styles/head-nav-style.js @@ -0,0 +1,12 @@ +import styled from "styled-components"; +import { colors } from "@/styles/colors"; + + +export const HeadNavContainer = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + height: 65px; + background-color: #fff; + border-bottom: 1px solid ${colors.gray[200]}; +`; \ No newline at end of file diff --git a/src/styles/rolling-page-styles.js b/src/styles/rolling-page-styles.js new file mode 100644 index 0000000..22ae3e7 --- /dev/null +++ b/src/styles/rolling-page-styles.js @@ -0,0 +1,167 @@ +import styled from "styled-components"; +import { colors } from "@/styles/colors"; +import { font } from "@/styles/font"; +import ShareIcon from "@/assets/icons/share.svg"; +//최상단헤더 컨테이너 +export const RollingHeaderContainer = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + width: 1200px; + margin: 0 auto; + padding: 13px 20px; + height: 65px; + background-color: background: rgba(255, 255, 255, 1); + +`; + +//유저 정보 컨테이너 TO. Ashley Kim +export const RollingHeaderUserInfo = styled.div` + display: flex; + align-items: center; + width: 227px; + height: 42px; + line-height: 42px; + letter-spacing: -1%; + ${font.bold28} + color: ${colors.gray[800]}; +`; + + +export const RollingHeaderRightContainer = styled.div` + display: flex; + align-items: center; + gap: 20px; +`; + + + +//유저 이미지 컨테이너 프로필 사진들과, 몇명이 작성중인지 표시 +export const RollingHeaderUserPeopleContainer = styled.div` + width: 228px; + display: flex; + align-items: center; + background-color: #fff; +`; + +//유저 이미지 프로필 사진들 +export const RollingHeaderUserPeopleImages = styled.div` + width: 76px; + height: 28px; + position: relative; + cursor: pointer; + +`; + +export const RollingHeaderUserPeopleImage = styled.img` + width: 28px; + height: 28px; + border-radius: 140px; + border: 1.4px solid #000; + position: relative; + margin-left: -10px; +`; + +export const RollingHeaderUserDefaultImage = styled(RollingHeaderUserPeopleImage)``; + +//몇명이 작성중인지 +export const RollingHeaderUserPeopleState = styled.div` + width: 160px; + height: 27px; + line-height: 27px; + ${font.bold18} + color: ${colors.gray[900]}; + text-align: center; +`; + +//이모지 컨테이너 드롭박스 포함, 추가 버튼 포함 +export const RollingHeaderImojiContainer = styled.div` + display: flex; + align-items: center; + gap: 8px; +`; + +export const RollingHeaderImojiIconContainer = styled.div` + display: inline-flex; + align-items: center; + justify-content: center; + width: auto; + height: 36px; + padding: 8px 12px; + text-align: center; + border-radius: 32px; + background: rgba(153, 153, 153, 1); + +`; + +export const RollingHeaderImojiIcon = styled.div` + display: flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + color: rgba(255, 255, 255, 1); +`; + +export const RollingHeaderImojiText = styled.span` + ${font.regular16} + color: rgba(255, 255, 255, 1) +`; + +export const RollingHeaderImojiEditButton = styled.button` + display: flex; + align-items: center; + justify-content: center; + gap: 7px; + width: 88px; + height: 36px; + border-radius: 6px; + background: #fff; + border: 1px solid ${colors.gray[300]}; + cursor: pointer; +`; + +export const RollingHeaderImojiEditButtonIcon = styled.img` + width: 20px; + height: 20px; +`; + +export const RollingHeaderImojiEditButtonText = styled.span` + ${font.regular16} + color: ${colors.gray[900]}; +`; + +export const RollingHeaderLinkShareButton = styled.div` + width: 56px; + height: 36px; + border-radius: 6px; + background-image: url("${ShareIcon}"); + background-size: 24px 24px; + background-repeat: no-repeat; + background-position: center; + padding: 12px 32px; + cursor: pointer; + border: 1px solid ${colors.gray[300]}; + +`; + +export const RollingHeaderArrowDown = styled.img` + width: 24px; + height: 24px; + cursor: pointer; +`; + +export const PerpendicularLine = styled.div` + border-left : 1px solid ${colors.gray[200]}; + height: 28px; +`; + + + +export const RollingPageContainer = styled.div` + background-color: ${colors.blue[100]}; + width: 100%; + margin: 0 auto; + padding: 20px; + height: 100vh; +`; \ No newline at end of file From 424f3eaf3385d8eede46cd65a455c2fd6e5ffb7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A0=95=EC=9B=90?= Date: Fri, 7 Nov 2025 10:25:46 +0900 Subject: [PATCH 71/91] =?UTF-8?q?Feat:=20=EB=A1=A4=EB=A7=81=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=ED=8D=BC=20=ED=97=A4=EB=8D=94=20UI=20=ED=83=9C?= =?UTF-8?q?=EB=B8=94=EB=A6=BF,=EB=AA=A8=EB=B0=94=EC=9D=BC=20=EB=B0=98?= =?UTF-8?q?=EC=9D=91=ED=98=95=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/rolling-page.jsx | 24 ++-- src/styles/head-nav-style.js | 8 +- src/styles/rolling-page-styles.js | 175 +++++++++++++++++++++++++++--- 3 files changed, 181 insertions(+), 26 deletions(-) diff --git a/src/pages/rolling-page.jsx b/src/pages/rolling-page.jsx index fa357c8..9aacc03 100644 --- a/src/pages/rolling-page.jsx +++ b/src/pages/rolling-page.jsx @@ -9,10 +9,12 @@ import { RollingHeaderUserPeopleState, RollingHeaderImojiContainer, RollingHeaderArrowDown, - PerpendicularLine, + PerpendicularLineFirst, + PerpendicularLineSecond, RollingHeaderImojiIconContainer, RollingHeaderImojiText, RollingHeaderImojiIcon, + RollingHeaderImojiEditButtonContainer, RollingHeaderImojiEditButton, RollingHeaderImojiEditButtonIcon, RollingHeaderImojiEditButtonText, @@ -24,6 +26,7 @@ import ArrowDownIcon from "@/assets/icons/arrow-down.svg"; import AddEmojiIcon from "@/assets/icons/add-emoji.svg"; import ShareIcon from "@/assets/icons/share.svg"; + export default function RollingPage() { return ( <> @@ -49,7 +52,7 @@ export default function RollingPage() { - + {/* //여기에서 함수를 불러와서 처리해야함 */} @@ -65,13 +68,18 @@ export default function RollingPage() { 12 - - - 추가 - + + + + 추가 + + + + + + + - - diff --git a/src/styles/head-nav-style.js b/src/styles/head-nav-style.js index e952219..17d8de0 100644 --- a/src/styles/head-nav-style.js +++ b/src/styles/head-nav-style.js @@ -1,5 +1,6 @@ import styled from "styled-components"; import { colors } from "@/styles/colors"; +import media from "@/styles/media"; export const HeadNavContainer = styled.div` @@ -9,4 +10,9 @@ export const HeadNavContainer = styled.div` height: 65px; background-color: #fff; border-bottom: 1px solid ${colors.gray[200]}; -`; \ No newline at end of file + + ${media.small` + display: none; + `} + +`; diff --git a/src/styles/rolling-page-styles.js b/src/styles/rolling-page-styles.js index 22ae3e7..eae18ee 100644 --- a/src/styles/rolling-page-styles.js +++ b/src/styles/rolling-page-styles.js @@ -2,6 +2,9 @@ import styled from "styled-components"; import { colors } from "@/styles/colors"; import { font } from "@/styles/font"; import ShareIcon from "@/assets/icons/share.svg"; +import media from "@/styles/media"; + + //최상단헤더 컨테이너 export const RollingHeaderContainer = styled.div` display: flex; @@ -10,21 +13,72 @@ export const RollingHeaderContainer = styled.div` width: 1200px; margin: 0 auto; padding: 13px 20px; - height: 65px; - background-color: background: rgba(255, 255, 255, 1); + height: 68px; + background-color: rgba(255, 255, 255, 1); + gap: 20px; + + ${media.large` + width: 1200px; + height: 68px; + margin: 0 auto; + padding: 13px 20px; + overflow-x: auto; + overflow-y: hidden; + gap: 20px; + `} + + ${media.medium` + width: 100%; + height: 68px; + margin: 0; + padding: 13px 20px; + overflow-x: auto; + overflow-y: hidden; + gap: 10px; + `} + + ${media.small` + flex-direction: column; + align-items: center; + height: auto; + width: 100%; + padding: 0px; + gap: 0px; + + `} `; + + + //유저 정보 컨테이너 TO. Ashley Kim export const RollingHeaderUserInfo = styled.div` display: flex; align-items: center; - width: 227px; + min-width: 227px; height: 42px; line-height: 42px; letter-spacing: -1%; ${font.bold28} color: ${colors.gray[800]}; + flex-shrink: 0; + + ${media.medium` + min-width: 150px; + height: 42px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + `} + + ${media.small` + width: 100%; + min-width: auto; + height: auto; + padding: 12px 20px; + border-bottom: 1px solid ${colors.gray[200]}; + `} `; @@ -32,6 +86,24 @@ export const RollingHeaderRightContainer = styled.div` display: flex; align-items: center; gap: 20px; + flex-shrink: 1; + min-width: 0; + + ${media.medium` + gap: 8px; + flex-shrink: 1; + min-width: 0; + + `} + + ${media.small` + width: 100%; + padding: 8px 20px; + + + `} + + `; @@ -41,7 +113,14 @@ export const RollingHeaderUserPeopleContainer = styled.div` width: 228px; display: flex; align-items: center; - background-color: #fff; + + ${media.medium` + display: none; + `} + + ${media.small` + display: none; + `} `; //유저 이미지 프로필 사진들 @@ -50,6 +129,15 @@ export const RollingHeaderUserPeopleImages = styled.div` height: 28px; position: relative; cursor: pointer; + ${media.medium` + + display: none; + `} + + ${media.small` + + display: none; + `} `; @@ -72,6 +160,14 @@ export const RollingHeaderUserPeopleState = styled.div` ${font.bold18} color: ${colors.gray[900]}; text-align: center; + ${media.medium` + + display: none; + `} + + ${media.small` + display: none; + `} `; //이모지 컨테이너 드롭박스 포함, 추가 버튼 포함 @@ -79,28 +175,38 @@ export const RollingHeaderImojiContainer = styled.div` display: flex; align-items: center; gap: 8px; + `; export const RollingHeaderImojiIconContainer = styled.div` - display: inline-flex; + display: flex; align-items: center; justify-content: center; width: auto; - height: 36px; + height: auto; padding: 8px 12px; text-align: center; border-radius: 32px; background: rgba(153, 153, 153, 1); + gap: 2px; + + ${media.small` + padding: 4px 8px; + `} `; export const RollingHeaderImojiIcon = styled.div` - display: flex; - align-items: center; - justify-content: center; + width: 24px; height: 24px; color: rgba(255, 255, 255, 1); + + ${media.small` + width: 20px; + height: 24px; + `} + `; export const RollingHeaderImojiText = styled.span` @@ -119,6 +225,16 @@ export const RollingHeaderImojiEditButton = styled.button` background: #fff; border: 1px solid ${colors.gray[300]}; cursor: pointer; + ${media.small` + width: 36px; + height: 32px; + `} +`; + +export const RollingHeaderImojiEditButtonContainer = styled.div` + display: flex; + align-items: center; + gap: 15px; `; export const RollingHeaderImojiEditButtonIcon = styled.img` @@ -129,6 +245,9 @@ export const RollingHeaderImojiEditButtonIcon = styled.img` export const RollingHeaderImojiEditButtonText = styled.span` ${font.regular16} color: ${colors.gray[900]}; + ${media.small` + display: none; + `} `; export const RollingHeaderLinkShareButton = styled.div` @@ -142,6 +261,13 @@ export const RollingHeaderLinkShareButton = styled.div` padding: 12px 32px; cursor: pointer; border: 1px solid ${colors.gray[300]}; + ${media.small` + width: 36px; + height: 32px; + background-size: 20px 20px; + padding: 8px 8px; + + `} `; @@ -151,17 +277,32 @@ export const RollingHeaderArrowDown = styled.img` cursor: pointer; `; + + + +export const RollingPageContainer = styled.div` +background-color: ${colors.blue[100]}; +width: 100%; +margin: 0 auto; +padding: 20px; +height: 100vh; +`; + + + export const PerpendicularLine = styled.div` - border-left : 1px solid ${colors.gray[200]}; + border-left: 1px solid ${colors.gray[200]}; height: 28px; `; +export const PerpendicularLineFirst = styled(PerpendicularLine)` + ${media.medium` + display: none; + `} + ${media.small` + display: none; + `} +`; -export const RollingPageContainer = styled.div` - background-color: ${colors.blue[100]}; - width: 100%; - margin: 0 auto; - padding: 20px; - height: 100vh; -`; \ No newline at end of file +export const PerpendicularLineSecond = styled(PerpendicularLine)``; \ No newline at end of file From 5cf2a45c6d1f3a0fbf856e92a5e92ce59b17416e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A0=95=EC=9B=90?= Date: Sun, 9 Nov 2025 02:29:51 +0900 Subject: [PATCH 72/91] =?UTF-8?q?Feat:=20=EB=A1=A4=EB=A7=81=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=ED=8D=BC=20=ED=97=A4=EB=8D=94=20=EC=9D=B4=EB=AA=A8?= =?UTF-8?q?=EC=A7=80=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 22 +++ package.json | 1 + src/pages/rolling-page-head.jsx | 235 ++++++++++++++++++++++++++++++ src/pages/rolling-page.jsx | 45 +----- src/styles/rolling-page-styles.js | 2 - 5 files changed, 265 insertions(+), 40 deletions(-) create mode 100644 src/pages/rolling-page-head.jsx diff --git a/package-lock.json b/package-lock.json index 8b81a7d..7e44d9c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "axios": "^1.13.2", "quill": "^2.0.3", + "emoji-picker-react": "^4.15.0", "react": "^19.1.1", "react-dom": "^19.1.1", "react-quill-new": "^3.6.0", @@ -1816,6 +1817,21 @@ "dev": true, "license": "ISC" }, + "node_modules/emoji-picker-react": { + "version": "4.15.0", + "resolved": "https://registry.npmjs.org/emoji-picker-react/-/emoji-picker-react-4.15.0.tgz", + "integrity": "sha512-41y22VM7Gamcrxkgm3dKO2pAaa56AtFbK2bBImZbaME1jh0C39vlI2qPWCNFVf96HOYN4SLLSE2alRSyC3QYYQ==", + "license": "MIT", + "dependencies": { + "flairup": "1.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">=16" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -2184,6 +2200,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/flairup": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/flairup/-/flairup-1.0.0.tgz", + "integrity": "sha512-IKlE+pNvL2R+kVL1kEhUYqRxVqeFnjiIvHWDMLFXNaqyUdFXQM2wte44EfMYJNHkW16X991t2Zg8apKkhv7OBA==", + "license": "MIT" + }, "node_modules/flat-cache": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", diff --git a/package.json b/package.json index fc182a8..e673689 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "dependencies": { "axios": "^1.13.2", "quill": "^2.0.3", + "emoji-picker-react": "^4.15.0", "react": "^19.1.1", "react-dom": "^19.1.1", "react-quill-new": "^3.6.0", diff --git a/src/pages/rolling-page-head.jsx b/src/pages/rolling-page-head.jsx new file mode 100644 index 0000000..5245fd6 --- /dev/null +++ b/src/pages/rolling-page-head.jsx @@ -0,0 +1,235 @@ +import React, { useState } from 'react'; +import EmojiPicker from 'emoji-picker-react'; +import styled from 'styled-components'; +import { colors } from '@/styles/colors'; +import media from '@/styles/media'; +import { font } from '@/styles/font'; +import { + RollingHeaderImojiContainer, + RollingHeaderImojiIconContainer, + RollingHeaderImojiText, + RollingHeaderImojiIcon, + RollingHeaderImojiEditButtonContainer, + RollingHeaderImojiEditButton, + RollingHeaderImojiEditButtonIcon, + RollingHeaderImojiEditButtonText, + RollingHeaderArrowDown, + PerpendicularLineSecond, + RollingHeaderLinkShareButton, +} from '@/styles/rolling-page-styles'; + +const EmojiPickerContainer = styled.div` + position: relative; + display: inline-block; +`; + +const EmojiPickerWrapper = styled.div` + position: fixed; + transform: translate(-60%, 2%); + z-index: 1000; + background: white; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + border: 1px solid ${colors.gray[300]}; + +`; + +const Overlay = styled.div` + position: fixed; + top: 0; + left: 0; + + background: transparent; + z-index: 999; +`; + +// 이모지 드롭다운 관련 스타일 +const EmojiDropdownContainer = styled.div` + position: relative; + display: inline-block; + +`; + +const EmojiDropdownWrapper = styled.div` + position: fixed; + transform: translate(-80%, 10%); + z-index: 1000; + background: white; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + border: 1px solid ${colors.gray[300]}; + padding: 24px; + width: auto; + max-height: 300px; + overflow-y: auto; +`; + +const EmojiDropdownGrid = styled.div` + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 8px; + ${media.medium` + grid-template-columns: repeat(3, 1fr); + `} + ${media.small` + grid-template-columns: repeat(3, 1fr); + `} +`; + +const EmojiDropdownItem = styled.div` + display: flex; + align-items: center; + justify-content: center; + width: auto; + height: auto; + padding: 8px 12px; + text-align: center; + border-radius: 32px; + background: rgba(153, 153, 153, 1); + gap: 2px; + + ${media.small` + padding: 4px 8px; + `} +`; + +const EmojiDropdownIcon = styled.div` +`; + +const EmojiDropdownCount = styled.span` + ${font.regular16} + color: rgba(255, 255, 255, 1) +`; + +// 이모지 피커 컴포넌트 +function EmojiPickerComponent({ isOpen, onClose, onEmojiSelect, children }) { + const handleEmojiClick = (emojiData) => { + onEmojiSelect(emojiData.emoji); + onClose(); + }; + + return ( + + {children} + {isOpen && ( + <> + + + + + + )} + + ); +} + +// 롤링 페이지 헤더 컴포넌트 +export default function RollingPageHeader({ + ArrowDownIcon, + AddEmojiIcon, + ShareIcon +}) { + const [isEmojiPickerOpen, setIsEmojiPickerOpen] = useState(false); + const [isEmojiDropdownOpen, setIsEmojiDropdownOpen] = useState(false); + const [selectedEmojis, setSelectedEmojis] = useState([ + { emoji: '😘', count: 12 }, + { emoji: '😍', count: 8 }, + { emoji: '👍', count: 15 }, + { emoji: '🎉', count: 5 }, + { emoji: '❤️', count: 20 }, + { emoji: '😂', count: 3 }, + { emoji: '🔥', count: 7 } + ]); + + const handleEmojiSelect = (emoji) => { + const existingEmojiIndex = selectedEmojis.findIndex(item => item.emoji === emoji); + + if (existingEmojiIndex !== -1) { + // 이미 존재하는 이모지면 카운트 증가 + const updatedEmojis = [...selectedEmojis]; + updatedEmojis[existingEmojiIndex].count += 1; + setSelectedEmojis(updatedEmojis); + } else { + // 새로운 이모지면 추가 + setSelectedEmojis([...selectedEmojis, { emoji, count: 1 }]); + } + }; + + const toggleEmojiPicker = () => { + setIsEmojiPickerOpen(!isEmojiPickerOpen); + }; + + const closeEmojiPicker = () => { + setIsEmojiPickerOpen(false); + }; + + const toggleEmojiDropdown = () => { + setIsEmojiDropdownOpen(!isEmojiDropdownOpen); + }; + + const closeEmojiDropdown = () => { + setIsEmojiDropdownOpen(false); + }; + + // 카운트 순으로 정렬하여 상위 3개만 추출 + const sortedEmojis = [...selectedEmojis].sort((a, b) => b.count - a.count); + const topThreeEmojis = sortedEmojis.slice(0, 3); + const hasMoreEmojis = selectedEmojis.length > 3; + + return ( + + {topThreeEmojis.map((emojiData, index) => ( + + {emojiData.emoji} + {emojiData.count} + + ))} + + {hasMoreEmojis && ( + + + {isEmojiDropdownOpen && ( + <> + + + + {sortedEmojis.map((emojiData, index) => ( + + {emojiData.emoji} + {emojiData.count} + + ))} + + + + )} + + )} + + + + + + 추가 + + + + + + + ); +} \ No newline at end of file diff --git a/src/pages/rolling-page.jsx b/src/pages/rolling-page.jsx index 9aacc03..2c4aea1 100644 --- a/src/pages/rolling-page.jsx +++ b/src/pages/rolling-page.jsx @@ -1,3 +1,4 @@ +import React from 'react'; import { RollingHeaderContainer, RollingHeaderUserInfo, @@ -7,21 +8,11 @@ import { RollingHeaderUserPeopleImage, RollingHeaderUserDefaultImage, RollingHeaderUserPeopleState, - RollingHeaderImojiContainer, - RollingHeaderArrowDown, PerpendicularLineFirst, - PerpendicularLineSecond, - RollingHeaderImojiIconContainer, - RollingHeaderImojiText, - RollingHeaderImojiIcon, - RollingHeaderImojiEditButtonContainer, - RollingHeaderImojiEditButton, - RollingHeaderImojiEditButtonIcon, - RollingHeaderImojiEditButtonText, - RollingHeaderLinkShareButton, RollingPageContainer, } from "@/styles/rolling-page-styles"; import HeadNav from "@/components/head-nav"; +import RollingPageHeader from "@/pages/rolling-page-head"; import ArrowDownIcon from "@/assets/icons/arrow-down.svg"; import AddEmojiIcon from "@/assets/icons/add-emoji.svg"; import ShareIcon from "@/assets/icons/share.svg"; @@ -53,33 +44,11 @@ export default function RollingPage() { - - {/* //여기에서 함수를 불러와서 처리해야함 */} - - 😘 - 12 - - - 😘 - 12 - - - 😘 - 12 - - - - - - 추가 - - - - - - - - + diff --git a/src/styles/rolling-page-styles.js b/src/styles/rolling-page-styles.js index eae18ee..34ccb73 100644 --- a/src/styles/rolling-page-styles.js +++ b/src/styles/rolling-page-styles.js @@ -50,8 +50,6 @@ export const RollingHeaderContainer = styled.div` `; - - //유저 정보 컨테이너 TO. Ashley Kim export const RollingHeaderUserInfo = styled.div` display: flex; From 70474b0c8310ac9d1a4bf274de27206fb07b3e15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A0=95=EC=9B=90?= Date: Sun, 9 Nov 2025 05:55:07 +0900 Subject: [PATCH 73/91] =?UTF-8?q?Feat:=20upstream=20develop=20=EB=B8=8C?= =?UTF-8?q?=EB=9E=9C=EC=B9=98=20merge,=20=EC=B9=B4=EC=B9=B4=EC=98=A4?= =?UTF-8?q?=ED=86=A1=20=EA=B3=B5=EC=9C=A0=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 + src/App.jsx | 26 ++-- src/components/common/share-modal.jsx | 180 ++++++++++++++++++++++++++ src/components/head-nav.jsx | 9 -- src/contexts/toast-context-state.jsx | 5 + src/pages/rolling-page-head.jsx | 25 +++- src/pages/rolling-page.jsx | 2 - 7 files changed, 226 insertions(+), 23 deletions(-) create mode 100644 src/components/common/share-modal.jsx delete mode 100644 src/components/head-nav.jsx create mode 100644 src/contexts/toast-context-state.jsx diff --git a/.gitignore b/.gitignore index cb0da3d..fd32680 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,5 @@ dist-ssr *.njsproj *.sln *.sw? + +.env \ No newline at end of file diff --git a/src/App.jsx b/src/App.jsx index 08b96e5..6d3af8e 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,5 +1,6 @@ import { Routes, Route } from "react-router"; import { GlobalStyle } from "@/styles/global-style"; +import ToastProvider from "@/contexts/toast-context"; import GlobalLayout from "@/components/common/global-layout"; import TestPage from "@/pages/test-page"; import MessagePage from "@/pages/message-page"; @@ -12,18 +13,21 @@ function App() { return ( <> - - }> - } /> - {/*} /> - } /> + + + }> + } /> + {/* } /> + } /> } /> - } /> */} - } /> - } /> - } /> - - + } /> + } /> */} + } /> + } /> + } /> + + + ); } diff --git a/src/components/common/share-modal.jsx b/src/components/common/share-modal.jsx new file mode 100644 index 0000000..0e27840 --- /dev/null +++ b/src/components/common/share-modal.jsx @@ -0,0 +1,180 @@ +import React, { useEffect } from 'react'; +import styled from 'styled-components'; +import { colors } from '@/styles/colors'; +import { font } from '@/styles/font'; +import media from '@/styles/media'; +import useToast from '@/hooks/use-toast'; + +const KAKAO_KEY = import.meta.env.VITE_KAKAO_KEY; +console.log(KAKAO_KEY); +const Overlay = styled.div` + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background: rgba(0, 0, 0, 0.5); + z-index: 1000; + display: flex; + justify-content: center; + align-items: center; +`; + +const ModalContainer = styled.div` + background: white; + border-radius: 16px; + padding: 40px; + width: 480px; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15); + + ${media.medium` + width: 400px; + padding: 30px; + `} + + ${media.small` + width: 320px; + padding: 24px; + `} +`; + +const ModalTitle = styled.h2` + ${font.bold24} + color: ${colors.gray[900]}; + margin-bottom: 24px; + text-align: center; +`; + +const ShareButtonGroup = styled.div` + display: flex; + flex-direction: column; + gap: 12px; +`; + +const ShareButton = styled.button` + display: flex; + align-items: center; + justify-content: center; + gap: 12px; + width: 100%; + padding: 16px; + background: ${colors.gray[100]}; + border: 1px solid ${colors.gray[300]}; + border-radius: 8px; + cursor: pointer; + transition: all 0.2s; + ${font.regular16} + color: ${colors.gray[900]}; + + &:hover { + background: ${colors.gray[200]}; + border-color: ${colors.gray[400]}; + } + + &:active { + transform: scale(0.98); + } +`; + +const KakaoButton = styled(ShareButton)` + background: #fee500; + border-color: #fee500; + color: #000000; + + &:hover { + background: #fdd835; + border-color: #fdd835; + } +`; + +const CloseButton = styled.button` + width: 100%; + margin-top: 16px; + padding: 6px; + background: transparent; + border: 1px solid ${colors.gray[300]}; + border-radius: 8px; + cursor: pointer; + ${font.regular16} + color: ${colors.gray[700]}; + transition: all 0.2s; + + &:hover { + background: ${colors.gray[50]}; + } +`; + +export default function ShareModal({ isOpen, onClose, shareUrl }) { + const { showToast } = useToast(); + + // 카카오 SDK 초기화 + useEffect(() => { + if (!window.Kakao) { + const script = document.createElement('script'); + script.src = 'https://t1.kakaocdn.net/kakao_js_sdk/2.7.7/kakao.min.js'; + script.integrity = 'sha384-tJkjbtDbvoxO+diRuDtwRO9JXR7pjWnfjfRn5ePUpl7e7RJCxKCwwnfqUAdXh53p'; + script.crossOrigin = 'anonymous'; + script.async = true; + + script.onload = () => { + if (window.Kakao && !window.Kakao.isInitialized()) { + window.Kakao.init(KAKAO_KEY); + } + }; + + document.head.appendChild(script); + } else if (!window.Kakao.isInitialized()) { + window.Kakao.init(KAKAO_KEY); + } + }, []); + + // URL 복사 기능 + const handleCopyUrl = async () => { + try { + await navigator.clipboard.writeText(shareUrl); + showToast('URL이 복사되었습니다.', 'success'); + onClose(); + } catch (err) { + console.error('URL 복사 실패:', err); + showToast('URL 복사에 실패했습니다.', 'delete'); + } + }; + + // 카카오톡 공유 기능 + const handleKakaoShare = () => { + if (window.Kakao) { + try { + window.Kakao.Share.sendScrap({ + requestUrl: shareUrl, + }); + } catch (err) { + console.error('카카오톡 공유 실패:', err); + showToast(true, '카카오톡 공유에 실패했습니다.'); + } + } else { + showToast(true, '카카오톡 SDK가 로드되지 않았습니다.'); + } + }; + + if (!isOpen) return null; + + return ( + + e.stopPropagation()}> + 공유하기 + + + 🗨️ + 카카오톡으로 공유하기 + + + 🔗 + URL 복사하기 + + + 닫기 + + + ); +} + diff --git a/src/components/head-nav.jsx b/src/components/head-nav.jsx deleted file mode 100644 index 7f22bb5..0000000 --- a/src/components/head-nav.jsx +++ /dev/null @@ -1,9 +0,0 @@ -import { HeadNavContainer } from "@/styles/head-nav-style"; - -export default function HeadNav() { - return ( - -

navigation

-
- ); -} \ No newline at end of file diff --git a/src/contexts/toast-context-state.jsx b/src/contexts/toast-context-state.jsx new file mode 100644 index 0000000..a5eb1e9 --- /dev/null +++ b/src/contexts/toast-context-state.jsx @@ -0,0 +1,5 @@ +import React, { createContext } from 'react'; + +export const ToastContext = createContext(); + + diff --git a/src/pages/rolling-page-head.jsx b/src/pages/rolling-page-head.jsx index 5245fd6..707c2f8 100644 --- a/src/pages/rolling-page-head.jsx +++ b/src/pages/rolling-page-head.jsx @@ -4,6 +4,7 @@ import styled from 'styled-components'; import { colors } from '@/styles/colors'; import media from '@/styles/media'; import { font } from '@/styles/font'; +import ShareModal from '@/components/common/share-modal'; import { RollingHeaderImojiContainer, RollingHeaderImojiIconContainer, @@ -138,6 +139,7 @@ export default function RollingPageHeader({ }) { const [isEmojiPickerOpen, setIsEmojiPickerOpen] = useState(false); const [isEmojiDropdownOpen, setIsEmojiDropdownOpen] = useState(false); + const [isShareModalOpen, setIsShareModalOpen] = useState(false); const [selectedEmojis, setSelectedEmojis] = useState([ { emoji: '😘', count: 12 }, { emoji: '😍', count: 8 }, @@ -178,11 +180,22 @@ export default function RollingPageHeader({ setIsEmojiDropdownOpen(false); }; + const openShareModal = () => { + setIsShareModalOpen(true); + }; + + const closeShareModal = () => { + setIsShareModalOpen(false); + }; + // 카운트 순으로 정렬하여 상위 3개만 추출 const sortedEmojis = [...selectedEmojis].sort((a, b) => b.count - a.count); const topThreeEmojis = sortedEmojis.slice(0, 3); const hasMoreEmojis = selectedEmojis.length > 3; + // 현재 페이지 URL 가져오기 + const currentUrl = window.location.href; + return ( {topThreeEmojis.map((emojiData, index) => ( @@ -228,8 +241,18 @@ export default function RollingPageHeader({ - + + + ); } \ No newline at end of file diff --git a/src/pages/rolling-page.jsx b/src/pages/rolling-page.jsx index 2c4aea1..749a8c8 100644 --- a/src/pages/rolling-page.jsx +++ b/src/pages/rolling-page.jsx @@ -11,7 +11,6 @@ import { PerpendicularLineFirst, RollingPageContainer, } from "@/styles/rolling-page-styles"; -import HeadNav from "@/components/head-nav"; import RollingPageHeader from "@/pages/rolling-page-head"; import ArrowDownIcon from "@/assets/icons/arrow-down.svg"; import AddEmojiIcon from "@/assets/icons/add-emoji.svg"; @@ -21,7 +20,6 @@ import ShareIcon from "@/assets/icons/share.svg"; export default function RollingPage() { return ( <> - To. Ashley Kim From 1ddb7ac87a1bc42fb9f69a13eae19d5a462d2bda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A0=95=EC=9B=90?= Date: Tue, 11 Nov 2025 03:17:28 +0900 Subject: [PATCH 74/91] =?UTF-8?q?Feat:=20=EA=B3=B5=EC=9C=A0=ED=95=98?= =?UTF-8?q?=EA=B8=B0=20UI=20=EC=88=98=EC=A0=95,=20=ED=94=84=EB=A1=9C?= =?UTF-8?q?=ED=95=84=20=EC=9D=B4=EB=AF=B8=EC=A7=80=EC=99=80=20=EB=AA=87?= =?UTF-8?q?=EB=AA=85=20=EC=9E=91=EC=84=B1=ED=96=88=EB=8A=94=EC=A7=80=20?= =?UTF-8?q?=ED=91=9C=EC=8B=9C=ED=95=98=EB=8A=94=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/common/header.jsx | 1 + src/components/common/modal-layout.jsx | 82 ++++++ src/components/common/share-modal.jsx | 180 ------------- src/components/rolling/emoji-display-list.jsx | 24 ++ src/components/rolling/emoji-dropdown.jsx | 108 ++++++++ .../rolling/emoji-picker-component.jsx | 62 +++++ .../rolling/header-action-buttons.jsx | 55 ++++ .../rolling/participant-section.jsx | 34 +++ src/components/rolling/participant-stats.jsx | 23 ++ src/components/rolling/profile-image-list.jsx | 25 ++ .../rolling/profile-overflow-badge.jsx | 32 +++ src/components/rolling/share-button-group.jsx | 70 +++++ src/components/rolling/share-modal.jsx | 55 ++++ src/hooks/use-emoji-manager.js | 41 +++ src/hooks/use-kakao-sdk.js | 41 +++ src/hooks/use-profile-images.js | 36 +++ src/hooks/use-share-actions.js | 52 ++++ src/pages/rolling-page-head.jsx | 254 ++++-------------- src/pages/rolling-page.jsx | 42 ++- src/styles/rolling-page-styles.js | 10 +- 20 files changed, 814 insertions(+), 413 deletions(-) create mode 100644 src/components/common/modal-layout.jsx delete mode 100644 src/components/common/share-modal.jsx create mode 100644 src/components/rolling/emoji-display-list.jsx create mode 100644 src/components/rolling/emoji-dropdown.jsx create mode 100644 src/components/rolling/emoji-picker-component.jsx create mode 100644 src/components/rolling/header-action-buttons.jsx create mode 100644 src/components/rolling/participant-section.jsx create mode 100644 src/components/rolling/participant-stats.jsx create mode 100644 src/components/rolling/profile-image-list.jsx create mode 100644 src/components/rolling/profile-overflow-badge.jsx create mode 100644 src/components/rolling/share-button-group.jsx create mode 100644 src/components/rolling/share-modal.jsx create mode 100644 src/hooks/use-emoji-manager.js create mode 100644 src/hooks/use-kakao-sdk.js create mode 100644 src/hooks/use-profile-images.js create mode 100644 src/hooks/use-share-actions.js diff --git a/src/components/common/header.jsx b/src/components/common/header.jsx index 6c4ee3f..44210f8 100644 --- a/src/components/common/header.jsx +++ b/src/components/common/header.jsx @@ -8,6 +8,7 @@ const ContainWrapper = styled.div` top: 0; background-color: white; border-bottom: 1px solid #ededed; + z-index: 1003; `; const Contain = styled.div` diff --git a/src/components/common/modal-layout.jsx b/src/components/common/modal-layout.jsx new file mode 100644 index 0000000..af54b7a --- /dev/null +++ b/src/components/common/modal-layout.jsx @@ -0,0 +1,82 @@ +import React from 'react'; +import styled from 'styled-components'; +import { colors } from '@/styles/colors'; +import { font } from '@/styles/font'; +import media from '@/styles/media'; + +const Overlay = styled.div` + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background: transparent; + z-index: 999; +`; + +const ModalContainer = styled.div` + background: white; + border-radius: 16px; + padding: 40px; + width: 480px; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15); + + ${media.medium` + width: 400px; + padding: 30px; + `} + + ${media.small` + width: 320px; + padding: 24px; + `} +`; + +const ModalTitle = styled.h2` + ${font.bold24} + color: ${colors.gray[900]}; + margin-bottom: 24px; + text-align: center; +`; + +const ModalContent = styled.div` + width: 100%; +`; + +const CloseButton = styled.button` + width: 100%; + margin-top: 16px; + padding: 6px; + background: transparent; + border: 1px solid ${colors.gray[300]}; + border-radius: 8px; + cursor: pointer; + ${font.regular16} + color: ${colors.gray[700]}; + transition: all 0.2s; + + &:hover { + background: ${colors.gray[50]}; + } +`; + +/** + * 공통 모달 레이아웃 컴포넌트 + * 책임: 모달의 기본 구조와 레이아웃 제공 + */ +export default function ModalLayout({ isOpen, onClose, title, children, showCloseButton = true }) { + if (!isOpen) return null; + + return ( + + e.stopPropagation()}> + {title && {title}} + {children} + {showCloseButton && ( + 닫기 + )} + + + ); +} + diff --git a/src/components/common/share-modal.jsx b/src/components/common/share-modal.jsx deleted file mode 100644 index 0e27840..0000000 --- a/src/components/common/share-modal.jsx +++ /dev/null @@ -1,180 +0,0 @@ -import React, { useEffect } from 'react'; -import styled from 'styled-components'; -import { colors } from '@/styles/colors'; -import { font } from '@/styles/font'; -import media from '@/styles/media'; -import useToast from '@/hooks/use-toast'; - -const KAKAO_KEY = import.meta.env.VITE_KAKAO_KEY; -console.log(KAKAO_KEY); -const Overlay = styled.div` - position: fixed; - top: 0; - left: 0; - width: 100vw; - height: 100vh; - background: rgba(0, 0, 0, 0.5); - z-index: 1000; - display: flex; - justify-content: center; - align-items: center; -`; - -const ModalContainer = styled.div` - background: white; - border-radius: 16px; - padding: 40px; - width: 480px; - box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15); - - ${media.medium` - width: 400px; - padding: 30px; - `} - - ${media.small` - width: 320px; - padding: 24px; - `} -`; - -const ModalTitle = styled.h2` - ${font.bold24} - color: ${colors.gray[900]}; - margin-bottom: 24px; - text-align: center; -`; - -const ShareButtonGroup = styled.div` - display: flex; - flex-direction: column; - gap: 12px; -`; - -const ShareButton = styled.button` - display: flex; - align-items: center; - justify-content: center; - gap: 12px; - width: 100%; - padding: 16px; - background: ${colors.gray[100]}; - border: 1px solid ${colors.gray[300]}; - border-radius: 8px; - cursor: pointer; - transition: all 0.2s; - ${font.regular16} - color: ${colors.gray[900]}; - - &:hover { - background: ${colors.gray[200]}; - border-color: ${colors.gray[400]}; - } - - &:active { - transform: scale(0.98); - } -`; - -const KakaoButton = styled(ShareButton)` - background: #fee500; - border-color: #fee500; - color: #000000; - - &:hover { - background: #fdd835; - border-color: #fdd835; - } -`; - -const CloseButton = styled.button` - width: 100%; - margin-top: 16px; - padding: 6px; - background: transparent; - border: 1px solid ${colors.gray[300]}; - border-radius: 8px; - cursor: pointer; - ${font.regular16} - color: ${colors.gray[700]}; - transition: all 0.2s; - - &:hover { - background: ${colors.gray[50]}; - } -`; - -export default function ShareModal({ isOpen, onClose, shareUrl }) { - const { showToast } = useToast(); - - // 카카오 SDK 초기화 - useEffect(() => { - if (!window.Kakao) { - const script = document.createElement('script'); - script.src = 'https://t1.kakaocdn.net/kakao_js_sdk/2.7.7/kakao.min.js'; - script.integrity = 'sha384-tJkjbtDbvoxO+diRuDtwRO9JXR7pjWnfjfRn5ePUpl7e7RJCxKCwwnfqUAdXh53p'; - script.crossOrigin = 'anonymous'; - script.async = true; - - script.onload = () => { - if (window.Kakao && !window.Kakao.isInitialized()) { - window.Kakao.init(KAKAO_KEY); - } - }; - - document.head.appendChild(script); - } else if (!window.Kakao.isInitialized()) { - window.Kakao.init(KAKAO_KEY); - } - }, []); - - // URL 복사 기능 - const handleCopyUrl = async () => { - try { - await navigator.clipboard.writeText(shareUrl); - showToast('URL이 복사되었습니다.', 'success'); - onClose(); - } catch (err) { - console.error('URL 복사 실패:', err); - showToast('URL 복사에 실패했습니다.', 'delete'); - } - }; - - // 카카오톡 공유 기능 - const handleKakaoShare = () => { - if (window.Kakao) { - try { - window.Kakao.Share.sendScrap({ - requestUrl: shareUrl, - }); - } catch (err) { - console.error('카카오톡 공유 실패:', err); - showToast(true, '카카오톡 공유에 실패했습니다.'); - } - } else { - showToast(true, '카카오톡 SDK가 로드되지 않았습니다.'); - } - }; - - if (!isOpen) return null; - - return ( - - e.stopPropagation()}> - 공유하기 - - - 🗨️ - 카카오톡으로 공유하기 - - - 🔗 - URL 복사하기 - - - 닫기 - - - ); -} - diff --git a/src/components/rolling/emoji-display-list.jsx b/src/components/rolling/emoji-display-list.jsx new file mode 100644 index 0000000..3e1492f --- /dev/null +++ b/src/components/rolling/emoji-display-list.jsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { + RollingHeaderImojiIconContainer, + RollingHeaderImojiText, + RollingHeaderImojiIcon, +} from '@/styles/rolling-page-styles'; + +/** + * 이모지 표시 리스트 컴포넌트 + * 책임: 상위 N개의 이모지를 화면에 표시 + */ +export default function EmojiDisplayList({ emojis }) { + return ( + <> + {emojis.map((emojiData, index) => ( + + {emojiData.emoji} + {emojiData.count} + + ))} + + ); +} + diff --git a/src/components/rolling/emoji-dropdown.jsx b/src/components/rolling/emoji-dropdown.jsx new file mode 100644 index 0000000..b5bcc45 --- /dev/null +++ b/src/components/rolling/emoji-dropdown.jsx @@ -0,0 +1,108 @@ +import React from 'react'; +import styled from 'styled-components'; +import { colors } from '@/styles/colors'; +import media from '@/styles/media'; +import { font } from '@/styles/font'; +import { RollingHeaderArrowDown } from '@/styles/rolling-page-styles'; + +const EmojiDropdownContainer = styled.div` + position: relative; + display: inline-block; +`; + +const EmojiDropdownWrapper = styled.div` + position: fixed; + transform: translate(-80%, 10%); + z-index: 1000; + background: white; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + border: 1px solid ${colors.gray[300]}; + padding: 24px; + width: auto; + max-height: 300px; + overflow-y: auto; +`; + +const EmojiDropdownGrid = styled.div` + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 8px; + ${media.medium` + grid-template-columns: repeat(3, 1fr); + `} + ${media.small` + grid-template-columns: repeat(3, 1fr); + `} +`; + +const EmojiDropdownItem = styled.div` + display: flex; + align-items: center; + justify-content: center; + width: auto; + height: auto; + padding: 8px 12px; + text-align: center; + border-radius: 32px; + background: rgba(153, 153, 153, 1); + gap: 2px; + + ${media.small` + padding: 4px 8px; + `} +`; + +const EmojiDropdownIcon = styled.div``; + +const EmojiDropdownCount = styled.span` + ${font.regular16} + color: rgba(255, 255, 255, 1); +`; + +const Overlay = styled.div` + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background: transparent; + z-index: 999; +`; + +/** + * 이모지 드롭다운 컴포넌트 + * 책임: 모든 이모지 목록을 드롭다운 형태로 표시 + */ +export default function EmojiDropdown({ + emojis, + isOpen, + onToggle, + onClose, + arrowDownIcon +}) { + return ( + + + {isOpen && ( + <> + + + + {emojis.map((emojiData, index) => ( + + {emojiData.emoji} + {emojiData.count} + + ))} + + + + )} + + ); +} + diff --git a/src/components/rolling/emoji-picker-component.jsx b/src/components/rolling/emoji-picker-component.jsx new file mode 100644 index 0000000..fc13037 --- /dev/null +++ b/src/components/rolling/emoji-picker-component.jsx @@ -0,0 +1,62 @@ +import React from 'react'; +import EmojiPicker from 'emoji-picker-react'; +import styled from 'styled-components'; +import { colors } from '@/styles/colors'; + +const EmojiPickerContainer = styled.div` + position: relative; + display: inline-block; +`; + +const EmojiPickerWrapper = styled.div` + position: fixed; + transform: translate(-60%, 2%); + z-index: 1000; + background: white; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + border: 1px solid ${colors.gray[300]}; +`; + +const Overlay = styled.div` + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background: transparent; + z-index: 999; +`; + +/** + * 이모지 선택기 컴포넌트 + * 책임: 이모지 피커 UI 렌더링 및 이모지 선택 이벤트 처리 + */ +export default function EmojiPickerComponent({ isOpen, onClose, onEmojiSelect, children }) { + const handleEmojiClick = (emojiData) => { + onEmojiSelect(emojiData.emoji); + onClose(); + }; + + return ( + + {children} + {isOpen && ( + <> + + + + + + )} + + ); +} + diff --git a/src/components/rolling/header-action-buttons.jsx b/src/components/rolling/header-action-buttons.jsx new file mode 100644 index 0000000..b00531d --- /dev/null +++ b/src/components/rolling/header-action-buttons.jsx @@ -0,0 +1,55 @@ +import React from 'react'; +import styled from 'styled-components'; +import EmojiPickerComponent from './emoji-picker-component'; +import { + RollingHeaderImojiEditButtonContainer, + RollingHeaderImojiEditButton, + RollingHeaderImojiEditButtonIcon, + RollingHeaderImojiEditButtonText, + PerpendicularLineSecond, + RollingHeaderLinkShareButton, +} from '@/styles/rolling-page-styles'; + +const ShareButtonWrapper = styled.div` + position: relative; +`; + +/** + * 헤더 액션 버튼 컴포넌트 + * 책임: 이모지 추가 버튼과 공유 버튼 렌더링 + */ +export default function HeaderActionButtons({ + isEmojiPickerOpen, + onToggleEmojiPicker, + onCloseEmojiPicker, + onEmojiSelect, + onShareClick, + addEmojiIcon, + shareIcon, + shareModalComponent, // ShareModal 컴포넌트를 props로 받음 +}) { + return ( + + + + + 추가 + + + + + + {shareModalComponent} + + + ); +} + diff --git a/src/components/rolling/participant-section.jsx b/src/components/rolling/participant-section.jsx new file mode 100644 index 0000000..7d27ee6 --- /dev/null +++ b/src/components/rolling/participant-section.jsx @@ -0,0 +1,34 @@ +import React from 'react'; +import { + RollingHeaderUserPeopleContainer, + RollingHeaderUserPeopleImages, +} from '@/styles/rolling-page-styles'; +import ProfileImageList from './profile-image-list'; +import ProfileOverflowBadge from './profile-overflow-badge'; +import ParticipantStats from './participant-stats'; +import useProfileImages from '@/hooks/use-profile-images'; + +/** + * 참여자 섹션 컴포넌트 + * 책임: 프로필 이미지와 참여자 통계를 조합하여 표시 + */ +export default function ParticipantSection({ profiles, maxVisible = 3 }) { + const { visibleProfiles, overflowCount, totalCount, hasOverflow } = + useProfileImages(profiles, maxVisible); + + return ( + + + {/* 보이는 프로필 이미지들 */} + + + {/* 오버플로우 뱃지 (+N) */} + {hasOverflow && } + + + {/* 참여자 통계 텍스트 */} + + + ); +} + diff --git a/src/components/rolling/participant-stats.jsx b/src/components/rolling/participant-stats.jsx new file mode 100644 index 0000000..e52c89c --- /dev/null +++ b/src/components/rolling/participant-stats.jsx @@ -0,0 +1,23 @@ +import React from 'react'; +import { RollingHeaderUserPeopleState } from '@/styles/rolling-page-styles'; + +/** + * 참여자 통계 컴포넌트 + * 책임: 참여자 수 텍스트 표시 + */ +export default function ParticipantStats({ count }) { + if (count === 0) { + return ( + + 아직 작성한 사람이 없어요 + + ); + } + + return ( + + {count}명이 작성했어요! + + ); +} + diff --git a/src/components/rolling/profile-image-list.jsx b/src/components/rolling/profile-image-list.jsx new file mode 100644 index 0000000..22e5551 --- /dev/null +++ b/src/components/rolling/profile-image-list.jsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { RollingHeaderUserPeopleImage } from '@/styles/rolling-page-styles'; + +/** + * 프로필 이미지 리스트 컴포넌트 + * 책임: 프로필 이미지들을 렌더링 (래퍼 없이 순수 이미지만) + */ +export default function ProfileImageList({ profiles }) { + if (!profiles || profiles.length === 0) { + return null; + } + + return ( + <> + {profiles.map((profile, index) => ( + + ))} + + ); +} + diff --git a/src/components/rolling/profile-overflow-badge.jsx b/src/components/rolling/profile-overflow-badge.jsx new file mode 100644 index 0000000..2fe5249 --- /dev/null +++ b/src/components/rolling/profile-overflow-badge.jsx @@ -0,0 +1,32 @@ +import React from 'react'; +import styled from 'styled-components'; +import { colors } from '@/styles/colors'; +import { font } from '@/styles/font'; + +const OverflowBadge = styled.div` + width: 28px; + height: 28px; + border-radius: 140px; + border: 1.4px solid ${colors.gray[900]}; + background: ${colors.gray[200]}; + display: flex; + align-items: center; + justify-content: center; + position: relative; + margin-left: -10px; + ${font.regular12} + color: ${colors.gray[700]}; +`; + +/** + * 프로필 오버플로우 뱃지 컴포넌트 + * 책임: 추가 인원 수를 표시 (+N) + */ +export default function ProfileOverflowBadge({ count }) { + if (count <= 0) { + return null; + } + + return +{count}; +} + diff --git a/src/components/rolling/share-button-group.jsx b/src/components/rolling/share-button-group.jsx new file mode 100644 index 0000000..dddc833 --- /dev/null +++ b/src/components/rolling/share-button-group.jsx @@ -0,0 +1,70 @@ +import React from 'react'; +import styled from 'styled-components'; +import { colors } from '@/styles/colors'; +import { font } from '@/styles/font'; + +const ButtonGroup = styled.div` + position: absolute; + top: calc(100% + 8px); + right: 0; + width: 140px; + height: auto; + display: flex; + flex-direction: column; + background: white; + border-radius: 8px; + border: 1px solid ${colors.gray[300]}; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + z-index: 1001; + padding: 10px 0px; +`; + +const ShareButton = styled.button` + display: flex; + align-items: center; + justify-content: center; + gap: 12px; + background: transparent; + width: 100%; + height: 50px; + border: none; + cursor: pointer; + transition: all 0.2s; + ${font.regular16} + color: ${colors.gray[900]}; + + &:hover { + background: ${colors.gray[200]}; + border-color: ${colors.gray[400]}; + + } + + &:active { + transform: scale(0.98); + } +`; + +const KakaoButton = styled(ShareButton)` + background: #fee500; + border-color: #fee500; + color: #000000; + + &:hover { + background: #fdd835; + border-color: #fdd835; + } +`; + +/** + * 공유 버튼 그룹 컴포넌트 + * 책임: 공유 방법별 버튼 UI 렌더링 + */ +export default function ShareButtonGroup({ onKakaoShare, onCopyUrl }) { + return ( + + 카카오톡 공유 + URL 복사 + + ); +} + diff --git a/src/components/rolling/share-modal.jsx b/src/components/rolling/share-modal.jsx new file mode 100644 index 0000000..a1a18dc --- /dev/null +++ b/src/components/rolling/share-modal.jsx @@ -0,0 +1,55 @@ +import React from 'react'; +import styled from 'styled-components'; + +import ShareButtonGroup from './share-button-group'; +import useKakaoSdk from '@/hooks/use-kakao-sdk'; +import useShareActions from '@/hooks/use-share-actions'; + +const Overlay = styled.div` + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background: transparent; + z-index: 999; +`; + +/** + * 공유 모달 컴포넌트 + * 책임: 공유 모달 UI 및 공유 액션 연결 + */ +export default function ShareModal({ isOpen, onClose, shareUrl }) { + // 카카오 SDK 초기화 + useKakaoSdk(); + + // 공유 기능 훅 + const { copyToClipboard, shareToKakao } = useShareActions(); + + // URL 복사 핸들러 + const handleCopyUrl = async () => { + const success = await copyToClipboard(shareUrl); + if (success) { + onClose(); + } + }; + + // 카카오톡 공유 핸들러 + const handleKakaoShare = () => { + shareToKakao(shareUrl); + }; + + if (!isOpen) return null; + + return ( + <> + + e.stopPropagation()} + onKakaoShare={handleKakaoShare} + onCopyUrl={handleCopyUrl} + /> + + ); +} + diff --git a/src/hooks/use-emoji-manager.js b/src/hooks/use-emoji-manager.js new file mode 100644 index 0000000..c47018f --- /dev/null +++ b/src/hooks/use-emoji-manager.js @@ -0,0 +1,41 @@ +import { useState } from 'react'; + +/** + * 이모지 관리 커스텀 훅 + * 책임: 이모지 상태 관리 및 비즈니스 로직 처리 + */ +export default function useEmojiManager(initialEmojis = []) { + const [selectedEmojis, setSelectedEmojis] = useState(initialEmojis); + + const handleEmojiSelect = (emoji) => { + const existingEmojiIndex = selectedEmojis.findIndex(item => item.emoji === emoji); + + if (existingEmojiIndex !== -1) { + // 이미 존재하는 이모지면 카운트 증가 + const updatedEmojis = [...selectedEmojis]; + updatedEmojis[existingEmojiIndex].count += 1; + setSelectedEmojis(updatedEmojis); + } else { + // 새로운 이모지면 추가 + setSelectedEmojis([...selectedEmojis, { emoji, count: 1 }]); + } + }; + + // 카운트 순으로 정렬 + const getSortedEmojis = () => { + return [...selectedEmojis].sort((a, b) => b.count - a.count); + }; + + // 상위 N개 추출 + const getTopEmojis = (count) => { + return getSortedEmojis().slice(0, count); + }; + + return { + selectedEmojis, + handleEmojiSelect, + getSortedEmojis, + getTopEmojis, + }; +} + diff --git a/src/hooks/use-kakao-sdk.js b/src/hooks/use-kakao-sdk.js new file mode 100644 index 0000000..54f240c --- /dev/null +++ b/src/hooks/use-kakao-sdk.js @@ -0,0 +1,41 @@ +import { useEffect } from 'react'; + +const KAKAO_KEY = import.meta.env.VITE_KAKAO_KEY; + +/** + * 카카오 SDK 초기화 커스텀 훅 + * 책임: 카카오 SDK 스크립트 로드 및 초기화 + */ +export default function useKakaoSdk() { + useEffect(() => { + // 이미 SDK가 로드되어 있으면 초기화만 수행 + if (window.Kakao) { + if (!window.Kakao.isInitialized()) { + window.Kakao.init(KAKAO_KEY); + } + return; + } + + // SDK 스크립트 동적 로드 + const script = document.createElement('script'); + script.src = 'https://t1.kakaocdn.net/kakao_js_sdk/2.7.7/kakao.min.js'; + script.integrity = 'sha384-tJkjbtDbvoxO+diRuDtwRO9JXR7pjWnfjfRn5ePUpl7e7RJCxKCwwnfqUAdXh53p'; + script.crossOrigin = 'anonymous'; + script.async = true; + + script.onload = () => { + if (window.Kakao && !window.Kakao.isInitialized()) { + window.Kakao.init(KAKAO_KEY); + } + }; + + document.head.appendChild(script); + + // 클린업 함수는 필요시 추가 (일반적으로는 SDK를 제거하지 않음) + }, []); + + return { + isKakaoReady: window.Kakao?.isInitialized() || false, + }; +} + diff --git a/src/hooks/use-profile-images.js b/src/hooks/use-profile-images.js new file mode 100644 index 0000000..8e83833 --- /dev/null +++ b/src/hooks/use-profile-images.js @@ -0,0 +1,36 @@ +import { useMemo } from 'react'; + +/** + * 프로필 이미지 데이터 처리 커스텀 훅 + * 책임: 프로필 이미지 데이터 가공 및 오버플로우 계산 + */ +export default function useProfileImages(profiles, maxVisible = 3) { + const processedData = useMemo(() => { + if (!profiles || profiles.length === 0) { + return { + visibleProfiles: [], + overflowCount: 0, + totalCount: 0, + hasOverflow: false, + }; + } + + const totalCount = profiles.length; + const hasOverflow = totalCount > maxVisible; + + // 오버플로우가 있으면 마지막 자리는 +N 표시용으로 비움 + const visibleCount = hasOverflow ? maxVisible - 1 : maxVisible; + const visibleProfiles = profiles.slice(0, visibleCount); + const overflowCount = hasOverflow ? totalCount - visibleCount : 0; + + return { + visibleProfiles, + overflowCount, + totalCount, + hasOverflow, + }; + }, [profiles, maxVisible]); + + return processedData; +} + diff --git a/src/hooks/use-share-actions.js b/src/hooks/use-share-actions.js new file mode 100644 index 0000000..fb57522 --- /dev/null +++ b/src/hooks/use-share-actions.js @@ -0,0 +1,52 @@ +import { useCallback } from 'react'; +import useToast from '@/hooks/use-toast'; + +/** + * 공유 기능 커스텀 훅 + * 책임: URL 복사 및 카카오톡 공유 비즈니스 로직 처리 + */ +export default function useShareActions() { + const { showToast } = useToast(); + + /** + * URL을 클립보드에 복사 + */ + const copyToClipboard = useCallback(async (url) => { + try { + await navigator.clipboard.writeText(url); + showToast('URL이 복사되었습니다.', 'success'); + return true; + } catch (err) { + console.error('URL 복사 실패:', err); + showToast('URL 복사에 실패했습니다.', 'delete'); + return false; + } + }, [showToast]); + + /** + * 카카오톡으로 공유 + */ + const shareToKakao = useCallback((url) => { + if (!window.Kakao) { + showToast('카카오톡 SDK가 로드되지 않았습니다.', 'delete'); + return false; + } + + try { + window.Kakao.Share.sendScrap({ + requestUrl: url, + }); + return true; + } catch (err) { + console.error('카카오톡 공유 실패:', err); + showToast('카카오톡 공유에 실패했습니다.', 'delete'); + return false; + } + }, [showToast]); + + return { + copyToClipboard, + shareToKakao, + }; +} + diff --git a/src/pages/rolling-page-head.jsx b/src/pages/rolling-page-head.jsx index 707c2f8..f165398 100644 --- a/src/pages/rolling-page-head.jsx +++ b/src/pages/rolling-page-head.jsx @@ -1,146 +1,27 @@ import React, { useState } from 'react'; -import EmojiPicker from 'emoji-picker-react'; -import styled from 'styled-components'; -import { colors } from '@/styles/colors'; -import media from '@/styles/media'; -import { font } from '@/styles/font'; -import ShareModal from '@/components/common/share-modal'; -import { - RollingHeaderImojiContainer, - RollingHeaderImojiIconContainer, - RollingHeaderImojiText, - RollingHeaderImojiIcon, - RollingHeaderImojiEditButtonContainer, - RollingHeaderImojiEditButton, - RollingHeaderImojiEditButtonIcon, - RollingHeaderImojiEditButtonText, - RollingHeaderArrowDown, - PerpendicularLineSecond, - RollingHeaderLinkShareButton, -} from '@/styles/rolling-page-styles'; - -const EmojiPickerContainer = styled.div` - position: relative; - display: inline-block; -`; - -const EmojiPickerWrapper = styled.div` - position: fixed; - transform: translate(-60%, 2%); - z-index: 1000; - background: white; - border-radius: 8px; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); - border: 1px solid ${colors.gray[300]}; - -`; - -const Overlay = styled.div` - position: fixed; - top: 0; - left: 0; - - background: transparent; - z-index: 999; -`; - -// 이모지 드롭다운 관련 스타일 -const EmojiDropdownContainer = styled.div` - position: relative; - display: inline-block; - -`; - -const EmojiDropdownWrapper = styled.div` - position: fixed; - transform: translate(-80%, 10%); - z-index: 1000; - background: white; - border-radius: 8px; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); - border: 1px solid ${colors.gray[300]}; - padding: 24px; - width: auto; - max-height: 300px; - overflow-y: auto; -`; - -const EmojiDropdownGrid = styled.div` - display: grid; - grid-template-columns: repeat(4, 1fr); - gap: 8px; - ${media.medium` - grid-template-columns: repeat(3, 1fr); - `} - ${media.small` - grid-template-columns: repeat(3, 1fr); - `} -`; - -const EmojiDropdownItem = styled.div` - display: flex; - align-items: center; - justify-content: center; - width: auto; - height: auto; - padding: 8px 12px; - text-align: center; - border-radius: 32px; - background: rgba(153, 153, 153, 1); - gap: 2px; - - ${media.small` - padding: 4px 8px; - `} -`; - -const EmojiDropdownIcon = styled.div` -`; - -const EmojiDropdownCount = styled.span` - ${font.regular16} - color: rgba(255, 255, 255, 1) -`; - -// 이모지 피커 컴포넌트 -function EmojiPickerComponent({ isOpen, onClose, onEmojiSelect, children }) { - const handleEmojiClick = (emojiData) => { - onEmojiSelect(emojiData.emoji); - onClose(); - }; - - return ( - - {children} - {isOpen && ( - <> - - - - - - )} - - ); -} - -// 롤링 페이지 헤더 컴포넌트 +import ShareModal from '@/components/rolling/share-modal'; +import EmojiDisplayList from '@/components/rolling/emoji-display-list'; +import EmojiDropdown from '@/components/rolling/emoji-dropdown'; +import HeaderActionButtons from '@/components/rolling/header-action-buttons'; +import useEmojiManager from '@/hooks/use-emoji-manager'; +import { RollingHeaderImojiContainer } from '@/styles/rolling-page-styles'; + +/** + * 롤링 페이지 헤더 컴포넌트 + * 책임: 전체 헤더 구성 요소 조합 및 상태 관리 + */ export default function RollingPageHeader({ ArrowDownIcon, AddEmojiIcon, ShareIcon }) { + // UI 상태 관리 const [isEmojiPickerOpen, setIsEmojiPickerOpen] = useState(false); const [isEmojiDropdownOpen, setIsEmojiDropdownOpen] = useState(false); const [isShareModalOpen, setIsShareModalOpen] = useState(false); - const [selectedEmojis, setSelectedEmojis] = useState([ + + // 이모지 상태 및 로직 관리 + const initialEmojis = [ { emoji: '😘', count: 12 }, { emoji: '😍', count: 8 }, { emoji: '👍', count: 15 }, @@ -148,22 +29,11 @@ export default function RollingPageHeader({ { emoji: '❤️', count: 20 }, { emoji: '😂', count: 3 }, { emoji: '🔥', count: 7 } - ]); + ]; - const handleEmojiSelect = (emoji) => { - const existingEmojiIndex = selectedEmojis.findIndex(item => item.emoji === emoji); - - if (existingEmojiIndex !== -1) { - // 이미 존재하는 이모지면 카운트 증가 - const updatedEmojis = [...selectedEmojis]; - updatedEmojis[existingEmojiIndex].count += 1; - setSelectedEmojis(updatedEmojis); - } else { - // 새로운 이모지면 추가 - setSelectedEmojis([...selectedEmojis, { emoji, count: 1 }]); - } - }; + const { handleEmojiSelect, getSortedEmojis, getTopEmojis } = useEmojiManager(initialEmojis); + // 이모지 피커 핸들러 const toggleEmojiPicker = () => { setIsEmojiPickerOpen(!isEmojiPickerOpen); }; @@ -172,6 +42,7 @@ export default function RollingPageHeader({ setIsEmojiPickerOpen(false); }; + // 이모지 드롭다운 핸들러 const toggleEmojiDropdown = () => { setIsEmojiDropdownOpen(!isEmojiDropdownOpen); }; @@ -180,6 +51,7 @@ export default function RollingPageHeader({ setIsEmojiDropdownOpen(false); }; + // 공유 모달 핸들러 const openShareModal = () => { setIsShareModalOpen(true); }; @@ -188,70 +60,46 @@ export default function RollingPageHeader({ setIsShareModalOpen(false); }; - // 카운트 순으로 정렬하여 상위 3개만 추출 - const sortedEmojis = [...selectedEmojis].sort((a, b) => b.count - a.count); - const topThreeEmojis = sortedEmojis.slice(0, 3); - const hasMoreEmojis = selectedEmojis.length > 3; + // 정렬된 이모지 및 상위 3개 추출 + const sortedEmojis = getSortedEmojis(); + const topThreeEmojis = getTopEmojis(3); + const hasMoreEmojis = sortedEmojis.length > 3; - // 현재 페이지 URL 가져오기 + // 현재 페이지 URL const currentUrl = window.location.href; return ( - {topThreeEmojis.map((emojiData, index) => ( - - {emojiData.emoji} - {emojiData.count} - - ))} + {/* 상위 3개 이모지 표시 */} + + {/* 더 많은 이모지가 있을 경우 드롭다운 */} {hasMoreEmojis && ( - - - {isEmojiDropdownOpen && ( - <> - - - - {sortedEmojis.map((emojiData, index) => ( - - {emojiData.emoji} - {emojiData.count} - - ))} - - - - )} - - )} - - - - - - 추가 - - - - - + )} - + } /> ); diff --git a/src/pages/rolling-page.jsx b/src/pages/rolling-page.jsx index 749a8c8..5524288 100644 --- a/src/pages/rolling-page.jsx +++ b/src/pages/rolling-page.jsx @@ -1,23 +1,27 @@ -import React from 'react'; +import React, { useState } from 'react'; import { RollingHeaderContainer, RollingHeaderUserInfo, RollingHeaderRightContainer, - RollingHeaderUserPeopleContainer, - RollingHeaderUserPeopleImages, - RollingHeaderUserPeopleImage, - RollingHeaderUserDefaultImage, - RollingHeaderUserPeopleState, PerpendicularLineFirst, RollingPageContainer, } from "@/styles/rolling-page-styles"; import RollingPageHeader from "@/pages/rolling-page-head"; +import ParticipantSection from "@/components/rolling/participant-section"; import ArrowDownIcon from "@/assets/icons/arrow-down.svg"; import AddEmojiIcon from "@/assets/icons/add-emoji.svg"; import ShareIcon from "@/assets/icons/share.svg"; - export default function RollingPage() { + // TODO: 실제로는 API에서 받아올 데이터 + // 임시 데이터 (나중에 API 호출로 대체) + const [profiles] = useState([ + { id: 1, name: '김철수', profileImageURL: 'https://via.placeholder.com/28' }, + { id: 2, name: '이영희', profileImageURL: 'https://via.placeholder.com/28' }, + { id: 3, name: '박민수', profileImageURL: 'https://via.placeholder.com/28' }, + + ]); + return ( <> @@ -26,35 +30,23 @@ export default function RollingPage() { - - {/* //여기에서 함수를 불러와서 처리해야함 */} - - - - - - - - - 23명이 작성 했어요! - - - + {/* 참여자 프로필 섹션 */} + + + {/* 이모지 및 공유 헤더 */} - - + - + {/* 롤링 페이퍼 컨텐츠가 들어갈 영역 */} - ); } diff --git a/src/styles/rolling-page-styles.js b/src/styles/rolling-page-styles.js index 34ccb73..f29f391 100644 --- a/src/styles/rolling-page-styles.js +++ b/src/styles/rolling-page-styles.js @@ -22,8 +22,7 @@ export const RollingHeaderContainer = styled.div` height: 68px; margin: 0 auto; padding: 13px 20px; - overflow-x: auto; - overflow-y: hidden; + gap: 20px; `} @@ -32,8 +31,7 @@ export const RollingHeaderContainer = styled.div` height: 68px; margin: 0; padding: 13px 20px; - overflow-x: auto; - overflow-y: hidden; + gap: 10px; `} @@ -123,9 +121,11 @@ export const RollingHeaderUserPeopleContainer = styled.div` //유저 이미지 프로필 사진들 export const RollingHeaderUserPeopleImages = styled.div` + display: flex; width: 76px; height: 28px; position: relative; + cursor: pointer; ${media.medium` @@ -143,7 +143,7 @@ export const RollingHeaderUserPeopleImage = styled.img` width: 28px; height: 28px; border-radius: 140px; - border: 1.4px solid #000; + border: 1.4px solid #fff; position: relative; margin-left: -10px; `; From 6cd31c81a78f2806c2c8c41dccf8e286657044d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A0=95=EC=9B=90?= Date: Wed, 12 Nov 2025 11:17:27 +0900 Subject: [PATCH 75/91] =?UTF-8?q?Feat:=20=EB=A6=AC=EB=B7=B0=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EB=B0=8F,=20=EC=B9=B4=EB=93=9C=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=20UI=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.jsx | 20 ++- src/components/rolling/card-contents.jsx | 59 +++++++ src/hooks/use-cards.js | 36 +++++ src/pages/rolling-page-edit.jsx | 58 +++++++ src/pages/rolling-page.jsx | 6 +- src/styles/rolling-page-styles.js | 190 ++++++++++++++++++++++- 6 files changed, 349 insertions(+), 20 deletions(-) create mode 100644 src/components/rolling/card-contents.jsx create mode 100644 src/hooks/use-cards.js create mode 100644 src/pages/rolling-page-edit.jsx diff --git a/src/App.jsx b/src/App.jsx index 6d3af8e..56f5e51 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -13,21 +13,19 @@ function App() { return ( <> - - - }> - } /> - {/* } /> + + }> + } /> + {/* } /> } /> } /> } /> } /> */} - } /> - } /> - } /> - - - + } /> + } /> + } /> + + ); } diff --git a/src/components/rolling/card-contents.jsx b/src/components/rolling/card-contents.jsx new file mode 100644 index 0000000..4c1336b --- /dev/null +++ b/src/components/rolling/card-contents.jsx @@ -0,0 +1,59 @@ +import { + CardContainer, + Card, + CardEditButton, + CardContentContainer, + CardContentStatus, + CardContentStatusContainer, + CardContentStatusProfileImage, + CardContentStatusProfileName, + CardContentStatusRelationship, + CardContentText, + CardContentDate, + CardContentStatusProfileContainer, + CardContentDeleteButton, +} from "@/styles/rolling-page-styles"; +import { useState } from "react"; +import useCards from "@/hooks/use-cards"; + + +export default function CardContents({ maxVisible = 6 }) { + const [cards] = useState([ + { id: 1, name: '김철수', profileImageURL: 'https://via.placeholder.com/28', relationship: 'friend' }, + { id: 2, name: '이영희', profileImageURL: 'https://via.placeholder.com/28', relationship: 'family' }, + { id: 3, name: '박민수', profileImageURL: 'https://via.placeholder.com/28', relationship: 'colleague' }, + { id: 4, name: '최영희', profileImageURL: 'https://via.placeholder.com/28', relationship: 'acquaintance' }, + { id: 5, name: 'dsadsa', profileImageURL: 'https://via.placeholder.com/28', relationship: 'friend' }, + { id: 6, name: 'qweqwe', profileImageURL: 'https://via.placeholder.com/28', relationship: 'family' }, + { id: 7, name: 'zxczxc', profileImageURL: 'https://via.placeholder.com/28', relationship: 'colleague' }, + { id: 8, name: 'm,nmnb', profileImageURL: 'https://via.placeholder.com/28', relationship: 'acquaintance' } + ]); + const { visibleCards } = useCards(cards, maxVisible); + + return ( + <> + + + {visibleCards.map((card) => ( + + + + + + + From. {card.name} + {card.relationship} + + + + + sdasdsa + 2025.11.12 + + + + ))} + + + ); +} \ No newline at end of file diff --git a/src/hooks/use-cards.js b/src/hooks/use-cards.js new file mode 100644 index 0000000..dcf0bfc --- /dev/null +++ b/src/hooks/use-cards.js @@ -0,0 +1,36 @@ +import { useMemo } from 'react'; + + +/** + * 카드 섹션 컴포넌트 + * 책임: 카드 데이터를 처리하고 표시 + */ +export default function useCards(cards, maxVisible = 6) { + const processedData = useMemo(() => { + if (!cards || cards.length === 0) { + return { + visibleCards: [], + overflowCount: 0, + totalCount: 0, + hasOverflow: false, + }; + } + + const totalCount = cards.length; + const hasOverflow = totalCount > maxVisible; + + const visibleCount = hasOverflow ? maxVisible - 1 : maxVisible; + const visibleCards = cards.slice(0, visibleCount); + const overflowCount = hasOverflow ? totalCount - visibleCount : 0; + + return { + visibleCards, + overflowCount, + totalCount, + hasOverflow, + }; + }, [cards, maxVisible]); + + return processedData; +} + diff --git a/src/pages/rolling-page-edit.jsx b/src/pages/rolling-page-edit.jsx new file mode 100644 index 0000000..8cea62b --- /dev/null +++ b/src/pages/rolling-page-edit.jsx @@ -0,0 +1,58 @@ +import React, { useState } from 'react'; +import { + RollingHeaderContainer, + RollingHeaderUserInfo, + RollingHeaderRightContainer, + PerpendicularLineFirst, + RollingPageContainer, + + +} from "@/styles/rolling-page-styles"; +import RollingPageHeader from "@/pages/rolling-page-head"; +import ParticipantSection from "@/components/rolling/participant-section"; +import ArrowDownIcon from "@/assets/icons/arrow-down.svg"; +import AddEmojiIcon from "@/assets/icons/add-emoji.svg"; +import ShareIcon from "@/assets/icons/share.svg"; +import CardContents from "@/components/rolling/card-contents"; + +export default function RollingPage() { + // TODO: 실제로는 API에서 받아올 데이터 + // 임시 데이터 (나중에 API 호출로 대체) + const [profiles] = useState([ + { id: 1, name: '김철수', profileImageURL: 'https://via.placeholder.com/28' }, + { id: 2, name: '이영희', profileImageURL: 'https://via.placeholder.com/28' }, + { id: 3, name: '박민수', profileImageURL: 'https://via.placeholder.com/28' }, + + ]); + + + return ( + <> + + + To. Ashley Kim + + + + {/* 참여자 프로필 섹션 */} + + + + + {/* 이모지 및 공유 헤더 */} + + + + + + + + + ); +} + + diff --git a/src/pages/rolling-page.jsx b/src/pages/rolling-page.jsx index 5524288..9c43cba 100644 --- a/src/pages/rolling-page.jsx +++ b/src/pages/rolling-page.jsx @@ -5,12 +5,15 @@ import { RollingHeaderRightContainer, PerpendicularLineFirst, RollingPageContainer, + + } from "@/styles/rolling-page-styles"; import RollingPageHeader from "@/pages/rolling-page-head"; import ParticipantSection from "@/components/rolling/participant-section"; import ArrowDownIcon from "@/assets/icons/arrow-down.svg"; import AddEmojiIcon from "@/assets/icons/add-emoji.svg"; import ShareIcon from "@/assets/icons/share.svg"; +import CardContents from "@/components/rolling/card-contents"; export default function RollingPage() { // TODO: 실제로는 API에서 받아올 데이터 @@ -22,6 +25,7 @@ export default function RollingPage() { ]); + return ( <> @@ -45,7 +49,7 @@ export default function RollingPage() { - {/* 롤링 페이퍼 컨텐츠가 들어갈 영역 */} + ); diff --git a/src/styles/rolling-page-styles.js b/src/styles/rolling-page-styles.js index f29f391..d2162db 100644 --- a/src/styles/rolling-page-styles.js +++ b/src/styles/rolling-page-styles.js @@ -3,6 +3,8 @@ import { colors } from "@/styles/colors"; import { font } from "@/styles/font"; import ShareIcon from "@/assets/icons/share.svg"; import media from "@/styles/media"; +import EditIcon from "@/assets/icons/plus.svg"; +import DeleteIcon from "@/assets/icons/deleted.svg"; //최상단헤더 컨테이너 @@ -278,13 +280,6 @@ export const RollingHeaderArrowDown = styled.img` -export const RollingPageContainer = styled.div` -background-color: ${colors.blue[100]}; -width: 100%; -margin: 0 auto; -padding: 20px; -height: 100vh; -`; @@ -303,4 +298,183 @@ export const PerpendicularLineFirst = styled(PerpendicularLine)` `} `; -export const PerpendicularLineSecond = styled(PerpendicularLine)``; \ No newline at end of file +export const PerpendicularLineSecond = styled(PerpendicularLine)``; + + + +export const RollingPageContainer = styled.div` +display: flex; +justify-content: center; +align-items: center; +background-color: ${colors.blue[100]}; +width: 100%; +margin: 0 auto; +padding: 20px; + +`; + + + +export const CardContainer = styled.div` + display: grid; + grid-template-columns: repeat(3, 1fr); + grid-template-rows: repeat(2, 1fr); + + gap: 20px; + ${media.medium` + grid-template-columns: repeat(2, 1fr); + grid-template-rows: repeat(3, 1fr); + `} + ${media.small` + grid-template-columns: repeat(1, 1fr); + grid-template-rows: repeat(6, 1fr); + `} +`; + +export const Card = styled.div` + display: flex; + justify-content: center; + align-items: center; + width: 384px; + height: 280px; + border-radius: 16px; + background-color: #fff; + position: relative; +`; + +export const CardEditButton = styled.button` + width: 56px; + height: 56px; + background-image: url("${EditIcon}"); + background-color: ${colors.gray[500]}; + border-radius: 100px; + border: none; + padding: 20px; + background-size: 24px 24px; + background-repeat: no-repeat; + background-position: center; + cursor: pointer; + &:hover { + background-color: ${colors.gray[400]}; + color: ${colors.gray[100]}; + } +`; + +export const CardContentContainer = styled.div` + display: flex; + flex-direction: column; + justify-content: center; + width: 100%; + height: 100%; + padding: 16px 24px; +`; + +export const CardContentStatus = styled.div` + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + height: auto; + gap: 14px; + border-bottom: 1px solid ${colors.gray[200]}; + padding-bottom: 16px; +`; + +export const CardContentStatusContainer = styled.div` + display: flex; + align-items: center; + gap: 14px; +`; + +export const CardContentStatusProfileContainer = styled.div` + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 6px; +`; + +export const CardContentFrom = styled.div` + + display: flex; + justify-content: center; + align-items: center; + width: 100%; + height: 100%; +`; + +export const CardContentStatusProfileImage = styled.img` + width: 56px; + height: 56px; + border-radius: 100px; + border: 1px solid ${colors.gray[300]}; +`; + +export const CardContentStatusProfileName = styled.div` + ${font.regular16} + color: ${colors.gray[900]}; +`; + +const relationshipColors = { + friend: colors.blue[100], + family: colors.green[100], + colleague: colors.purple[100], + acquaintance: colors.beige[100], +}; + +const relationshipTextColors = { + friend: colors.blue[500], + family: colors.green[500], + colleague: colors.purple[600], + acquaintance: colors.beige[500], +}; + +// const relationshipLabels = { +// friend: '친구', +// family: '가족', +// colleague: '동료', +// acquaintance: '지인', +// }; + +export const CardContentStatusRelationship = styled.div` + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0 8px; + height: 20px; + border-radius: 4px; + ${font.regular14} + color: ${props => relationshipTextColors[props.$relationship] || colors.gray[500]}; + + background-color: ${props => relationshipColors[props.$relationship] || colors.gray[500]}; +`; + +export const CardContentText = styled.div` + width: 100%; + height: 100%; + ${font.regular16} + color: ${colors.gray[600]}; + padding-top: 16px; + cursor: pointer; +`; + +export const CardContentDate = styled.div` + ${font.regular12} + color: ${colors.gray[400]}; +`; + +export const CardContentDeleteButton = styled.div` + width: 40px; + height: 40px; + background-image: url("${DeleteIcon}"); + border-radius: 6px; + border: 1px solid ${colors.gray[300]}; + padding: 20px; + background-size: 24px 24px; + background-repeat: no-repeat; + background-position: center; + cursor: pointer; + &:hover { + background-color: ${colors.gray[200]}; + color: ${colors.gray[100]}; + } +`; \ No newline at end of file From e2000a86c697945e672186bf7940eeaee51c1703 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A0=95=EC=9B=90?= Date: Wed, 12 Nov 2025 11:53:27 +0900 Subject: [PATCH 76/91] =?UTF-8?q?Chore:=20PR=20=EB=B9=8C=EB=93=9C=20?= =?UTF-8?q?=EC=98=A4=EB=A5=98=EB=A1=9C=20=EC=9D=B8=ED=95=9C=20=EC=9E=AC?= =?UTF-8?q?=ED=91=B8=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.jsx | 13 ++++++------- src/hooks/use-kakao-sdk.js | 2 +- src/hooks/use-share-actions.js | 11 +++++------ 3 files changed, 12 insertions(+), 14 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index 56f5e51..ba3d4cb 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,6 +1,5 @@ import { Routes, Route } from "react-router"; import { GlobalStyle } from "@/styles/global-style"; -import ToastProvider from "@/contexts/toast-context"; import GlobalLayout from "@/components/common/global-layout"; import TestPage from "@/pages/test-page"; import MessagePage from "@/pages/message-page"; @@ -8,6 +7,7 @@ import MainPage from "@/pages/main-page"; import PostPage from "@/pages/post-page"; import TempPage from "@/pages/temp-page"; import ToastTestPage from "@/pages/toast-test-page"; +import RollingPage from "@/pages/rolling-page"; function App() { return ( @@ -16,13 +16,12 @@ function App() { }> } /> - {/* } /> - } /> - } /> - } /> - } /> */} - } /> + } /> + {/* } /> */} } /> + } /> + } /> + } /> } /> diff --git a/src/hooks/use-kakao-sdk.js b/src/hooks/use-kakao-sdk.js index 54f240c..7a8ac58 100644 --- a/src/hooks/use-kakao-sdk.js +++ b/src/hooks/use-kakao-sdk.js @@ -7,6 +7,7 @@ const KAKAO_KEY = import.meta.env.VITE_KAKAO_KEY; * 책임: 카카오 SDK 스크립트 로드 및 초기화 */ export default function useKakaoSdk() { + useEffect(() => { // 이미 SDK가 로드되어 있으면 초기화만 수행 if (window.Kakao) { @@ -31,7 +32,6 @@ export default function useKakaoSdk() { document.head.appendChild(script); - // 클린업 함수는 필요시 추가 (일반적으로는 SDK를 제거하지 않음) }, []); return { diff --git a/src/hooks/use-share-actions.js b/src/hooks/use-share-actions.js index fb57522..03bd9ea 100644 --- a/src/hooks/use-share-actions.js +++ b/src/hooks/use-share-actions.js @@ -1,12 +1,12 @@ import { useCallback } from 'react'; -import useToast from '@/hooks/use-toast'; +import { useToast } from '@/hooks/use-toast'; /** * 공유 기능 커스텀 훅 * 책임: URL 복사 및 카카오톡 공유 비즈니스 로직 처리 */ export default function useShareActions() { - const { showToast } = useToast(); + const showToast = useToast(); /** * URL을 클립보드에 복사 @@ -14,11 +14,10 @@ export default function useShareActions() { const copyToClipboard = useCallback(async (url) => { try { await navigator.clipboard.writeText(url); - showToast('URL이 복사되었습니다.', 'success'); + showToast.success('URL이 복사되었습니다.'); return true; } catch (err) { console.error('URL 복사 실패:', err); - showToast('URL 복사에 실패했습니다.', 'delete'); return false; } }, [showToast]); @@ -28,7 +27,6 @@ export default function useShareActions() { */ const shareToKakao = useCallback((url) => { if (!window.Kakao) { - showToast('카카오톡 SDK가 로드되지 않았습니다.', 'delete'); return false; } @@ -36,10 +34,11 @@ export default function useShareActions() { window.Kakao.Share.sendScrap({ requestUrl: url, }); + showToast.success('카카오톡으로 공유되었습니다.'); + return true; } catch (err) { console.error('카카오톡 공유 실패:', err); - showToast('카카오톡 공유에 실패했습니다.', 'delete'); return false; } }, [showToast]); From 7589a095495bc95882e6aa1979dfc0c2710ce69c Mon Sep 17 00:00:00 2001 From: BZzzzi Date: Wed, 12 Nov 2025 14:37:22 +0900 Subject: [PATCH 77/91] =?UTF-8?q?Refactor:=20ToastProvider=20=EC=84=B1?= =?UTF-8?q?=EB=8A=A5=20=EC=B5=9C=EC=A0=81=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - hideToast, showToast 함수를 useCallback으로 메모이제이션하여 불필요한 리렌더링 방지 - 닫기 애니메이션 타이밍 3000ms에서 300ms로 조정 - 자동 닫기 타이밍을 3000ms에서 5000ms로 조정 --- src/components/common/toast-provider.jsx | 41 +++++++++++++----------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/src/components/common/toast-provider.jsx b/src/components/common/toast-provider.jsx index 662e0e2..a54e466 100644 --- a/src/components/common/toast-provider.jsx +++ b/src/components/common/toast-provider.jsx @@ -1,4 +1,4 @@ -import { useState, useRef } from "react"; +import { useState, useRef, useCallback } from "react"; import { createPortal } from "react-dom"; import Toast from "@/components/common/toast"; import { ToastContext } from "@/hooks/use-toast"; @@ -9,7 +9,7 @@ export function ToastProvider({ children }) { const autoCloseTimerRef = useRef(null); const closeAnimTimerRef = useRef(null); - const hideToast = () => { + const hideToast = useCallback(() => { if (autoCloseTimerRef.current) { clearTimeout(autoCloseTimerRef.current); autoCloseTimerRef.current = null; @@ -28,26 +28,29 @@ export function ToastProvider({ children }) { closeAnimTimerRef.current = setTimeout(() => { setToast(null); closeAnimTimerRef.current = null; - }, 3000); - }; + }, 300); + }, []); - const showToast = (message, type = "success") => { - if (autoCloseTimerRef.current) clearTimeout(autoCloseTimerRef.current); - if (closeAnimTimerRef.current) { - clearTimeout(closeAnimTimerRef.current); - } + const showToast = useCallback( + (message, type = "success") => { + if (autoCloseTimerRef.current) clearTimeout(autoCloseTimerRef.current); + if (closeAnimTimerRef.current) { + clearTimeout(closeAnimTimerRef.current); + } - setToast({ - message, - type, - key: Date.now(), - isClosing: false, - }); + setToast({ + message, + type, + key: Date.now(), + isClosing: false, + }); - autoCloseTimerRef.current = setTimeout(() => { - hideToast(); - }, 3000); - }; + autoCloseTimerRef.current = setTimeout(() => { + hideToast(); + }, 5000); + }, + [hideToast] + ); const contextValue = { success: (message) => showToast(message, "success"), From 8333f6d1f5a4a93f98a52d9ee605b7ec88fbddd9 Mon Sep 17 00:00:00 2001 From: BZzzzi Date: Wed, 12 Nov 2025 14:38:30 +0900 Subject: [PATCH 78/91] =?UTF-8?q?Feat:=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EB=9D=BC=EC=9A=B0=ED=8C=85=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - /list 경로에 ListPage 컴포넌트 라우팅 설정 - App.jsx에 ListPage import 및 Route 추가 - list-page.jsx 기본 컴포넌트 생성 --- src/App.jsx | 1 + src/pages/list-page.jsx | 3 +++ 2 files changed, 4 insertions(+) create mode 100644 src/pages/list-page.jsx diff --git a/src/App.jsx b/src/App.jsx index ba3d4cb..9f4a9ca 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -4,6 +4,7 @@ import GlobalLayout from "@/components/common/global-layout"; import TestPage from "@/pages/test-page"; import MessagePage from "@/pages/message-page"; import MainPage from "@/pages/main-page"; +import ListPage from "@/pages/list-page"; import PostPage from "@/pages/post-page"; import TempPage from "@/pages/temp-page"; import ToastTestPage from "@/pages/toast-test-page"; diff --git a/src/pages/list-page.jsx b/src/pages/list-page.jsx new file mode 100644 index 0000000..ca3e299 --- /dev/null +++ b/src/pages/list-page.jsx @@ -0,0 +1,3 @@ +export default function ListPage() { + return <>Hello; +} From 032b2fd29d4f8e1d860815b4d414321cca86e297 Mon Sep 17 00:00:00 2001 From: BZzzzi Date: Wed, 12 Nov 2025 14:53:12 +0900 Subject: [PATCH 79/91] =?UTF-8?q?Refactor:=20=EC=9E=84=EC=8B=9C=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EB=9D=BC=EC=9A=B0=ED=8A=B8=20?= =?UTF-8?q?=EA=B2=BD=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.jsx | 2 +- src/pages/temp-page.jsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index 9f4a9ca..57d237c 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -18,7 +18,7 @@ function App() { }> } /> } /> - {/* } /> */} + } /> } /> } /> } /> diff --git a/src/pages/temp-page.jsx b/src/pages/temp-page.jsx index cb732a2..9fd7121 100644 --- a/src/pages/temp-page.jsx +++ b/src/pages/temp-page.jsx @@ -24,7 +24,7 @@ export default function TempPage() { 리스트 페이지 - + 롤페 페이지 From e06ecc38bdff0de14a0e28b0d691ff4f505f7bb9 Mon Sep 17 00:00:00 2001 From: BZzzzi Date: Wed, 12 Nov 2025 16:43:08 +0900 Subject: [PATCH 80/91] =?UTF-8?q?Refactor:=20=EC=9E=84=EC=8B=9C=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EB=B0=8F=20=ED=97=A4=EB=8D=94=20?= =?UTF-8?q?=EB=B2=84=ED=8A=BC=20=EB=A7=81=ED=81=AC=20=EA=B2=BD=EB=A1=9C=20?= =?UTF-8?q?=EC=9E=AC=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.jsx | 6 +++--- src/components/common/global-layout.jsx | 2 +- src/components/common/header.jsx | 2 +- src/contexts/toast-context-state.jsx | 5 ----- src/pages/temp-page.jsx | 6 +++--- 5 files changed, 8 insertions(+), 13 deletions(-) delete mode 100644 src/contexts/toast-context-state.jsx diff --git a/src/App.jsx b/src/App.jsx index 57d237c..928e238 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -21,9 +21,9 @@ function App() { } /> } /> } /> - } /> - } /> - } /> + } /> + } /> + } /> diff --git a/src/components/common/global-layout.jsx b/src/components/common/global-layout.jsx index db2b1ed..143890f 100644 --- a/src/components/common/global-layout.jsx +++ b/src/components/common/global-layout.jsx @@ -1,7 +1,7 @@ import { Outlet, useLocation } from "react-router"; import Header from "@/components/common/header"; -const PAGES_WITH_BUTTON = ["main-page", "list-page"]; +const PAGES_WITH_BUTTON = ["main", "list"]; export default function GlobalLayout() { const location = useLocation(); diff --git a/src/components/common/header.jsx b/src/components/common/header.jsx index 44210f8..ccb1485 100644 --- a/src/components/common/header.jsx +++ b/src/components/common/header.jsx @@ -48,7 +48,7 @@ export default function Header({ showButton }) { {showButton && ( - + diff --git a/src/contexts/toast-context-state.jsx b/src/contexts/toast-context-state.jsx deleted file mode 100644 index a5eb1e9..0000000 --- a/src/contexts/toast-context-state.jsx +++ /dev/null @@ -1,5 +0,0 @@ -import React, { createContext } from 'react'; - -export const ToastContext = createContext(); - - diff --git a/src/pages/temp-page.jsx b/src/pages/temp-page.jsx index 9fd7121..a605c6a 100644 --- a/src/pages/temp-page.jsx +++ b/src/pages/temp-page.jsx @@ -30,15 +30,15 @@ export default function TempPage() { 롤페 생성 페이지 - + 롤페 메시지 페이지 - + 테스트 페이지 - + toast 테스트 페이지 From 6735707755f1bb685a46f03f7d216403f72b0bc5 Mon Sep 17 00:00:00 2001 From: yeseung00 Date: Fri, 14 Nov 2025 17:19:11 +0900 Subject: [PATCH 81/91] =?UTF-8?q?Feat:=20=EB=A9=94=EC=8B=9C=EC=A7=80=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20UI=20=EB=94=94=EC=9E=90=EC=9D=B8,?= =?UTF-8?q?=20=EB=93=9C=EB=A1=AD=EB=8B=A4=EC=9A=B4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 생성하기 버튼에 공용 컴포넌트 사용 - 폼 인풋 스타일 적용 - ‘상대와의 관계’, ‘폰트선택’ 드롭다운 컴포넌트 구현# --- src/App.jsx | 10 +++++----- src/components/message/drop-down.jsx | 1 + 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index 928e238..d283b13 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -17,13 +17,13 @@ function App() { }> } /> - } /> + {/*} /> } /> } /> - } /> - } /> - } /> - } /> + } /> */} + } /> + } /> + } /> diff --git a/src/components/message/drop-down.jsx b/src/components/message/drop-down.jsx index 37d34df..e2302e2 100644 --- a/src/components/message/drop-down.jsx +++ b/src/components/message/drop-down.jsx @@ -12,6 +12,7 @@ const DropDownWrapper = styled.div` const DropDownTrigger = styled.button` width: 100%; + height: 50px; display: flex; justify-content: space-between; align-items: center; From c8e6a67d07037e8842ba2f63f3d03b94739585d7 Mon Sep 17 00:00:00 2001 From: yeseung00 Date: Sat, 15 Nov 2025 04:00:12 +0900 Subject: [PATCH 82/91] =?UTF-8?q?Feat:=20reach=20text=20editor=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 1 + package.json | 1 + src/components/message/reach-text-editor.jsx | 64 ++++++++++---------- 3 files changed, 35 insertions(+), 31 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7e44d9c..1bbe48d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "axios": "^1.13.2", "quill": "^2.0.3", "emoji-picker-react": "^4.15.0", + "quill": "^2.0.3", "react": "^19.1.1", "react-dom": "^19.1.1", "react-quill-new": "^3.6.0", diff --git a/package.json b/package.json index e673689..805b901 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "axios": "^1.13.2", "quill": "^2.0.3", "emoji-picker-react": "^4.15.0", + "quill": "^2.0.3", "react": "^19.1.1", "react-dom": "^19.1.1", "react-quill-new": "^3.6.0", diff --git a/src/components/message/reach-text-editor.jsx b/src/components/message/reach-text-editor.jsx index 27abe38..069e683 100644 --- a/src/components/message/reach-text-editor.jsx +++ b/src/components/message/reach-text-editor.jsx @@ -1,12 +1,20 @@ -import React, { useMemo } from "react"; +import React from "react"; import ReactQuill from "react-quill-new"; - import "react-quill-new/dist/quill.snow.css"; import styled from "styled-components"; - import { font } from "@/styles/font"; import { colors } from "@/styles/colors"; +import Quill from "quill"; + +const toolbar = Quill.import("modules/toolbar"); +const list = Quill.import("formats/list"); + +if (list) { + Quill.register(list, true); +} +if (toolbar) { +} const EditorContainer = styled.div` min-height: 243px; @@ -46,36 +54,30 @@ const EditorContainer = styled.div` `; function RichTextEditor({ value, onChange }) { - const modules = useMemo( - () => ({ - toolbar: [ - ["bold", "italic", "underline"], - [ - { align: "" }, - { align: "center" }, - { align: "right" }, - { align: "justify" }, - ], - [{ list: "ordered" }, { list: "bullet" }], - ["link", "image"], + const modules = { + toolbar: [ + ["bold", "italic", "underline"], + [ + { align: "" }, + { align: "center" }, + { align: "right" }, + { align: "justify" }, ], - }), - [] - ); - - const formats = useMemo( - () => [ - "bold", - "italic", - "underline", - "align", - "list", - "bullet", - "link", - "image", + [{ list: "ordered" }, { list: "bullet" }], + ["link", "image"], ], - [] - ); + }; + + const formats = [ + "bold", + "italic", + "underline", + "align", + "list", + "bullet", + "link", + "image", + ]; return ( From 75456f32f108baf96f942469f5d6cf5975e07a68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A0=95=EC=9B=90?= Date: Mon, 17 Nov 2025 12:08:53 +0900 Subject: [PATCH 83/91] =?UTF-8?q?Chore:=20=EB=B9=8C=EB=93=9C=20=EC=98=A4?= =?UTF-8?q?=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../{rolling-head-emoji-share.jsx => rolling-page-header.jsx} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/components/rolling/{rolling-head-emoji-share.jsx => rolling-page-header.jsx} (100%) diff --git a/src/components/rolling/rolling-head-emoji-share.jsx b/src/components/rolling/rolling-page-header.jsx similarity index 100% rename from src/components/rolling/rolling-head-emoji-share.jsx rename to src/components/rolling/rolling-page-header.jsx From 412998324e57f1158e289eeee5b653bef3f66dd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A0=95=EC=9B=90?= Date: Mon, 17 Nov 2025 13:43:31 +0900 Subject: [PATCH 84/91] =?UTF-8?q?Chore:=20=EB=B9=8C=EB=93=9C=20=EC=98=A4?= =?UTF-8?q?=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/rolling-page.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/rolling-page.jsx b/src/pages/rolling-page.jsx index 00826ac..4661aba 100644 --- a/src/pages/rolling-page.jsx +++ b/src/pages/rolling-page.jsx @@ -9,7 +9,7 @@ import { CardPageDeleteButton, CardContainerWrapper, } from "@/styles/rolling-page-styles"; -import RollingPageHeader from "@/components/rolling/rolling-page-head"; +import RollingPageHeader from "@/components/rolling/rolling-page-header"; import ParticipantSection from "@/components/rolling/participant-section"; import CardContents from "@/components/rolling/card-contents"; import DeleteConfirmModal from "@/components/rolling/delete-confirm-modal"; From b078e9fde46962d407256d042838a3244f819e71 Mon Sep 17 00:00:00 2001 From: BZzzzi Date: Mon, 17 Nov 2025 15:21:30 +0900 Subject: [PATCH 85/91] =?UTF-8?q?Fix:=20=EB=B9=8C=EB=93=9C=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 18 +++++++++--------- src/App.jsx | 5 +---- src/pages/temp-page.jsx | 2 +- src/styles/list-page-styles.js | 4 ++-- src/styles/rolling-page-styles.js | 27 ++++++++++++++------------- 5 files changed, 27 insertions(+), 29 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1e45e87..c1244cd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3110,15 +3110,6 @@ "node": ">=8" } }, - "node_modules/throttle-debounce": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-2.3.0.tgz", - "integrity": "sha512-H7oLPV0P7+jgvrk+6mwwwBDmxTaxnu9HMXmloNLXwnNO0ZxZ31Orah2n8lU1eMPvsaowP2CX+USCgyovXfdOFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/swiper": { "version": "12.0.3", "resolved": "https://registry.npmjs.org/swiper/-/swiper-12.0.3.tgz", @@ -3138,6 +3129,15 @@ "node": ">= 4.7.0" } }, + "node_modules/throttle-debounce": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-2.3.0.tgz", + "integrity": "sha512-H7oLPV0P7+jgvrk+6mwwwBDmxTaxnu9HMXmloNLXwnNO0ZxZ31Orah2n8lU1eMPvsaowP2CX+USCgyovXfdOFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", diff --git a/src/App.jsx b/src/App.jsx index 440141b..ab2f861 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -18,12 +18,9 @@ function App() { }> } /> } /> - {/* } /> */} - - {/* 롤링 페이퍼 뷰어/편집 모드 */} + } /> } /> } /> - } /> } /> } /> diff --git a/src/pages/temp-page.jsx b/src/pages/temp-page.jsx index a605c6a..7be29b1 100644 --- a/src/pages/temp-page.jsx +++ b/src/pages/temp-page.jsx @@ -24,7 +24,7 @@ export default function TempPage() { 리스트 페이지 - + 롤페 페이지 diff --git a/src/styles/list-page-styles.js b/src/styles/list-page-styles.js index 5e0b627..9ffb025 100644 --- a/src/styles/list-page-styles.js +++ b/src/styles/list-page-styles.js @@ -1,7 +1,7 @@ import styled from "styled-components"; import Button from "@/components/common/button"; import { Link } from "react-router"; -import { RollingHeaderImojiContainer } from "@/styles/rolling-page-styles"; +import { RollingHeaderEmojiContainer } from "@/styles/rolling-page-styles"; import { font } from "@/styles/font"; import { colors } from "@/styles/colors"; import media from "@/styles/media"; @@ -282,7 +282,7 @@ export const WriterCountText = styled.div` } `; -export const EmojiWrapper = styled(RollingHeaderImojiContainer)` +export const EmojiWrapper = styled(RollingHeaderEmojiContainer)` border-top: 1px solid rgba(0, 0, 0, 0.12); padding-top: 17px; font-size: 14px; diff --git a/src/styles/rolling-page-styles.js b/src/styles/rolling-page-styles.js index 4cf7d4a..de78981 100644 --- a/src/styles/rolling-page-styles.js +++ b/src/styles/rolling-page-styles.js @@ -9,7 +9,6 @@ import EditIcon from "@/assets/icons/plus.svg"; import DeleteIcon from "@/assets/icons/deleted.svg"; //최상단헤더 컨테이너 -export const RollingHeaderContainer = styled.div` export const RollingHeaderContainer = styled.div` display: flex; justify-content: space-between; @@ -20,7 +19,6 @@ export const RollingHeaderContainer = styled.div` background-color: rgba(255, 255, 255, 1); gap: 20px; - ${media.large` width: 1200px; height: 68px; @@ -300,9 +298,16 @@ export const PerpendicularLineFirst = styled(PerpendicularLine)` export const PerpendicularLineSecond = styled(PerpendicularLine)``; - -const RollingPageWrapper = ({ $backgroundcolor, $backgroundimage, ...rest }) => { - return React.createElement('div', { $backgroundcolor, $backgroundimage, ...rest }); +const RollingPageWrapper = ({ + $backgroundcolor, + $backgroundimage, + ...rest +}) => { + return React.createElement("div", { + $backgroundcolor, + $backgroundimage, + ...rest, + }); }; export const RollingPageContainer = styled(RollingPageWrapper)` display: flex; @@ -318,7 +323,6 @@ export const RollingPageContainer = styled(RollingPageWrapper)` padding: 63px 216px 113px 216px; gap: 11px; - ${media.medium` padding: 5% 2%; `} @@ -328,8 +332,6 @@ export const RollingPageContainer = styled(RollingPageWrapper)` `} `; - - export const CardContainerWrapper = styled.div` display: flex; flex-direction: column; @@ -476,8 +478,6 @@ const relationshipTextColors = { acquaintance: colors.beige[500], }; - - export const CardContentStatusRelationship = styled.div` display: inline-flex; align-items: center; @@ -486,9 +486,11 @@ export const CardContentStatusRelationship = styled.div` height: 20px; border-radius: 4px; ${font.regular14} - color: ${(props) => relationshipTextColors[props.$relationship] || colors.gray[500]}; + color: ${(props) => + relationshipTextColors[props.$relationship] || colors.gray[500]}; - background-color: ${(props) => relationshipColors[props.$relationship] || colors.gray[500]}; + background-color: ${(props) => + relationshipColors[props.$relationship] || colors.gray[500]}; `; export const CardContentText = styled.div` @@ -503,7 +505,6 @@ export const CardContentText = styled.div` display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; - `; export const CardContentDate = styled.div` From 9565adc55d29f581b1b91f2982eceaf0fdecc172 Mon Sep 17 00:00:00 2001 From: BZzzzi Date: Mon, 17 Nov 2025 15:32:48 +0900 Subject: [PATCH 86/91] =?UTF-8?q?Fix:=20=EB=B3=91=ED=95=A9=20=EC=B6=A9?= =?UTF-8?q?=EB=8F=8C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/list-user-api.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/api/list-user-api.js b/src/api/list-user-api.js index a1b511d..27a28db 100644 --- a/src/api/list-user-api.js +++ b/src/api/list-user-api.js @@ -1,11 +1,7 @@ // 카드 리스트 유저 GET import axios from "axios"; -const baseURL = import.meta.env.VITE_API_BASE_URL; - -export async function getRecipients({ limit, offset, sort }) { - const res = await axios.get( - `${baseURL}/recipients/?limit=${limit}&offset=${offset}&sort=${sort}` - ); +export async function getRecipients({ url }) { + const res = await axios.get(url); return res.data; } From c44811326358c2187c824dd697c8e9b33cd5ad6e Mon Sep 17 00:00:00 2001 From: BZzzzi Date: Mon, 17 Nov 2025 17:28:53 +0900 Subject: [PATCH 87/91] =?UTF-8?q?Refactor:=20=EA=B0=9C=EB=B0=9C=20?= =?UTF-8?q?=ED=8E=B8=EC=9D=98=EB=A5=BC=20=EC=9C=84=ED=95=9C=20=EC=9E=84?= =?UTF-8?q?=EC=8B=9C=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EB=B0=8F=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.jsx | 10 +---- src/pages/test-page.jsx | 84 ----------------------------------- src/pages/toast-test-page.jsx | 22 --------- 3 files changed, 2 insertions(+), 114 deletions(-) delete mode 100644 src/pages/test-page.jsx delete mode 100644 src/pages/toast-test-page.jsx diff --git a/src/App.jsx b/src/App.jsx index ab2f861..b55b91a 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,13 +1,10 @@ import { Routes, Route } from "react-router"; import { GlobalStyle } from "@/styles/global-style"; import GlobalLayout from "@/components/common/global-layout"; -import TestPage from "@/pages/test-page"; import MessagePage from "@/pages/message-page"; import MainPage from "@/pages/main-page"; import ListPage from "@/pages/list-page"; import PostPage from "@/pages/post-page"; -import TempPage from "@/pages/temp-page"; -import ToastTestPage from "@/pages/toast-test-page"; import RollingPage from "@/pages/rolling-page"; function App() { @@ -16,15 +13,12 @@ function App() { }> - } /> - } /> + } /> } /> } /> } /> } /> - } /> - } /> - } /> + } /> diff --git a/src/pages/test-page.jsx b/src/pages/test-page.jsx deleted file mode 100644 index 0f345bb..0000000 --- a/src/pages/test-page.jsx +++ /dev/null @@ -1,84 +0,0 @@ -import styled from "styled-components"; -import { colors } from "@/styles/colors"; -import { font } from "@/styles/font"; -import media from "@/styles/media"; -import Button from "@/components/common/button"; - -const Container = styled.div` - padding: 40px 20px; - margin: 0 auto; -`; - -const Title = styled.h1` - ${font.bold28} - color: ${colors.purple[600]}; - margin-bottom: 40px; -`; - -const ResponsiveBox = styled.div` - padding: 30px; - border-radius: 12px; - text-align: center; - transition: all 0.3s; - - ${media.small` - background: ${colors.blue[200]}; - ${font.regular14} - `} - - ${media.medium` - background: ${colors.green[200]}; - ${font.regular16} - `} - - ${media.large` - background: ${colors.purple[200]}; - ${font.regular18} - `} -`; - -const CustomButton = styled(Button)` - width: 100%; - padding: 30px; -`; - -export default function TestPage() { - return ( - - 공용 스타일 & 컴포넌트 테스트 - - 창 크기를 조절해보세요! -
-
- 모바일(~599px): 파란색 배경, 작은 폰트 -
- 태블릿(600~1023px): 초록색 배경, 중간 폰트 -
- 데스크톱(1024px~): 보라색 배경, 큰 폰트 -
- - - - - - - - custom button - custom button -
- ); -} diff --git a/src/pages/toast-test-page.jsx b/src/pages/toast-test-page.jsx deleted file mode 100644 index 6333b29..0000000 --- a/src/pages/toast-test-page.jsx +++ /dev/null @@ -1,22 +0,0 @@ -import styled from "styled-components"; -import Button from "@/components/common/button"; -import { useToast } from "@/hooks/use-toast"; - -const Container = styled.div` - padding: 40px; - display: flex; - flex-direction: column; - gap: 20px; -`; - -export default function ToastTestPage() { - const toast = useToast(); - - return ( - -

Toast 테스트 페이지 (5초 후 자동 사라짐)

- - -
- ); -} From bddb4ea9b2d855d685f9e1fbf85d4ecc5dc88356 Mon Sep 17 00:00:00 2001 From: BZzzzi Date: Mon, 17 Nov 2025 17:29:44 +0900 Subject: [PATCH 88/91] =?UTF-8?q?Refactor:=20API=20=EC=A3=BC=EC=86=8C=20?= =?UTF-8?q?=EA=B8=B0=EC=88=98-=ED=8C=80=20=EC=9C=BC=EB=A1=9C=20=ED=86=B5?= =?UTF-8?q?=EC=9D=BC=20=EB=B0=8F=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EB=A7=81?= =?UTF-8?q?=ED=81=AC=20=EC=A0=95=EC=83=81=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/client.js | 12 ++++++------ src/components/list/card-list.jsx | 2 +- src/pages/main-page.jsx | 10 +++++++--- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/api/client.js b/src/api/client.js index 4dcc951..8b0ce0a 100644 --- a/src/api/client.js +++ b/src/api/client.js @@ -1,7 +1,7 @@ import axios from "axios"; // 기수-팀 번호 설정 (환경변수로 관리 가능) -const TEAM_CODE = "2-1"; // 추후 환경변수로 변경 가능 +const TEAM_CODE = "20-1"; // 추후 환경변수로 변경 가능 // API 기본 설정 const BASE_URL = `https://rolling-api.vercel.app/${TEAM_CODE}`; @@ -11,11 +11,11 @@ const BASE_URL = `https://rolling-api.vercel.app/${TEAM_CODE}`; * 책임: axios 인스턴스 생성 및 기본 설정 */ const apiClient = axios.create({ - baseURL: BASE_URL, - timeout: 10000, - headers: { - "Content-Type": "application/json", - }, + baseURL: BASE_URL, + timeout: 10000, + headers: { + "Content-Type": "application/json", + }, }); export default apiClient; diff --git a/src/components/list/card-list.jsx b/src/components/list/card-list.jsx index 323db3c..30d0af1 100644 --- a/src/components/list/card-list.jsx +++ b/src/components/list/card-list.jsx @@ -22,7 +22,7 @@ export function CardList({ title, userList, onLoadMore, nextCheck }) { const navigate = useNavigate(); const handleCardClick = (id) => { - navigate(`/rolling/${id}`); + navigate(`/post/${id}`); }; const handleLoadMore = async () => { diff --git a/src/pages/main-page.jsx b/src/pages/main-page.jsx index b8ca66a..38e6fe6 100644 --- a/src/pages/main-page.jsx +++ b/src/pages/main-page.jsx @@ -167,15 +167,19 @@ export default function MainPage() { Point. 01 - 누구나 손쉽게, 온라인 롤링 페이퍼를 만들 수 있어요 + + 누구나 손쉽게, 온라인 롤링 페이퍼를 만들 수 있어요 + 로그인 없이 자유롭게 만들어요. Point. 02 서로에게 이모지로 감정을 표현해보세요 - 롤링 페이퍼에 이모지를 추가할 수 있어요. + + 롤링 페이퍼에 이모지를 추가할 수 있어요. + - + 구경해보기 From 90db4d020373b5f792dea0b7cd25134e4a729656 Mon Sep 17 00:00:00 2001 From: BZzzzi Date: Mon, 17 Nov 2025 17:30:21 +0900 Subject: [PATCH 89/91] =?UTF-8?q?Refactor:=20=EB=A9=94=EC=8B=9C=EC=A7=80?= =?UTF-8?q?=20post=20=ED=9B=84,=20=EB=A6=AC=EB=8B=A4=EC=9D=B4=EB=A0=89?= =?UTF-8?q?=ED=8A=B8=20=EB=B0=8F=20=EC=A3=BC=EC=86=8C=20=EC=A0=95=EC=83=81?= =?UTF-8?q?=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/use-message-form.js | 8 ++++++-- src/pages/message-page.jsx | 1 - 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/hooks/use-message-form.js b/src/hooks/use-message-form.js index 64f6073..90dd1de 100644 --- a/src/hooks/use-message-form.js +++ b/src/hooks/use-message-form.js @@ -1,5 +1,6 @@ import { useState, useMemo, useEffect } from "react"; import axios from "axios"; +import { useNavigate, useParams } from "react-router"; // API 호스트 루트 const BASE_URL = "https://rolling-api.vercel.app"; @@ -36,7 +37,7 @@ const useInput = (initialValue, validate) => { export const useMessageForm = () => { // 상태 정의 - + const { id } = useParams(); const fromInput = useInput("", (val) => val.trim().length > 0); const relationshipDropdown = useInput( RELATIONSHIP_OPTIONS[0].value, @@ -114,6 +115,7 @@ export const useMessageForm = () => { }, []); // [POST 요청] 폼 데이터 전송 (Axios 사용) + const navigate = useNavigate(); const handleSubmit = async (e) => { e.preventDefault(); @@ -147,7 +149,7 @@ export const useMessageForm = () => { }; // 롤링페이퍼 ID가 필요하다면 이 훅을 사용하는 컴포넌트에서 recipientId를 주입해야 합니다. - const POST_API_URL = `${BASE_URL}/messages/`; + const POST_API_URL = `${BASE_URL}/20-1/recipients/${id}/messages/`; try { console.log(`[API CALL] POST 요청 시작: ${POST_API_URL}`); @@ -156,6 +158,8 @@ export const useMessageForm = () => { const response = await axios.post(POST_API_URL, formData); console.log("롤링페이퍼 생성 성공. 서버 응답:", response.data); + + navigate(`/post/${id}`); return true; } catch (error) { console.error("롤링페이퍼 생성 중 API 오류 발생:", error); diff --git a/src/pages/message-page.jsx b/src/pages/message-page.jsx index 09ed23a..33a1fa2 100644 --- a/src/pages/message-page.jsx +++ b/src/pages/message-page.jsx @@ -7,7 +7,6 @@ import DropDown from "@/components/message/drop-down"; import FromInput from "@/components/message/from-input"; import { useMessageForm } from "@/hooks/use-message-form"; import RichTextEditor from "@/components/message/reach-text-editor"; - import ProfileImageSelector from "@/components/message/profile-image-selector"; export const FormInputStyle = css` From 13f56d8884d4af5885d1afef0bd2af911aa02015 Mon Sep 17 00:00:00 2001 From: BZzzzi Date: Mon, 17 Nov 2025 17:47:15 +0900 Subject: [PATCH 90/91] =?UTF-8?q?Feat:=20=EB=9D=BC=EC=9A=B0=ED=8C=85=20?= =?UTF-8?q?=EC=9D=B4=EC=8A=88=20=ED=95=B4=EA=B2=B0=EC=9D=84=20=EC=9C=84?= =?UTF-8?q?=ED=95=B4=20=EB=B0=B0=EA=B2=BD=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20?= =?UTF-8?q?=EA=B2=BD=EB=A1=9C=EB=A5=BC=20import=20=EB=B0=A9=EC=8B=9D?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/main-page.jsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/pages/main-page.jsx b/src/pages/main-page.jsx index 38e6fe6..baf9e29 100644 --- a/src/pages/main-page.jsx +++ b/src/pages/main-page.jsx @@ -4,6 +4,8 @@ import { font } from "@/styles/font"; import media from "@/styles/media"; import Button from "@/components/common/button"; import { Link } from "react-router"; +import mainVisual01 from "@/assets/images/main-visual-01.webp"; +import mainVisual02 from "@/assets/images/main-visual-02.webp"; const Container = styled.div` max-width: 1200px; @@ -27,7 +29,7 @@ const MainFlexBox = styled.div` flex-direction: column; gap: 20px; background-color: ${colors.surface}; - background-image: url(./src/assets/images/main-visual-01.webp); + background-image: url(${mainVisual01}); background-size: 60%; background-repeat: no-repeat; background-position: right 7px top 60px; @@ -73,7 +75,7 @@ const MainFlexBoxRightPosition = styled.div` flex-direction: column; gap: 20px; background-color: ${colors.surface}; - background-image: url(./src/assets/images/main-visual-02.webp); + background-image: url(${mainVisual02}); background-size: 60%; background-repeat: no-repeat; background-position: left -7px top 60px; From 90cb036bb0dc96766c633185a6cca9fd4c3c5d77 Mon Sep 17 00:00:00 2001 From: BZzzzi Date: Mon, 17 Nov 2025 18:07:53 +0900 Subject: [PATCH 91/91] =?UTF-8?q?Feat:=20=EB=9D=BC=EC=9A=B0=ED=8C=85=20?= =?UTF-8?q?=EC=9D=B4=EC=8A=88=20=ED=95=B4=EA=B2=B0=EC=9D=84=20=EC=9C=84?= =?UTF-8?q?=ED=95=B4=20=EB=B0=B0=EA=B2=BD=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20?= =?UTF-8?q?=EA=B2=BD=EB=A1=9C=EB=A5=BC=20import=20=EB=B0=A9=EC=8B=9D?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/common/toggle.jsx | 33 ++++++-------------------------- src/pages/main-page.jsx | 6 ++++-- 2 files changed, 10 insertions(+), 29 deletions(-) diff --git a/src/components/common/toggle.jsx b/src/components/common/toggle.jsx index 8f8f442..d1ec36a 100644 --- a/src/components/common/toggle.jsx +++ b/src/components/common/toggle.jsx @@ -4,6 +4,7 @@ import { font } from "@/styles/font"; import media from "@/styles/media"; import { useEffect, useState } from "react"; import axios from "axios"; +import selectCircle from "@/assets/images/select-circle.webp"; const ToggleSection = styled.div` width: 100%; @@ -81,7 +82,7 @@ const ToggleDiv = styled.div` background-color: ${(props) => props.$bgColor}; cursor: pointer; background-image: ${(props) => - props.$active ? "url(./src/assets/images/select-circle.webp)" : null}; + props.$active ? `url(${selectCircle})` : null}; background-repeat: no-repeat; background-size: 44px 44px; background-position: center; @@ -109,9 +110,7 @@ const ToggleImg = styled.div` border: 1px solid rgba(0, 0, 0, 0.08); border-radius: 16px; background-image: ${(props) => - props.$active - ? "url(./src/assets/images/select-circle.webp)" - : "url(${$bgImgs})"}, + props.$active ? `url(${selectCircle})` : `url(${props.bgImgs})`}, url(${(props) => props.$bgImgs}); background-repeat: no-repeat; background-size: 44px 44px, 180%; @@ -127,15 +126,6 @@ const ToggleImg = styled.div` `} `; -const ImgSelect = styled.div` - width: 100%; - height: 100%; - background-image: url(./src/assets/images/select-circle.webp); - background-repeat: no-repeat; - background-size: 44px 44px; - background-position: center; -`; - export default function Toggle({ bgColors, isSelectDiv, @@ -143,17 +133,8 @@ export default function Toggle({ isSelectImg, setIsSelectImg, }) { - // const bgColors = [ - // colors.beige[200], - // colors.purple[200], - // colors.blue[200], - // colors.green[200], - // ]; - const [imgs, setImgs] = useState([]); const [toggle, setToggle] = useState(false); - // const [isSelectDiv, setIsSelectDiv] = useState(bgColors[0]); - // const [isSelectImg, setIsSelectImg] = useState(imgs[0]); useEffect(() => { axios @@ -161,9 +142,9 @@ export default function Toggle({ .then((response) => { setImgs(response.data.imageUrls); - if (response.data.imageUrls.length > 0) { - setIsSelectImg(response.data.imageUrls[0]); - } + // if (response.data.imageUrls.length > 0) { + // setIsSelectImg(response.data.imageUrls[0]); + // } }) .catch((error) => { console.error("배경 이미지 가져오기에 실패했습니다.", error); @@ -214,8 +195,6 @@ export default function Toggle({ $bgImgs={bgImg} /> ))} - {/* {isSelectImg === 1 && } - */} )} diff --git a/src/pages/main-page.jsx b/src/pages/main-page.jsx index 38e6fe6..baf9e29 100644 --- a/src/pages/main-page.jsx +++ b/src/pages/main-page.jsx @@ -4,6 +4,8 @@ import { font } from "@/styles/font"; import media from "@/styles/media"; import Button from "@/components/common/button"; import { Link } from "react-router"; +import mainVisual01 from "@/assets/images/main-visual-01.webp"; +import mainVisual02 from "@/assets/images/main-visual-02.webp"; const Container = styled.div` max-width: 1200px; @@ -27,7 +29,7 @@ const MainFlexBox = styled.div` flex-direction: column; gap: 20px; background-color: ${colors.surface}; - background-image: url(./src/assets/images/main-visual-01.webp); + background-image: url(${mainVisual01}); background-size: 60%; background-repeat: no-repeat; background-position: right 7px top 60px; @@ -73,7 +75,7 @@ const MainFlexBoxRightPosition = styled.div` flex-direction: column; gap: 20px; background-color: ${colors.surface}; - background-image: url(./src/assets/images/main-visual-02.webp); + background-image: url(${mainVisual02}); background-size: 60%; background-repeat: no-repeat; background-position: left -7px top 60px;