From cfd6bbb8486c8d34c8fca65af39cbc754acb4ced Mon Sep 17 00:00:00 2001 From: toris-dev Date: Mon, 1 Dec 2025 22:32:38 +0900 Subject: [PATCH 01/32] =?UTF-8?q?feat:=20=EB=8F=84=EB=A9=94=EC=9D=B8?= =?UTF-8?q?=EB=B3=84=20=ED=8C=A8=ED=82=A4=EC=A7=80=20=EB=B6=84=EB=A6=AC=20?= =?UTF-8?q?(couple,=20planner,=20recommendation,=20user,=20api)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 커플 관련 기능을 packages/couple로 분리 - 여행 계획 관련 기능을 packages/planner로 분리 - 추천 시스템을 packages/recommendation로 분리 - 사용자 관련 기능을 packages/user로 분리 - API 클라이언트를 packages/api로 분리 - 유틸리티 함수를 packages/utils로 분리 --- packages/api/clients/index.ts | 7 + packages/api/clients/notification-service.ts | 119 ++++++ packages/api/eslint.config.mjs | 49 +++ packages/api/index.ts | 10 + packages/api/package.json | 22 ++ packages/api/supabase/client.ts | 14 + packages/api/supabase/index.ts | 7 + packages/api/supabase/server.ts | 37 ++ packages/api/tsconfig.json | 18 + .../calendar/components/calendar-grid.tsx | 172 +++++++++ .../calendar/components/calendar-sidebar.tsx | 50 +++ .../calendar/components/couple-required.tsx | 25 ++ .../components/create-event-dialog.tsx | 186 +++++++++ .../components/event-detail-dialog.tsx | 105 +++++ .../calendar/components/events-list.tsx | 101 +++++ .../calendar/components/place-search.tsx | 119 ++++++ .../calendar/hooks/use-calendar-data.ts | 107 +++++ .../calendar/hooks/use-calendar-events.ts | 33 ++ .../calendar/hooks/use-event-actions.ts | 72 ++++ .../calendar/hooks/use-event-form.ts | 49 +++ packages/couple/components/calendar/index.ts | 20 + packages/couple/components/calendar/types.ts | 26 ++ packages/couple/eslint.config.mjs | 49 +++ packages/couple/index.ts | 11 + packages/couple/package.json | 26 ++ packages/couple/services/calendar-service.ts | 364 ++++++++++++++++++ packages/couple/services/index.ts | 8 + packages/couple/tsconfig.json | 20 + packages/couple/types/index.ts | 37 ++ .../travel/components/course-info-overlay.tsx | 35 ++ .../travel/components/place-detail-card.tsx | 72 ++++ .../travel/components/travel-sidebar.tsx | 149 +++++++ .../travel/hooks/use-recommended-places.ts | 28 ++ .../travel/hooks/use-travel-courses.ts | 233 +++++++++++ packages/planner/components/travel/index.ts | 15 + packages/planner/components/travel/types.ts | 17 + packages/planner/eslint.config.mjs | 49 +++ packages/planner/index.ts | 9 + packages/planner/package.json | 29 ++ packages/planner/services/index.ts | 7 + .../planner/services/travel-service.client.ts | 176 +++++++++ packages/planner/services/travel-service.ts | 284 ++++++++++++++ packages/planner/tsconfig.json | 20 + packages/planner/types/index.ts | 18 + packages/recommendation/eslint.config.mjs | 49 +++ packages/recommendation/index.ts | 9 + packages/recommendation/package.json | 26 ++ packages/recommendation/services/index.ts | 6 + .../services/recommendation-service.ts | 311 +++++++++++++++ packages/recommendation/tsconfig.json | 20 + packages/recommendation/types/index.ts | 22 ++ packages/user/auth/config.ts | 44 +++ packages/user/auth/index.ts | 6 + .../components/auth/components/auth-form.tsx | 111 ++++++ .../auth/components/auth-mode-toggle.tsx | 21 + .../auth/components/login-header.tsx | 29 ++ .../auth/components/oauth-buttons.tsx | 71 ++++ .../components/auth/hooks/use-auth-actions.ts | 90 +++++ .../components/auth/hooks/use-auth-error.ts | 20 + .../components/auth/hooks/use-auth-form.ts | 35 ++ .../user/components/auth/hooks/use-oauth.ts | 59 +++ packages/user/components/auth/index.ts | 17 + packages/user/components/auth/types.ts | 7 + .../profile/components/profile-card.tsx | 150 ++++++++ .../profile/components/profile-header.tsx | 24 ++ .../profile/components/profile-stats.tsx | 34 ++ .../profile/components/settings-section.tsx | 250 ++++++++++++ .../components/profile/couple-connection.tsx | 323 ++++++++++++++++ .../profile/hooks/use-email-change.ts | 110 ++++++ .../profile/hooks/use-password-change.ts | 93 +++++ .../components/profile/hooks/use-profile.ts | 85 ++++ packages/user/components/profile/index.ts | 14 + packages/user/components/profile/types.ts | 42 ++ packages/user/eslint.config.mjs | 49 +++ packages/user/index.ts | 10 + packages/user/package.json | 26 ++ packages/user/tsconfig.json | 20 + packages/utils/eslint.config.mjs | 49 +++ packages/utils/index.ts | 40 ++ packages/utils/package.json | 21 + packages/utils/tsconfig.json | 18 + 81 files changed, 5315 insertions(+) create mode 100644 packages/api/clients/index.ts create mode 100644 packages/api/clients/notification-service.ts create mode 100644 packages/api/eslint.config.mjs create mode 100644 packages/api/index.ts create mode 100644 packages/api/package.json create mode 100644 packages/api/supabase/client.ts create mode 100644 packages/api/supabase/index.ts create mode 100644 packages/api/supabase/server.ts create mode 100644 packages/api/tsconfig.json create mode 100644 packages/couple/components/calendar/components/calendar-grid.tsx create mode 100644 packages/couple/components/calendar/components/calendar-sidebar.tsx create mode 100644 packages/couple/components/calendar/components/couple-required.tsx create mode 100644 packages/couple/components/calendar/components/create-event-dialog.tsx create mode 100644 packages/couple/components/calendar/components/event-detail-dialog.tsx create mode 100644 packages/couple/components/calendar/components/events-list.tsx create mode 100644 packages/couple/components/calendar/components/place-search.tsx create mode 100644 packages/couple/components/calendar/hooks/use-calendar-data.ts create mode 100644 packages/couple/components/calendar/hooks/use-calendar-events.ts create mode 100644 packages/couple/components/calendar/hooks/use-event-actions.ts create mode 100644 packages/couple/components/calendar/hooks/use-event-form.ts create mode 100644 packages/couple/components/calendar/index.ts create mode 100644 packages/couple/components/calendar/types.ts create mode 100644 packages/couple/eslint.config.mjs create mode 100644 packages/couple/index.ts create mode 100644 packages/couple/package.json create mode 100644 packages/couple/services/calendar-service.ts create mode 100644 packages/couple/services/index.ts create mode 100644 packages/couple/tsconfig.json create mode 100644 packages/couple/types/index.ts create mode 100644 packages/planner/components/travel/components/course-info-overlay.tsx create mode 100644 packages/planner/components/travel/components/place-detail-card.tsx create mode 100644 packages/planner/components/travel/components/travel-sidebar.tsx create mode 100644 packages/planner/components/travel/hooks/use-recommended-places.ts create mode 100644 packages/planner/components/travel/hooks/use-travel-courses.ts create mode 100644 packages/planner/components/travel/index.ts create mode 100644 packages/planner/components/travel/types.ts create mode 100644 packages/planner/eslint.config.mjs create mode 100644 packages/planner/index.ts create mode 100644 packages/planner/package.json create mode 100644 packages/planner/services/index.ts create mode 100644 packages/planner/services/travel-service.client.ts create mode 100644 packages/planner/services/travel-service.ts create mode 100644 packages/planner/tsconfig.json create mode 100644 packages/planner/types/index.ts create mode 100644 packages/recommendation/eslint.config.mjs create mode 100644 packages/recommendation/index.ts create mode 100644 packages/recommendation/package.json create mode 100644 packages/recommendation/services/index.ts create mode 100644 packages/recommendation/services/recommendation-service.ts create mode 100644 packages/recommendation/tsconfig.json create mode 100644 packages/recommendation/types/index.ts create mode 100644 packages/user/auth/config.ts create mode 100644 packages/user/auth/index.ts create mode 100644 packages/user/components/auth/components/auth-form.tsx create mode 100644 packages/user/components/auth/components/auth-mode-toggle.tsx create mode 100644 packages/user/components/auth/components/login-header.tsx create mode 100644 packages/user/components/auth/components/oauth-buttons.tsx create mode 100644 packages/user/components/auth/hooks/use-auth-actions.ts create mode 100644 packages/user/components/auth/hooks/use-auth-error.ts create mode 100644 packages/user/components/auth/hooks/use-auth-form.ts create mode 100644 packages/user/components/auth/hooks/use-oauth.ts create mode 100644 packages/user/components/auth/index.ts create mode 100644 packages/user/components/auth/types.ts create mode 100644 packages/user/components/profile/components/profile-card.tsx create mode 100644 packages/user/components/profile/components/profile-header.tsx create mode 100644 packages/user/components/profile/components/profile-stats.tsx create mode 100644 packages/user/components/profile/components/settings-section.tsx create mode 100644 packages/user/components/profile/couple-connection.tsx create mode 100644 packages/user/components/profile/hooks/use-email-change.ts create mode 100644 packages/user/components/profile/hooks/use-password-change.ts create mode 100644 packages/user/components/profile/hooks/use-profile.ts create mode 100644 packages/user/components/profile/index.ts create mode 100644 packages/user/components/profile/types.ts create mode 100644 packages/user/eslint.config.mjs create mode 100644 packages/user/index.ts create mode 100644 packages/user/package.json create mode 100644 packages/user/tsconfig.json create mode 100644 packages/utils/eslint.config.mjs create mode 100644 packages/utils/index.ts create mode 100644 packages/utils/package.json create mode 100644 packages/utils/tsconfig.json diff --git a/packages/api/clients/index.ts b/packages/api/clients/index.ts new file mode 100644 index 0000000..8a71138 --- /dev/null +++ b/packages/api/clients/index.ts @@ -0,0 +1,7 @@ +/** + * API Clients + * + * API 클라이언트 및 서비스 + */ + +export * from "./notification-service" diff --git a/packages/api/clients/notification-service.ts b/packages/api/clients/notification-service.ts new file mode 100644 index 0000000..34c8e13 --- /dev/null +++ b/packages/api/clients/notification-service.ts @@ -0,0 +1,119 @@ +import { createClient } from "@lovetrip/api/supabase/client" + +interface SendNotificationParams { + title: string + body: string + url?: string + userId?: string + userIds?: string[] +} + +export class NotificationService { + private static instance: NotificationService + + private getSupabase() { + return createClient() + } + + static getInstance(): NotificationService { + if (!NotificationService.instance) { + NotificationService.instance = new NotificationService() + } + return NotificationService.instance + } + + async sendPushNotification(params: SendNotificationParams): Promise { + try { + const response = await fetch("/api/push/send", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(params), + }) + + const result = await response.json() + + if (!response.ok) { + console.error("[v0] Failed to send push notification:", result.error) + return false + } + + console.log("[v0] Push notification sent:", result.message) + return true + } catch (error) { + console.error("[v0] Error sending push notification:", error) + return false + } + } + + async sendToCurrentUser(title: string, body: string, url?: string): Promise { + const supabase = this.getSupabase() + const { + data: { user }, + } = await supabase.auth.getUser() + + if (!user) { + console.error("[v0] No authenticated user found") + return false + } + + return this.sendPushNotification({ + title, + body, + url, + userId: user.id, + }) + } + + async sendToUsers( + userIds: string[], + title: string, + body: string, + url?: string + ): Promise { + return this.sendPushNotification({ + title, + body, + url, + userIds, + }) + } + + async sendTravelPlanNotification( + planId: string, + type: "created" | "updated" | "reminder" + ): Promise { + const supabase = this.getSupabase() + const { data: plan } = await supabase + .from("travel_plans") + .select("title, user_id") + .eq("id", planId) + .single() + + if (!plan) return false + + const messages = { + created: { + title: "새로운 여행 계획이 생성되었습니다!", + body: `"${plan.title}" 계획을 확인해보세요.`, + }, + updated: { + title: "여행 계획이 업데이트되었습니다", + body: `"${plan.title}" 계획에 변경사항이 있습니다.`, + }, + reminder: { + title: "여행 일정 알림", + body: `"${plan.title}" 여행이 곧 시작됩니다!`, + }, + } + + return this.sendPushNotification({ + ...messages[type], + url: `/?plan=${planId}`, + userId: plan.user_id, + }) + } +} + +export const notificationService = NotificationService.getInstance() diff --git a/packages/api/eslint.config.mjs b/packages/api/eslint.config.mjs new file mode 100644 index 0000000..d9fee71 --- /dev/null +++ b/packages/api/eslint.config.mjs @@ -0,0 +1,49 @@ +import js from "@eslint/js" +import tseslint from "typescript-eslint" +import globals from "globals" + +export default tseslint.config( + { + ignores: [ + "**/node_modules/**", + "**/.next/**", + "**/dist/**", + "**/build/**", + "**/*.stories.tsx", + "**/*.config.*", + "**/sw.js", + "**/next-env.d.ts", + "**/*.test.ts", + "**/*.test.tsx", + ], + }, + js.configs.recommended, + ...tseslint.configs.recommended, + { + languageOptions: { + ecmaVersion: 2020, + sourceType: "module", + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + }, + globals: { + ...globals.browser, + ...globals.node, + ...globals.es2020, + }, + }, + rules: { + "@typescript-eslint/no-unused-vars": [ + "warn", + { + argsIgnorePattern: "^_", + varsIgnorePattern: "^_", + }, + ], + "@typescript-eslint/no-explicit-any": "warn", + }, + } +) + diff --git a/packages/api/index.ts b/packages/api/index.ts new file mode 100644 index 0000000..9e04fa1 --- /dev/null +++ b/packages/api/index.ts @@ -0,0 +1,10 @@ +/** + * @lovetrip/api + * + * API 클라이언트 패키지 + * - Supabase 클라이언트 + * - 외부 API 클라이언트 (Tour API 등) + */ + +export * from "./supabase" +export * from "./clients" diff --git a/packages/api/package.json b/packages/api/package.json new file mode 100644 index 0000000..d5e7415 --- /dev/null +++ b/packages/api/package.json @@ -0,0 +1,22 @@ +{ + "name": "@lovetrip/api", + "version": "0.1.0", + "type": "module", + "private": true, + "main": "./index.ts", + "types": "./index.ts", + "exports": { + ".": "./index.ts", + "./supabase": "./supabase/index.ts", + "./clients": "./clients/index.ts" + }, + "scripts": { + "lint": "eslint .", + "type-check": "tsc --noEmit" + }, + "dependencies": { + "@supabase/ssr": "latest", + "@supabase/supabase-js": "latest" + }, + "devDependencies": {} +} diff --git a/packages/api/supabase/client.ts b/packages/api/supabase/client.ts new file mode 100644 index 0000000..7e28871 --- /dev/null +++ b/packages/api/supabase/client.ts @@ -0,0 +1,14 @@ +import { createBrowserClient } from "@supabase/ssr" + +export function createClient() { + const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL + const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY + + if (!supabaseUrl || !supabaseAnonKey) { + throw new Error( + "Missing Supabase environment variables. Please set NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY in your .env.local file." + ) + } + + return createBrowserClient(supabaseUrl, supabaseAnonKey) +} diff --git a/packages/api/supabase/index.ts b/packages/api/supabase/index.ts new file mode 100644 index 0000000..e8b7cbf --- /dev/null +++ b/packages/api/supabase/index.ts @@ -0,0 +1,7 @@ +/** + * Supabase 클라이언트 + */ + +export * from "./client" +export * from "./server" + diff --git a/packages/api/supabase/server.ts b/packages/api/supabase/server.ts new file mode 100644 index 0000000..598e8cb --- /dev/null +++ b/packages/api/supabase/server.ts @@ -0,0 +1,37 @@ +import { createServerClient } from "@supabase/ssr" +import { cookies } from "next/headers" + +/** + * Especially important if using Fluid compute: Don't put this client in a + * global variable. Always create a new client within each function when using + * it. + */ +export async function createClient() { + const cookieStore = await cookies() + + const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL + const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY + + if (!supabaseUrl || !supabaseAnonKey) { + throw new Error( + "Missing Supabase environment variables. Please set NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY in your .env.local file." + ) + } + + return createServerClient(supabaseUrl, supabaseAnonKey, { + cookies: { + getAll() { + return cookieStore.getAll() + }, + setAll(cookiesToSet) { + try { + cookiesToSet.forEach(({ name, value, options }) => cookieStore.set(name, value, options)) + } catch { + // The "setAll" method was called from a Server Component. + // This can be ignored if you have middleware refreshing + // user sessions. + } + }, + }, + }) +} diff --git a/packages/api/tsconfig.json b/packages/api/tsconfig.json new file mode 100644 index 0000000..a523a06 --- /dev/null +++ b/packages/api/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "moduleResolution": "bundler", + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true + }, + "include": ["**/*.ts", "**/*.tsx"], + "exclude": ["node_modules", "dist"] +} + diff --git a/packages/couple/components/calendar/components/calendar-grid.tsx b/packages/couple/components/calendar/components/calendar-grid.tsx new file mode 100644 index 0000000..5451b50 --- /dev/null +++ b/packages/couple/components/calendar/components/calendar-grid.tsx @@ -0,0 +1,172 @@ +"use client" + +import { Button } from "@lovetrip/ui/components/button" +import { Card, CardContent, CardHeader, CardTitle } from "@lovetrip/ui/components/card" +import { Badge } from "@lovetrip/ui/components/badge" +import { Plus } from "lucide-react" +import { motion } from "framer-motion" +import type { CalendarEvent } from "@lovetrip/couple/services" +import type { CurrentUserInfo, PartnerInfo } from "../types" + +interface CalendarGridProps { + currentMonth: Date + events: CalendarEvent[] + currentUserInfo: CurrentUserInfo | null + partnerInfo: PartnerInfo | null + onDateClick: (date: Date) => void + onEventClick: (event: CalendarEvent) => void + onDeleteEvent: (eventId: string) => void + onNavigateMonth: (direction: "prev" | "next") => void + onTodayClick: () => void +} + +export function CalendarGrid({ + currentMonth, + events, + currentUserInfo, + partnerInfo, + onDateClick, + onEventClick, + onDeleteEvent, + onNavigateMonth, + onTodayClick, +}: CalendarGridProps) { + const getDaysInMonth = () => { + const year = currentMonth.getFullYear() + const month = currentMonth.getMonth() + const firstDay = new Date(year, month, 1) + const lastDay = new Date(year, month + 1, 0) + const daysInMonth = lastDay.getDate() + const startingDayOfWeek = firstDay.getDay() + + const days = [] + // 빈 칸 추가 + for (let i = 0; i < startingDayOfWeek; i++) { + days.push(null) + } + // 날짜 추가 + for (let day = 1; day <= daysInMonth; day++) { + days.push(new Date(year, month, day)) + } + return days + } + + const getEventsForDate = (date: Date | null) => { + if (!date) return [] + const dateStr = date.toISOString().split("T")[0] + return events.filter(event => event.start_time.startsWith(dateStr)) + } + + const days = getDaysInMonth() + const weekDays = ["일", "월", "화", "수", "목", "금", "토"] + + return ( + + +
+
+ + {currentMonth.getFullYear()}년 {currentMonth.getMonth() + 1}월 + +
+
+ + + +
+
+
+ +
+ {weekDays.map(day => ( +
+ {day} +
+ ))} +
+
+ {days.map((date, index) => { + const dayEvents = getEventsForDate(date) + const isToday = date && date.toDateString() === new Date().toDateString() + + return ( + { + if (date && dayEvents.length === 0) { + onDateClick(date) + } + }} + > + {date && ( + <> +
+ {date.getDate()} +
+
+ {dayEvents.slice(0, 2).map(event => { + const isMyEvent = currentUserInfo?.id === event.created_by + const eventColor = isMyEvent + ? "bg-primary/20 border-primary/50" + : "bg-accent/20 border-accent/50" + const eventNickname = isMyEvent + ? currentUserInfo?.nickname || "나" + : partnerInfo?.nickname || "파트너" + + return ( + { + e.stopPropagation() + if (event.place_id) { + onEventClick(event) + } else { + if (confirm(`"${event.title}" 일정을 삭제하시겠습니까?`)) { + onDeleteEvent(event.id) + } + } + }} + > +
+ {eventNickname} + {event.title} +
+
+ ) + })} + {dayEvents.length > 2 && ( +
+ +{dayEvents.length - 2}개 더 +
+ )} + {dayEvents.length === 0 && ( +
+ +
+ )} +
+ + )} +
+ ) + })} +
+
+
+ ) +} + diff --git a/packages/couple/components/calendar/components/calendar-sidebar.tsx b/packages/couple/components/calendar/components/calendar-sidebar.tsx new file mode 100644 index 0000000..20c616e --- /dev/null +++ b/packages/couple/components/calendar/components/calendar-sidebar.tsx @@ -0,0 +1,50 @@ +"use client" + +import { Card, CardContent, CardHeader, CardTitle } from "@lovetrip/ui/components/card" +import type { SharedCalendar } from "@lovetrip/couple/services" +import { CreateEventDialog } from "./create-event-dialog" + +interface CalendarSidebarProps { + calendars: SharedCalendar[] + selectedCalendar: string | null + onSelectCalendar: (calendarId: string) => void + onEventCreated?: () => void +} + +export function CalendarSidebar({ + calendars, + selectedCalendar, + onSelectCalendar, + onEventCreated, +}: CalendarSidebarProps) { + return ( +
+ + + 캘린더 + + + {calendars.map(cal => ( + + ))} + + + + +
+ ) +} + diff --git a/packages/couple/components/calendar/components/couple-required.tsx b/packages/couple/components/calendar/components/couple-required.tsx new file mode 100644 index 0000000..d2527de --- /dev/null +++ b/packages/couple/components/calendar/components/couple-required.tsx @@ -0,0 +1,25 @@ +"use client" + +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@lovetrip/ui/components/card" +import { Button } from "@lovetrip/ui/components/button" +import { Heart } from "lucide-react" + +export function CoupleRequired() { + return ( +
+ + + + + 커플 연결이 필요합니다 + + 캘린더를 사용하려면 먼저 커플과 연결해주세요 + + + + + +
+ ) +} + diff --git a/packages/couple/components/calendar/components/create-event-dialog.tsx b/packages/couple/components/calendar/components/create-event-dialog.tsx new file mode 100644 index 0000000..2a43574 --- /dev/null +++ b/packages/couple/components/calendar/components/create-event-dialog.tsx @@ -0,0 +1,186 @@ +"use client" + +import { useState } from "react" +import { Button } from "@lovetrip/ui/components/button" +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@lovetrip/ui/components/dialog" +import { Input } from "@lovetrip/ui/components/input" +import { Label } from "@lovetrip/ui/components/label" +import { Textarea } from "@lovetrip/ui/components/textarea" +import { Plus, X } from "lucide-react" +import Image from "next/image" +import { useEventForm } from "../hooks/use-event-form" +import { useEventActions } from "../hooks/use-event-actions" +import { PlaceSearch } from "./place-search" + +interface CreateEventDialogProps { + selectedCalendar: string | null + onSuccess?: () => void +} + +export function CreateEventDialog({ selectedCalendar, onSuccess }: CreateEventDialogProps) { + const [isDialogOpen, setIsDialogOpen] = useState(false) + const { + newEvent, + setNewEvent, + selectedPlace, + setSelectedPlace, + showPlaceSearch, + setShowPlaceSearch, + placeSearchQuery, + setPlaceSearchQuery, + searchResults, + setSearchResults, + resetForm, + } = useEventForm() + + const { handleCreateEvent } = useEventActions(selectedCalendar || null, onSuccess) + + const handleSubmit = async () => { + if (!selectedCalendar) { + return + } + + await handleCreateEvent(newEvent, () => { + resetForm() + setIsDialogOpen(false) + }) + } + + const handleSelectPlace = (place: typeof selectedPlace) => { + if (!place) return + setSelectedPlace(place) + setNewEvent({ + ...newEvent, + title: place.name, + location: place.address || place.name, + place_id: place.id, + }) + setShowPlaceSearch(false) + setPlaceSearchQuery("") + setSearchResults([]) + } + + const handleRemovePlace = () => { + setSelectedPlace(null) + setNewEvent({ + ...newEvent, + place_id: "", + }) + } + + return ( + + + + + + + 새 일정 추가 + 커플과 공유할 일정을 추가하세요 + +
+
+ + setNewEvent({ ...newEvent, title: e.target.value })} + placeholder="일정 제목" + /> +
+
+ + setNewEvent({ ...newEvent, start_time: e.target.value })} + /> +
+
+ + setNewEvent({ ...newEvent, end_time: e.target.value })} + /> +
+
+ + {selectedPlace ? ( +
+
+ {selectedPlace.image_url && ( +
+ {selectedPlace.name} +
+ )} +
+

{selectedPlace.name}

+ {selectedPlace.address && ( +

+ {selectedPlace.address} +

+ )} +
+
+ +
+ ) : ( + + )} +
+
+ + setNewEvent({ ...newEvent, location: e.target.value })} + placeholder="장소를 입력하세요" + disabled={!!selectedPlace} + /> +
+
+ +