Skip to content
Merged
12 changes: 12 additions & 0 deletions src/assets/icons/landing/info.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions src/assets/icons/landing/kakao.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions src/assets/icons/landing/up.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 3 additions & 2 deletions src/assets/icons/up.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/images/landing/1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/images/landing/10.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/images/landing/2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/images/landing/3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/images/landing/4.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/images/landing/5.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/images/landing/6.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/images/landing/9.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
43 changes: 43 additions & 0 deletions src/common/KakaoShareButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import KakaoIcon from '@assets/icons/landing/kakao.svg?react';

type KakaoShareButtonProps = {
onClick?: () => void;
className?: string;
fixed?: boolean; // 고정 하단 바 형태로 노출
offsetBottomPx?: number;
};

// 328 width container 기준: 좌우 padding 18px, 상하 18px, 세로 여백 90px
const KakaoShareButton = ({
onClick,
className,
fixed = true,
offsetBottomPx = 24,
}: KakaoShareButtonProps) => {
const bottomWithSafeArea = `calc(${offsetBottomPx}px + env(safe-area-inset-bottom))`;
const base =
'flex w-[328px] items-center justify-center rounded-[16px] px-[18px] py-[18px]';
const layout = fixed
? `fixed left-1/2 -translate-x-1/2 z-[9999] ${base}`
: `mx-auto mt-[58px] ${base}`;
// 시안 색상(카카오 노란색 계열)
const styles = 'bg-[#FFD400] text-gray-900 shadow-lg';

return (
<button
type="button"
onClick={onClick}
className={`${layout} ${styles} ${className ?? ''}`}
style={fixed ? { bottom: bottomWithSafeArea } : undefined}
>
<div className="flex w-full items-center justify-center gap-3">
<KakaoIcon className="h-[25px] w-[25px]" />
<span className="text-[18px] font-[600] leading-[26px]">
카카오톡에 링크 보내기
</span>
</div>
</button>
);
};

export default KakaoShareButton;
34 changes: 34 additions & 0 deletions src/common/MobileFrame.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import React from 'react';

type MobileFrameProps = {
children: React.ReactNode;
className?: string;
};

/**
* 모바일 전용 프레임: 360px 기준, 390px까지 확장
* - 아주 작은 화면에서는 가로 스크롤을 방지하기 위해 w-full
* - min-[360px] 이상에서 360px 고정, min-[391px] 이상에서 390px 고정
* - 안전 영역(inset) 반영
*/
const MobileFrame = ({ children, className }: MobileFrameProps) => {
const baseClass =
'mx-auto w-full max-w-[390px] min-[360px]:w-[360px] min-[391px]:w-[390px] min-h-screen bg-white';
const paddingClass = 'px-4';

return (
<div className="flex w-full justify-center">
<div
className={`${baseClass} ${paddingClass}${className ? ` ${className}` : ''}`}
style={{
paddingTop: 'env(safe-area-inset-top)',
paddingBottom: 'env(safe-area-inset-bottom)',
}}
>
{children}
</div>
</div>
);
};

export default MobileFrame;
66 changes: 66 additions & 0 deletions src/common/ScrollTopButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { useEffect, useState } from 'react';
import UpIcon from '@assets/icons/up.svg?react';

type ScrollTopButtonProps = {
showAfter?: number; // pixels scrolled before showing
offsetBottomPx?: number;
offsetRightPx?: number;
withinMobileFrame?: boolean; // 모바일 프레임(360~390px) 안쪽에 위치
};

const ScrollTopButton = ({
showAfter = 200,
offsetBottomPx = 24,
offsetRightPx = 24,
withinMobileFrame = true,
}: ScrollTopButtonProps) => {
const [visible, setVisible] = useState(false);
const [rightOffset, setRightOffset] = useState<number>(offsetRightPx);

useEffect(() => {
const onScroll = () => {
setVisible(window.scrollY > showAfter);
};
onScroll();
window.addEventListener('scroll', onScroll);
return () => window.removeEventListener('scroll', onScroll);
}, [showAfter]);

// 뷰포트 너비 변화에 따라 모바일 프레임(360~390px) 내부 우측으로 정렬
useEffect(() => {
if (!withinMobileFrame) {
setRightOffset(offsetRightPx);
return;
}
const computeRight = () => {
const vw = window.innerWidth;
const frameWidth = Math.max(360, Math.min(390, vw));
const gutter = Math.max(0, (vw - frameWidth) / 2);
setRightOffset(gutter + offsetRightPx);
};
computeRight();
window.addEventListener('resize', computeRight);
return () => window.removeEventListener('resize', computeRight);
}, [offsetRightPx, withinMobileFrame]);

const handleClick = () => {
window.scrollTo({ top: 0, behavior: 'smooth' });
};

const bottomWithSafeArea = `calc(${offsetBottomPx}px + env(safe-area-inset-bottom))`;

return (
<button
aria-label="scroll-to-top"
onClick={handleClick}
className={`fixed z-[9999] flex h-[42px] w-[42px] items-center justify-center rounded-full border border-gray-200 bg-white shadow-lg transition-opacity duration-200 hover:shadow-xl ${
visible ? 'opacity-100' : 'pointer-events-none opacity-0'
}`}
style={{ bottom: bottomWithSafeArea, right: rightOffset }}
>
<UpIcon className="h-5 w-5 text-gray-400" />
</button>
);
};

