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.
9 changes: 6 additions & 3 deletions src/common/CheckList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,15 @@ import Plus from '@assets/icons/plus.svg?react';
import BookMarkIcon from '@assets/icons/bookmark.svg?react';
import { useUpdateTodoMutation } from '@hook/todo/useUpdateTodoMutation.ts';

type ChecklistItem = string | { id?: number; text: string };
type ChecklistItem = string | { id?: number; text: string; saveCount?: number };

interface CheckListProps {
lists: ChecklistItem[];
checkedIds?: number[];
className?: string;
onChange?: (checkedIds: number[]) => void;
showAddButton?: boolean;
saveCount?: number;
}

const CheckList = ({
Expand All @@ -34,7 +35,7 @@ const CheckList = ({
const isMyToPage = location.pathname.startsWith('/mytodo/list');
const { mutate: updateTodo } = useUpdateTodoMutation();
const normalized = lists.map((item) =>
typeof item === 'string' ? { text: item } : item
typeof item === 'string' ? { text: item, saveCount: 0 } : item
);

const [editIndex, setEditIndex] = useState<number | null>(null);
Expand Down Expand Up @@ -264,7 +265,9 @@ const CheckList = ({
<>
<div className="mr-3 flex items-center gap-1 text-gray-500">
<BookMarkIcon className="h-[18px] w-[18px]" />
<span className="text-sm font-B03-SB">999</span>
<span className="text-sm font-B03-SB">
{item.saveCount ?? 0}
</span>
</div>
<button
onClick={() => handleEdit(idx)}
Expand Down
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;
5 changes: 5 additions & 0 deletions src/hook/useJobQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,8 @@ export interface EachTodos {
completed: boolean;
isMemoExist: boolean;
isPublic: boolean;
saveCount?: number;
isSaved?: boolean;
},
];
}
Expand Down Expand Up @@ -234,5 +236,8 @@ export const useEachTodosQuery = (todoGroupId: number) => {
return useQuery<EachTodos>({
queryKey: ['EachTodos', todoGroupId],
queryFn: () => EachTodos(todoGroupId),
retry: false, // 404 등 에러시 불필요한 재시도 방지 (체감 속도 향상)
refetchOnWindowFocus: false,
staleTime: 60 * 1000,
});
};
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;
Loading