issue #60
All checks were successful
Test CI / build (push) Successful in 37s

- 일정 목록 조회 ui 일부 수정
- 일정 상세 조회 로직 구현 중
This commit is contained in:
geonhee-min
2025-12-12 17:05:08 +09:00
parent 78e3bdbda0
commit 8015eb45db
14 changed files with 1048 additions and 91 deletions

3
.npmrc Normal file
View File

@@ -0,0 +1,3 @@
@baekyangdan:registry=https://gitea.bkdhome.p-e.kr/api/packages/baekyangdan/npm/
//gitea.bkdhome.p-e.kr/api/packages/baekyangdan/npm/:_authToken=d39c7d88c52806df7522ce2b340b6577c5ec5082
always-auth=true

842
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -11,6 +11,7 @@
"preview": "vite preview"
},
"dependencies": {
"@baekyangdan/core-utils": "^1.0.4",
"@diceui/mention": "^0.8.0",
"@hookform/resolvers": "^5.2.2",
"@radix-ui/react-accordion": "^1.2.12",

View File

@@ -5,84 +5,68 @@ export type ColorPaletteType = {
export const ColorPalette: Record<any, ColorPaletteType> = {
Black: {
index: 0,
style: '#000000'
},
White: {
index: 1,
style: '#FFFFFF'
},
SerenityBlue: {
index: 2,
index: 0,
style: '#92A8D1'
},
CoralPink: {
index: 3,
index: 1,
style: '#F08080'
},
MintIcing: {
index: 4,
index: 2,
style: '#C1E1C1'
},
Vanilla: {
index: 5,
index: 3,
style: '#FFFACD'
},
Wheat: {
index: 6,
index: 4,
style: '#F5DEB3'
},
AliceBlue: {
index: 7,
style: '#F0F8FF'
},
Lavender: {
index: 8,
index: 5,
style: '#E6E6FA'
},
SageGreen: {
index: 9,
index: 6,
style: '#b2ac88'
},
CloudWhite: {
index: 10,
style: '#F2F2ED'
},
LightGray: {
index: 11,
index: 7,
style: '#D3D3D3'
},
LightKhakki: {
index: 12,
index: 8,
style: '#F0F8E6'
},
DustyRose: {
index: 13,
index: 9,
style: '#D8BFD8'
},
CreamBeige: {
index: 14,
index: 10,
style: '#FAF0E6'
},
Oatmeal: {
index: 15,
index: 11,
style: '#FDF5E6'
},
CharcoalLight: {
index: 16,
index: 12,
style: '#A9A9A9'
},
PeachCream: {
index: 17,
index: 13,
style: '#FFDAB9'
},
LavenderBlue: {
index: 18,
index: 14,
style :'#CCCCFF'
},
SeaFoamGreen: {
index: 19,
index: 15,
style: '#93E9BE'
}
}

View File

@@ -9,7 +9,7 @@ import type {
import { useAuthStore } from '@/store/authStore';
import { RefreshAccessTokenResponse } from '@/data/response/account/RefreshAccessTokenResponse';
import type { AuthData } from '@/data/AuthData';
import { UnauthorizedCode, UnauthorizedMessage } from '@baekyangdan/core-utils';
export class BaseNetwork {
protected instance: AxiosInstance;
@@ -79,7 +79,7 @@ export class BaseNetwork {
if (
status === 401
&& errorCode === 'AccessTokenExpired'
&& errorCode === UnauthorizedCode.ACCESS_TOKEN_EXPIRED
&& !originalRequest._retry
) {
originalRequest._retry = true;
@@ -97,7 +97,7 @@ export class BaseNetwork {
if (!authData) {
useAuthStore.getState().logout();
return Promise.reject("no refresh token");
return Promise.reject(UnauthorizedMessage.INVALID_TOKEN);
}
if (this.isRefreshing) {

View File

@@ -17,10 +17,10 @@ interface CustomCalendarProps {
interface EventBarPosition extends ScheduleListData {
positionStyle: React.CSSProperties;
trackIndex: number;
isOverflow?: boolean;
segmentId: string;
}
const MAX_VISIBLE_EVENTS = 3;
const OVERFLOW_TRACK_INDEX = MAX_VISIBLE_EVENTS + 1;
const TRACK_HEIGHT = 20;
const TOP_OFFSET_FROM_CELL = 35;
const DATE_FORMAT_ARIA = 'EEEE, MMMM do, yyyy';
@@ -34,6 +34,9 @@ export const CustomCalendar = ({ data }: CustomCalendarProps) => {
const [popoverAlign, setPopoverAlign] = useState<'start' | 'end'>('end');
const [month, setMonth] = useState(new Date());
const [currentDataMonth, setCurrentDataMonth] = useState(month);
const [windowSize, setWindowSize] = useState({ width: window.innerWidth, height: window.innerHeight });
const [maxVisibleEvents, setMaxVisibleEvents] = useState(3);
const [overflowTrackIndex, setOverflowTrackIndex] = useState(maxVisibleEvents + 1);
const [scheduleList, setScheduleList] = useState<Array<ScheduleListData>>([]);
const [barPositions, setBarPositions] = useState<Array<EventBarPosition>>([]);
const scheduleNetwork = new ScheduleNetwork();
@@ -49,8 +52,28 @@ export const CustomCalendar = ({ data }: CustomCalendarProps) => {
useEffect(() => {
updateWeekCount();
const handleResize = () => {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight
});
if (window.innerHeight >= 850) {
setMaxVisibleEvents(3);
setOverflowTrackIndex(4);
} else {
setMaxVisibleEvents(2);
setOverflowTrackIndex(3);
}
};
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
}
}, []);
useLayoutEffect(() => {
updateWeekCount();
@@ -151,7 +174,7 @@ export const CustomCalendar = ({ data }: CustomCalendarProps) => {
let assignedTrack = -1;
for (let track = 0; track < MAX_VISIBLE_EVENTS; track++) {
for (let track = 0; track < maxVisibleEvents; track++) {
let isAvailable = true;
for (const day of eventDays) {
const dayKey = format(day, DATE_FORMAT_KEY);
@@ -192,33 +215,50 @@ export const CustomCalendar = ({ data }: CustomCalendarProps) => {
const regularPositions: EventBarPosition[] = [];
// 달력을 초과한 부분에 대한 처리
const visibleDayKeys = Array.from(cellInfoMap.keys());
if (!visibleDayKeys || visibleDayKeys.length === 0) return;
const calendarStart = parse(visibleDayKeys.at(0)!, DATE_FORMAT_KEY, new Date());
const calendarEnd = parse(visibleDayKeys.at(-1)!, DATE_FORMAT_KEY, new Date());
scheduleListWithTrack.forEach(schedule => {
const startKey = format(new Date(schedule.startDate), DATE_FORMAT_KEY);
const endKey = format(new Date(schedule.endDate), DATE_FORMAT_KEY);
const startDateObj = new Date(schedule.startDate);
const endDateObj = new Date(schedule.endDate);
const startInfo = cellInfoMap.get(startKey);
const endInfo = cellInfoMap.get(endKey);
const renderStartDate = startDateObj > calendarStart ? startDateObj : calendarStart;
const renderEndDate = endDateObj < calendarEnd ? endDateObj : calendarEnd;
if (startInfo && endInfo) {
const startRect = startInfo.rect;
const endRect = endInfo.rect;
const renderStartKey = format(renderStartDate, DATE_FORMAT_KEY);
const renderEndKey = format(renderEndDate, DATE_FORMAT_KEY);
const baseTop = startRect.top - containerRect.top;
const top = baseTop + TOP_OFFSET_FROM_CELL + (schedule.trackIndex * (TRACK_HEIGHT + 5));
const left = startRect.left - containerRect.left + 5;
const width = endRect.right - startRect.left - 10;
const allRenderDays = eachDayOfInterval({ start: renderStartDate, end: renderEndDate });
regularPositions.push({
...schedule,
trackIndex: schedule.trackIndex,
positionStyle: {
top: `${top}px`,
left: `${left}px`,
width: `${width}px`,
height: `${TRACK_HEIGHT}px`
}
});
}
let segmentStartDay = renderStartDate;
allRenderDays.forEach((currentDay, index) => {
const dayOfWeek = currentDay.getDay();
const isLastDayOfEvent = index === allRenderDays.length - 1;
if (dayOfWeek === 6 && !isLastDayOfEvent) {
const segmentEndDay = currentDay;
createAndPushPosition(
regularPositions, schedule,
segmentStartDay, segmentEndDay,
cellInfoMap, containerRect
);
segmentStartDay = new Date(currentDay);
segmentStartDay.setDate(segmentStartDay.getDate() + 1);
} else if (isLastDayOfEvent) {
createAndPushPosition(
regularPositions, schedule,
segmentStartDay, currentDay,
cellInfoMap, containerRect
);
}
});
});
const overflowPositions: EventBarPosition[] = [];
@@ -229,7 +269,7 @@ export const CustomCalendar = ({ data }: CustomCalendarProps) => {
const dayRect = dayInfo.rect;
const baseTop = dayRect.top - containerRect.top;
const top = baseTop + TOP_OFFSET_FROM_CELL + ((OVERFLOW_TRACK_INDEX - 1) * (TRACK_HEIGHT + 5));
const top = baseTop + TOP_OFFSET_FROM_CELL + ((overflowTrackIndex - 1) * (TRACK_HEIGHT + 5));
const left = dayRect.left - containerRect.left + 5;
const width = dayInfo.cell.clientWidth - 10;
@@ -239,7 +279,9 @@ export const CustomCalendar = ({ data }: CustomCalendarProps) => {
startDate: Converter.dateToUTC9(new Date()),
endDate: Converter.dateToUTC9(new Date()),
style: '#9CA3AF',
trackIndex: OVERFLOW_TRACK_INDEX,
trackIndex: overflowTrackIndex,
isOverflow: true,
segmentId: dayKey,
positionStyle: {
top: `${top}px`,
left: `${left}px`,
@@ -250,7 +292,46 @@ export const CustomCalendar = ({ data }: CustomCalendarProps) => {
}
});
setBarPositions([...regularPositions, ...overflowPositions]);
}, [scheduleList, month, currentDataMonth]);
}, [scheduleList, month, currentDataMonth, windowSize]);
const createAndPushPosition = (
positions: EventBarPosition[],
schedule: ScheduleListData & { trackIndex: number },
segmentStartDay: Date,
segmentEndDay: Date,
cellInfoMap: Map<string, { cell: HTMLElement, rect: DOMRect }>,
containerRect: DOMRect
) => {
const renderStartKey = format(segmentStartDay, DATE_FORMAT_KEY);
const renderEndKey = format(segmentEndDay, DATE_FORMAT_KEY);
const startInfo = cellInfoMap.get(renderStartKey);
const endInfo = cellInfoMap.get(renderEndKey);
if (startInfo && endInfo) {
const startRect = startInfo.rect;
const endRect = endInfo.rect;
const baseTop = startRect.top - containerRect.top;
const top = baseTop + TOP_OFFSET_FROM_CELL + (schedule.trackIndex * (TRACK_HEIGHT + 5));
const left = startRect.left - containerRect.left + 5;
const width = endRect.right - startRect.left - 10;
positions.push({
...schedule,
trackIndex: schedule.trackIndex,
id: schedule.id,
segmentId: `${schedule.id}-${renderStartKey}`,
positionStyle: {
top: `${top}px`,
left: `${left}px`,
width: `${width}px`,
height: `${TRACK_HEIGHT}px`
}
})
}
}
const handleMonthChange = async (month: Date) => {
setMonth(month);
@@ -313,6 +394,29 @@ export const CustomCalendar = ({ data }: CustomCalendarProps) => {
open={popoverOpen}
onOpenChange={handleOpenChange}
>
{
barPositions.map(pos => (
<PopoverTrigger
className="z-100"
onClick={(e) => {
e.stopPropagation();
}}
asChild
>
<div
key={pos.segmentId}
className={cn(
`flex flex-row justify-start items-center absolute z-100`,
"py-0.5 px-2 rounded-sm text-xs text-white overflow-hidden"
)}
style={{...pos.positionStyle, backgroundColor: pos.style}}
>
{pos.name}
</div>
</PopoverTrigger>
))
}
<Calendar
mode="single"
className="h-full w-full border border-indigo-200 rounded-lg shadow-sm shadow-indigo-200"
@@ -350,20 +454,7 @@ export const CustomCalendar = ({ data }: CustomCalendarProps) => {
)
}}
/>
{
barPositions.map(pos => (
<div
key={pos.id}
className={cn(
`flex flex-row justify-start items-center absolute`,
"py-0.5 px-2 rounded-sm text-xs text-white overflow-hidden pointer-events-none"
)}
style={{...pos.positionStyle, backgroundColor: pos.style}}
>
{pos.name}
</div>
))
}
<SchedulePopover
date={selectedDate}
open={popoverOpen}

View File

@@ -2,6 +2,7 @@ import { PopoverContent } from '@/components/ui/popover';
import { useEffect, useState } from 'react';
import { ScheduleCreateContent } from './content/ScheduleCreateContent';
import { ScheduleListContent } from './content/ScheduleListContent';
import { ScheduleDetailContent } from './content/ScheduleDetailContent';
interface ScheduleSheetProps {
date: Date | undefined;
@@ -12,6 +13,7 @@ interface ScheduleSheetProps {
export const SchedulePopover = ({ date, open, popoverSide, popoverAlign }: ScheduleSheetProps) => {
const [mode, setMode] = useState<'list' | 'create' | 'detail' | 'update'>('list');
const [detailId, setDetailId] = useState('');
useEffect(() => {
if (!open) {
@@ -42,6 +44,7 @@ export const SchedulePopover = ({ date, open, popoverSide, popoverAlign }: Sched
case 'list':
return <ScheduleListContent
setMode={setMode}
setId={setDetailId}
date={date}
popoverAlign={popoverAlign}
popoverSide={popoverSide}
@@ -56,7 +59,14 @@ export const SchedulePopover = ({ date, open, popoverSide, popoverAlign }: Sched
open={open}
/>
case 'detail':
return <DetailContent />
return <ScheduleDetailContent
setMode={setMode}
date={date}
popoverAlign={popoverAlign}
popoverSide={popoverSide}
open={open}
id={detailId}
/>
case 'update':
return <UpdateContent />
}

View File

@@ -11,5 +11,9 @@ export interface ScheduleCreateContentProps extends BaseProps {
}
export interface ScheduleListContentProps extends BaseProps {
setId: (id: string) => void;
}
export interface ScheduleDetailContentProps extends BaseProps {
id: string;
}

View File

@@ -52,7 +52,7 @@ export const ScheduleCreateContent = ({ date, setMode, popoverSide, popoverAlign
endTime: getCurrentTimeString('standard'),
type: "once",
status: "yet",
style: getPaletteByKey('Black').style,
style: getPaletteByKey('SerenityBlue').style,
dayList: "",
participantList: []
}
@@ -312,6 +312,7 @@ export const ScheduleCreateContent = ({ date, setMode, popoverSide, popoverAlign
"bg-white text-red-400",
"hover:text-white hover:bg-red-400"
)}
onClick={() => setMode('list')}
>
</Button>

View File

@@ -0,0 +1,11 @@
import type { ScheduleDetailContentProps } from './ContentProps';
export const ScheduleDetailContent = ({ date, setMode, popoverSide, popoverAlign, id }: ScheduleDetailContentProps) => {
return (
<div
className="w-full h-full flex flex-col justify-start items-start gap-4"
>
{id}
</div>
)
}

View File

@@ -14,7 +14,7 @@ import { ScheduleListData } from "@/data/response";
import { Converter } from "@/util/Converter";
import { ScheduleListTile } from "../tile/ScheduleListTile";
export const ScheduleListContent = ({ date, setMode, popoverAlign, popoverSide, open }: ScheduleListContentProps) => {
export const ScheduleListContent = ({ date, setMode, popoverAlign, popoverSide, open, setId }: ScheduleListContentProps) => {
const [isLoading, setIsLoading] = useState(false);
const [scheduleList, setScheduleList] = useState<Array<ScheduleListData>>([]);
const scheduleNetwork = new ScheduleNetwork();
@@ -65,6 +65,11 @@ export const ScheduleListContent = ({ date, setMode, popoverAlign, popoverSide,
}
}
const moveToDetail = (id: string) => {
setId(id);
setMode('detail');
}
return (
<div className="w-full h-full flex flex-col gap-4">
<div className="relative w-full h-10 border-b border-b-indigo-300 flex flex-row items-center justify-center">
@@ -83,7 +88,8 @@ export const ScheduleListContent = ({ date, setMode, popoverAlign, popoverSide,
>
<div className="w-full h-full flex flex-col justify-start items-start gap-3">
{ scheduleList.map(schedule => (
<ScheduleListTile
<ScheduleListTile
onClick={moveToDetail}
data={schedule}
setMode={setMode}
/>

View File

@@ -72,14 +72,14 @@ export const ColorPickPopover = ({ setColor }: ColorPickPopoverProps) => {
>
<span
className={cn(
"text-indigo-300 group-hover:text-white transition-all duration-150"
"text-indigo-300 select-none group-hover:text-white text-sm transition-all duration-150"
)}
>
{ seeMore ? " 접기 " : "더 보기"}
</span>
<div
className={cn(
"w-0 h-0 border-l-8 border-l-transparent border-r-8 border-r-transparent border-b-14 border-b-indigo-300",
"w-0 h-0 border-l-6 border-l-transparent border-r-6 border-r-transparent border-b-10 border-b-indigo-300",
"group-hover:border-b-white trnasition-all duration-150",
!seeMore && "rotate-180"
)}

View File

@@ -306,7 +306,7 @@ export const TimePickPopover = ({ ...props }: TimePickPopoverProps) => {
</Button>
</PopoverTrigger>
<PopoverContent
className="w-fit h-42 flex flex-row p-0"
className="200 w-fit h-42 flex flex-row p-0"
align={'end'}
side={popoverAlign === 'start' ? 'bottom' : 'top'}
>

View File

@@ -5,17 +5,29 @@ import { Converter } from "@/util/Converter";
interface ScheduleListTileProps {
setMode: (mode: 'list' | 'create' | 'detail' | 'update') => void;
data: ScheduleListData;
onClick: (id: string) => void;
}
export const ScheduleListTile = ({ setMode, data }: ScheduleListTileProps) => {
export const ScheduleListTile = ({ setMode, data, onClick }: ScheduleListTileProps) => {
const formatter = Converter.isoStringToFormattedString;
const handleOnClickTile = (id: string) => {
onClick(id);
}
return (
<div
className="w-full h-15 rounded-sm border flex flex-row items-center cursor-default group"
className="w-full select-none h-15 rounded-sm border shadow-sm shadow-indigo-200 flex flex-row items-center cursor-default group"
style={{
borderColor: data.style
}}
onClick={() => handleOnClickTile(data.id)}
>
<div className={`w-6 h-full rounded-l-xs group-hover:w-10 transition-all duration-150`} style={{backgroundColor: `${data.style}CC`}} />
<div
className="w-[calc(100%-24px)] px-2 h-full flex flex-col justify-center items-start"
className="w-[calc(100%-24px)] border-l px-2 h-full flex flex-col justify-center items-start"
style={{
borderLeftColor: data.style
}}
>
<div className="flex-6 h-full flex flex-row justify-end items-start">
<span