export default ScrollTopButton;
13 changes: 13 additions & 0 deletions src/outlet/BlankLayout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Outlet } from 'react-router-dom';

const BlankLayout = () => {
return (
<div className="min-h-screen w-full bg-white">
<main className="w-full">
<Outlet />
</main>
</div>
);
};

export default BlankLayout;
178 changes: 178 additions & 0 deletions src/pages/landing/LandingPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import MobileFrame from '@common/MobileFrame';
import MobileLogo from '@assets/icons/mobileLogo.svg?react';
import LandingHeroImg from '@assets/images/landing/9.png';
import LandingBubbleImg from '@assets/images/landing/10.png';
import Card1 from '@assets/images/landing/1.png';
import Card2 from '@assets/images/landing/2.png';
import LandingImg3 from '@assets/images/landing/3.png';
import LandingImg4 from '@assets/images/landing/4.png';
import LandingImg5 from '@assets/images/landing/5.png';
import LandingImg6 from '@assets/images/landing/6.png';
import ScrollTopButton from '@common/ScrollTopButton';
import KakaoShareButton from '@common/KakaoShareButton';
import InfoIcon from '@assets/icons/info.svg?react';

const LandingPage = () => {
return (
<MobileFrame className="mt-4 bg-gray-100">
<main className="flex min-h-screen flex-col items-center justify-start pt-0">
{/* Top bar (mobile header) */}
<div className="z-10 flex h-[50px] w-full items-center justify-center bg-white">
<MobileLogo className="h-[28px] w-[81px]" />
</div>

{/* Content area with gray-100 background */}
<div className="flex w-full flex-1 flex-col items-center bg-gray-100">
{/* Gradient rounded card */}
<div className="z-10 mt-6 h-[505px] w-[328px] overflow-hidden rounded-[24px] border border-gray-200 shadow-sm">
<div className="relative h-full w-full bg-[linear-gradient(180deg,#E8E6FF_0%,#B7AEFF_100%)]">
<div className="px-6 pt-8 text-center">
<p className="font-T02-M text-[14px] leading-[22px] text-white/90">
내게 딱 맞는 직업은 뭘까?
</p>
<p className="mt-4 text-[28px] leading-[40px] text-white font-T02-B">
인생 2막의 시작은
<br />
두드림과 함께 하세요!
</p>
</div>
{/* Bottom white overlay inside the card with info notes */}
<div className="absolute bottom-0 left-0 right-0 h-[120px] bg-white/40">
<div className="h-full w-full px-4 py-3">
<div className="flex items-center gap-2 text-gray-800">
<InfoIcon className="h-5 w-5 shrink-0 text-gray-700" />
<p className="flex-1 text-[14px] font-[600] leading-[22px]">
두드림은 PC 웹에서만 이용 가능해요
</p>
</div>
<div className="mt-2 flex items-center gap-2 text-gray-800">
<InfoIcon className="h-5 w-5 shrink-0 text-gray-700" />
<p className="mt-2 flex-1 text-[14px] font-[600] leading-[22px]">
카카오톡에 링크 보내기 버튼을 누른 뒤, PC로 접속해보세요!
</p>
</div>
</div>
</div>
<img
src={LandingHeroImg}
alt="landing-hero"
className="absolute bottom-[140px] left-1/2 z-10 h-[200px] w-[200px] -translate-x-1/2"
/>
</div>
Comment on lines +56 to +61
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

LCP(히어로 이미지) 최적화: fetchpriority/명시 크기 지정

히어로 이미지는 LCP 후보이므로 우선순위를 높이고 레이아웃 시프트를 줄이기 위해 명시 크기와 fetchpriority를 권장합니다. alt도 더 구체적으로요.

-              <img
-                src={LandingHeroImg}
-                alt="landing-hero"
-                className="absolute bottom-[140px] left-1/2 z-10 h-[200px] w-[200px] -translate-x-1/2"
-              />
+              <img
+                src={LandingHeroImg}
+                alt="두드림 랜딩 히어로 일러스트"
+                className="absolute bottom-[140px] left-1/2 z-10 h-[200px] w-[200px] -translate-x-1/2"
+                width={200}
+                height={200}
+                loading="eager"
+                decoding="async"
+                fetchpriority="high"
+              />
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<img
src={LandingHeroImg}
alt="landing-hero"
className="absolute bottom-[140px] left-1/2 z-10 h-[200px] w-[200px] -translate-x-1/2"
/>
</div>
<img
src={LandingHeroImg}
alt="두드림 랜딩 히어로 일러스트"
className="absolute bottom-[140px] left-1/2 z-10 h-[200px] w-[200px] -translate-x-1/2"
width={200}
height={200}
loading="eager"
decoding="async"
fetchpriority="high"
/>
🤖 Prompt for AI Agents
In src/pages/landing/LandingPage.tsx around lines 56 to 61, the hero image is an
LCP candidate but currently lacks explicit width/height and fetch priority and
uses a generic alt; add explicit numeric width and height attributes matching
the rendered size (e.g., 200 x 200), add fetchpriority="high" to prioritize
loading, and replace the alt with a more descriptive string (e.g., "Product hero
illustration showing X") to reduce CLS and improve accessibility and LCP.

</div>

{/* Second headline */}
<p className="mt-10 w-[328px] text-[20px] font-[700] leading-[28px] text-gray-900">
퇴직 후 , 나에게 꼭 맞는
<br />
두번째 커리어는?
</p>

{/* White rounded card under gradient card */}
<div className="relative mt-10 h-[295px] w-[328px] rounded-[24px] border border-gray-200 bg-white p-6 shadow-sm">
{/* Bubble image bottom center */}
<img
src={LandingBubbleImg}
alt="landing-bubble"
className="absolute bottom-0 left-1/2 h-[120px] w-auto -translate-x-1/2"
/>
Comment on lines +74 to +78
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

폴드 이하 이미지 지연 로딩/접근성 개선

접근성 향상과 초기 페인트 가속을 위해 아래 이미지들에 lazy/async를 적용하고 alt를 내용 기반으로 보완하세요.

-            <img
-              src={LandingBubbleImg}
-              alt="landing-bubble"
+            <img
+              src={LandingBubbleImg}
+              alt="말풍선 장식 이미지"
               className="absolute bottom-0 left-1/2 h-[120px] w-auto -translate-x-1/2"
+              loading="lazy"
+              decoding="async"
             />
@@
-            <img
-              src={Card2}
-              alt="card-2"
+            <img
+              src={Card2}
+              alt="추천 카드 예시 2"
               className="absolute right-[24px] top-4 z-0 h-[200px] w-auto rotate-[6deg] rounded-[20px]"
+              loading="lazy"
+              decoding="async"
             />
-            <img
-              src={Card1}
-              alt="card-1"
+            <img
+              src={Card1}
+              alt="추천 카드 예시 1"
               className="absolute left-[24px] top-0 z-10 h-[220px] w-auto -rotate-[6deg] rounded-[20px]"
+              loading="lazy"
+              decoding="async"
             />
@@
-            <img
-              src={LandingImg3}
-              alt="landing-3"
+            <img
+              src={LandingImg3}
+              alt="서비스 기능 소개 일러스트 3"
               className="mx-auto h-auto w-full"
+              loading="lazy"
+              decoding="async"
             />
@@
-            <img
-              src={LandingImg4}
-              alt="landing-4"
+            <img
+              src={LandingImg4}
+              alt="서비스 기능 소개 일러스트 4"
               className="mx-auto h-auto w-full"
+              loading="lazy"
+              decoding="async"
             />
@@
-            <img
-              src={LandingImg5}
-              alt="landing-5"
+            <img
+              src={LandingImg5}
+              alt="서비스 기능 소개 일러스트 5"
               className="mx-auto h-auto w-full"
+              loading="lazy"
+              decoding="async"
             />
@@
-            <img
-              src={LandingImg6}
-              alt="landing-6"
+            <img
+              src={LandingImg6}
+              alt="서비스 기능 소개 일러스트 6"
               className="mx-auto h-auto w-full"
+              loading="lazy"
+              decoding="async"
             />

Also applies to: 108-117, 131-136, 148-152, 158-161, 165-168

🤖 Prompt for AI Agents
In src/pages/landing/LandingPage.tsx around lines 74-78 (and also apply to
108-117, 131-136, 148-152, 158-161, 165-168): the below-the-fold images need
lazy/async loading and improved alt text for accessibility and faster initial
paint. Add loading="lazy" and decoding="async" (and optionally
fetchPriority="low") attributes to these <img> tags, and replace the generic alt
values with concise, content-descriptive strings (or use alt="" and
role="presentation" only if the image is purely decorative). Ensure any
decorative images are explicitly marked as such and that all informative images
have meaningful alt text.

{/* Text bubbles */}
<div className="flex flex-col items-start gap-3">
<div className="inline-flex h-[36px] max-w-full items-center rounded-[100px] bg-gray-100 px-4 py-6">
<span className="text-[13px] leading-[24px] text-gray-500">
아이들을 다 키우고 나니 어떤 일을 해야할지 모르겠어
</span>
</div>
<div className="inline-flex h-[36px] max-w-full items-center self-center rounded-[100px] bg-gray-100 px-4 py-3">
<span className="text-[13px] leading-[24px] text-gray-500">
체력이 많이 들지 않는 직업은 없을까?
</span>
</div>
<div className="inline-flex h-[36px] max-w-full items-center rounded-[100px] bg-gray-100 px-4 py-3">
<span className="text-[13px] leading-[24px] text-gray-500">
어떤 것부터 준비해야 할까?
</span>
</div>
</div>
</div>

{/* CTA headline */}
<p className="mt-10 w-[328px] text-center text-[22px] font-[700] text-gray-900">
두드림에서 나에게 딱 맞는
<br />
직업을 추천받아보세요!
</p>

{/* Overlapping cards illustration */}
<div className="relative mt-6 h-[220px] w-[328px]">
<img
src={Card2}
alt="card-2"
className="absolute right-[24px] top-4 z-0 h-[200px] w-auto rotate-[6deg] rounded-[20px]"
/>
<img
src={Card1}
alt="card-1"
className="absolute left-[24px] top-0 z-10 h-[220px] w-auto -rotate-[6deg] rounded-[20px]"
/>
</div>

{/* Helper message */}
<p className="mt-8 w-[328px] text-center text-[22px] font-[700] text-gray-900">
혼자 준비하려니 막막하셨나요?
</p>
<p className="font-T02-M mt-3 w-[328px] text-center text-[14px] leading-[22px] text-gray-700">
같이 준비하는 드리머들의 할 일을 보며
<br />내 할 일을 작성해보세요
</p>

{/* Illustration 3 and copy */}
<div className="mt-6 w-[328px]">
<img
src={LandingImg3}
alt="landing-3"
className="mx-auto h-auto w-full"
/>
</div>
<p className="mt-6 w-[328px] text-center text-[22px] font-[700] text-gray-900">
두드림과 함께 해봐요!
</p>
<p className="font-T02-M mt-3 w-[328px] text-center text-[14px] leading-[22px] text-gray-700">
두드림에서 직업,학원,구직 정보까지
<br />
필요한 정보를 함께 모아보세요
</p>

{/* Illustration 4 */}
<div className="mt-6 w-[200px]">
<img
src={LandingImg4}
alt="landing-4"
className="mx-auto h-auto w-full"
/>
</div>

{/* Illustration 5,6 */}
<div className="mt-6 w-[320px]">
<img
src={LandingImg5}
alt="landing-5"
className="mx-auto h-auto w-full"
/>
</div>
<div className="mb-14 mt-6 w-[320px]">
<img
src={LandingImg6}
alt="landing-6"
className="mx-auto h-auto w-full"
/>
</div>
</div>
</main>
<ScrollTopButton offsetBottomPx={100} showAfter={0} withinMobileFrame />
<KakaoShareButton fixed offsetBottomPx={24} />
</MobileFrame>
);
};

export default LandingPage;
5 changes: 5 additions & 0 deletions src/route/Router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import LoginPage from '@pages/login/LoginPage';
import SignupFunnel from '@pages/signup/SignupFunnel';
import HideLayout from '@outlet/HideLayout';
import ShowLayout from '@outlet/ShowLayout';
import BlankLayout from '@outlet/BlankLayout';
import OnBoardingPage from '@pages/onboard/OnBoardingPage.tsx';
import Home from '@pages/home/Home';
import JobRecommendPage from '@pages/jobRecommend/JobRecommendPage.tsx';
Expand All @@ -34,6 +35,7 @@ import FindIdDisplayPage from '@pages/idFind/FindIdDisplayPage.tsx';
import ChangePwdPage from '@pages/pwdFind/ChangePwdPage.tsx';
import JobSelect from '@pages/jobSelect/JobSelect';
import Community from '@pages/community/Community';
import LandingPage from '@pages/landing/LandingPage.tsx';

function PageViewTracker() {
const { pathname, search } = useLocation();
Expand Down Expand Up @@ -102,6 +104,9 @@ const Router = () => {
<Route path="/mypage" element={<Mypage />} />
<Route path="/community" element={<Community />} />
</Route>
<Route element={<BlankLayout />}>
<Route path="/landing" element={<LandingPage />} />
</Route>
</Routes>
</BrowserRouter>
);
Expand Down