From a50d38f1996f6ec57848d3234141bee0bbe35071 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A7=80=EC=9B=85?= Date: Wed, 15 Jul 2026 09:40:06 +0900 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20=EC=BA=98=EB=A6=B0=EB=8D=94=20supab?= =?UTF-8?q?ase=20=EC=97=B0=EB=8F=99=20=EB=B0=8F=20=EC=9D=BC=EC=A0=95=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=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 --- .../api/create-calendar-event.ts | 34 ++++ .../api/delete-calendar-event.ts | 20 +++ .../get-calendar-events-by-workspace-id.ts | 19 +++ .../api/update-calendar-event.ts | 42 +++++ .../use-calendar-events-by-workspace-id.ts | 15 ++ .../api/use-create-calendar-event.ts | 27 +++ .../api/use-delete-calendar-event.ts | 21 +++ .../api/use-update-calendar-event.ts | 28 +++ src/entities/calendar-event/index.ts | 10 +- .../model/calendar-event.mapper.ts | 159 ++++++++++++++++++ .../mock-calendar-events-by-workspace.ts | 142 ---------------- .../model/use-calendar-events-store.ts | 44 ----- .../manage-calendar/ui/CalendarView.tsx | 155 +++++++++++------ 13 files changed, 476 insertions(+), 240 deletions(-) create mode 100644 src/entities/calendar-event/api/create-calendar-event.ts create mode 100644 src/entities/calendar-event/api/delete-calendar-event.ts create mode 100644 src/entities/calendar-event/api/get-calendar-events-by-workspace-id.ts create mode 100644 src/entities/calendar-event/api/update-calendar-event.ts create mode 100644 src/entities/calendar-event/api/use-calendar-events-by-workspace-id.ts create mode 100644 src/entities/calendar-event/api/use-create-calendar-event.ts create mode 100644 src/entities/calendar-event/api/use-delete-calendar-event.ts create mode 100644 src/entities/calendar-event/api/use-update-calendar-event.ts create mode 100644 src/entities/calendar-event/model/calendar-event.mapper.ts delete mode 100644 src/entities/calendar-event/model/mock-calendar-events-by-workspace.ts delete mode 100644 src/features/manage-calendar/model/use-calendar-events-store.ts diff --git a/src/entities/calendar-event/api/create-calendar-event.ts b/src/entities/calendar-event/api/create-calendar-event.ts new file mode 100644 index 0000000..bc3fa05 --- /dev/null +++ b/src/entities/calendar-event/api/create-calendar-event.ts @@ -0,0 +1,34 @@ +'use server'; + +import { getCurrentUserId } from '@/shared/api/supabase/current-user'; +import { createSupabaseServerClient } from '@/shared/api/supabase/server'; + +import { toCalendarEventInsert } from '../model/calendar-event.mapper'; +import type { CalendarEventColor } from '../model/calendar-event.types'; + +export async function createCalendarEvent(params: { + workspaceId: string; + title: string; + date: string; + time: string | null; + color: CalendarEventColor; +}): Promise { + const supabase = await createSupabaseServerClient(); + const currentUserId = await getCurrentUserId(); + + const payload = toCalendarEventInsert({ + workspaceId: params.workspaceId, + title: params.title.trim(), + date: params.date, + time: params.time?.trim() || null, + color: params.color, + createdBy: currentUserId, + }); + + const { error } = await supabase.from('calendar_events').insert(payload); + + if (error) { + console.error('[calendar/createCalendarEvent] insert 실패:', error); + throw new Error('일정 생성에 실패했습니다. 잠시 후 다시 시도해주세요.'); + } +} diff --git a/src/entities/calendar-event/api/delete-calendar-event.ts b/src/entities/calendar-event/api/delete-calendar-event.ts new file mode 100644 index 0000000..f4e1e11 --- /dev/null +++ b/src/entities/calendar-event/api/delete-calendar-event.ts @@ -0,0 +1,20 @@ +'use server'; + +import { createSupabaseServerClient } from '@/shared/api/supabase/server'; + +export async function deleteCalendarEvent(params: { + workspaceId: string; + eventId: string; +}): Promise { + const supabase = await createSupabaseServerClient(); + const { error } = await supabase + .from('calendar_events') + .delete() + .eq('workspace_id', params.workspaceId) + .eq('id', params.eventId); + + if (error) { + console.error('[calendar/deleteCalendarEvent] delete 실패:', error); + throw new Error('일정 삭제에 실패했습니다. 잠시 후 다시 시도해주세요.'); + } +} diff --git a/src/entities/calendar-event/api/get-calendar-events-by-workspace-id.ts b/src/entities/calendar-event/api/get-calendar-events-by-workspace-id.ts new file mode 100644 index 0000000..0bf32b1 --- /dev/null +++ b/src/entities/calendar-event/api/get-calendar-events-by-workspace-id.ts @@ -0,0 +1,19 @@ +import { getSupabaseBrowserClient } from '@/shared/api/supabase/client'; + +import { toCalendarEvent, type CalendarEventRow } from '../model/calendar-event.mapper'; +import type { CalendarEvent } from '../model/calendar-event.types'; + +export async function getCalendarEventsByWorkspaceId(workspaceId: string): Promise { + const supabase = getSupabaseBrowserClient(); + const { data, error } = await supabase + .from('calendar_events') + .select('id, workspace_id, title, starts_at, description') + .eq('workspace_id', workspaceId) + .order('starts_at'); + + if (error) { + throw new Error(`일정 조회에 실패했습니다: ${error.message}`); + } + + return ((data ?? []) as CalendarEventRow[]).map(toCalendarEvent); +} diff --git a/src/entities/calendar-event/api/update-calendar-event.ts b/src/entities/calendar-event/api/update-calendar-event.ts new file mode 100644 index 0000000..e731888 --- /dev/null +++ b/src/entities/calendar-event/api/update-calendar-event.ts @@ -0,0 +1,42 @@ +'use server'; + +import { createSupabaseServerClient } from '@/shared/api/supabase/server'; + +import { toCalendarEventInsert } from '../model/calendar-event.mapper'; +import type { CalendarEventColor } from '../model/calendar-event.types'; + +export async function updateCalendarEvent(params: { + workspaceId: string; + eventId: string; + title: string; + date: string; + time: string | null; + color: CalendarEventColor; +}): Promise { + const supabase = await createSupabaseServerClient(); + const payload = toCalendarEventInsert({ + workspaceId: params.workspaceId, + title: params.title.trim(), + date: params.date, + time: params.time?.trim() || null, + color: params.color, + createdBy: '', + }); + + const { error } = await supabase + .from('calendar_events') + .update({ + title: payload.title, + description: payload.description, + event_type: payload.event_type, + starts_at: payload.starts_at, + ends_at: payload.ends_at, + }) + .eq('workspace_id', params.workspaceId) + .eq('id', params.eventId); + + if (error) { + console.error('[calendar/updateCalendarEvent] update 실패:', error); + throw new Error('일정 수정에 실패했습니다. 잠시 후 다시 시도해주세요.'); + } +} diff --git a/src/entities/calendar-event/api/use-calendar-events-by-workspace-id.ts b/src/entities/calendar-event/api/use-calendar-events-by-workspace-id.ts new file mode 100644 index 0000000..a4e4fc9 --- /dev/null +++ b/src/entities/calendar-event/api/use-calendar-events-by-workspace-id.ts @@ -0,0 +1,15 @@ +'use client'; + +import { useQuery } from '@tanstack/react-query'; + +import { getCalendarEventsByWorkspaceId } from './get-calendar-events-by-workspace-id'; + +export const calendarEventsByWorkspaceQueryKey = (workspaceId: string) => + ['calendar-events', 'workspace', workspaceId] as const; + +export function useCalendarEventsByWorkspaceId(workspaceId: string) { + return useQuery({ + queryKey: calendarEventsByWorkspaceQueryKey(workspaceId), + queryFn: () => getCalendarEventsByWorkspaceId(workspaceId), + }); +} diff --git a/src/entities/calendar-event/api/use-create-calendar-event.ts b/src/entities/calendar-event/api/use-create-calendar-event.ts new file mode 100644 index 0000000..a88a013 --- /dev/null +++ b/src/entities/calendar-event/api/use-create-calendar-event.ts @@ -0,0 +1,27 @@ +'use client'; + +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { toast } from 'sonner'; + +import type { CalendarEventColor } from '../model/calendar-event.types'; +import { createCalendarEvent } from './create-calendar-event'; +import { calendarEventsByWorkspaceQueryKey } from './use-calendar-events-by-workspace-id'; + +export function useCreateCalendarEvent(workspaceId: string) { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (params: { + title: string; + date: string; + time: string | null; + color: CalendarEventColor; + }) => createCalendarEvent({ workspaceId, ...params }), + onSettled: () => { + queryClient.invalidateQueries({ queryKey: calendarEventsByWorkspaceQueryKey(workspaceId) }); + }, + onError: (error) => { + toast.error(error instanceof Error ? error.message : '일정 생성에 실패했습니다'); + }, + }); +} diff --git a/src/entities/calendar-event/api/use-delete-calendar-event.ts b/src/entities/calendar-event/api/use-delete-calendar-event.ts new file mode 100644 index 0000000..19e24cc --- /dev/null +++ b/src/entities/calendar-event/api/use-delete-calendar-event.ts @@ -0,0 +1,21 @@ +'use client'; + +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { toast } from 'sonner'; + +import { deleteCalendarEvent } from './delete-calendar-event'; +import { calendarEventsByWorkspaceQueryKey } from './use-calendar-events-by-workspace-id'; + +export function useDeleteCalendarEvent(workspaceId: string) { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (eventId: string) => deleteCalendarEvent({ workspaceId, eventId }), + onSettled: () => { + queryClient.invalidateQueries({ queryKey: calendarEventsByWorkspaceQueryKey(workspaceId) }); + }, + onError: (error) => { + toast.error(error instanceof Error ? error.message : '일정 삭제에 실패했습니다'); + }, + }); +} diff --git a/src/entities/calendar-event/api/use-update-calendar-event.ts b/src/entities/calendar-event/api/use-update-calendar-event.ts new file mode 100644 index 0000000..79bece3 --- /dev/null +++ b/src/entities/calendar-event/api/use-update-calendar-event.ts @@ -0,0 +1,28 @@ +'use client'; + +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { toast } from 'sonner'; + +import type { CalendarEventColor } from '../model/calendar-event.types'; +import { updateCalendarEvent } from './update-calendar-event'; +import { calendarEventsByWorkspaceQueryKey } from './use-calendar-events-by-workspace-id'; + +export function useUpdateCalendarEvent(workspaceId: string) { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (params: { + eventId: string; + title: string; + date: string; + time: string | null; + color: CalendarEventColor; + }) => updateCalendarEvent({ workspaceId, ...params }), + onSettled: () => { + queryClient.invalidateQueries({ queryKey: calendarEventsByWorkspaceQueryKey(workspaceId) }); + }, + onError: (error) => { + toast.error(error instanceof Error ? error.message : '일정 수정에 실패했습니다'); + }, + }); +} diff --git a/src/entities/calendar-event/index.ts b/src/entities/calendar-event/index.ts index e286977..e58a018 100644 --- a/src/entities/calendar-event/index.ts +++ b/src/entities/calendar-event/index.ts @@ -1,7 +1,15 @@ -export { getMockCalendarEventsByWorkspaceId } from './model/mock-calendar-events-by-workspace'; export type { CalendarEvent, CalendarEventColor, CalendarEventFormValues, } from './model/calendar-event.types'; +export { formatCalendarEventTimeLabel } from './model/calendar-event.mapper'; +export { getCalendarEventsByWorkspaceId } from './api/get-calendar-events-by-workspace-id'; +export { + calendarEventsByWorkspaceQueryKey, + useCalendarEventsByWorkspaceId, +} from './api/use-calendar-events-by-workspace-id'; +export { useCreateCalendarEvent } from './api/use-create-calendar-event'; +export { useDeleteCalendarEvent } from './api/use-delete-calendar-event'; +export { useUpdateCalendarEvent } from './api/use-update-calendar-event'; export { CalendarEventChip } from './ui/CalendarEventChip'; diff --git a/src/entities/calendar-event/model/calendar-event.mapper.ts b/src/entities/calendar-event/model/calendar-event.mapper.ts new file mode 100644 index 0000000..93823e1 --- /dev/null +++ b/src/entities/calendar-event/model/calendar-event.mapper.ts @@ -0,0 +1,159 @@ +import type { CalendarEvent, CalendarEventColor } from './calendar-event.types'; + +export interface CalendarEventRow { + id: string; + workspace_id: string; + title: string; + starts_at: string; + description: string | null; +} + +interface CalendarEventMetadata { + color?: CalendarEventColor; + time?: string | null; +} + +const defaultColor: CalendarEventColor = 'violet'; + +function isCalendarEventColor(value: unknown): value is CalendarEventColor { + return ( + value === 'violet' || + value === 'purple' || + value === 'blue' || + value === 'green' || + value === 'amber' || + value === 'coral' || + value === 'pink' + ); +} + +function parseMetadata(description: string | null): CalendarEventMetadata { + if (!description) { + return {}; + } + + try { + const parsed = JSON.parse(description) as CalendarEventMetadata; + return parsed && typeof parsed === 'object' ? parsed : {}; + } catch { + return {}; + } +} + +function formatIsoDateInKst(isoDateTime: string) { + const formatter = new Intl.DateTimeFormat('en-CA', { + timeZone: 'Asia/Seoul', + year: 'numeric', + month: '2-digit', + day: '2-digit', + }); + + return formatter.format(new Date(isoDateTime)); +} + +export function normalizeCalendarEventTime(value: string | null | undefined): string | null { + if (!value) { + return null; + } + + const trimmedTime = value.trim(); + if (!trimmedTime) { + return null; + } + + const basicMatch = trimmedTime.match(/^(\d{1,2}):(\d{2})$/); + if (basicMatch) { + const hour = Number(basicMatch[1]); + const minute = Number(basicMatch[2]); + + if (hour >= 0 && hour <= 23 && minute >= 0 && minute <= 59) { + return `${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}`; + } + } + + const koreanMatch = trimmedTime.match(/^(오전|오후)\s*(\d{1,2}):(\d{2})$/); + if (koreanMatch) { + const [, meridiem, rawHour, rawMinute] = koreanMatch; + let hour = Number(rawHour); + const minute = Number(rawMinute); + + if (minute < 0 || minute > 59 || hour < 1 || hour > 12) { + return null; + } + + if (meridiem === '오전') { + if (hour === 12) hour = 0; + } else if (hour < 12) { + hour += 12; + } + + return `${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}`; + } + + return null; +} + +export function formatCalendarEventTimeLabel(value: string | null | undefined): string { + const normalizedTime = normalizeCalendarEventTime(value); + + if (!normalizedTime) { + return '시간 미정'; + } + + const [rawHour, rawMinute] = normalizedTime.split(':'); + const hour = Number(rawHour); + const minute = Number(rawMinute); + const meridiem = hour < 12 ? '오전' : '오후'; + const displayHour = hour % 12 === 0 ? 12 : hour % 12; + + return `${meridiem} ${displayHour}:${String(minute).padStart(2, '0')}`; +} + +export function toCalendarEvent(row: CalendarEventRow): CalendarEvent { + const metadata = parseMetadata(row.description); + + return { + id: row.id, + workspaceId: row.workspace_id, + title: row.title, + date: formatIsoDateInKst(row.starts_at), + time: normalizeCalendarEventTime(metadata.time), + color: isCalendarEventColor(metadata.color) ? metadata.color : defaultColor, + }; +} + +function parseTimeTo24Hour(time: string) { + const normalizedTime = normalizeCalendarEventTime(time); + if (!normalizedTime) { + return { hour: 0, minute: 0 }; + } + + const [hour, minute] = normalizedTime.split(':'); + return { hour: Number(hour), minute: Number(minute) }; +} + +export function toCalendarEventInsert(params: { + workspaceId: string; + title: string; + date: string; + time: string | null; + color: CalendarEventColor; + createdBy: string; +}) { + const { hour, minute } = parseTimeTo24Hour(params.time ?? ''); + const startsAt = `${params.date}T${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}:00+09:00`; + + return { + workspace_id: params.workspaceId, + created_by: params.createdBy, + task_id: null, + title: params.title, + description: JSON.stringify({ + time: params.time, + color: params.color, + }), + event_type: 'meeting' as const, + starts_at: startsAt, + ends_at: null, + }; +} diff --git a/src/entities/calendar-event/model/mock-calendar-events-by-workspace.ts b/src/entities/calendar-event/model/mock-calendar-events-by-workspace.ts deleted file mode 100644 index 15cb513..0000000 --- a/src/entities/calendar-event/model/mock-calendar-events-by-workspace.ts +++ /dev/null @@ -1,142 +0,0 @@ -import type { CalendarEvent } from './calendar-event.types'; - -const mockCalendarEventsByWorkspaceId: Record = { - test: [ - { - id: 'calendar-event-1', - workspaceId: 'test', - title: 'API 명세 마감', - date: '2025-07-02', - time: null, - color: 'violet', - }, - { - id: 'calendar-event-2', - workspaceId: 'test', - title: '백로그 정리', - date: '2025-07-02', - time: '오후 1:00', - color: 'blue', - }, - { - id: 'calendar-event-3', - workspaceId: 'test', - title: '와이어프레임', - date: '2025-07-03', - time: null, - color: 'purple', - }, - { - id: 'calendar-event-4', - workspaceId: 'test', - title: '스프린트', - date: '2025-07-03', - time: null, - color: 'blue', - }, - { - id: 'calendar-event-5', - workspaceId: 'test', - title: '사용자 리서치', - date: '2025-07-05', - time: null, - color: 'blue', - }, - { - id: 'calendar-event-6', - workspaceId: 'test', - title: '회의 안건 정리', - date: '2025-07-05', - time: '오전 10:00', - color: 'amber', - }, - { - id: 'calendar-event-7', - workspaceId: 'test', - title: '랜딩 디자인', - date: '2025-07-08', - time: null, - color: 'violet', - }, - { - id: 'calendar-event-8', - workspaceId: 'test', - title: '카피 문구 검토', - date: '2025-07-08', - time: '오후 2:00', - color: 'purple', - }, - { - id: 'calendar-event-9', - workspaceId: 'test', - title: 'MVP 완성', - date: '2025-07-14', - time: null, - color: 'green', - }, - { - id: 'calendar-event-10', - workspaceId: 'test', - title: '중간 점검 회의', - date: '2025-07-14', - time: '오후 4:00', - color: 'blue', - }, - { - id: 'calendar-event-11', - workspaceId: 'test', - title: 'QA 시작', - date: '2025-07-22', - time: null, - color: 'coral', - }, - { - id: 'calendar-event-12', - workspaceId: 'test', - title: '버그 우선순위 정리', - date: '2025-07-22', - time: '오전 11:00', - color: 'pink', - }, - { - id: 'calendar-event-13', - workspaceId: 'test', - title: '배포 준비', - date: '2025-07-28', - time: null, - color: 'violet', - }, - { - id: 'calendar-event-14', - workspaceId: 'test', - title: '릴리즈 체크리스트', - date: '2025-07-28', - time: '오후 1:00', - color: 'green', - }, - { - id: 'calendar-event-15', - workspaceId: 'test', - title: '최종 발표 리허설', - date: '2025-07-30', - time: '오후 3:00', - color: 'violet', - }, - { - id: 'calendar-event-16', - workspaceId: 'test', - title: '발표 자료 검수', - date: '2025-07-30', - time: '오후 5:00', - color: 'amber', - }, - ], -}; - -export function getMockCalendarEventsByWorkspaceId(workspaceId: string): CalendarEvent[] { - return ( - mockCalendarEventsByWorkspaceId[workspaceId]?.map((event) => ({ - ...event, - })) ?? [] - ); -} diff --git a/src/features/manage-calendar/model/use-calendar-events-store.ts b/src/features/manage-calendar/model/use-calendar-events-store.ts deleted file mode 100644 index 6ae5feb..0000000 --- a/src/features/manage-calendar/model/use-calendar-events-store.ts +++ /dev/null @@ -1,44 +0,0 @@ -'use client'; - -import { create } from 'zustand'; -import type { CalendarEvent } from '@/entities/calendar-event'; - -interface CalendarEventsStore { - calendarEventsByWorkspaceId: Record; - initializeWorkspace: (workspaceId: string, events: CalendarEvent[]) => void; - addCalendarEvent: (workspaceId: string, event: CalendarEvent) => void; - removeCalendarEvent: (workspaceId: string, eventId: string) => void; -} - -export const useCalendarEventsStore = create((set) => ({ - calendarEventsByWorkspaceId: {}, - initializeWorkspace: (workspaceId, events) => - set((state) => { - if (state.calendarEventsByWorkspaceId[workspaceId]) { - return state; - } - - return { - calendarEventsByWorkspaceId: { - ...state.calendarEventsByWorkspaceId, - [workspaceId]: events, - }, - }; - }), - addCalendarEvent: (workspaceId, event) => - set((state) => ({ - calendarEventsByWorkspaceId: { - ...state.calendarEventsByWorkspaceId, - [workspaceId]: [...(state.calendarEventsByWorkspaceId[workspaceId] ?? []), event], - }, - })), - removeCalendarEvent: (workspaceId, eventId) => - set((state) => ({ - calendarEventsByWorkspaceId: { - ...state.calendarEventsByWorkspaceId, - [workspaceId]: (state.calendarEventsByWorkspaceId[workspaceId] ?? []).filter( - (event) => event.id !== eventId, - ), - }, - })), -})); diff --git a/src/features/manage-calendar/ui/CalendarView.tsx b/src/features/manage-calendar/ui/CalendarView.tsx index 0a80550..0747c94 100644 --- a/src/features/manage-calendar/ui/CalendarView.tsx +++ b/src/features/manage-calendar/ui/CalendarView.tsx @@ -1,13 +1,17 @@ 'use client'; -import { useEffect, useState } from 'react'; -import { ChevronLeft, ChevronRight, Plus, X } from 'lucide-react'; +import { useState } from 'react'; +import { ChevronLeft, ChevronRight, Pencil, Plus, X } from 'lucide-react'; import { CalendarEventChip, - getMockCalendarEventsByWorkspaceId, type CalendarEvent, type CalendarEventColor, type CalendarEventFormValues, + formatCalendarEventTimeLabel, + useCalendarEventsByWorkspaceId, + useCreateCalendarEvent, + useDeleteCalendarEvent, + useUpdateCalendarEvent, } from '@/entities/calendar-event'; import { plusJakartaSans } from '@/shared/lib/fonts'; import { @@ -15,7 +19,6 @@ import { createCalendarMonthLabel, getInitialCalendarDate, } from '../model/calendar-utils'; -import { useCalendarEventsStore } from '../model/use-calendar-events-store'; interface CalendarViewProps { workspaceId: string; @@ -58,45 +61,35 @@ const defaultFormValues: CalendarEventFormValues = { color: 'violet', }; -function createCalendarEventId() { - if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') { - return `calendar-event-${crypto.randomUUID()}`; - } - - return `calendar-event-${Math.random().toString(36).slice(2, 10)}`; -} - function formatSelectedDateLabel(isoDate: string) { const [year, month, day] = isoDate.split('-'); return `${year}년 ${Number(month)}월 ${Number(day)}일`; } export function CalendarView({ workspaceId }: CalendarViewProps) { - const initialEvents = getMockCalendarEventsByWorkspaceId(workspaceId); - const initializeWorkspace = useCalendarEventsStore((state) => state.initializeWorkspace); - const addCalendarEvent = useCalendarEventsStore((state) => state.addCalendarEvent); - const removeCalendarEvent = useCalendarEventsStore((state) => state.removeCalendarEvent); - const storedEvents = useCalendarEventsStore( - (state) => state.calendarEventsByWorkspaceId[workspaceId], - ); - - useEffect(() => { - initializeWorkspace(workspaceId, initialEvents); - }, [initialEvents, initializeWorkspace, workspaceId]); - - const calendarEvents = storedEvents ?? initialEvents; + const calendarEventsQuery = useCalendarEventsByWorkspaceId(workspaceId); + const createCalendarEventMutation = useCreateCalendarEvent(workspaceId); + const deleteCalendarEventMutation = useDeleteCalendarEvent(workspaceId); + const updateCalendarEventMutation = useUpdateCalendarEvent(workspaceId); + const calendarEvents = calendarEventsQuery.data ?? []; const [currentMonth, setCurrentMonth] = useState(getInitialCalendarDate); const [selectedDate, setSelectedDate] = useState('2025-07-30'); const [isAddEventOpen, setIsAddEventOpen] = useState(false); const [formValues, setFormValues] = useState(defaultFormValues); const [hasSubmitted, setHasSubmitted] = useState(false); + const [editingEventId, setEditingEventId] = useState(null); const monthGrid = createCalendarMonthGrid(currentMonth); const monthLabel = createCalendarMonthLabel(currentMonth); const isTitleValid = formValues.title.trim().length > 0; const [year, month, day] = selectedDate.split('-'); - const modalDateLabel = `${year}년 ${Number(month)}월 ${Number(day)}일 일정 추가`; + const isEditMode = editingEventId !== null; + const modalDateLabel = `${year}년 ${Number(month)}월 ${Number(day)}일 일정 ${isEditMode ? '수정' : '추가'}`; const selectedDateLabel = formatSelectedDateLabel(selectedDate); + const isSubmitting = + createCalendarEventMutation.isPending || + updateCalendarEventMutation.isPending || + deleteCalendarEventMutation.isPending; const eventsByDate = calendarEvents.reduce>( (accumulator, event) => { @@ -109,29 +102,53 @@ export function CalendarView({ workspaceId }: CalendarViewProps) { const openAddEventModal = (isoDate: string) => { setSelectedDate(isoDate); + setEditingEventId(null); setFormValues(defaultFormValues); setHasSubmitted(false); setIsAddEventOpen(true); }; - const handleSubmit = (event: React.FormEvent) => { + const openEditEventModal = (calendarEvent: CalendarEvent) => { + setSelectedDate(calendarEvent.date); + setEditingEventId(calendarEvent.id); + setFormValues({ + title: calendarEvent.title, + time: calendarEvent.time ?? '', + color: calendarEvent.color, + }); + setHasSubmitted(false); + setIsAddEventOpen(true); + }; + + const handleSubmit = async (event: React.FormEvent) => { event.preventDefault(); setHasSubmitted(true); - if (!isTitleValid) { + if (!isTitleValid || isSubmitting) { return; } - addCalendarEvent(workspaceId, { - id: createCalendarEventId(), - workspaceId, - title: formValues.title.trim(), - date: selectedDate, - time: formValues.time.trim() || null, - color: formValues.color, - }); - - setIsAddEventOpen(false); + try { + if (isEditMode && editingEventId) { + await updateCalendarEventMutation.mutateAsync({ + eventId: editingEventId, + title: formValues.title.trim(), + date: selectedDate, + time: formValues.time.trim() || null, + color: formValues.color, + }); + } else { + await createCalendarEventMutation.mutateAsync({ + title: formValues.title.trim(), + date: selectedDate, + time: formValues.time.trim() || null, + color: formValues.color, + }); + } + setIsAddEventOpen(false); + } catch { + return; + } }; return ( @@ -260,7 +277,22 @@ export function CalendarView({ workspaceId }: CalendarViewProps) {
- {selectedDateEvents.length > 0 ? ( + {calendarEventsQuery.isPending ? ( +
+

일정을 불러오는 중이에요

+
+ ) : calendarEventsQuery.isError ? ( +
+

일정을 불러오지 못했어요

+ +
+ ) : selectedDateEvents.length > 0 ? ( selectedDateEvents.map((calendarEvent) => (

- {calendarEvent.time ?? '시간 미정'} + {formatCalendarEventTimeLabel(calendarEvent.time)}

- +
+ + +
)) @@ -332,6 +376,7 @@ export function CalendarView({ workspaceId }: CalendarViewProps) { setFormValues((current) => ({ ...current, title: event.target.value })) } @@ -354,12 +399,13 @@ export function CalendarView({ workspaceId }: CalendarViewProps) { 시간 (선택) setFormValues((current) => ({ ...current, time: event.target.value })) } - placeholder="예: 오후 3:00" className="text-brand-ink focus:ring-brand mt-2 h-11 w-full rounded-[16px] bg-[#f1f3fb] px-4 text-[14px] transition outline-none placeholder:text-[#a8afc8] focus:ring-1" /> @@ -371,6 +417,7 @@ export function CalendarView({ workspaceId }: CalendarViewProps) { From 1cc36dbd6c814df7434eae7387b4de44d921637e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A7=80=EC=9B=85?= Date: Wed, 15 Jul 2026 11:31:28 +0900 Subject: [PATCH 2/3] =?UTF-8?q?feat:=20=EC=BA=98=EB=A6=B0=EB=8D=94=20?= =?UTF-8?q?=EC=9D=BC=EC=A0=95=20=EC=82=AD=EC=A0=9C=20mutation=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/manage-calendar/ui/CalendarView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/manage-calendar/ui/CalendarView.tsx b/src/features/manage-calendar/ui/CalendarView.tsx index 0747c94..240f94a 100644 --- a/src/features/manage-calendar/ui/CalendarView.tsx +++ b/src/features/manage-calendar/ui/CalendarView.tsx @@ -325,7 +325,7 @@ export function CalendarView({ workspaceId }: CalendarViewProps) {