Compare commits

..

5 Commits

Author SHA1 Message Date
2c8dcf9db7 issue #60
All checks were successful
Test CI / build (push) Successful in 17s
- 일정 등록 및 조회 컴포넌트 설계 및 구현 중
2025-12-06 00:19:25 +09:00
geonhee-min
4a8e761b3d issue #60
All checks were successful
Test CI / build (push) Successful in 18s
- 날짜 선택 및 해당 날짜 일정 조회 화면 구현 중
2025-12-05 17:10:58 +09:00
0c8e0893c7 issue #60
All checks were successful
Test CI / build (push) Successful in 17s
- 일정 등록 및 조회 컴포넌트 설계 및 구현 중
2025-12-05 00:05:33 +09:00
geonhee-min
7df60fe004 issue #60
All checks were successful
Test CI / build (push) Successful in 17s
- 캘린더 ui 구현 중
2025-12-04 17:00:02 +09:00
daab622638 issue #59
All checks were successful
Test CI / build (push) Successful in 17s
- 일정 메인 화면 구현 중
2025-12-03 22:51:13 +09:00
14 changed files with 610 additions and 31 deletions

View File

@@ -10,6 +10,7 @@ import { DayButton, DayPicker, getDefaultClassNames } from "react-day-picker"
import { cn } from "@/lib/utils"
import { Button, buttonVariants } from "@/components/ui/button"
import { format } from "date-fns"
function Calendar({
className,
@@ -36,8 +37,28 @@ function Calendar({
)}
captionLayout={captionLayout}
formatters={{
formatCaption: (month) => format(month, "yyyy년 MM월"),
formatWeekdayName: (weekday) => {
switch(weekday.getDay()) {
case 0:
return '일';
case 1:
return '월';
case 2:
return '화';
case 3:
return '수';
case 4:
return '목';
case 5:
return '금';
case 6:
return '토';
}
return '';
},
formatMonthDropdown: (date) =>
date.toLocaleString("default", { month: "short" }),
date.toLocaleString("", { month: "short" }),
...formatters,
}}
classNames={{

99
src/const/ColorPalette.ts Normal file
View File

@@ -0,0 +1,99 @@
export type ColorPaletteType = {
index: number;
style: string;
main: boolean;
}
export const ColorPalette: Record<any, ColorPaletteType> = {
Black: {
index: 0,
style: '#000000',
main: true
},
White: {
index: 1,
style: '#FFFFFF',
main: true
},
PeachCream: {
index: 2,
style: '#FFDAB9',
main: false
},
CoralPink: {
index: 3,
style: '#F08080',
main: true
},
MintIcing: {
index: 4,
style: '#C1E1C1',
main: false
},
Vanilla: {
index: 5,
style: '#FFFACD',
main: true
},
Wheat: {
index: 6,
style: '#F5DEB3',
main: false
},
AliceBlue: {
index: 7,
style: '#F0F8FF',
main: true
},
Lavender: {
index: 8,
style: '#E6E6FA',
main: false
},
LightAqua: {
index: 9,
style: '#A8E6CF',
main: true
},
CloudWhite: {
index: 10,
style: '#F0F8FF',
main: false
},
LightGray: {
index: 11,
style: '#D3D3D3',
main: true
},
LightKhakki: {
index: 12,
style: '#F0F8E6',
main: false
},
DustyRose: {
index: 13,
style: '#D8BFD8',
main: true
},
CreamBeige: {
index: 14,
style: '#FAF0E6',
main: true,
},
Oatmeal: {
index: 15,
style: '#FDF5E6',
main: false
},
CharcoalLight: {
index: 16,
style: '#A9A9A9',
main: true
},
Custom: {
index: 17,
style: 'transparent',
main: false
},
}

67
src/hooks/use-palette.ts Normal file
View File

@@ -0,0 +1,67 @@
import { ColorPalette, type ColorPaletteType } from "@/const/ColorPalette";
export function usePalette() {
const ColorPaletteType = typeof ColorPalette;
const getPaletteNameList = () => {
return Object.keys(ColorPalette);
}
const getMainPaletteList = () => {
const paletteKeys = Object.keys(ColorPalette);
let paletteList: ColorPaletteType[] = [];
paletteKeys.forEach((paletteKey) => {
const key = paletteKey as keyof typeof ColorPalette;
const palette: ColorPaletteType = ColorPalette[key];
if (palette.main) {
paletteList.push(palette);
}
});
paletteList = paletteList.sort((a, b) => a.index - b.index);
return paletteList;
}
const getExtraPaletteList = () => {
const paletteKeys = Object.keys(ColorPalette);
let paletteList: ColorPaletteType[] = [];
paletteKeys.forEach((paletteKey) => {
const key = paletteKey as keyof typeof ColorPalette;
const palette: ColorPaletteType = ColorPalette[key];
if (!palette.main) {
paletteList.push(palette);
}
});
paletteList = paletteList.sort((a, b) => a.index - b.index);
return paletteList;
}
const getAllPaletteList = [...getMainPaletteList(), ...getExtraPaletteList()].sort((a, b) => a.index - b.index);
const getPaletteByKey = (key: keyof typeof ColorPalette) => {
return ColorPalette[key];
}
const getCustomColor = (style: string) => {
return {
style: `#${style}`,
main: false
} as ColorPaletteType;
}
const getStyle = (palette: ColorPaletteType) => {
return palette.style;
}
return {
ColorPaletteType,
getPaletteNameList,
getMainPaletteList,
getExtraPaletteList,
getAllPaletteList,
getPaletteByKey,
getCustomColor,
getStyle
}
}

27
src/hooks/use-viewport.ts Normal file
View File

@@ -0,0 +1,27 @@
import { useState, useEffect } from 'react';
const useViewport = () => {
const [width, setWidth] = useState(
typeof window !== 'undefined' ? window.innerWidth : 0
);
const [height, setHeight] = useState(
typeof window !== 'undefined' ? window.innerHeight : 0
);
useEffect(() => {
const handleResize = () => {
setWidth(window.innerWidth);
setHeight(window.innerHeight);
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}
}, []);
return { width, height };
}
export default useViewport;

View File

@@ -124,6 +124,7 @@ html, body, #root {
height: 100%;
min-width: 1280px;
min-height: 720px;
max-height: 1080px;
}
/* Chrome, Safari, Edge */
@@ -133,7 +134,19 @@ input[type="number"]::-webkit-outer-spin-button {
margin: 0;
}
.rdp-day {
aspect-ratio: unset;
}
/* Firefox */
input[type="number"] {
-moz-appearance: textfield;
}
.rdp-week:not(:first-child) {
@apply border-t;
}
.rdp-day:not(:first-child) {
@apply border-l;
}

View File

@@ -1,5 +1,5 @@
import SideBar from "@/ui/component/SideBar";
import { Outlet } from "react-router-dom";
import { Outlet, useNavigate } from "react-router-dom";
import { SidebarProvider } from "@/components/ui/sidebar";
import Header from "@/ui/component/Header";
import { useAuthStore } from '@/store/authStore';
@@ -11,9 +11,22 @@ import {
OctagonXIcon,
TriangleAlertIcon,
} from "lucide-react";
import { useState } from "react";
export default function Layout() {
const { authData } = useAuthStore();
const [open, setOpen] = useState(false);
const navigate = useNavigate();
const pathname = location.pathname;
const goTo = (path: string) => {
console.log(path);
console.log(pathname);
if (path === pathname) return;
navigate(path);
setOpen(false);
}
return (
<>
<Toaster
@@ -27,10 +40,11 @@ export default function Layout() {
}}
/>
<SidebarProvider
defaultOpen={false}
open={open}
onOpenChange={setOpen}
id="root"
>
<SideBar />
<SideBar goTo={goTo} />
<div className="flex flex-col w-full h-full">
{ authData ? <Header /> : null}
<div className="w-full h-full p-2.5">

View File

@@ -5,11 +5,20 @@ import {
SidebarFooter,
SidebarHeader
} from '@/components/ui/sidebar';
import { PageRouting } from '@/const/PageRouting';
interface SideBarProps {
goTo: (path: string) => void;
}
export default function SideBar({ goTo } : SideBarProps) {
export default function SideBar() {
return (
<Sidebar forceSheet={true}>
<SidebarHeader></SidebarHeader>
<SidebarContent className="flex flex-col p-4 cursor-default">
<div onClick={() => goTo(PageRouting["HOME"].path)}>Home</div>
<div onClick={() => goTo(PageRouting["SCHEDULES"].path)}>Schedules</div>
</SidebarContent>
</Sidebar>
);
}

View File

@@ -0,0 +1,190 @@
import { cn } from "@/lib/utils";
import { Calendar } from "@/components/ui/calendar";
import { useLayoutEffect, useRef, useState } from "react";
import { getDefaultClassNames } from "react-day-picker";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { ScrollArea } from "@/components/ui/scroll-area";
import { isSameDay, getWeeksInMonth, getWeekOfMonth } from "date-fns";
import { SchedulePopover } from "../popover/SchedulePopover";
interface CustomCalendarProps {
data?: any;
}
export const CustomCalendar = ({ data }: CustomCalendarProps) => {
const [weekCount, setWeekCount] = useState(5);
const [selectedDate, setSelectedDate] = useState<Date | undefined>(undefined);
const [popoverOpen, setPopoverOpen] = useState(false);
const [popoverSide, setPopoverSide] = useState<'right' | 'left'>('right');
const [popoverAlign, setPopoverAlign] = useState<'start' | 'end'>('end');
const defaultClassNames = getDefaultClassNames();
const containerRef = useRef<HTMLDivElement>(null);
const updateWeekCount = () => {
if (containerRef === null) return;
if (!containerRef.current) return;
const weeks = containerRef.current.querySelectorAll('.rdp-week');
if (weeks?.length) setWeekCount(weeks.length);
}
useLayoutEffect(() => {
updateWeekCount();
}, []);
const handleOpenChange = (open: boolean) => {
setPopoverOpen(open);
if (!open) {
setTimeout(() => {
setSelectedDate(undefined);
}, 150);
}
}
const handleDaySelect = (date: Date | undefined) => {
if (!date) {
setPopoverOpen(false);
setTimeout(() => {
setSelectedDate(undefined);
}, 150);
return;
}
if (date) {
setSelectedDate(date);
const dayOfWeek = date.getDay();
if (0 <= dayOfWeek && dayOfWeek < 4) {
setPopoverSide('right');
} else {
setPopoverSide('left');
}
const options = { weekStartsOn: 0 as 0 };
const totalWeeks = getWeeksInMonth(date, options);
const currentWeekNumber = getWeekOfMonth(date, options);
const threshold = Math.ceil(totalWeeks / 2);
if (currentWeekNumber <= threshold) {
setPopoverAlign('start');
} else {
setPopoverAlign('end');
}
requestAnimationFrame(() => {
setPopoverOpen(true);
})
}
}
return (
<div
className="w-full h-full"
ref={containerRef}
>
<Popover
open={popoverOpen}
onOpenChange={handleOpenChange}
>
<Calendar
mode="single"
className="h-full w-full border rounded-lg"
selected={selectedDate}
onSelect={handleDaySelect}
onMonthChange={() => {
// month 바뀐 직후 DOM 변화가 생기므로 다음 프레임에서 계산
requestAnimationFrame(() => {
updateWeekCount();
});
}}
classNames={{
months: cn(
defaultClassNames.months,
"w-full h-full relative"
),
nav: cn(
defaultClassNames.nav,
"flex w-full item-center gap-1 justify-around absolute top-0 inset-x-0"
),
month: cn(
defaultClassNames.month,
"h-full w-full flex flex-col"
),
month_grid: cn(
defaultClassNames.month_grid,
"w-full h-full flex-1"
),
weeks: cn(
defaultClassNames.weeks,
"w-full h-full"
),
weekdays: cn(
defaultClassNames.weekdays,
"w-full"
),
week: cn(
defaultClassNames.week,
`w-full`
),
day: cn(
defaultClassNames.day,
`w-[calc(100%/7)] rounded-none`
),
day_button: cn(
defaultClassNames.day_button,
"h-full w-full flex p-2 justify-start items-start",
"hover:bg-transparent",
"data-[selected-single=true]:bg-transparent data-[selected-single=true]:text-black"
),
selected: cn(
defaultClassNames.selected,
"h-full border-0 fill-transparent"
),
today: cn(
defaultClassNames.today,
"h-full"
),
}}
styles={{
day: {
height: `calc(100%/${weekCount})`
},
}}
components={{
Day: ({ day, ...props }) => {
const date = day.date;
const isSelected = selectedDate && isSameDay(selectedDate, date);
return (
<td {...props}>
{ isSelected
? <PopoverTrigger asChild>
{props.children}
</PopoverTrigger>
: props.children
}
</td>
)
},
DayButton: ({ day, ...props}) => (
<button
{...props}
disabled={day.outside}
>
{props.children}
</button>
)
}}
/>
<SchedulePopover
date={selectedDate}
popoverSide={popoverSide}
popoverAlign={popoverAlign}
/>
</Popover>
</div>
)
}

View File

@@ -0,0 +1,72 @@
import { PopoverContent } from "@/components/ui/popover"
import type { ColorPaletteType } from "@/const/ColorPalette"
import { usePalette } from "@/hooks/use-palette";
import { useState } from "react";
interface ColorPickPopoverProps {
setColor: (color: ColorPaletteType) => void;
}
export const ColorPickPopover = ({ setColor }: ColorPickPopoverProps) => {
const [seeMore, setSeeMore] = useState(false);
const {
getMainPaletteList,
getExtraPaletteList,
getCustomColor
} = usePalette();
const mainPaletteList = getMainPaletteList();
const extraPaletteList = getExtraPaletteList();
const getSlicedList = (paletteList: ColorPaletteType[], length: number) => {
const slicedList: ColorPaletteType[][] = [];
let index = 0;
while (index < paletteList.length) {
slicedList.push(paletteList.slice(index, index + length));
index += length;
}
return slicedList;
}
return (
<PopoverContent
className="flex flex-col gap-1.5 w-fit"
>
{getSlicedList(mainPaletteList, 5).map((list) => (
<div className="flex flex-row gap-2.5">
{list.map((palette) => (
<div
className="rounded-full w-5 h-5 border border-gray-300"
style={{ backgroundColor: `${palette.style}` }}
onClick={() => setColor(palette)}
/>
))}
</div>
))}
{
!seeMore
? <div className="w-full" onClick={() => setSeeMore(true)}> </div>
: <>
{getSlicedList(extraPaletteList, 5).map((list) => (
<div className="flex flex-row gap-2.5">
{list.map((palette) => (
<div
className="rounded-full w-5 h-5 border border-gray-300"
style={{
backgroundColor: `${palette.style !== 'transparent' && palette.style}`,
background: `${palette.style === 'transparent' && 'linear-gradient(135deg, black 50%, white 50%)' }`
}}
onClick={() => setColor(palette)}
/>
))}
</div>
))}
</>
}
{
seeMore
? <div className="w-full" onClick={() => setSeeMore(false)}></div>
: null
}
</PopoverContent>
)
}

View File

@@ -0,0 +1,75 @@
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Sheet, SheetContent, SheetHeader } from '@/components/ui/sheet';
import { cn } from '@/lib/utils';
import { useEffect, useState } from 'react';
import { usePalette } from '@/hooks/use-palette';
import { type ColorPaletteType } from '@/const/ColorPalette';
import { ColorPickPopover } from './ColorPickPopover';
import { Input } from '@/components/ui/input';
interface ScheduleSheetProps {
date: Date | undefined;
popoverSide: 'left' | 'right';
popoverAlign: 'start' | 'end';
}
export const SchedulePopover = ({ date, popoverSide, popoverAlign }: ScheduleSheetProps) => {
const {
ColorPaletteType,
getPaletteNameList,
getMainPaletteList,
getAllPaletteList,
getCustomColor,
getPaletteByKey,
getStyle
} = usePalette();
const defaultColor = getPaletteByKey('Black');
const [scheduleColor, setScheduleColor] = useState(defaultColor);
const [colorPopoverOpen, setColorPopoverOpen] = useState(false);
const selectColor = (color: ColorPaletteType) => {
setScheduleColor(color);
setColorPopoverOpen(false);
}
return (
<PopoverContent
className="rounded-xl xl:w-[calc(100vw/4)] xl:max-w-[480px] min-w-[320px]"
align={popoverAlign} side={popoverSide}
>
<ScrollArea
className={
cn(
"[&>div>div:last-child]:hidden min-h-[125px] h-[calc(100vh/2)] p-2.5 w-full flex flex-col",
)
}
>
<div className="w-full flex flex-row justify-center items-center gap-4">
<Popover open={colorPopoverOpen} onOpenChange={setColorPopoverOpen}>
<PopoverTrigger asChild>
<div
className={cn(
'rounded-full w-5 h-5 border-2 border-gray-300',
)}
style={{
backgroundColor: `${scheduleColor.style !== 'transparent' && scheduleColor.style}`,
background: `${scheduleColor.style === 'transparent' && 'linear-gradient(135deg, black 50%, white 50%)' }`
}}
/>
</PopoverTrigger>
<ColorPickPopover
setColor={selectColor}
/>
</Popover>
<Input
placeholder="제목"
className="font-bold border-t-0 border-r-0 border-l-0 p-0 border-b-2 rounded-none shadow-none border-indigo-300 focus-visible:ring-0 focus-visible:border-b-indigo-500"
style={{
fontSize: '20px'
}}
/>
</div>
</ScrollArea>
</PopoverContent>
)
}

View File

@@ -21,7 +21,7 @@ import { useIsMobile } from '@/hooks/use-mobile';
export default function LoginPage() {
const [isLoading, setIsLoading] = useState<boolean>(false);
const [autoLogin, setAutoLogin] = useState<boolean>(false);
const [autoLogin, setAutoLogin] = useState<boolean>(localStorage.getItem('autoLogin') === 'true');
const { login } = useAuthStore();
const isMobile = useIsMobile();
const navigate = useNavigate();
@@ -36,7 +36,7 @@ export default function LoginPage() {
const { id, password } = { id: loginForm.watch('id'), password: loginForm.watch('password') };
useEffect(() => {
localStorage.setItem('autoLogin', `${autoLogin}`)
localStorage.setItem('autoLogin', `${autoLogin}`);
}, [autoLogin]);
const moveToSignUpPage = useCallback(() => {
@@ -74,13 +74,19 @@ export default function LoginPage() {
refreshToken: res.data.refreshToken!
};
login({...data});
if (autoLogin) {
localStorage.setItem('auth-storage', JSON.stringify({ state: data }));
}
moveToHomePage();
return "로그인 성공";
} else {
throw new Error(res.data.message);
}
},
error: (err: Error) => err.message || "에러 발생"
error: (err: Error) => {
setIsLoading(false);
return err.message || "에러 발생"
}
}
);
}

View File

@@ -1,26 +1,11 @@
import { Calendar } from "@/components/ui/calendar";
import { DayButton } from "react-day-picker";
import { CustomCalendar } from "@/ui/component/calendar/CustomCalendar";
export function ScheduleMainPage() {
return (
<div className="w-full h-full flex flex-col justify-start items-center">
<Calendar
mode="single"
className="rounded-lg w-full h-full max-h-10/12 border"
components={{
Weeks: (props) => (
<tbody {...props} className={props.className}></tbody>
),
Week: (props) => (
<tr {...props} className={props.className + " h-1/10"}></tr>
),
Day: (props) => (
<td {...props} className={props.className + " h-1"}></td>
)
}}
>
</Calendar>
<div
className="w-full h-full p-2"
>
<CustomCalendar />
</div>
)
}

View File

@@ -5,6 +5,7 @@ import path from 'path'
// https://vite.dev/config/
export default defineConfig({
server: {
host: '0.0.0.0',
port: 5185
},
plugins: [